From f32743d8bd560d94e1f15349bb9d87b19e060195 Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Sat, 16 May 2026 19:55:49 -0700 Subject: [PATCH] feat(equipment): My reservations on /me + docs /me gains a "My equipment reservations" section (the member's own reservations with status and window). ARCHITECTURE documents the equipment module; HANDOFF pass-23 records Classes deployed and the code-complete-but-local equipment module. --- app/(app)/me/page.tsx | 50 ++++++++++++++++++++++++++++++++++++++++++- docs/ARCHITECTURE.md | 23 +++++++++++++++++++- docs/HANDOFF.md | 22 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/app/(app)/me/page.tsx b/app/(app)/me/page.tsx index 945d500..40ced86 100644 --- a/app/(app)/me/page.tsx +++ b/app/(app)/me/page.tsx @@ -8,8 +8,9 @@ import { type CertStatus, } from '@/lib/certifications-logic' import { PERMISSIONS, PERMISSION_CODES } from '@/lib/permissions-catalog' -import { getMyClassSignups } from '@/lib/actions' +import { getMyClassSignups, getMyReservations } from '@/lib/actions' import { SIGNUP_STATUS_LABEL, SESSION_STATUS_LABEL } from '@/lib/classes-logic' +import { RESERVATION_STATUS_LABEL } from '@/lib/equipment-logic' export const dynamic = 'force-dynamic' @@ -90,6 +91,18 @@ export default async function MePage() { const classSignups: ClassSignup[] = 'data' in signupRes ? (signupRes.data as ClassSignup[]) : [] const sessionOf = (cs: ClassSignup) => Array.isArray(cs.class_sessions) ? cs.class_sessions[0] : cs.class_sessions + + const reservationRes = await getMyReservations() + type Reservation = { + id: string + starts_at: string + ends_at: string + status: string + notes: string | null + equipment: { name: string; location: string | null } | { name: string; location: string | null }[] | null + } + const reservations: Reservation[] = 'data' in reservationRes ? (reservationRes.data as Reservation[]) : [] + const equipOf = (r: Reservation) => (Array.isArray(r.equipment) ? r.equipment[0] : r.equipment) const titleOf = (cs: ClassSignup) => { const sess = sessionOf(cs) const c = sess ? (Array.isArray(sess.classes) ? sess.classes[0] : sess.classes) : null @@ -213,6 +226,41 @@ export default async function MePage() { )} + +
+

+ My equipment reservations +

+ {reservations.length === 0 ? ( +

+ You have no reservations. Browse what you can reserve at{' '} + Equipment. +

+ ) : ( + + )} +
) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bacbf5e..03db5a9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -420,7 +420,8 @@ Migrations 012-030 are tracked authoritatively in `docs/DATABASE_SCHEMA.md` customizable permissions + Ops ACLs, self-change hardening, `026`-`029` custom forms + waivers + onboarding form step + per-space form slug + invite roles, `030` certifications + Instructor capability, `031` secrets SELECT honors -`ops.secrets.read`, `032` classes + sessions + signups). +`ops.secrets.read`, `032` classes + sessions + signups, `033` equipment + +reservations). ### Forms & waivers (migrations 026-029; complete Phases 1-5) @@ -489,6 +490,26 @@ path, so it still requires the acting instructor to hold reports the certificates were skipped (no service-role bypass of the guarded certifications surface). No anonymous path. +### Equipment (migration 033) + +`equipment` (registry; status available/maintenance/retired; optional +`required_certification_id`; `is_active` archive) and +`equipment_reservations` (member time-window reservation) back a tool +reservation system. Pure logic in `lib/equipment-logic.ts` +(`intervalsOverlap`, `hasConflict`, `reservationEligibility`), unit-tested. +Server actions in `lib/actions/equipment.ts`. One additive permission, +`equipment.manage` (registry CRUD + adjust/cancel any reservation, gates +`/equipment/manage` via `lib/equipment-guard.ts`). Members reserve with +only membership. `/equipment` is the member catalog (reserve), `/me` lists +the member's own reservations. RLS is additive and default-deny: equipment +readable by managers (all) or members (active only), reservations by +managers (all) or the member (own) with manager-only UPDATE and **no +INSERT/DELETE policy** so reserve/cancel funnels through one validated +service-client action that enforces the equipment status, the no-overlap +rule, and the required-certification gate (checked against the normal +`member_certifications` data; an `equipment.manage` holder gets the +override and may book on a member's behalf). No anonymous path. + --- ## 13. Known Limitations diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 3774d0e..182b5f4 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,6 +4,28 @@ Append-only. Newest entries on top. Keep each entry to one screen. --- +## 2026-05-17 (pass 23) — Classes DEPLOYED; Equipment module (3) code-complete LOCAL + +Branch `main`. Classes module (pass-22) was deployed this pass (push `2096228`, Actions run 25979364584 success 1m? ; migration `032` applied; `/classes`, `/classes/manage`, `/me` smoke 307→login, not browser-verified). User then chose to continue the locked order → Equipment (module 3). + +### Equipment registry + reservations (locked module 3) — CODE-COMPLETE, LOCAL, 6 commits, NOT deployed +Phase-by-phase, each gated (vitest + `pnpm build` read in-run) + own commit: +- A: migration `033_equipment.sql` (`equipment`, `equipment_reservations`) mirrored in `schema.sql` (in-place seed fn + appended Section 20); `equipment.manage` perm (group Equipment) seeded+backfilled; types; DATABASE_SCHEMA/DB_SCHEMA_MAP. Additive default-deny RLS; reservations have SELECT (manage or own) + manager UPDATE, NO INSERT/DELETE (reserve/cancel via validated service-client action). No DB overlap constraint (no btree_gist dep) — overlap enforced in the action via pure logic. +- B: pure `lib/equipment-logic.ts` (intervalsOverlap, hasConflict, reservationEligibility) + 9 tests (suite 391) + Zod. +- C: `lib/actions/equipment.ts` (registry CRUD; listEquipmentForMembers w/ member_certified flag via service client; reserve/cancel validated service-client w/ status+overlap+required-cert, equipment.manage override + book-for-member; listEquipmentReservations; getMyReservations) + `lib/equipment-guard.ts` + barrel + API_REFERENCE. +- D: `/equipment/manage` admin pages + sidebar Admin "Manage equipment". +- E: `/equipment` member browse + reserve (cert gate shown up front) + sidebar member "Equipment". +- F: "My equipment reservations" on `/me` + ARCHITECTURE + this entry. + +Cert-gate (user-approved): reserving cert-gated equipment requires an active `member_certifications` for the member; an `equipment.manage` holder overrides and may book on a member's behalf. Cert check reads `member_certifications` via the service client (boolean only) since a non-cert-permissioned equipment manager can't read it under RLS — no write/PII bypass of the guarded surface. + +### Open / not done +- Equipment is LOCAL (6 commits). Awaiting deploy approval. After push: confirm `033` applied, board has `equipment.manage`, exercise add equipment (+ required cert) → member reserve (cert gate / overlap) → manager override → `/me`. NOT browser-verified. +- Pure logic unit-tested; action orchestration (overlap/cert/override/RLS) NOT (same systemic no-mock-harness gap). +- Roadmap remaining: module 4 Door epic (member_cards + configurable door integration + native HeatSync adapter + the universal API-call UI builder; largest/riskiest, design-first, do last). Cert/feature nav links for non-admin permission-holders still admin-block-only (known minor gap). + +--- + ## 2026-05-17 (pass 22) — Classes module (2) code-complete LOCAL; roadmap reaffirmed Branch `main`. Incident-INSERT RLS bug confirmed RESOLVED by the user (closed; see pass-21 note). User asked to continue the locked roadmap; chose Classes (module 2) next; reaffirmed the locked order (Certs done → Classes → Equipment → Door epic, which is where the "custom API-call UI builder" lives, module 4, design-first/last).