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.
175 lines
5.6 KiB
Markdown
175 lines
5.6 KiB
Markdown
---
|
|
type: API Endpoint
|
|
title: Admin routes
|
|
description: Employee CRUD + pay rates, org-wide and per-employee stats, shift planning admin, payroll, and CSV export — all requireAdmin.
|
|
resource: backend/src/routes/admin.ts
|
|
tags: [api, admin]
|
|
timestamp: 2026-08-16T00:00:00Z
|
|
---
|
|
|
|
# Admin routes
|
|
|
|
Mounted at `/api/admin`. Every route requires `requireAuth` + `requireAdmin`
|
|
(role resolved from `ADMIN_EMAILS`, see [Auth flow](/docs/architecture/auth-flow.md)).
|
|
|
|
# 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](/docs/data-model/pay-rates.md)) — 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](/docs/data-model/pay-rates.md) 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](/docs/data-model/employees.md) 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](/docs/data-model/shift-slots.md) /
|
|
[shift_signups](/docs/data-model/shift-signups.md) 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`](./shifts-routes.md).
|
|
|
|
```
|
|
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](/docs/data-model/shift-signups.md).
|
|
|
|
```
|
|
DELETE /api/admin/shifts/:slotId/assign/:employeeId
|
|
204 on success
|
|
```
|
|
|
|
## Payroll admin
|
|
|
|
See [month_closures](/docs/data-model/month-closures.md) and
|
|
[payroll](/docs/data-model/payroll.md) 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](/docs/data-model/employees.md)
|
|
- [attendance_events](/docs/data-model/attendance-events.md)
|
|
- [pay_rates](/docs/data-model/pay-rates.md), [month_closures](/docs/data-model/month-closures.md), [payroll](/docs/data-model/payroll.md)
|
|
- [shift_slots](/docs/data-model/shift-slots.md), [shift_signups](/docs/data-model/shift-signups.md)
|
|
- [Attendance routes](./attendance-routes.md)
|
|
- [Shift planning routes (employee)](./shifts-routes.md), [Closure routes (employee)](./closure-routes.md)
|
|
- [Frontend stores](/docs/frontend/stores.md) — `adminStore` owns the
|
|
selected month and drives both the summary table and the per-employee
|
|
chart off it; `adminShiftStore` and `adminPayrollStore` own the other
|
|
two admin tabs
|