Logged-in users can like a listing (job/shift/talent); dislike is removed per request — only likes.
- Like model (polymorphic by TargetType+TargetId) + EF migration; unique per (user, listing).
- POST /like toggles the like (auth required) and returns {liked, count}.
- Detail pages: the old ♡ Save / ✕ Dismiss buttons are replaced by a single heart Like button that
shows the live count and toggles in place; clicking while logged out redirects to login.
- New «❤️ پسندیدهها» page (/Me/Liked) lists everything the user liked (open listings only), with a
nav entry shown only when authenticated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
We captured Divar's privacy-fuzzed coords on RawListing but discarded them for
the listings that need them: unnamed-facility shifts/jobs dropped them (to avoid
piling on the shared placeholder) and applicants had no coordinate field at all.
- Add Lat/Lng to Shift, JobOpening, TalentListing (migration ListingApproxCoords).
- Publish stores the source ad's approx coords on each aggregated listing.
- Detail pages render the map from the listing's own coords (fallback: facility),
and aggregated coords show as a shaded «محدودهٔ تقریبی» circle (not a precise
pin) via _NeshanMap data-approx, with a disclaimer. Applicants get a map card
(they had none) + the page now loads the Neshan key.
Only Divar provides coords; the map needs NeshanMapKey set in admin settings.
Existing rows get coords once reprocessed (RawListing already has them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a lazy-loaded contact modal. Any element with data-contact-type +
data-contact-id (the «📞 تماس» button on shift/job/talent/recommendation cards,
and the contact CTA on the three detail pages) opens a modal that fetches the
listing's numbers from a new GET /contact endpoint and renders them with click-
to-call links. Numbers are loaded only on click, so they never sit in list-page
HTML (privacy / anti-scrape). The endpoint logs the same Apply interest signal
for shift/job that the old inline-reveal POST did, and falls back to the
facility phone (or Divar source link for talent) when an ad has no own contacts.
Verified locally: GET /contact?type=shift&id=1 → {title, contacts:[{value:
'021-82032000', href:'tel:...'}]}, and the modal opens and renders on the shift
detail page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaced the plain "interest recorded" alert with a styled .contact-reveal
card that fades/slides in and lists each channel as its own row (icon +
label + value + action button). Shift/job show facility phone + Bale;
talent shows all its ContactMethods in the same table style.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- Parser now reads the currency: ریال amounts (incl. «میلیون ریال» and
numbers with no تومان unit but ≥200M) are converted to تومان (÷10), so
«۴۰۰٬۰۰۰٬۰۰۰ ریال» shows as ۴۰٬۰۰۰٬۰۰۰ تومان instead of 400M.
- Aggregated facility fallback name no longer embeds the source
(«مرکز درمانی (از مدجابز)» → «مرکز درمانی (نامشخص)»).
- Talent details only ever names Divar as a fallback source (when the
number couldn't be extracted); medjobs/telegram are never shown publicly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /Admin/Ingested: "حذف گروهی همهی منتشرشدهها" button removes, in one
transaction, every aggregated Shift/Job/Talent published from ingestion
plus the approved (Normalized) raw items that produced them. Confirms
first and reports counts. Raw rows deleted before the posts (they hold
the FKs); DB cascade clears applications/interest events.
- Talent details: when the contact number couldn't be extracted (e.g.
Divar's login-gated reveal), show a prominent "مشاهده شماره در دیوار/مدجابز ↗"
link to the original ad instead of the call button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>