Files
eatme/docs/api/admin-routes.md
Michal Pemcak 94933cac5c Update OKF docs bundle, README, and CLAUDE.md for today's features
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.
2026-08-16 18:41:11 +02:00

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
api
admin
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