feat: shift assignment workflow with claim, approve, reject, cancel, and bulk approve
Implements the complete ShiftAssignment lifecycle: - ShiftAssignmentStatus enum with allowed transitions - ShiftAssignmentService with claim/assign/approve/reject/cancel/bulkApprove - ShiftAssignmentController with event-scoped endpoints - ShiftAssignmentPolicy (organizer + volunteer self-cancel) - VolunteerAvailability model, controller, and sync endpoint - Refactored ShiftController to delegate to service layer - 31 workflow tests covering all paths and multi-tenancy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
120
dev-docs/API.md
120
dev-docs/API.md
@@ -136,6 +136,126 @@ Returns 422 with `errors`, `current_status`, `requested_status`, and `allowed_tr
|
||||
> in the URL must be the parent festival's ID (matching `section.event_id`), not the
|
||||
> sub-event's ID.
|
||||
|
||||
## Shift Assignments
|
||||
|
||||
- `GET /events/{event}/shift-assignments` — list assignments for event (paginated, 50/page)
|
||||
- `POST /events/{event}/shift-assignments/{shiftAssignment}/approve` — approve pending assignment
|
||||
- `POST /events/{event}/shift-assignments/{shiftAssignment}/reject` — reject pending assignment
|
||||
- `POST /events/{event}/shift-assignments/{shiftAssignment}/cancel` — cancel assignment
|
||||
- `POST /events/{event}/shift-assignments/bulk-approve` — bulk approve multiple assignments
|
||||
|
||||
### Query Parameters (index)
|
||||
|
||||
- `status` — filter by assignment status (`pending_approval`, `approved`, `rejected`, `cancelled`, `completed`)
|
||||
- `shift_id` — filter by shift
|
||||
- `person_id` — filter by person
|
||||
- `section_id` — filter by festival section
|
||||
|
||||
### Assign Body
|
||||
|
||||
`POST /events/{event}/sections/{section}/shifts/{shift}/assign`
|
||||
|
||||
```json
|
||||
{ "person_id": "01JXYZ..." }
|
||||
```
|
||||
|
||||
Organizer manually assigns a person. Assignment is pre-approved (status = `approved`).
|
||||
Validates: shift must be `open`, capacity not full (`slots_total`), no time slot conflict.
|
||||
|
||||
### Claim Body
|
||||
|
||||
`POST /events/{event}/sections/{section}/shifts/{shift}/claim`
|
||||
|
||||
```json
|
||||
{ "person_id": "01JXYZ..." }
|
||||
```
|
||||
|
||||
Volunteer claims a shift. Status depends on `festival_section.crew_auto_accepts`:
|
||||
- `true` → status = `approved`, `auto_approved = true`
|
||||
- `false` → status = `pending_approval`
|
||||
|
||||
Validates: shift must be `open`, person must be `approved`, claiming capacity not full (`slots_open_for_claiming`), no time slot conflict.
|
||||
|
||||
### Reject Body
|
||||
|
||||
```json
|
||||
{ "reason": "Onvoldoende ervaring voor deze rol." }
|
||||
```
|
||||
|
||||
### Bulk Approve Body
|
||||
|
||||
```json
|
||||
{ "assignment_ids": ["ulid1", "ulid2", ...] }
|
||||
```
|
||||
|
||||
Response includes per-assignment result: `approved` or `skipped` (with reason).
|
||||
|
||||
### ShiftAssignmentResource
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "01JXYZ...",
|
||||
"shift_id": "01JXYZ...",
|
||||
"person_id": "01JXYZ...",
|
||||
"time_slot_id": "01JXYZ...",
|
||||
"status": "pending_approval",
|
||||
"auto_approved": false,
|
||||
"assigned_by": null,
|
||||
"assigned_at": "2026-04-10T12:00:00+00:00",
|
||||
"approved_by": null,
|
||||
"approved_at": null,
|
||||
"rejection_reason": null,
|
||||
"hours_expected": null,
|
||||
"hours_completed": null,
|
||||
"checked_in_at": null,
|
||||
"checked_out_at": null,
|
||||
"is_cancellable": true,
|
||||
"is_approvable": true,
|
||||
"created_at": "2026-04-10T12:00:00+00:00",
|
||||
"person": { "..." },
|
||||
"shift": { "..." }
|
||||
}
|
||||
```
|
||||
|
||||
### Status Transitions
|
||||
|
||||
- `pending_approval` → `approved`, `rejected`, `cancelled`
|
||||
- `approved` → `cancelled`, `completed`
|
||||
- `rejected` → (terminal)
|
||||
- `cancelled` → (terminal)
|
||||
- `completed` → (terminal)
|
||||
|
||||
### Authorization
|
||||
|
||||
| Action | Who |
|
||||
|--------|-----|
|
||||
| assign | org_admin, event_manager |
|
||||
| claim | authenticated org member |
|
||||
| approve / reject / bulk-approve | org_admin, event_manager |
|
||||
| cancel | org_admin, event_manager, or the volunteer's own user |
|
||||
|
||||
## Volunteer Availabilities
|
||||
|
||||
- `GET /events/{event}/persons/{person}/availabilities` — list availabilities
|
||||
- `POST /events/{event}/persons/{person}/availabilities/sync` — sync (replace all)
|
||||
|
||||
### Sync Body
|
||||
|
||||
```json
|
||||
{
|
||||
"availabilities": [
|
||||
{ "time_slot_id": "01JXYZ...", "preference_level": 5 },
|
||||
{ "time_slot_id": "01JABC...", "preference_level": 2 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Replaces all existing availabilities for the person. `preference_level` is optional (default: 3, range: 1–5).
|
||||
|
||||
Validates:
|
||||
- All `time_slot_id`s must belong to the event (or parent festival)
|
||||
- Time slot `person_type` must match the person's crowd type `system_type`
|
||||
|
||||
## Persons
|
||||
|
||||
- `GET /events/{event}/persons`
|
||||
|
||||
Reference in New Issue
Block a user