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
@@ -170,6 +170,27 @@ public class IngestionService
?? cities.FirstOrDefault(c => c.IsActive) ?? cities.First();
var district = districts.FirstOrDefault(x => x.Name == districtName && x.CityId == city.Id);
var kindStr = (d?.Kind ?? parsed.Kind.ToString()).ToLowerInvariant();
// «آماده به کار» — a worker offering themselves. No facility involved.
if (parsed.Kind == ListingKind.Talent || kindStr.Contains("talent") || kindStr.Contains("آماده"))
{
_db.TalentListings.Add(new TalentListing
{
Role = role, City = city, DistrictId = district?.Id,
PersonName = parsed.PersonName, YearsExperience = parsed.YearsExperience,
IsLicensed = parsed.IsLicensed, AreaNote = parsed.AreaNote,
Availability = parsed.EmploymentType, Gender = parsed.Gender,
PayType = parsed.SharePercent is not null && parsed.PayAmount is null ? PayType.Percentage
: parsed.PayAmount is null ? PayType.Negotiable : PayType.PerShift,
PayAmount = parsed.PayAmount, SharePercent = parsed.SharePercent,
Phone = parsed.Phone, Description = raw.RawText,
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
});
raw.Status = RawListingStatus.Normalized;
return;
}
var facilityName = !string.IsNullOrWhiteSpace(d?.FacilityName) ? d!.FacilityName!.Trim()
: !string.IsNullOrWhiteSpace(parsed.FacilityName) ? parsed.FacilityName!.Trim()
: $"مرکز درمانی (از {raw.SourceChannel})";
@@ -186,8 +207,7 @@ public class IngestionService
facilities.Add(facility); // so later listings in this run match it too
}
var kind = (d?.Kind ?? parsed.Kind.ToString()).ToLowerInvariant();
if (kind.Contains("job") || kind.Contains("استخدام"))
if (kindStr.Contains("job") || kindStr.Contains("استخدام"))
{
_db.JobOpenings.Add(new JobOpening
{