# Flow — Build & Publish Roster → Auto-Assign (Store Manager)

> Persona: **Rohit Shah** (SM), Game Zone Ahmedabad-1. Source: `WORKFLOW_GUIDE` Stages C → D + `PROJECT_PLAN §6`, `FOUNDATION_SPEC §3`. Screens: `SM-ROSTER` / `SM-ROSTER-UPLOAD` → `SM-ROSTER-PUBLISH`. Modals: `modals/upload-errors.html`, `modals/publish-confirm.html`.

---

## Preconditions
- Rohit is logged in (`/me` resolved role = SM, gameZone = Ahmedabad-1). The OH has created the Game Zone, its rides, and bound Rohit as the one Store Manager. Staff are onboarded with **certified rides** and **reports-to** set. Shift templates (Morning 10:00–16:00, Evening 16:00–22:00) exist.

## Steps

1. **Open the roster builder.** Rohit opens `SM-ROSTER` (week view). Rows = rides, columns = days. `GET /roster?weekOf=…&view=week`.

2. **Place staff — drag OR bulk upload.**
   - **Drag path:** Rohit drags **Ramesh** from the staff palette onto **Trampoline · Morning** for the day. `POST /roster/entries { userId, rideId, shiftId, date }` → a DRAFT `RosterEntry`. He toggles the **opener** flag on one entry per shift (the 114-pt opener, `isOpener`).
   - **Bulk path (`SM-ROSTER-UPLOAD`):** Rohit downloads the template (`email, ride_code, shift_name, date, is_opener`), fills it, and uploads. **Step 2 validates** (`POST /roster/upload?dryRun=true`): the **upload-errors modal** lists bad rows ("row 9: email not in your Game Zone", "row 4: ride_code TRAMP not found", "row 12: duplicate person/ride/shift/day"). Commit is blocked until errors resolve (or "valid rows only"). A clean commit (`POST /roster/upload`) creates the DRAFT entries.

3. **Resolve conflicts + gaps.** A person double-booked on two rides in one shift shows a **conflict warning** on the cell. An uncovered required ride+shift shows a **gap-alert** badge (`danger`). Rohit marks any **leave** (`SM-ROSTER-LEAVE`) — an approved leave on a rostered day re-fires the gap recompute.

4. **Preview the period.** On `SM-ROSTER-PUBLISH`, `GET /roster/publish/preview?weekOf=…` returns # entries, rides covered, remaining gaps, and whether it was already published. Rohit reviews the gap warnings.

5. **Publish.** Rohit clicks **Publish period** → the **publish-confirm modal** (`publish-confirm.html`) states plainly: *"Publishing fires checklist auto-assign for every rostered person — they receive their checklists for the shift."* He confirms → `POST /roster/publish` (`If-Match`).

6. **Server: publish + auto-assign (one transaction, `FOUNDATION §3`).** Entries → `PUBLISHED`. For each entry, the engine matches `role + certified ride + shift → templates` (daily always; weekly/monthly/etc. only on their due date) and, for the one `isOpener` entry, adds the **Park Opening 114-pt**. It upserts `ChecklistAssignment` + `ChecklistInstance` (state `NOT_STARTED`, tagged `gameZoneId` + `rideId` + due window). Re-publishing is **idempotent** (`@@unique(rosterEntryId, templateId)`) — no duplicate instances. A `ROSTER_PUBLISHED` audit row + notifications fire.

## Rules enforced in this flow
- **Publishing is the only trigger for auto-assign** — a DRAFT roster assigns nothing.
- **Certified-ride gate:** a person not certified on the ride gets no ride-safety template for it (`§9 #2`).
- **One opener per shift** gets the 114-pt, not every monitor.
- **Scope:** every entry, upload row, and instance is tagged to **Ahmedabad-1**; an out-of-GZ email/ride is a row error, never a silent cross-zone insert.
- **Concurrency:** publish sends `If-Match`; a stale roster version → 409 ("reload — the roster changed").

## Result
- The roster is **Published**; every rostered person sees exactly their shift's checklists on their `*-CHK-TODAY` — automatically. Rohit can now watch them climb the cascade in `SM-CHK-OVERVIEW` and approve at his level (see `approve-cascade-and-sendback.md`).
