Documents the shift planning and closure/payroll modules (new tables, routes, stores), the dark redesign, and the docker-compose-broken-host workaround. Adds CLAUDE.md pointing agents at docs/. Translates README to English and keeps host-specific infrastructure details out of the repo.
5.6 KiB
type, title, description, resource, tags, timestamp
| type | title | description | resource | tags | timestamp | ||
|---|---|---|---|---|---|---|---|
| API Endpoint | Admin routes | Employee CRUD + pay rates, org-wide and per-employee stats, shift planning admin, payroll, and CSV export — all requireAdmin. | backend/src/routes/admin.ts |
|
2026-08-16T00:00:00Z |
Admin routes
Mounted at /api/admin. Every route requires requireAuth + requireAdmin
(role resolved from ADMIN_EMAILS, see Auth flow).
Examples
GET /api/admin/employees
200 -> { "employees": (Employee & { hourly_rate: number })[] } // includes inactive (soft-deleted) rows
hourly_rate is the employee's current rate (getCurrentRate, see
pay_rates) — joined in at read time, not a
column on employees itself.
POST /api/admin/employees/:id/rate
Body: { "hourly_rate": number, "valid_from"?: "YYYY-MM-DD" }
201 -> { "hourly_rate": number }
Appends a new pay_rates row rather than overwriting — see that doc for the retroactive-first-rate behavior.
POST /api/admin/employees
Body: { "email": string, "name"?: string }
201 -> { "employee": Employee }
Upserts by email — see employees for the reactivate-on-re-add behavior.
DELETE /api/admin/employees/:id
204 on success, 404 if not found.
Soft delete only (active = 0) — never removes the row or its attendance
history.
GET /api/admin/stats?month=YYYY-MM
200 -> {
"range": { "from": string, "to": string },
"totals": { "totalWorkedMs": number, "totalBreakMs": number, "shiftCount": number },
"employees": [{ "employee": Employee, "totalWorkedMs": number, "totalBreakMs": number, "shiftCount": number }]
}
Same calendar-month default as the employee's own stats endpoint.
GET /api/admin/stats/:id?month=YYYY-MM
200 -> { "employee": Employee, "range": {...}, "sessions": Session[], "totalWorkedMs": number, "totalBreakMs": number, "shiftCount": number }
404 -> employee not found
GET /api/admin/stats/:id/export?month=YYYY-MM
200, Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="<email>_<YYYY-MM>.csv"
Columns: Datum, Příchod, Odchod, Pauza (h:mm), Odpracováno (h:mm), one row
per shift, sorted chronologically (backend/src/util/csv.ts). Prefixed with
a UTF-8 BOM so Excel renders the Czech diacritics correctly. Downloaded from
the frontend as a plain <a href=... download> — the session cookie rides
along automatically since it's a same-origin GET.
Shift planning admin
See shift_slots / shift_signups for the data model.
GET /api/admin/shifts?period=YYYY-MM
200 -> { "slots": AdminSlot[] } // AdminSlot includes occupied employees per slot
Same lazy-generation trigger as the employee-side
GET /api/shifts/available.
POST /api/admin/shifts
Body: { "date", "start_time", "end_time", "capacity", "note"? }
201 -> { "id": number }
400 -> "Stejná směna už existuje." (unique date+start+end already taken)
PATCH /api/admin/shifts/:slotId
Body: { "date", "start_time", "end_time", "capacity", "note"? }
200 -> { "ok": true }
400 -> capacity dropped below the number of already-approved signups
POST /api/admin/shifts/:slotId/assign
Body: { "employeeId": number }
201 -> { "ok": true }
Goes through the exact same collision/capacity checks as a self-signup — see shift_signups.
DELETE /api/admin/shifts/:slotId/assign/:employeeId
204 on success
Payroll admin
See month_closures and payroll for the state machines these drive.
GET /api/admin/payroll?period=YYYY-MM
200 -> { "rows": PayrollOverviewRow[] }
One row per active employee, whether or not they've touched their
closure for that period yet (closure_status defaults to
"waiting_employee" when no row exists).
POST /api/admin/payroll/:employeeId/adjustments
Body: { "period", "tips_amount", "bonus_amount", "other_amount" }
200 -> { "payroll": Payroll }
400 -> not confirmed yet, or already locked
POST /api/admin/payroll/:employeeId/lock
Body: { "period": "YYYY-MM" }
200 -> { "payroll": Payroll }
400 -> employee hasn't confirmed, or still has an open shift
POST /api/admin/payroll/:employeeId/reopen
Body: { "period": "YYYY-MM" }
200 -> { "payroll": Payroll }
400 -> not currently locked+ready (e.g. already paid)
POST /api/admin/payroll/:employeeId/paid
Body: { "period": "YYYY-MM" }
200 -> { "payroll": Payroll }
400 -> not yet locked (status isn't "ready")
GET /api/admin/payroll/export?period=YYYY-MM
200, Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="mzdy_<YYYY-MM>.csv"
Built by payrollToCsv in backend/src/util/csv.ts, same BOM-prefixed
pattern as the attendance CSV export below.
Related
- employees
- attendance_events
- pay_rates, month_closures, payroll
- shift_slots, shift_signups
- Attendance routes
- Shift planning routes (employee), Closure routes (employee)
- Frontend stores —
adminStoreowns the selected month and drives both the summary table and the per-employee chart off it;adminShiftStoreandadminPayrollStoreown the other two admin tabs