Initial commit — Hamkadr (همکادر) healthcare-staffing marketplace

ASP.NET Core 10 Razor Pages + PostgreSQL/EF Core. RTL Persian, Jalali dates, self-hosted Vazirmatn, teal/coral brand.

Features:
- Shift listings: browse/filter (city, district, role, type, pay), weekly Jalali calendar, detail + interest handoff, near-me distance sort
- Hiring (استخدام) listings with employment type + salary range
- Pattern-engine recommendations + anonymous interest tracking (visitor cookie)
- Heuristic Persian listing-parser + admin queue (raw channel post → shift/job)
- Phone-OTP cookie auth + visitor-history linking + profile

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 01:43:55 +03:30
commit 2fb86a435e
150 changed files with 90993 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
# JobsMedical — Medical Shift Marketplace (Iran)
> A niche platform connecting **GP doctors** with **hospitals & clinics** that have open shifts.
> Replaces the chaotic hunt across Bale / Telegram channels / Divar with one structured,
> filterable view organized by **hospital, location, and weekly shift calendar**.
Date: 2026-06-02 · Market: Iran (Persian/Farsi, RTL) · Revenue: TBD
---
## 1. The problem & the bet
**Today:** A GP doctor wanting locum/extra shifts scrolls dozens of Telegram/Bale channels and
Divar posts. Listings are unstructured text, no filters, no calendar, no trust signals, expire
silently. Hospitals broadcast to the same noisy channels and get random DMs.
**Our bet:** The value isn't *more* listings — it's **structure + filtering + trust**.
"Show me open GP shifts, in Tehran, this week, paid X" should be one screen, not 40 channels.
**Why a marketplace is hard (and our answer):**
- *Cold-start problem* — empty marketplace helps no one. We solve supply first via the **mixed
strategy**: (1) admin posts manually + (2) auto-aggregate from channels → so the site looks
full from day one, then (3) convert hospitals to self-serve posting over time.
---
## 2. The two sides
| | Doctors (supply of labor) | Hospitals / Clinics (demand) |
|---|---|---|
| Who | GPs (پزشک عمومی) seeking shifts | Hospitals, clinics, درمانگاه‌ها |
| Want | Find shifts fast: by city, date, pay | Fill open shifts with vetted doctors |
| Onboard via | Phone OTP signup, build profile | Phase 2 self-serve; phase 1 we post for them |
| Trust signal | نظام پزشکی (medical license) number | Verified facility badge |
---
## 3. MVP scope (Phase 1 — "the structured board")
Goal: a doctor can **find and express interest in a relevant shift in under 60 seconds**.
**Must have:**
1. **Shift browse + filter** — by city, hospital, date range, specialty, shift type (day/night), pay.
2. **Weekly calendar view** per hospital — the signature feature. Jalali (Shamsi) week grid showing
open shifts per day.
3. **Shift detail page** — hospital, location (map pin), date, hours, pay, requirements, how to apply.
4. **Doctor signup/login** — phone OTP (standard in Iran), basic profile (name, license, city, specialty).
5. **Express interest / apply** — doctor taps "interested" → handoff to facility contact (call/Bale/
in-app message). Track applications.
6. **Admin panel** — we add/edit shifts manually, and review/normalize aggregated listings before
they go live.
7. **Aggregation ingest (basic)** — pull text from selected Telegram/Bale channels into a "raw
listings" queue for admin to normalize. (Start semi-manual; full automation later.)
**Explicitly NOT in MVP** (Phase 2+):
- Hospital self-serve posting & dashboards
- Payments / monetization
- Ratings & reviews
- Automated matching/recommendations, push alerts
- Full unattended scraping pipeline
---
## 4. Data model (core entities)
```
User id, phone, full_name, role(doctor|facility_admin|admin), created_at
DoctorProfile user_id, license_no(نظام پزشکی), specialty, city, years_exp, bio, verified
Facility id, name, type(hospital|clinic|درمانگاه), city, address, lat,lng,
phone, bale_id, verified, owner_user_id(nullable, phase 2)
Shift id, facility_id, date(jalali stored as ISO), start_time, end_time,
specialty_required, shift_type(day|night|on_call), pay_amount, pay_type,
description, status(open|filled|expired|cancelled),
source(direct|admin|aggregated), source_url, created_at
Application id, shift_id, doctor_id, status(interested|accepted|rejected|withdrawn),
message, created_at
RawListing id, source_channel, raw_text, parsed_json, status(new|normalized|discarded),
linked_shift_id, fetched_at ← aggregation staging area
City id, name, province ← canonical city list for filters
```
Key decisions:
- **Dates stored as ISO (UTC) in DB, displayed as Jalali** in the UI. Never store Jalali strings.
- A `Shift` can originate from a `RawListing` (aggregation) or be created directly (admin/facility).
- Phone is the unique identity; OTP login, no passwords.
---
## 5. Tech architecture
Constraints that drive choices: **Iran hosting** (many global SaaS block Iranian IPs/payments →
prefer self-hostable, open-source), **SEO matters** (doctors will find shifts via Google → need
server-rendered pages), **RTL + Jalali**, **phone OTP** via Iranian SMS providers.
**Recommended stack:**
- **Frontend + backend:** Next.js (App Router) + TypeScript — SSR for SEO, one codebase, great RTL support.
- **UI:** Tailwind CSS (RTL mode) + a Persian font (Vazirmatn). Jalali via `dayjs-jalali` or `jalaali-js`.
- **DB:** PostgreSQL + Prisma ORM.
- **Auth:** Phone OTP. SMS via **Kavenegar** or **SMS.ir** (Iranian providers). Sessions via JWT/cookies.
- **Maps:** Neshan or Balad map tiles (Iranian, work without VPN) instead of Google Maps.
- **Aggregation:** A small worker (Node) using Telegram Bot API / Telethon-style reader for Bale,
writing into `RawListing`. Runs as a separate scheduled job.
- **Hosting / CI:** Self-hosted on an Iranian VPS, deployed via your **Gitea + Nexus** pipeline
(soroush ci/cd method). Docker Compose: web + postgres + worker.
> If you'd rather build in **.NET** (you have the Nexus/.NET pipeline set up), the equivalent is
> ASP.NET Core + Blazor/Razor + EF Core + Postgres. Same architecture, different language. ← confirm.
**Why not Google Maps / Vercel / Firebase / Stripe:** all routinely blocked or unusable from Iran.
Everything above is self-hostable or has an Iranian equivalent.
---
## 6. Key screens
1. **Home** — hero + search (city, specialty, date) + "shifts this week" + featured hospitals. SEO landing.
2. **Shift listing** — filter sidebar + result cards + map toggle.
3. **Weekly calendar** (per hospital or per city) — Shamsi week grid, open shifts as chips.
4. **Shift detail** — all info + map + "I'm interested" CTA.
5. **Doctor onboarding** — phone OTP → profile form.
6. **Doctor dashboard** — my applications, saved shifts.
7. **Admin** — shift CRUD, raw-listing normalization queue, facility management.
---
## 7. Phased roadmap
- **Phase 0 — Foundation:** repo, Docker, Postgres+Prisma schema, RTL+Jalali shell, OTP auth.
- **Phase 1 — MVP board:** browse/filter, calendar, shift detail, doctor signup, apply, admin CRUD,
basic raw-listing ingest. → *launch to a few hospitals + a doctor Telegram group.*
- **Phase 2 — Self-serve + trust:** hospital posting dashboards, facility/doctor verification,
in-app messaging, saved searches + alerts (via Bale bot).
- **Phase 3 — Monetization:** pick model (likely **hospitals pay to post** or **commission per
filled shift** — decide after we see what converts), Iranian payment gateway (Zarinpal/IDPay).
- **Phase 4 — Scale:** full automated aggregation, recommendations, expand beyond GPs to specialists.
---
## 8. Open questions to revisit
- Monetization model (deferred — validate demand first).
- Which specific Telegram/Bale channels to aggregate first?
- Start single-city (Tehran?) to concentrate liquidity, or national from day one?
- Build language: **TypeScript/Next.js** (recommended) vs **.NET** (your existing pipeline)?
```