38031cb189
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>
214 lines
13 KiB
Plaintext
214 lines
13 KiB
Plaintext
@page "{id:int}"
|
||
@model JobsMedical.Web.Pages.Admin.ReviewModel
|
||
@{
|
||
ViewData["Title"] = "بررسی و انتشار آگهی";
|
||
var r = Model.Raw!;
|
||
}
|
||
|
||
<div class="page-head">
|
||
<div class="container"><h1>بررسی و انتشار آگهی</h1><p class="muted">منبع: @r.SourceChannel</p></div>
|
||
</div>
|
||
|
||
<div class="container section">
|
||
@if (Model.Error is not null)
|
||
{
|
||
<div class="alert alert-error" style="margin-bottom:16px;">⚠ @Model.Error</div>
|
||
}
|
||
<div class="detail-grid">
|
||
<div>
|
||
<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)
|
||
{
|
||
<div class="card card-pad" style="margin-top:16px;">
|
||
<h3 style="margin-top:0;">🤖 تشخیص خودکار (پارسر)</h3>
|
||
<div class="rec-reasons">
|
||
@foreach (var note in Model.Parsed.Notes)
|
||
{
|
||
<span class="rec-reason">• @note</span>
|
||
}
|
||
@if (Model.Parsed.CityName is not null) { <span class="rec-reason">• شهر: @Model.Parsed.CityName</span> }
|
||
@if (Model.Parsed.DistrictName is not null) { <span class="rec-reason">• محله: @Model.Parsed.DistrictName</span> }
|
||
@if (Model.Parsed.Phone is not null) { <span class="rec-reason">• تلفن: @Model.Parsed.Phone</span> }
|
||
</div>
|
||
<p class="muted" style="font-size:12px; margin-bottom:0;">اینها فقط پیشنهاد هستند؛ قبل از انتشار بررسی و اصلاح کن.</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<aside>
|
||
<form method="post" class="card card-pad">
|
||
<div class="filter-group">
|
||
<label>نوع آگهی</label>
|
||
<select name="Kind" id="kindSelect">
|
||
<option value="0" selected="@(Model.Kind == JobsMedical.Web.Models.ListingKind.Shift)">شیفت</option>
|
||
<option value="1" selected="@(Model.Kind == JobsMedical.Web.Models.ListingKind.Job)">استخدام</option>
|
||
<option value="2" selected="@(Model.Kind == JobsMedical.Web.Models.ListingKind.Talent)">آماده به کار (معرفی نیرو)</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group" id="facilityGroup">
|
||
<label>مرکز درمانی</label>
|
||
<select name="FacilityId">
|
||
<option value="0" selected="@(Model.FacilityId == 0)">— انتخاب نشده —</option>
|
||
@foreach (var f in Model.Facilities)
|
||
{
|
||
<option value="@f.Id" selected="@(Model.FacilityId == f.Id)">@f.Name — @f.City?.Name</option>
|
||
}
|
||
</select>
|
||
<input type="text" name="NewFacilityName" placeholder="یا نام مرکز جدید را وارد کن…" style="margin-top:6px;" />
|
||
<p class="muted" style="font-size:11px; margin:4px 0 0;">اگر مرکز در فهرست نیست، نامش را اینجا بنویس تا بهصورت «تأییدنشده» ساخته شود.</p>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>نقشها (میتوانی چند مورد انتخاب کنی)</label>
|
||
<div class="role-checks">
|
||
@foreach (var role in Model.Roles)
|
||
{
|
||
<label class="role-check">
|
||
<input type="checkbox" name="RoleIds" value="@role.Id" checked="@(Model.RoleIds.Contains(role.Id))" />
|
||
<span>@role.Name</span>
|
||
</label>
|
||
}
|
||
</div>
|
||
<p class="muted" style="font-size:11px; margin:4px 0 0;">برای آگهی چندتخصصی (مثل «پرستار سالمند و کودک») همهی نقشها را تیک بزن — برای هر نقش یک آگهی جدا ساخته میشود.</p>
|
||
</div>
|
||
|
||
<div class="filter-group">
|
||
<label>جنسیت مورد نیاز</label>
|
||
<select name="GenderRequirement">
|
||
<option value="0" selected="@(Model.GenderRequirement == JobsMedical.Web.Models.Gender.Any)">فرقی نمیکند</option>
|
||
<option value="1" selected="@(Model.GenderRequirement == JobsMedical.Web.Models.Gender.Male)">آقا</option>
|
||
<option value="2" selected="@(Model.GenderRequirement == JobsMedical.Web.Models.Gender.Female)">خانم</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="shiftFields">
|
||
<div class="filter-group">
|
||
<label>تاریخ شیفت (میلادی)</label>
|
||
<input type="date" name="ShiftDate" value="@Model.ShiftDate.ToString("yyyy-MM-dd")" dir="ltr" />
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>نوع شیفت</label>
|
||
<select name="ShiftType">
|
||
<option value="0" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Day)">صبح</option>
|
||
<option value="1" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Evening)">عصر</option>
|
||
<option value="2" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Night)">شب</option>
|
||
<option value="3" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.OnCall)">آنکال</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group" style="display:flex; gap:8px;">
|
||
<div style="flex:1;"><label>شروع</label><input type="time" name="StartTime" value="@Model.StartTime.ToString("HH:mm")" dir="ltr" /></div>
|
||
<div style="flex:1;"><label>پایان</label><input type="time" name="EndTime" value="@Model.EndTime.ToString("HH:mm")" dir="ltr" /></div>
|
||
</div>
|
||
<div class="filter-group" style="display:flex; gap:8px;">
|
||
<div style="flex:1;"><label>مبلغ مقطوع (تومان)</label><input type="number" name="PayAmount" value="@Model.PayAmount" dir="ltr" /></div>
|
||
<div style="flex:1;"><label>یا سهم درآمد (٪)</label><input type="number" name="SharePercent" value="@Model.SharePercent" min="1" max="100" dir="ltr" /></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="jobFields" style="display:none;">
|
||
<div class="filter-group">
|
||
<label>عنوان موقعیت</label>
|
||
<input type="text" name="Title" value="@Model.Title" />
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>نوع همکاری</label>
|
||
<select name="EmploymentType">
|
||
<option value="0" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.FullTime)">تماموقت</option>
|
||
<option value="1" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.PartTime)">پارهوقت</option>
|
||
<option value="2" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.Contract)">قراردادی</option>
|
||
<option value="3" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.Plan)">طرح</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group" style="display:flex; gap:8px;">
|
||
<div style="flex:1;"><label>حقوق از</label><input type="number" name="SalaryMin" value="@Model.SalaryMin" dir="ltr" /></div>
|
||
<div style="flex:1;"><label>تا</label><input type="number" name="SalaryMax" value="@Model.SalaryMax" dir="ltr" /></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="talentFields" style="display:none;">
|
||
<div class="filter-group">
|
||
<label>نام فرد (اختیاری)</label>
|
||
<input type="text" name="PersonName" value="@Model.PersonName" placeholder="مثلاً دکتر سپیده علیزاده" />
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>شهر</label>
|
||
<select name="TalentCityId">
|
||
@foreach (var c in Model.Cities)
|
||
{
|
||
<option value="@c.Id" selected="@(Model.TalentCityId == c.Id)">@c.Name</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="filter-group" style="display:flex; gap:8px;">
|
||
<div style="flex:1;"><label>سابقه (سال)</label><input type="number" name="YearsExperience" value="@Model.YearsExperience" min="0" max="60" dir="ltr" /></div>
|
||
<div style="flex:1;"><label>محدوده کاری</label><input type="text" name="AreaNote" value="@Model.AreaNote" placeholder="مثلاً فقط منطقه ۱" /></div>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>شماره تماس</label>
|
||
<input type="text" name="Phone" value="@Model.Phone" placeholder="۰۹۱۲…" dir="ltr" />
|
||
</div>
|
||
<div class="filter-group">
|
||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||
<input type="checkbox" name="IsLicensed" value="true" style="width:auto;" checked="@Model.IsLicensed" /> پروانهدار
|
||
</label>
|
||
</div>
|
||
<div class="filter-group" style="display:flex; gap:8px;">
|
||
<div style="flex:1;"><label>دستمزد مدنظر (تومان)</label><input type="number" name="PayAmount" value="@Model.PayAmount" dir="ltr" /></div>
|
||
<div style="flex:1;"><label>یا سهم درآمد (٪)</label><input type="number" name="SharePercent" value="@Model.SharePercent" min="1" max="100" dir="ltr" /></div>
|
||
</div>
|
||
<p class="muted" style="font-size:11px; margin:4px 0 0;">برای «آماده به کار» نیازی به مرکز نیست؛ شماره تماس مهمترین فیلد است.</p>
|
||
</div>
|
||
|
||
<div class="filter-group">
|
||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||
<input type="checkbox" name="Negotiable" value="true" style="width:auto;" checked="@Model.Negotiable" /> توافقی
|
||
</label>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>توضیحات</label>
|
||
<textarea name="Description" rows="3">@Model.Description</textarea>
|
||
</div>
|
||
|
||
<button type="submit" asp-page-handler="Publish" asp-route-id="@r.Id" class="btn btn-accent btn-block btn-lg">انتشار</button>
|
||
<button type="submit" asp-page-handler="Discard" asp-route-id="@r.Id" class="btn btn-outline btn-block" style="margin-top:8px;">رد و حذف از صف</button>
|
||
</form>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
|
||
@section Scripts {
|
||
<script>
|
||
var kind = document.getElementById('kindSelect');
|
||
var facilityGroup = document.getElementById('facilityGroup');
|
||
// Show one section and DISABLE the hidden ones so duplicate-named inputs
|
||
// (PayAmount/SharePercent appear in both shift and talent) aren't submitted.
|
||
function setSection(el, on) {
|
||
if (!el) return;
|
||
el.style.display = on ? 'block' : 'none';
|
||
el.querySelectorAll('input,select,textarea').forEach(function (i) { i.disabled = !on; });
|
||
}
|
||
function toggleKind() {
|
||
var v = kind.value;
|
||
setSection(document.getElementById('shiftFields'), v === '0');
|
||
setSection(document.getElementById('jobFields'), v === '1');
|
||
setSection(document.getElementById('talentFields'), v === '2');
|
||
setSection(facilityGroup, v !== '2'); // facility only for shift/job
|
||
}
|
||
kind.addEventListener('change', toggleKind);
|
||
toggleKind();
|
||
</script>
|
||
}
|