- .gitea/workflows/ci-cd.yml: dotnet build via mirror.soroushasadi.com; self-hosted deploy with pg_dump backup, rollback tag, scoped recreate, /healthz wait, prune - Dockerfile (sdk/aspnet 10 via Nexus) + nuget.docker.config + .dockerignore - docker-compose.prod.yml: app on 127.0.0.1:APP_PORT, Postgres internal-only + named volume - deploy/nginx-hamkadr.ir.conf + DEPLOY.md (ports: 22/80/443 only; DB never exposed) - Prod seeds reference data only (no demo listings); ForwardedHeaders for nginx TLS; /healthz endpoint Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
همکادر — Hamkadr
سامانهی واسط میان کادر درمان (پزشک، پرستار، ماما، تکنسین) و بیمارستانها و کلینیکها برای یافتن شیفت و موقعیت استخدامی. بهجای گشتن در کانالهای تلگرام/بله و دیوار، همهی فرصتها یکجا — بر اساس مرکز درمانی، محل و تقویم هفتگی شمسی، با پیشنهاد هوشمند متناسب با علاقهمندی و فعالیت هر فرد.
A two-sided healthcare-staffing marketplace (shifts and permanent hiring) connecting all clinical staff with hospitals/clinics. See PLAN.md and FEATURES.md.
Stack
- ASP.NET Core (Razor Pages), .NET 10 — server-rendered for SEO
- PostgreSQL + EF Core (Npgsql)
- RTL Persian UI, Jalali (Shamsi) dates via
System.Globalization.PersianCalendar - Pattern-engine recommendations + anonymous interest tracking (cookie-based visitor id)
- Self-hosted Vazirmatn font (
wwwroot/fonts, no CDN), teal + coral brand palette - Location filters: city → district/neighborhood, plus "near me" (browser geolocation → Haversine distance sort)
Run locally
# 1. Start PostgreSQL (host port 5433)
docker compose up -d
# 2. Run the web app (applies migrations + seeds Tehran sample data on startup)
dotnet run --project src/JobsMedical.Web
Then open the URL printed in the console (e.g. http://localhost:5020).
Pages
| Route | Page |
|---|---|
/ |
خانه — hero, role/city search, پیشنهادهای ویژه شما, latest shifts |
/Shifts |
فهرست با فیلتر (شهر، محله/منطقه، نقش، مرکز، نوع شیفت، حقوق) + «نزدیک من» |
/Shifts/Details/{id} |
جزئیات + اعلام تمایل / ذخیره / رد (همه رویدادها ثبت میشوند) |
/Jobs |
موقعیتهای استخدامی با فیلتر (شهر، محله، نقش، نوع همکاری) + «نزدیک من» |
/Jobs/Details/{id} |
جزئیات موقعیت استخدامی + اعلام تمایل / ذخیره / رد |
/Calendar |
تقویم هفتگی شمسی شیفتها |
/Facilities |
فهرست مراکز درمانی |
/Preferences |
تنظیم علاقهمندیها (نقش، شهر، نوع شیفت، حقوق) |
/Account/Login |
ورود/ثبتنام با کد یکبارمصرف موبایل (OTP) |
/Account/Profile |
پروفایل: شیفتها/موقعیتهای ذخیرهشده و اعلام تمایلها |
/Admin |
پنل مدیریت (نقش Admin): صف آگهیهای خام |
/Admin/Review/{id} |
بررسی خودکار (پارسر) و انتشار آگهی بهصورت شیفت یا استخدام |
/Admin/Facilities |
تأیید/لغو تأیید مراکز درمانی (نشان «تأیید شده») |
/Employer |
پنل کارفرما: مراکز من + شمار شیفت/استخدام/متقاضی |
/Employer/RegisterFacility |
ثبت مرکز درمانی (خودسرویس → نقش FacilityAdmin) |
/Employer/PostShift، /PostJob |
انتشار شیفت یا موقعیت استخدامی |
/Employer/Listings |
مدیریت آگهیها (بستن/بازگشایی/حذف) + لیست متقاضیان با تماس |
How recommendations work (Stage 1 — pattern engine)
RecommendationService scores open shifts against (a) explicit UserPreferences and (b) recent
InterestEvents (view/save/apply/dismiss), and returns the top matches each with a Persian
reason ("متناسب با نقش مورد علاقه شما"، "چون به فرصتهای پرستار علاقه نشان دادی"). Fully
explainable, no AI infra, works from the first visit. The behavioral log is the fuel for the
later collaborative + ML/embedding stages (see FEATURES.md Part A).
Project status
Done: multi-role domain model, Postgres + migrations, RTL shell, browse/filter (incl. role, district, near-me), weekly Jalali calendar, shift detail + interest handoff, interest tracking + pattern-engine recommendation feed + preferences, self-hosted Vazirmatn + teal/coral palette, hiring (استخدام) listings, admin queue + heuristic listing-parser (raw channel post → structured shift/job), phone-OTP auth + visitor-history linking + profile, employer side (self-serve facility registration → FacilityAdmin role, post/manage shifts & jobs, applicants list with contact), Tehran seed data.
Both sides of the marketplace now work end-to-end: an employer self-registers a facility, posts a shift, it appears publicly, a logged-in user applies, and the employer sees that applicant's phone in their dashboard.
Compensation models
Shifts support fixed (مقطوع), hourly (ساعتی), profit-share (درصدی / سهم درآمد), توافقی, or a
choice between a fixed amount and a share % ("… یا … به انتخاب شما"). JalaliDate.PayLabel
centralizes the display; Shift.SharePercent holds the percentage; the listing-parser detects
"۵۰٪ / درصد / سهم" from raw posts; and /Shifts has a "سهم درآمد" filter.
Listing parser (Stage 1)
IListingParser / HeuristicListingParser extracts kind (shift vs hire), role, shift type,
employment type, pay, city/district, and phone from a raw Persian post via keyword + regex
heuristics — no AI dependency (LLM APIs are blocked from Iran). Admin reviews the prefilled
form and publishes. Swap in an LlmListingParser later behind the same interface.
Auth
Phone OTP via OtpService (in-memory codes; dev shows the code on screen — wire Kavenegar/SMS.ir
for prod). Cookie auth; the configured Auth:AdminPhone gets the Admin role. On login the
anonymous hk_vid visitor (and its interest history) is linked to the user account.
Next:
- Unified recommendations across shifts and jobs (currently shift-focused)
- Self-serve facility posting + dashboards; verification badges
- Real SMS gateway + Neshan/Balad interactive maps
- LLM-backed listing parser; automated channel aggregation worker