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.
This commit is contained in:
+49
-1
@@ -8,8 +8,9 @@ import {
|
|||||||
type CertStatus,
|
type CertStatus,
|
||||||
} from '@/lib/certifications-logic'
|
} from '@/lib/certifications-logic'
|
||||||
import { PERMISSIONS, PERMISSION_CODES } from '@/lib/permissions-catalog'
|
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 { SIGNUP_STATUS_LABEL, SESSION_STATUS_LABEL } from '@/lib/classes-logic'
|
||||||
|
import { RESERVATION_STATUS_LABEL } from '@/lib/equipment-logic'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
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 classSignups: ClassSignup[] = 'data' in signupRes ? (signupRes.data as ClassSignup[]) : []
|
||||||
const sessionOf = (cs: ClassSignup) =>
|
const sessionOf = (cs: ClassSignup) =>
|
||||||
Array.isArray(cs.class_sessions) ? cs.class_sessions[0] : cs.class_sessions
|
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 titleOf = (cs: ClassSignup) => {
|
||||||
const sess = sessionOf(cs)
|
const sess = sessionOf(cs)
|
||||||
const c = sess ? (Array.isArray(sess.classes) ? sess.classes[0] : sess.classes) : null
|
const c = sess ? (Array.isArray(sess.classes) ? sess.classes[0] : sess.classes) : null
|
||||||
@@ -213,6 +226,41 @@ export default async function MePage() {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground mb-3">
|
||||||
|
My equipment reservations
|
||||||
|
</h2>
|
||||||
|
{reservations.length === 0 ? (
|
||||||
|
<p className="font-sans text-sm text-muted-foreground">
|
||||||
|
You have no reservations. Browse what you can reserve at{' '}
|
||||||
|
<a href="/equipment" className="text-primary underline">Equipment</a>.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y rounded-lg border border-border">
|
||||||
|
{reservations.map(r => {
|
||||||
|
const eq = equipOf(r)
|
||||||
|
return (
|
||||||
|
<li key={r.id} className="p-4 flex items-start justify-between gap-4 flex-wrap">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-sans text-sm font-semibold text-foreground">{eq?.name ?? 'Equipment'}</span>
|
||||||
|
<Badge variant={r.status === 'reserved' ? 'default' : 'outline'}>
|
||||||
|
{RESERVATION_STATUS_LABEL[r.status] ?? r.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="font-mono text-[10px] text-muted-foreground mt-1">
|
||||||
|
{new Date(r.starts_at).toLocaleString()} – {new Date(r.ends_at).toLocaleString()}
|
||||||
|
{eq?.location ? ` · ${eq.location}` : ''}
|
||||||
|
</p>
|
||||||
|
{r.notes && <p className="font-sans text-xs text-muted-foreground mt-0.5">{r.notes}</p>}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+22
-1
@@ -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
|
customizable permissions + Ops ACLs, self-change hardening, `026`-`029` custom
|
||||||
forms + waivers + onboarding form step + per-space form slug + invite roles,
|
forms + waivers + onboarding form step + per-space form slug + invite roles,
|
||||||
`030` certifications + Instructor capability, `031` secrets SELECT honors
|
`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)
|
### 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
|
reports the certificates were skipped (no service-role bypass of the
|
||||||
guarded certifications surface). No anonymous path.
|
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
|
## 13. Known Limitations
|
||||||
|
|||||||
@@ -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
|
## 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).
|
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).
|
||||||
|
|||||||
Reference in New Issue
Block a user