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

4.2 KiB

type, title, description, resource, tags, timestamp
type title description resource tags timestamp
Frontend Module Zustand stores authStore, attendanceStore, adminStore, shiftPlanningStore, adminShiftStore, closureStore, adminPayrollStore — where all app/fetch logic lives, keeping components thin. frontend/src/store
frontend
zustand
state
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.

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