# ShiftSchedule

## Description

ShiftSchedule is **the shift table itself (シフト表)** — one record for "the shift schedule covering 2026-08-01 to 2026-08-31". It is the header a planner actually works in: the period is exactly the from/to entered when generating shifts, so one generation run produces one ShiftSchedule with its Shifts hanging off it.

Its reason to exist is that "confirm next month's shift schedule" had no home. Committing a schedule to workers used to mean publishing one slot at a time, which made the commitment neither atomic nor visible as a single business fact. ShiftSchedule gives that act one record and one command (`confirmShiftSchedule`).

ShiftSchedule carries the **publish axis** only. The other axis — who staffs each slot — is `ShiftPlacement`, and it stays changeable after confirmation: a same-day substitution is a change of plan, not a republication of the table. Separating the two is what removed the old confusion where "confirming a shift" ambiguously meant "this slot exists" or "this person is on it".

Periods are deliberately **allowed to overlap**. Membership is the explicit `Shift.shiftScheduleId` FK, so two shift schedules covering the same dates — a second site, a supplementary 応援 shift schedule, a partial revision — are unambiguous rather than a conflict to reject.

## Domain Model Definitions

### Model type

Stateful

#### State Transitions

```mermaid
stateDiagram-v2
    [*] --> DRAFT : createShiftSchedule
    DRAFT --> CONFIRMED : confirmShiftSchedule
    CONFIRMED --> [*]
```

| Operation | From | To | Command |
|-----------|------|----|---------|
| confirm | DRAFT | CONFIRMED | [confirmShiftSchedule](../command/ConfirmShiftSchedule.md) |

### Command Definitions

- createShiftSchedule — open a shift schedule period in `DRAFT`
- confirmShiftSchedule — commit the whole period to workers (`DRAFT` → `CONFIRMED`)

### Query Definitions

- getShiftSchedule — single lookup by id, any status

### Models

- ShiftSchedule

### Invariants

- `status` is one of `DRAFT` / `CONFIRMED`; `CONFIRMED` is terminal — a committed table is corrected by cancelling or adding individual slots, not by un-confirming it underneath the people already reading it
- `endDate` is on or after `startDate`
- Every `Shift` belongs to exactly one ShiftSchedule (`Shift.shiftScheduleId`, required), and its `date` falls within `[startDate, endDate]`
- While `DRAFT`, the period's shifts are freely editable; once `CONFIRMED`, `updateShift` is rejected and a change to a slot means cancelling it (`cancelShift`) or adding a new one
- A shift added to a `CONFIRMED` shift schedule is committed immediately — there is no per-slot publish step to hold it back
- Staffing is **not** frozen by confirmation: `ShiftPlacement` may be created, swapped, or released at any time, including on the day itself
- ShiftSchedule periods may overlap; overlap is not an error and is not deduplicated
- `confirmedAt` is set when the shift schedule is confirmed and null while `DRAFT`

### Relationships

- **Referenced by Shift**: `Shift.shiftScheduleId` is the period a slot belongs to; the shift's committed-ness is this shift schedule's `status`
- **Indirectly governs ShiftPlacement**: placements reach a ShiftSchedule through their Shift, but are not frozen by its confirmation
