Files
eatme/docs/frontend/stores.md
Michal Pemcak fffcb73ea4 Initial commit: EatMe attendance tracker
Google-SSO PWA for bistro employee clock-in/out, admin employee
management, and stats with CSV export. Express + SQLite backend,
React + Zustand frontend in the light-mono-tui design language.
Multi-stage Dockerfile, compose.yaml for image-based deploys, nginx
reverse-proxy template, and an OKF documentation bundle in docs/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 18:41:11 +02:00

2.9 KiB

type, title, description, resource, tags, timestamp
type title description resource tags timestamp
Frontend Module Zustand stores authStore, attendanceStore, and adminStore — where all app/fetch logic lives, keeping components thin. frontend/src/store
frontend
zustand
state
2026-08-12T00: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.

Related