Per-ad contacts for shifts/jobs, stale-applicant filter, review source link
Phone fix: shifts/jobs showed Facility.Phone, but unnamed ads all share one placeholder facility, so every such listing displayed the same stale number while the ad's real phone sat unused in the description. ContactMethod is now attachable to a Shift/JobOpening (not just talent); ingestion stores the ad's own number(s) on each listing and the detail pages render them (new _ContactList partial), falling back to the facility phone only when the ad had none. Migration ShiftJobContacts (nullable owner FKs) — auto-applies on deploy. Stale applicants: skip «آماده به کار» posts older than 7 days at ingest, by the source's real timestamp (Telegram <time>, Bale date) or a Persian time-ago phrase in the text (Divar «۲ هفته پیش»). Recorded as Discarded; shifts/jobs are not aged out. Admin: Review page now shows a «مشاهده آگهی در منبع» link (RawListing.SourceUrl) so the source post can be checked before publishing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,16 @@
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">متن خام</h3>
|
||||
<p style="white-space:pre-wrap; margin:0;">@r.RawText</p>
|
||||
@if (!string.IsNullOrWhiteSpace(r.SourceUrl))
|
||||
{
|
||||
<p style="margin:12px 0 0;">
|
||||
<a class="btn btn-outline" href="@r.SourceUrl" target="_blank" rel="noopener noreferrer">🔗 مشاهده آگهی در منبع (@r.SourceChannel)</a>
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted" style="font-size:12px; margin:12px 0 0;">لینک منبع برای این آگهی ثبت نشده است.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.Parsed is not null)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@{
|
||||
var j = Model.Job!;
|
||||
var f = j.Facility!;
|
||||
var jobContacts = (j.Contacts ?? new List<JobsMedical.Web.Models.ContactMethod>()).ToList();
|
||||
ViewData["Title"] = j.Title;
|
||||
ViewData["Description"] = $"{j.Title} در {f.Name}، {f.City?.Name}. موقعیت استخدامی برای {j.Role?.Name}.";
|
||||
// Don't let Google index filled/expired openings (avoids dead "Job for jobs" results).
|
||||
@@ -38,24 +39,32 @@
|
||||
@if (Model.ShowContact)
|
||||
{
|
||||
<div class="contact-reveal" style="margin-bottom:16px;">
|
||||
<h4>✓ راههای ارتباطی مرکز</h4>
|
||||
@if (!string.IsNullOrEmpty(f.Phone))
|
||||
<h4>✓ راههای ارتباطی</h4>
|
||||
@if (jobContacts.Count > 0)
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">📞 تلفن</span><span class="c-val" dir="ltr">@f.Phone</span></span>
|
||||
<a class="btn btn-accent" href="tel:@f.Phone">تماس</a>
|
||||
</div>
|
||||
@* Numbers from THIS ad (aggregated) — the correct, per-listing contacts. *@
|
||||
<partial name="_ContactList" model="jobContacts" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(f.BaleId))
|
||||
else if (!string.IsNullOrEmpty(f.Phone) || !string.IsNullOrEmpty(f.BaleId))
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">💬 بله</span><span class="c-val" dir="ltr">@f.BaleId</span></span>
|
||||
<a class="btn btn-outline" href="https://ble.ir/@f.BaleId" target="_blank" rel="noopener">باز کردن</a>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(f.Phone))
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">📞 تلفن مرکز</span><span class="c-val" dir="ltr">@f.Phone</span></span>
|
||||
<a class="btn btn-accent" href="tel:@f.Phone">تماس</a>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(f.BaleId))
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">💬 بله</span><span class="c-val" dir="ltr">@f.BaleId</span></span>
|
||||
<a class="btn btn-outline" href="https://ble.ir/@f.BaleId" target="_blank" rel="noopener">باز کردن</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (string.IsNullOrEmpty(f.Phone) && string.IsNullOrEmpty(f.BaleId))
|
||||
else
|
||||
{
|
||||
<p class="muted" style="margin:0;">شمارهای برای این مرکز ثبت نشده است.</p>
|
||||
<p class="muted" style="margin:0;">شمارهای ثبت نشده است.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ public class DetailsModel : PageModel
|
||||
.Include(j => j.Facility).ThenInclude(f => f.City)
|
||||
.Include(j => j.Facility).ThenInclude(f => f.District)
|
||||
.Include(j => j.Role)
|
||||
.Include(j => j.Contacts)
|
||||
.FirstOrDefaultAsync(j => j.Id == id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@model IReadOnlyList<JobsMedical.Web.Models.ContactMethod>
|
||||
@* Renders one row per contact channel (phone/Bale/Telegram/email/…) with a clickable action.
|
||||
Shared by the shift, job, and applicant detail pages. *@
|
||||
@foreach (var c in Model.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
var href = JobsMedical.Web.Services.ContactInfo.Href(c.Type, c.Value);
|
||||
var label = JobsMedical.Web.Services.ContactInfo.Label(c.Type);
|
||||
var icon = JobsMedical.Web.Services.ContactInfo.Icon(c.Type);
|
||||
var cls = c.Type is JobsMedical.Web.Models.ContactType.Mobile or JobsMedical.Web.Models.ContactType.Phone ? "btn-accent" : "btn-outline";
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">@icon @label</span><span class="c-val" dir="ltr">@c.Value</span></span>
|
||||
@if (href is not null)
|
||||
{
|
||||
<a class="btn @cls" href="@href" target="_blank" rel="noopener">@(c.Type is JobsMedical.Web.Models.ContactType.Mobile or JobsMedical.Web.Models.ContactType.Phone ? "تماس" : "باز کردن")</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
@{
|
||||
var s = Model.Shift!;
|
||||
var f = s.Facility!;
|
||||
var shiftContacts = (s.Contacts ?? new List<JobsMedical.Web.Models.ContactMethod>()).ToList();
|
||||
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.
|
||||
@@ -37,24 +38,32 @@
|
||||
@if (Model.ShowContact)
|
||||
{
|
||||
<div class="contact-reveal" style="margin-bottom:16px;">
|
||||
<h4>✓ راههای ارتباطی مرکز</h4>
|
||||
@if (!string.IsNullOrEmpty(f.Phone))
|
||||
<h4>✓ راههای ارتباطی</h4>
|
||||
@if (shiftContacts.Count > 0)
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">📞 تلفن</span><span class="c-val" dir="ltr">@f.Phone</span></span>
|
||||
<a class="btn btn-accent" href="tel:@f.Phone">تماس</a>
|
||||
</div>
|
||||
@* Numbers from THIS ad (aggregated) — the correct, per-listing contacts. *@
|
||||
<partial name="_ContactList" model="shiftContacts" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(f.BaleId))
|
||||
else if (!string.IsNullOrEmpty(f.Phone) || !string.IsNullOrEmpty(f.BaleId))
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">💬 بله</span><span class="c-val" dir="ltr">@f.BaleId</span></span>
|
||||
<a class="btn btn-outline" href="https://ble.ir/@f.BaleId" target="_blank" rel="noopener">باز کردن</a>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(f.Phone))
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">📞 تلفن مرکز</span><span class="c-val" dir="ltr">@f.Phone</span></span>
|
||||
<a class="btn btn-accent" href="tel:@f.Phone">تماس</a>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(f.BaleId))
|
||||
{
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">💬 بله</span><span class="c-val" dir="ltr">@f.BaleId</span></span>
|
||||
<a class="btn btn-outline" href="https://ble.ir/@f.BaleId" target="_blank" rel="noopener">باز کردن</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (string.IsNullOrEmpty(f.Phone) && string.IsNullOrEmpty(f.BaleId))
|
||||
else
|
||||
{
|
||||
<p class="muted" style="margin:0;">شمارهای برای این مرکز ثبت نشده است.</p>
|
||||
<p class="muted" style="margin:0;">شمارهای ثبت نشده است.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ public class DetailsModel : PageModel
|
||||
Shift = await _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Role)
|
||||
.Include(s => s.Contacts)
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
|
||||
if (Shift is not null)
|
||||
|
||||
Reference in New Issue
Block a user