490821a637
- Talent «آماده به کار» now has its own freshness window (21 days, vs 30 for jobs) since availability goes stale fast; archiver, browse, and home use TalentCutoffUtc. - Expired/filled job openings and past/filled shifts now emit robots noindex so Google drops dead listings instead of keeping soft-404 pages. (Talent details were already noindex.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
208 lines
11 KiB
Plaintext
208 lines
11 KiB
Plaintext
@page "{id:int}"
|
|
@model JobsMedical.Web.Pages.Shifts.DetailsModel
|
|
@{
|
|
var s = Model.Shift!;
|
|
var f = s.Facility!;
|
|
ViewData["Title"] = $"شیفت {s.SpecialtyRequired} - {f.Name}";
|
|
ViewData["Description"] = $"شیفت {s.SpecialtyRequired} در {f.Name}، {f.City?.Name}، تاریخ {JalaliDate.ToLongDate(s.Date)} از ساعت {JalaliDate.Time(s.StartTime)}.";
|
|
// Past/filled shifts shouldn't stay in the index as dead pages.
|
|
if (s.Status != JobsMedical.Web.Models.ShiftStatus.Open || s.Date < DateOnly.FromDateTime(DateTime.UtcNow))
|
|
ViewData["NoIndex"] = true;
|
|
var (badgeClass, typeLabel) = s.ShiftType switch
|
|
{
|
|
ShiftType.Day => ("badge-day", "شیفت صبح"),
|
|
ShiftType.Evening => ("badge-evening", "شیفت عصر"),
|
|
ShiftType.Night => ("badge-night", "شیفت شب"),
|
|
_ => ("badge-oncall", "آنکال"),
|
|
};
|
|
}
|
|
|
|
<div class="page-head">
|
|
<div class="container">
|
|
<div class="row" style="display:flex; gap:10px; align-items:center;">
|
|
<span class="badge @badgeClass">@typeLabel</span>
|
|
@if (f.IsVerified)
|
|
{
|
|
<span class="badge badge-verified">✓ مرکز تأیید شده</span>
|
|
}
|
|
</div>
|
|
<h1 style="margin-top:8px;">@s.SpecialtyRequired — @f.Name</h1>
|
|
<p class="muted">📍 @f.City?.Name @(string.IsNullOrEmpty(f.Address) ? "" : "، " + f.Address)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container section has-action-bar">
|
|
<div class="detail-grid">
|
|
<div>
|
|
@if (Model.ShowContact)
|
|
{
|
|
<div class="alert alert-success">
|
|
✓ تمایل شما ثبت شد. برای هماهنگی شیفت با مرکز درمانی تماس بگیرید:
|
|
<strong>@(f.Phone ?? "شماره ثبت نشده")</strong>
|
|
@if (!string.IsNullOrEmpty(f.BaleId))
|
|
{
|
|
<text> — بله: @f.BaleId</text>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="card card-pad">
|
|
<h3 style="margin-top:0;">جزئیات شیفت</h3>
|
|
<div class="info-row"><span class="k">تاریخ</span><span class="v">@JalaliDate.WeekDayName(s.Date)، @JalaliDate.ToLongDate(s.Date)</span></div>
|
|
<div class="info-row"><span class="k">ساعت</span><span class="v">@JalaliDate.Time(s.StartTime) تا @JalaliDate.Time(s.EndTime)</span></div>
|
|
<div class="info-row"><span class="k">مدت</span><span class="v">@JalaliDate.ToPersianDigits(s.DurationHours.ToString("0.#")) ساعت</span></div>
|
|
<div class="info-row"><span class="k">نقش مورد نیاز</span><span class="v">@(s.Role?.Name ?? s.SpecialtyRequired)</span></div>
|
|
@if (s.GenderRequirement != Gender.Any)
|
|
{
|
|
<div class="info-row"><span class="k">جنسیت</span><span class="v">@JalaliDate.GenderLabel(s.GenderRequirement)</span></div>
|
|
}
|
|
<div class="info-row"><span class="k">پرداخت</span><span class="v" style="color:var(--primary-dark)">@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)</span></div>
|
|
<div style="padding-top:12px;">
|
|
<span class="k" style="font-size:13px; color:var(--muted);">بازه ساعت کاری در شبانهروز</span>
|
|
<partial name="_HourBar" model="s" />
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(s.Description))
|
|
{
|
|
<div class="card card-pad" style="margin-top:16px;">
|
|
<h3 style="margin-top:0;">توضیحات</h3>
|
|
<p class="muted" style="margin:0;">@s.Description</p>
|
|
</div>
|
|
}
|
|
|
|
@if (Model.MoreAtFacility.Count > 0)
|
|
{
|
|
<h3 style="margin:26px 0 14px;">شیفتهای دیگر این مرکز</h3>
|
|
<div class="grid grid-3">
|
|
@foreach (var more in Model.MoreAtFacility)
|
|
{
|
|
<partial name="_ShiftCard" model="more" />
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<aside>
|
|
<div class="card card-pad">
|
|
<div class="pay" style="font-size:19px; margin-bottom:6px; color:var(--primary-dark); font-weight:800;">
|
|
@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)
|
|
</div>
|
|
@if (s.PayAmount is not null && s.SharePercent is not null)
|
|
{
|
|
<p class="muted" style="font-size:13px; margin-top:0;">میتوانی هنگام هماهنگی، یکی از دو حالت را با مرکز انتخاب کنی.</p>
|
|
}
|
|
@if (Model.Saved)
|
|
{
|
|
<div class="alert alert-success" style="margin-bottom:12px;">✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.</div>
|
|
}
|
|
<div class="aside-apply">
|
|
<form method="post">
|
|
<button type="submit" asp-page-handler="Interest" asp-route-id="@s.Id"
|
|
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
|
</form>
|
|
<p class="muted center" style="font-size:12px; margin:8px 0;">با اعلام تمایل، اطلاعات تماس مرکز نمایش داده میشود.</p>
|
|
</div>
|
|
<div style="display:flex; gap:8px;">
|
|
<form method="post" style="flex:1;">
|
|
<button type="submit" asp-page-handler="Save" asp-route-id="@s.Id"
|
|
class="btn btn-outline btn-block">♡ ذخیره</button>
|
|
</form>
|
|
<form method="post" style="flex:1;">
|
|
<button type="submit" asp-page-handler="Dismiss" asp-route-id="@s.Id"
|
|
class="btn btn-outline btn-block">✕ علاقهمند نیستم</button>
|
|
</form>
|
|
</div>
|
|
@if (Model.Reported)
|
|
{
|
|
<p class="muted" style="font-size:12px; margin:8px 0 0;">✓ گزارش شما ثبت شد. متشکریم.</p>
|
|
}
|
|
else
|
|
{
|
|
<details style="margin-top:10px;">
|
|
<summary class="muted" style="font-size:12px; cursor:pointer;">گزارش تخلف یا اطلاعات نادرست</summary>
|
|
<form method="post" action="/report" style="margin-top:8px;">
|
|
<input type="hidden" name="targetType" value="Shift" />
|
|
<input type="hidden" name="targetId" value="@s.Id" />
|
|
<input type="hidden" name="label" value="@(s.Role?.Name) — @s.Facility?.Name" />
|
|
<input type="hidden" name="returnUrl" value="/Shifts/Details/@s.Id" />
|
|
<textarea name="reason" rows="2" placeholder="دلیل گزارش..." required></textarea>
|
|
<button type="submit" class="btn btn-outline btn-block" style="margin-top:6px;">ارسال گزارش</button>
|
|
</form>
|
|
</details>
|
|
<details style="margin-top:6px;">
|
|
<summary class="muted" style="font-size:12px; cursor:pointer;">شکایت از این مرکز (@f.Name)</summary>
|
|
<form method="post" action="/report" style="margin-top:8px;">
|
|
<input type="hidden" name="targetType" value="Facility" />
|
|
<input type="hidden" name="targetId" value="@f.Id" />
|
|
<input type="hidden" name="label" value="@f.Name" />
|
|
<input type="hidden" name="returnUrl" value="/Shifts/Details/@s.Id" />
|
|
<textarea name="reason" rows="2" placeholder="شکایت یا گزارش درباره این مرکز..." required></textarea>
|
|
<button type="submit" class="btn btn-outline btn-block" style="margin-top:6px;">ثبت شکایت</button>
|
|
</form>
|
|
</details>
|
|
}
|
|
</div>
|
|
|
|
<div class="card card-pad" style="margin-top:16px;">
|
|
<h3 style="margin-top:0;">موقعیت مکانی</h3>
|
|
@if (f.Lat is not null && f.Lng is not null)
|
|
{
|
|
var latS = f.Lat.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
var lngS = f.Lng.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
@if (!string.IsNullOrEmpty(Model.MapKey))
|
|
{
|
|
<div id="facmap" data-lat="@latS" data-lng="@lngS" style="height:200px; border-radius:10px; overflow:hidden; border:1px solid var(--line);"></div>
|
|
}
|
|
else
|
|
{
|
|
<div style="background:var(--primary-soft); border-radius:10px; height:140px; display:grid; place-items:center; color:var(--primary-dark); text-align:center; padding:10px;">
|
|
🗺️<br /><small class="muted" dir="ltr">@latS، @lngS</small>
|
|
</div>
|
|
}
|
|
<a class="btn btn-outline btn-block" style="margin-top:8px;" target="_blank" rel="noopener"
|
|
href="https://neshan.org/maps/@(latS),@(lngS),16z">مسیریابی در نشان</a>
|
|
}
|
|
else
|
|
{
|
|
<p class="muted" style="margin:0;">مختصات این مرکز هنوز ثبت نشده است.</p>
|
|
}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
|
|
@* Sticky bottom action bar — mobile only. Always-reachable primary action (native-app feel). *@
|
|
<div class="mobile-action-bar">
|
|
@if (Model.ShowContact)
|
|
{
|
|
@if (!string.IsNullOrEmpty(f.Phone))
|
|
{
|
|
<a class="btn btn-accent btn-lg cta-main" href="tel:@f.Phone">📞 تماس با مرکز</a>
|
|
}
|
|
else
|
|
{
|
|
<span class="cta-main center muted" style="align-self:center;">اطلاعات تماس در بالای صفحه</span>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<form method="post" class="cta-main">
|
|
<button type="submit" asp-page-handler="Interest" asp-route-id="@s.Id" class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده تماس</button>
|
|
</form>
|
|
<form method="post">
|
|
<button type="submit" asp-page-handler="Save" asp-route-id="@s.Id" class="btn btn-outline btn-lg" aria-label="ذخیره" title="ذخیره">♡</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Shift?.Facility?.Lat is not null)
|
|
{
|
|
<partial name="_NeshanMap" model="Model.MapKey" />
|
|
}
|
|
|
|
@section Head {
|
|
@{ var bu = $"{ViewContext.HttpContext.Request.Scheme}://{ViewContext.HttpContext.Request.Host}"; }
|
|
@Html.Raw("<script type=\"application/ld+json\">" + JobsMedical.Web.Services.SeoJsonLd.ShiftPosting(s, bu) + "</script>")
|
|
}
|