Files
eatme/docs/frontend/stores.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

85 lines
4.2 KiB
Markdown

---
type: Frontend Module
title: Zustand stores
description: authStore, attendanceStore, adminStore, shiftPlanningStore, adminShiftStore, closureStore, adminPayrollStore — where all app/fetch logic lives, keeping components thin.
resource: frontend/src/store
tags: [frontend, zustand, state]
timestamp: 2026-08-16T00:00:00Z
---
# Zustand stores
Convention (matching the user's other `~/mywork` projects): one flat
`create<State>()` per concern in `frontend/src/store/<name>Store.ts`,
default-exported as `use<Name>Store`. State and actions live together;
async/API calls are colocated directly in the actions via `set`/`get` — no
separate service layer. No `persist`/`devtools`/`immer`/slices are used
here. Components read via selectors (`useXStore(s => s.field)`) and call
actions; they don't own fetch/interval logic themselves.
# `authStore.ts`
`{ user, loading, loginError }` + `init()` (calls `GET /auth/me` once on
app mount, from `App.tsx`), `loginWithGoogle(credential)`, `logout()`. No
persistence — the real session lives in the httpOnly cookie, so on reload
the store just re-asks the backend via `init()`.
# `attendanceStore.ts`
Employee's own clock state. `{ status, stats, live, busy, error }` plus
`load()`, `recordEvent(type)`, `startPolling()`/`stopPolling()`.
- `startPolling` sets two intervals: one that calls `load()` every 60s
(keeps the server truth in sync), and one that recomputes `live` every
1s from the last-fetched `stats` against a fresh `Date` — via
`withLiveTime` (`frontend/src/lib/liveSession.ts`) — so an open shift's
duration counts up smoothly instead of freezing until the next poll.
`EmployeeApp.tsx` starts/stops this based on `status` (only polls while
`working`/`on_break`).
- **Gotcha this hit in practice**: `live` is a plain cached field, updated
by `set()` inside the tick/poll callbacks — it is deliberately *not* a
selector method like `liveStats: () => withLiveTime(...)` called as
`useAttendanceStore(s => s.liveStats())`. That pattern returns a new
object on every call, which breaks React's `useSyncExternalStore`
(used internally by zustand v5) — "Maximum update depth exceeded" from an
infinite render loop, since the snapshot is never referentially stable
between renders even when nothing actually changed.
# `adminStore.ts`
Employee list + org/individual stats + the shared `month` ("YYYY-MM") used
by both the summary table and the selected employee's detail chart.
`setMonth`/`prevMonth`/`nextMonth` update `month` and re-fetch `loadStats()`
plus (if an employee is selected) `selectEmployee()` together, so the
summary and the detail chart always show the same period. `removeEmployee`
calls the soft-delete endpoint — see [employees](/docs/data-model/employees.md).
# `shiftPlanningStore.ts` / `adminShiftStore.ts`
Employee and admin sides of shift planning, kept as two separate stores
(not one shared with a role flag) since the shapes barely overlap — the
employee store tracks `available`/`mine` slot lists for a period, the
admin store tracks the full per-period slot list plus create/update/assign
actions. Both hold their own `period` ("YYYY-MM") and re-fetch on
`prevPeriod`/`nextPeriod`, same pattern as `adminStore`'s `month`.
# `closureStore.ts` / `adminPayrollStore.ts`
Employee and admin sides of the month-end/payroll workflow. `closureStore`
is small — `{ period, closure, summary }` plus `confirm()`.
`adminPayrollStore` owns the per-period overview rows and the
`saveAdjustments`/`lock`/`reopen`/`markPaid` actions, each just POSTing and
then re-`load()`-ing rather than optimistically patching local state —
deliberate, since the server recomputes `base_amount` from live attendance
data on several of these calls and the UI should always reflect that, not
a stale client guess.
# Related
- [attendance_events](/docs/data-model/attendance-events.md) — the state
machine and `Session` shape these stores fetch
- [shift_slots](/docs/data-model/shift-slots.md), [month_closures](/docs/data-model/month-closures.md), [payroll](/docs/data-model/payroll.md)
- [Attendance API](/docs/api/attendance-routes.md)
- [Admin API](/docs/api/admin-routes.md)
- [Shift planning routes](/docs/api/shifts-routes.md), [Closure routes](/docs/api/closure-routes.md)