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.
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 |
|
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().
startPollingsets two intervals: one that callsload()every 60s (keeps the server truth in sync), and one that recomputesliveevery 1s from the last-fetchedstatsagainst a freshDate— viawithLiveTime(frontend/src/lib/liveSession.ts) — so an open shift's duration counts up smoothly instead of freezing until the next poll.EmployeeApp.tsxstarts/stops this based onstatus(only polls whileworking/on_break).- Gotcha this hit in practice:
liveis a plain cached field, updated byset()inside the tick/poll callbacks — it is deliberately not a selector method likeliveStats: () => withLiveTime(...)called asuseAttendanceStore(s => s.liveStats()). That pattern returns a new object on every call, which breaks React'suseSyncExternalStore(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
- attendance_events — the state
machine and
Sessionshape these stores fetch - shift_slots, month_closures, payroll
- Attendance API
- Admin API
- Shift planning routes, Closure routes