# MedWorkforce — Duty Rosters + Credential Compliance (SIFA plan P7-T1)

The **medical HR layer** for the Shifa One platform. **Ground rule: extend/bind
to the existing Essentials HR module and MedOrganization credentials — do NOT
rebuild HR.** MedWorkforce never re-implements employees, attendance, leave or
payroll (Essentials owns those; payroll is a core `transactions` row with
`type='payroll'`). It adds duty rosters (shift scheduling for clinical staff), a
swap-approval workflow, and a credential-compliance dashboard over
MedOrganization's licence tracking.

Depends on **MedOrganization** (`module.json` → `requires: ["MedOrganization"]`).

---

## What it adds

| Concern | Where it lives |
|---|---|
| Reusable shift definitions (Morning/Evening/Night/On-call) | `medwf_shift_templates` + `ShiftTemplate` |
| Weekly duty-roster board (staff × day cells) | `medwf_roster_assignments` + `RosterService` |
| Employee-initiated shift swaps + approval | `medwf_swap_requests` + `RosterService::approveSwap()` |
| Periodic review / mandatory-training / competency tracking | `medwf_credential_reviews` + `CredentialReview` |
| Compliance dashboard (per-staff licence status + reviews) | `ComplianceService::buildDashboard()` |

All tables are `business_id`-scoped and prefixed `medwf_`. Migrations dated
`2026_07_07_00xx`. References into ERP / ClinicCore / MedOrganization tables are
plain indexed columns (no cross-DB FK constraints), matching the existing module
convention (see MedBilling).

---

## How it binds — never rebuilds

### Essentials HR (extend, fail-soft)

- **Staff** on the roster are the SAME ERP `users` (`App\User`) that Essentials
  attendance/leave track — `medwf_roster_assignments.user_id` points at core
  `users`, resolved via `User::forDropdown()`.
- `RosterService::mirrorToEssentials()` OPTIONALLY writes a matching row into
  **`essentials_shifts`** (matched by shift name) + **`essentials_user_shifts`**
  when Essentials is installed, so the Essentials attendance / auto-clockout
  engine recognises the roster shift. Guarded by `Schema::hasTable()` — a clean
  no-op (roster still works standalone) when Essentials is absent. Note
  `essentials_user_shifts` has **no** `business_id`; it is scoped through the
  shift, so the mirror looks the shift up by `(business_id, name)` first.
- Payroll is untouched — it remains a core `transactions` row (`type='payroll'`,
  `essentials_*` columns), exactly as the MedAccounting finding documented.

### MedOrganization credentials (bind, read-only, fail-soft)

`ComplianceService` **reads** MedOrganization's licence master — it never
duplicates or writes it:

- **`org_staff_licenses`** (Schema::hasTable guard): columns
  `user_id, license_type, license_number, specialization, issuing_authority,
  issue_date, expiry_date, status, verified_at`. Per-staff status is rolled up
  using the SAME window MedOrganization's `medorg:license-expiry-alerts` cron
  uses — `config('medorganization.license_alert_days', 30)` (falling back to
  `medworkforce.credential_expiring_days`):
  - `expired` — `expiry_date < today` **or** `status = 'expired'`
  - `expiring` — `today ≤ expiry_date ≤ today + window` (and not `rejected`)
  - `valid` — a `verified` licence in date
  - `missing` — MedOrganization present but no in-date licence row
- **`org_user_departments` → `clc_departments`** (both guarded): the staff
  member's (primary) department name for the dashboard column.
- The `medwf_credential_reviews` overlay tracks the obligations the licence
  master does *not* hold — mandatory training, competencies, ad-hoc reviews.

When MedOrganization is absent the dashboard still renders from the review
overlay alone (with a visible warning banner).

---

## UI (under `/med-workforce`)

Same middleware stack as MedBilling
(`web, auth, SetSessionData, language, timezone, AdminSidebarMenu,
module:MedWorkforce`). The **Workforce** sidebar section (order ~47) links:

| Screen | Route | Permission |
|---|---|---|
| Duty Roster (weekly board, click-to-assign, bulk assign, swap) | `medwf.roster.index` | `medwf.roster.view` / `.manage` |
| Shift Templates (CRUD) | `medwf.templates.index` | `medwf.roster.manage` |
| Swap Requests (approve / reject) | `medwf.swaps.index` | `medwf.swap.approve` |
| Credential Compliance (dashboard + add review) | `medwf.compliance.index` | `medwf.credentials.view` / `.manage` |

**Roster board:** rows = staff (filter by department / location), columns = the
seven days of the selected week, cells = shift chips coloured by template. Click
an empty cell to assign, click a chip to edit; the edit modal also raises a swap
request. "Bulk assign" paints a template across selected staff × a date range
(optionally skipping weekends). Conflict detection (same-day double-booking) and
an optional min-rest check surface as **non-blocking warnings** — the save always
succeeds.

---

## Permissions

`medwf.roster.manage`, `medwf.roster.view`, `medwf.swap.approve`,
`medwf.credentials.view`, `medwf.credentials.manage` — surfaced in the role
editor via `DataController::user_permissions()` and registered by both the
`register_medworkforce_permissions` migration and `MedwfPermissionSeeder`. Admins
bypass them via the core `Gate::before` for `Admin#<business_id>`.

---

## Install / seed

Standard module lifecycle (superadmin-gated `InstallController`, LOWERCASE
`medworkforce_version` System property). `MedWorkforceDatabaseSeeder` runs:

1. `MedwfPermissionSeeder` — idempotent Spatie permission rows.
2. `MedwfDemoSeeder` — **`Shifa One` only** (warns, not silent, elsewhere): 4
   shift templates, a week of roster assignments for the existing `*.shifa`
   staff across the hospital/clinic locations, 1 pending swap, and a couple of
   credential reviews. Idempotent via `updateOrCreate`/`firstOrCreate`.

The superadmin package flag is **`medworkforce_module`**.

> **Enablement note:** as with every module, `modules_statuses.json` must carry
> `"MedWorkforce": true` for nwidart to load it. That file lives outside this
> module directory and was intentionally **not** modified here (hard rule: zero
> changes outside `Modules/MedWorkforce`). Add the flag when enabling.

---

## Fail-soft guarantees

Every cross-module read is wrapped in `class_exists` / `Schema::hasTable`
guards and try/catch. MedWorkforce runs standalone (roster + templates + reviews)
with Essentials and/or MedOrganization absent; it only *enriches* when they are
present. It writes to **no** core or other-module table except the optional,
guarded Essentials shift mirror described above.
