workforce orchestration · operational intelligence
The operations layer for a small team running after-school programs across multiple schools. Schedules, staffing, gear, attendance, mileage — in one place instead of spreadsheets, group texts, and memory.
next.js 14 · typescript · supabase (postgres + rls) · zustand · tailwind · vercel
Polymorphic rendering over unified storage. Field trips, pop-ups, and classes all live in one schedule_row table. Each type has its own card component that reads from the same columns and an optional stations jsonb. The table stays flat; the variation lives at the view layer where it can change without migrations.
Direct manipulation holds the data model accountable. Every edit happens in place — click anything to edit, drag staff pills to toss them off a card, Ctrl+C/V to clone whole days, no publish step to remember. The interaction model was rewritten three times converging on this. The data model has to survive every gesture because writes are atomic and row-scoped.
Nested relationships survive clone operations. Copy-paste on a field trip duplicates the row with a new id while preserving school_id, time_range, class_type, the staff array, and the stations jsonb — including each station's nested staff list. One write path serves all three entry types.
Optimistic UI with rollback. State lives in a single Zustand store with optimistic mutations and server reconciliation. Clean separation across the Next.js server/client boundary.
Copy-paste on field trips was brutal. The stations jsonb was dropping on clone, the school_id lookup was failing for "No School" entries, and the match key between source and destination rows was ambiguous. Each fix had to hold without regressing the class or pop-up paths, because they all share one write path.
Drag latency from refetch-on-save. Every drag between days triggered a full refetch after the save committed. Fix: optimistic local update first, refetch only for cross-day reconciliation.
Mobile performance on Samsung Chrome. The Vibe Board was freezing on mid-range Android. GPU layer bloat, unthrottled scroll handlers, and content-visibility:auto style-invalidation compounded. Fix: debounced scroll, CSS containment to isolate reflows across lanes, removed the style-invalidation trap. A three-layer splash dismiss (CSS animation + animationend + setTimeout fallback) covered the remaining main-thread congestion.
Stations nesting still open. Field trips have multiple stations (Art, STEAM, Robotics) each with their own staff — but the Vision Board card has no room to show that nesting inline. Three designs on the table so far: inline expand, side panel, keep-the-card-simple. None fully resolved.
The first 80% takes a week. The last 20% takes a month.