Applicants: 1→N contact methods with types (phone/email/Instagram/Telegram/Bale/site)
CI/CD / CI · dotnet build (push) Successful in 1m32s
CI/CD / Deploy · hamkadr (push) Successful in 1m31s

- ContactMethod entity (Type + Value + SortOrder) 1→N on TalentListing (+ migration).
- Parser extracts ALL contacts: multiple phones + landlines, email, and
  socials (Instagram/Telegram/Bale/WhatsApp/website) via URLs and Persian
  keyword cues; primary Phone kept for cards.
- ContactInfo helper: per-type label/icon/clickable href (tel:/mailto:/t.me/…).
- Ingestion attaches contacts to each (fanned-out) talent listing; manual
  Review re-parses to attach them + the admin-typed phone.
- Talent details renders the full contact list as buttons; falls back to the
  single phone, then the Divar source link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-08 11:10:19 +03:30
parent 48760c4e83
commit e4dc5180ad
13 changed files with 1882 additions and 19 deletions
@@ -134,6 +134,19 @@ public class ReviewModel : PageModel
Error = "شهری برای انتشار آگهی «آماده به کار» موجود نیست.";
return RedirectToPage(new { id });
}
// Re-parse the raw text to recover all contact channels (phones/email/socials).
var roleNames = await _db.Roles.Select(r => r.Name).ToListAsync();
var parsedContacts = _parser
.Parse(Raw.RawText, roleNames, await CityNamesAsync(), await DistrictNamesAsync())
.Contacts.Select((c, i) => new ContactMethod { Type = c.Type, Value = c.Value, SortOrder = i })
.ToList();
// Include the admin-typed phone if it isn't already captured.
if (!string.IsNullOrWhiteSpace(Phone))
{
var digits = new string(Phone.Where(char.IsDigit).ToArray());
if (!parsedContacts.Any(c => new string(c.Value.Where(char.IsDigit).ToArray()) == digits))
parsedContacts.Insert(0, new ContactMethod { Type = ContactType.Mobile, Value = Phone.Trim(), SortOrder = -1 });
}
var talent = new TalentListing
{
RoleId = RoleId,
@@ -153,6 +166,7 @@ public class ReviewModel : PageModel
Status = ShiftStatus.Open,
Source = ShiftSource.Aggregated,
SourceUrl = Raw.SourceUrl,
Contacts = parsedContacts,
};
_db.TalentListings.Add(talent);
await _db.SaveChangesAsync();
@@ -66,11 +66,31 @@
<aside>
<div class="card card-pad">
<h3 style="margin-top:0;">تماس</h3>
@if (telHref is not null)
<h3 style="margin-top:0;">راه‌های ارتباطی</h3>
@{ var contacts = (t.Contacts ?? new List<JobsMedical.Web.Models.ContactMethod>()).OrderBy(c => c.SortOrder).ToList(); }
@if (contacts.Count > 0)
{
foreach (var c in contacts)
{
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";
if (href is not null)
{
<a href="@href" class="btn @cls btn-block" dir="ltr" style="margin-bottom:6px; justify-content:space-between;" target="_blank" rel="nofollow noopener">
<span>@c.Value</span><span>@icon @label</span>
</a>
}
else
{
<div class="muted" style="margin-bottom:6px;">@icon @label: <span dir="ltr">@c.Value</span></div>
}
}
}
else if (telHref is not null)
{
<a href="@telHref" class="btn btn-accent btn-block btn-lg" dir="ltr">📞 @t.Phone</a>
<p class="muted" style="font-size:12px; margin:10px 0 0;">با این فرد مستقیم تماس بگیرید.</p>
}
else if (isDivar)
{
@@ -19,6 +19,7 @@ public class DetailsModel : PageModel
.Include(t => t.City)
.Include(t => t.District)
.Include(t => t.Role)
.Include(t => t.Contacts)
.FirstOrDefaultAsync(t => t.Id == id);
if (Item is null) return NotFound();
return Page();