Files
soroush.asadi 931b7b6ffb Add scrape/ingestion engine + validation, and 24h shift hour-range visualization
Scrape engine (Services/Scraping/): pluggable IListingSource (working sample + Telegram/Divar credential-ready stubs) → IngestionService (content-hash dedupe → parse → validate → review queue) → ListingValidator (completeness score + spam screen) → IngestionWorker (config-gated hosted service). RawListing gains ContentHash/Confidence/ValidationNotes; RawListingStatus.Flagged. Admin /Admin gets run-now, source list, confidence + flagged queue.

Hour-range viz: _HourBar 24h timeline bar (colored by type, overnight wrap) on shift cards, recommendation cards, and detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:18:19 +03:30

104 lines
6.8 KiB
Markdown

# همکادر — Hamkadr
سامانه‌ی واسط میان **کادر درمان** (پزشک، پرستار، ماما، تکنسین) و **بیمارستان‌ها و کلینیک‌ها**
برای یافتن **شیفت و موقعیت استخدامی**. به‌جای گشتن در کانال‌های تلگرام/بله و دیوار،
همه‌ی فرصت‌ها یک‌جا — بر اساس مرکز درمانی، محل و تقویم هفتگی شمسی، با **پیشنهاد هوشمند**
متناسب با علاقه‌مندی و فعالیت هر فرد.
A two-sided healthcare-staffing marketplace (shifts **and** permanent hiring) connecting all
clinical staff with hospitals/clinics. See [PLAN.md](PLAN.md) and [FEATURES.md](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
```bash
# 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
`InterestEvent`s (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](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.
### Scrape / ingestion engine
Pluggable `IListingSource`s (working `SampleListingSource`; credential-ready `Telegram`/`Divar`
stubs) → `IngestionService` **dedupes by content hash → parses → validates → enqueues** as
`RawListing` (status New / Flagged / Discarded-spam) with a confidence score. `ListingValidator`
scores completeness (role, location, pay, phone, length) and screens spam. `IngestionWorker`
(hosted, config-gated `Ingestion:Enabled`) runs it on a timer; admins can also run it on demand
from `/Admin`. `IListingParser` / `HeuristicListingParser` does the field extraction (kind, role,
shift type, employment, pay, **profit-share %**, city/district, phone) — **no AI dependency** (LLM
APIs are blocked from Iran). Admin reviews the prefilled form and publishes. Swap an
`LlmListingParser`/real sources behind the same interfaces later.
### Hour-range visualization
Every shift card, recommendation card, and detail page shows a **24-hour timeline bar**
(`_HourBar`) with the shift's hours filled and colored by type; overnight shifts wrap past
midnight into two segments.
### 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