--- type: SQLite Table title: shift_signups description: An employee's claim on a shift slot — approved or cancelled. resource: backend/src/db/index.ts tags: [data-model, sqlite, shift-planning] timestamp: 2026-08-16T00:00:00Z --- # shift_signups # Schema | Column | Type | Description | |---|---|---| | `id` | INTEGER PK | autoincrement | | `slot_id` | INTEGER | references [shift_slots](./shift-slots.md) | | `employee_id` | INTEGER | references [employees](./employees.md) | | `status` | TEXT | `approved` \| `cancelled` | | `created_at` | TEXT | ISO 8601 UTC | | `cancelled_at` / `cancelled_by` | TEXT \| NULL | when, and by whom (admin email or the employee's own) | Cancelling never deletes the row (`status = 'cancelled'`), so who was ever signed up for a slot stays visible in history. # Signup rules Enforced in `signupInternal` (`backend/src/services/shiftPlanning.ts`), in this order, identically whether the employee signs themselves up or the admin assigns them: 1. Slot exists and isn't `closed`. 2. Not already approved for this exact slot. 3. **No time overlap** with any of the employee's other approved slots, anywhere — not just the same day. Computed from real start/end `Date`s (`slotStart`/`slotEnd`), so a slot whose `end_time <= start_time` correctly counts as ending the next day. 4. Slot isn't already at capacity. All of this runs synchronously in a single request — better-sqlite3 is synchronous, so there's no `LockService`-style mutex needed the way the source Apps Script version required (`signupEmployeeToSlot_` in `gscript/ShiftPlanningService.js`). # Related - [shift_slots](./shift-slots.md) - [Shift planning API](/docs/api/shifts-routes.md)