Add «آماده به کار» (talent) listing type — workers offering themselves
CI/CD / CI · dotnet build (push) Successful in 1m41s
CI/CD / Deploy · hamkadr (push) Has been cancelled

Adds a third listing kind alongside Shift/Job for healthcare staff who
advertise their own availability (very common in Iranian medical
channels, e.g. "دندانپزشک آماده همکاری… ۵۰٪ تسویه"). These have no
facility; the contact phone is the key field.

- Model: TalentListing (role, person name, years, licensed, city/district,
  area note, availability, gender, comp, phone) + ListingKind.Talent +
  RawListing.LinkedTalentId + DbSet/relations/indexes + EF migration.
- Parser: detect آماده‌به‌کار/جویای کار → Kind=Talent; extract person name,
  years of experience, licensed flag, area («منطقه ۱»), phone. Facility
  name extraction now skipped for talent.
- Validator: talent path scores role + phone + medical (no facility/pay
  required).
- Ingestion auto-publish: creates a TalentListing for talent kind.
- Review (manual publish): Talent option + talent fields; publishes a
  TalentListing without a facility. Shift/Job facility now falls back to a
  shared «نامشخص / ثبت نشده» record when the ad names none — publishing
  never fails on a missing facility.
- Browse /Talent (indexable, filters: city/district/role/gender),
  details /Talent/Details (noindex — personal contact, tel: call button),
  _TalentCard, badge-talent, nav link, home section.
- Sitemap includes /Talent; robots disallows /Talent/Details. Archiver
  expires stale talent listings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-08 08:01:12 +03:30
parent bdcca5e548
commit 4e5df73cf7
24 changed files with 2327 additions and 34 deletions
+49 -4
View File
@@ -46,9 +46,10 @@
<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">
<div class="filter-group" id="facilityGroup">
<label>مرکز درمانی</label>
<select name="FacilityId">
<option value="0" selected="@(Model.FacilityId == 0)">— انتخاب نشده —</option>
@@ -123,6 +124,40 @@
</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" /> توافقی
@@ -143,10 +178,20 @@
@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 isJob = kind.value === '1';
document.getElementById('jobFields').style.display = isJob ? 'block' : 'none';
document.getElementById('shiftFields').style.display = isJob ? 'none' : 'block';
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();