Applicants: auto-tags + deep search w/ highlight; never delete (archive instead)
CI/CD / CI · dotnet build (push) Successful in 2m1s
CI/CD / Deploy · hamkadr (push) Successful in 2m36s

- Tags: parser extracts cert/skill keywords (mmt, ICU/CCU, دیالیز, اتاق عمل,
  اورژانس, مسئول فنی, پروانه‌دار…) + role + city into TalentListing.Tags
  (+ migration); shown as chips on cards.
- Deep search on /Talent: «جستجوی عمیق» box does Postgres ILIKE across
  tags, description, person, area, role, city (every term must match);
  matches are highlighted with <mark> via SearchHighlight.
- Never delete: ShiftStatus.Archived + the admin «بایگانی گروهی» action now
  ARCHIVES aggregated posts (hidden from site, kept in DB) and leaves the
  raw crawl rows intact — a permanent archive for future analytics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-08 11:25:32 +03:30
parent e4dc5180ad
commit 6b657c7795
15 changed files with 1752 additions and 22 deletions
@@ -29,6 +29,7 @@ public class ParsedListing
public bool IsLicensed { get; set; } // پروانه‌دار
public string? AreaNote { get; set; } // «فقط منطقه ۱»
public List<ParsedContact> Contacts { get; set; } = new(); // phones, email, socials…
public List<string> Tags { get; set; } = new(); // cert/skill keywords for search
public List<string> Notes { get; set; } = new(); // what was/wasn't detected (shown to admin)
}
@@ -170,6 +171,12 @@ public class HeuristicListingParser : IListingParser
if (p.FacilityName is not null) p.Notes.Add($"مرکز: {p.FacilityName}");
}
// --- Tags (certs/skills for deep search): mmt, icu, پروانه‌دار, اتاق عمل … ---
p.Tags = ExtractTags(text);
if (p.RoleNames.Count > 0) p.Tags.AddRange(p.RoleNames);
if (p.IsLicensed && !p.Tags.Contains("پروانه‌دار")) p.Tags.Add("پروانه‌دار");
p.Tags = p.Tags.Distinct().ToList();
// --- Contacts (phones, email, socials — one ad may have several) ---
p.Contacts = ExtractContacts(raw ?? text);
p.Phone = p.Contacts.FirstOrDefault(c => c.Type is ContactType.Mobile or ContactType.Phone)?.Value;
@@ -355,6 +362,40 @@ public class HeuristicListingParser : IListingParser
return list.Take(8).ToList();
}
// Canonical tag → trigger words found in the post.
private static readonly (string Tag, string[] Needles)[] TagDict =
{
("mmt", new[] { "mmt", "ام ام تی", "ام‌ام‌تی" }),
("ICU", new[] { "icu", "آی سی یو", "آی‌سی‌یو" }),
("CCU", new[] { "ccu", "سی سی یو", "سی‌سی‌یو" }),
("NICU", new[] { "nicu", "ان آی سی یو", "نوزادان" }),
("BLS", new[] { "bls" }),
("ACLS", new[] { "acls" }),
("دیالیز", new[] { "دیالیز" }),
("اتاق عمل", new[] { "اتاق عمل", "اسکراب" }),
("بیهوشی", new[] { "بیهوشی" }),
("تریاژ", new[] { "تریاژ" }),
("تزریقات", new[] { "تزریقات", "تزریق" }),
("پانسمان", new[] { "پانسمان", "زخم" }),
("سونوگرافی", new[] { "سونوگرافی" }),
("رادیولوژی", new[] { "رادیولوژی" }),
("اورژانس", new[] { "اورژانس", "فوریت" }),
("مسئول فنی", new[] { "مسئول فنی" }),
("طرح", new[] { "طرح" }),
("سالمند", new[] { "سالمند" }),
("کودک", new[] { "کودک", "اطفال" }),
("همراه بیمار", new[] { "همراه بیمار" }),
("پروانه‌دار", new[] { "پروانه" }),
};
private static List<string> ExtractTags(string text)
{
var tags = new List<string>();
foreach (var (tag, needles) in TagDict)
if (ContainsAny(text, needles)) tags.Add(tag);
return tags;
}
private static string UrlHandle(string url)
{
var u = url.Split('?')[0].TrimEnd('/');