feat: Enhance reservation functionality

This commit is contained in:
2026-04-22 10:23:22 -04:00
parent 7f1e82acc2
commit 534d66c774
25 changed files with 1236 additions and 91 deletions

View File

@@ -82,9 +82,25 @@
---
## Planned Features (not yet built)
See `docs/planning/user-stories.md` for full stories and open questions.
| Feature | Stories | Phase |
|---------|---------|-------|
| Open Reservations + crew sign-up | US-01 US-07 | Near-term |
| Discord notifications (open reservations) | US-08 | Near-term |
| Check-out / Check-in forms | US-10, US-11 | Near-term |
| SMS / in-app notifications | US-09 | Longer-term |
---
## Open Items
- [ ] Are cancel-reservation and admin Edge Functions planned?
- [ ] What is the full set of admin pages needed?
- [ ] What is the club name for display in the app?
- [X] What is the season start date / go-live target?
- [ ] US-07: What constitutes a "non-member" on a reservation?
- [ ] US-10: What fields are on the check-out/in forms?
- [ ] US-08: Discord webhook URL / channel — confirm stored in Vault at kv/oys/

View File

@@ -1,100 +0,0 @@
# Session Handoff: Historical Booking Constraint + Offline Cache + Booking Draft
**Date:** 2026-04-21
**Session Duration:** ~2 hours
**Session Focus:** Enforced historical booking rule, fixed Edge Function error surfacing, implemented offline cache system with Realtime updates, and fixed slot-click navigation from schedule to create page using a booking draft composable.
**Context Usage at Handoff:** ~75%
---
## What Was Accomplished
1. **Historical booking constraint** — tests written and Edge Function enforced → `tests/integration/booking-constraints.test.ts`, `supabase/functions/create-reservation/index.ts`
2. **Error message fix** — "Can not book a reservation in the past." is the user-facing message for `historical_booking_not_allowed` (422)
3. **Fixed `functions-js` v2 error body parsing**`response.data` is `null` on non-2xx; body must be read from `response.error.context.json()``app/pages/reservations/create.vue`
4. **Historical booking uses `end_time`** — a booking is historical only when its `end_time < now()` (not `start_time`)
5. **Offline cache system** — localStorage, 24h TTL, Realtime-updated → `app/composables/useAppCache.ts`, `app/composables/useOfflineStatus.ts`
6. **Booking draft composable** — module-level ref for cross-page state (replaces broken query-param deep-link) → `app/composables/useBookingDraft.ts`
7. **Offline indicator** — fixed chip in top-right corner in `app/app.vue`; Realtime channel `app-cache-sync` patches `slots:`, `intervals:`, and `boats` cache on DB changes
8. **Schedule page cache integration**`app/pages/schedule.vue`: `fetchBoats` and `fetchSchedule` write to cache on success, read from cache when offline
9. **Create page booking draft**`app/pages/reservations/create.vue`: `onIonViewWillEnter` reads draft (no async fetch needed), jumps to step 2 with pre-filled boat + slot; also cache-aware `loadSlots` for offline step 1
10. **CLAUDE.md caching rule documented** — "Every table/view read from Supabase must be written to `useAppCache` on success and read from it when offline" — includes pattern, key conventions, Realtime guidance
---
## Exact State of Work in Progress
- **Booking draft navigation**: complete — clicking a slot in schedule sets draft and routes to `/reservations/create`; create page reads draft in `onIonViewWillEnter` and goes directly to step 2
- **Offline cache**: complete for `boats`, `intervals`, `slots` (schedule + create pages); NOT yet applied to other pages (`/boat`, `/reference`, `/profile`, admin pages)
- **Realtime**: subscribed to `reservations`, `intervals`, `boats` in `app.vue`; if new tables are cached in future, add subscriptions to `app-cache-sync` channel
---
## Decisions Made This Session
- **Historical = `end_time < now()`** BECAUSE a session that started in the past but hasn't ended yet is still bookable — STATUS: confirmed
- **Query params abandoned for slot navigation** BECAUSE `+` in ISO timestamps (`+00:00`) URL-encodes to `%2B`, which can be decoded as a space by some parsers; async boat fetch adds an auth-timing race; `onIonViewWillEnter` route-timing is unreliable — CHOSE module-level reactive `ref` (booking draft) instead — STATUS: confirmed
- **`functions-js` v2 error body is in `error.context`, not `data`** — `data` is `null` on non-2xx; `error.context` is the raw `Response` — applies to all error code handling in `create.vue` — STATUS: confirmed
- **Cache key for schedule data = ISO week Monday** (`cache.weekKey(utcIso)``weekMonday(date)`) so desktop (week view) and mobile (day view) share the same cache entries — STATUS: confirmed
- **Offline = read-only** — booking submission is not attempted offline (Edge Function call fails naturally); no explicit offline guard on submit — ASSUMED acceptable
---
## Key Numbers Generated or Discovered This Session
- `functions-js` version in use: `2.100.0` (confirmed from `node_modules`)
- Cache TTL: 24 hours (ms: `86_400_000`)
- Realtime channel name: `app-cache-sync`
- Error code for historical booking: `historical_booking_not_allowed` (HTTP 422)
- localStorage key prefix: `cache:` (e.g., `cache:boats`, `cache:slots:2026-04-20`, `cache:intervals:2026-04-20`)
---
## Files Created or Modified
| File Path | Action | Description |
|-----------|--------|-------------|
| `app/composables/useOfflineStatus.ts` | Created | Module-level `isOnline` ref; wires `online`/`offline` browser events |
| `app/composables/useAppCache.ts` | Created | localStorage cache; `get` (TTL-enforced), `peek` (stale-ok), `set`, `invalidate`, `invalidatePrefix`, `weekKey` |
| `app/composables/useBookingDraft.ts` | Created | Module-level `BookingDraft` ref; `set(boat, startTime, endTime)` / `take()` |
| `app/app.vue` | Modified | Offline chip (fixed top-right, warning colour); Realtime `app-cache-sync` channel for `reservations`, `intervals`, `boats` |
| `app/pages/schedule.vue` | Modified | `fetchBoats`/`fetchSchedule` cache-aware; `bookSlot` sets draft instead of query params |
| `app/pages/reservations/create.vue` | Modified | `onIonViewWillEnter` consumes draft (jumps to step 2); `loadSlots` cache-aware; `functions-js` v2 error body fix; `historical_booking_not_allowed` in `codeMessages` |
| `supabase/functions/create-reservation/index.ts` | Modified | Historical guard: `endDate < now()` → admin-only (422 `historical_booking_not_allowed`); admin skips cert check + booking-limit checks |
| `tests/integration/booking-constraints.test.ts` | Modified | Added `describe('historical booking constraint')`: member rejected (422), admin allowed (201), skipper rejected (422) |
| `CLAUDE.md` | Modified | Added "Offline Cache" section under Architecture with pattern, key conventions, Realtime guidance, and booking-draft pattern |
---
## What the NEXT Session Should Do
1. **Verify slot-click flow end to end**: open schedule page, click an available slot, confirm create page opens at step 2 with the correct boat name and time pre-filled, submit a reservation
2. **Apply cache pattern to remaining pages**: `/boat` (boats list), `/reference` (reference_docs), `/profile` (member record), admin pages as needed — follow the pattern documented in `CLAUDE.md`
3. **Decide on the DatePicker replacement** (OPEN from previous session): `<DatePicker inline>` on `app/pages/index.vue:59` is the only PrimeVue usage — options are remove the calendar card, keep PrimeVue, or use native `<input type="date">`
4. **Build `app/pages/admin/reservations.vue`** — admin view of all bookings (file exists, likely scaffolded)
5. **Wire cancel-reservation** — OPEN: is a cancel Edge Function planned?
---
## Open Questions Requiring User Input
- [ ] `app/pages/index.vue:59` — what replaces `<DatePicker inline>`? Remove card, keep PrimeVue, or native input? Impacts whether PrimeVue stays in the stack
- [ ] Is a cancel-reservation Edge Function planned? Impacts backend scope before April 30
- [ ] Should offline submission attempts show an explicit "You are offline — cannot book" message rather than a generic network error?
---
## Assumptions That Need Validation
- ASSUMED: `app/pages/admin/reservations.vue` is scaffolded but incomplete — verify by reading the file
- ASSUMED: offline booking submission failing silently (network error toast) is acceptable — validate with Patrick
- ASSUMED: `onIonViewWillEnter` fires before the user can interact with the page — if there is a visible flash of step 1 before the draft is consumed, add `step.value = 2` synchronously before the `await` in the handler
---
## Files to Load Next Session
- `docs/summaries/handoff-2026-04-21-historical-booking-offline-cache.md` — this file
- `docs/summaries/00-project-brief.md` — project context
- `app/pages/reservations/create.vue` — if continuing booking flow work
- `app/pages/admin/reservations.vue` — if building admin bookings view
- `app/pages/index.vue` — if resolving the DatePicker question

View File

@@ -0,0 +1,124 @@
# Session Handoff: Schedule Interactivity, Admin Features, Routing Fix
**Date:** 2026-04-22
**Session Duration:** ~3 hours
**Session Focus:** Fixed IonRouterOutlet route params bug; made schedule page fully interactive on desktop; added admin create/edit capabilities; added member names to schedule; documented Ionic+Nuxt watch-outs and new user stories.
**Context Usage at Handoff:** ~85%
---
## What Was Accomplished
1. **Diagnosed and fixed `params: {}` bug on edit page** → root cause: Nuxt's auto-imported `useRoute()` always returns empty params inside IonRouterOutlet. Fix: `import { useRoute, useRouter } from 'vue-router'` directly. Documented in CLAUDE.md under "Ionic + Nuxt Watch-outs".
- Output: `app/pages/reservations/edit/[id].vue` (import added)
- Output: `CLAUDE.md` (new "Ionic + Nuxt Watch-outs" section under Routing)
2. **Made desktop schedule slots interactive** → previously desktop grid had no click handlers. Added same tappable logic as mobile: available slots → create flow; own booked slots → action sheet with Edit/Cancel.
- Output: `app/pages/schedule.vue` (template changes in desktop grid)
3. **Added confirmation dialog before cancelling from schedule** → both mobile and desktop Cancel actions now show `alertController` confirmation before proceeding.
- Output: `app/pages/schedule.vue` (`confirmCancelSlot()` wrapping `cancelSlot()`)
4. **Added admin interactivity on schedule** → admins can tap any booked slot (not just own). Action sheet header shows member name for admin context.
- Output: `app/pages/schedule.vue` (`authStore.isAdmin` checks in tappable conditions and `handleSlotClick`)
5. **Added member names to schedule slots** → put `member_name` into `reservation_slots` view via LEFT JOIN on `members`. `security_invoker=false` means the JOIN bypasses RLS — no separate query needed, no RLS block for non-admins.
- Output: `supabase/migrations/20260422000000_reservation_slots_add_user_id.sql`
- Output: `app/types/supabase.ts` (added `user_id`, `member_name` to `reservation_slots` Row)
- Output: `app/pages/schedule.vue` (replaced `getUserNameById` calls with `slot.memberName`)
6. **Fixed slot display** → desktop slots now show `HH:MM-HH:MM` on one line. All slots (available + booked, mobile + desktop) render a consistent 3-row structure (time, status, member) with `min-height` reserved for the member row so sizes are uniform.
- Output: `app/pages/schedule.vue` (template restructure + CSS)
7. **Admin member selector on create reservation** → when admin, step 2 of create flow shows a Member dropdown (defaults to "Self (admin)"). Selected member passed as `target_user_id` to edge function.
- Output: `app/pages/reservations/create.vue`
8. **Admin member selector on edit reservation** → when admin, edit page loads all members and shows a Member selector pre-populated from the reservation's `user_id`. Saved in update payload.
- Output: `app/pages/reservations/edit/[id].vue`
9. **Edge function: admin target_user_id support** → accepts optional `target_user_id` in body. If caller is admin and field is set, uses it as the reservation owner. Admins bypass cert check, weekly limit, and weekend limit. Historical booking guard simplified (was duplicating the admin fetch). Overlap constraint still applies to all.
- Output: `supabase/functions/create-reservation/index.ts`
10. **Documented user stories** → created `docs/planning/user-stories.md` with US-01US-13 across four feature areas. Updated `docs/summaries/00-project-brief.md` with planned features table.
- Output: `docs/planning/user-stories.md` (new file)
- Output: `docs/summaries/00-project-brief.md` (planned features + open items)
---
## Exact State of Work in Progress
- All changes typecheck clean (`yarn typecheck` → no errors)
- Migration `20260422000000` applied to local DB via `supabase db reset`
- Migration NOT yet pushed to dev/prod Supabase — needs `supabase db push` or deploy pipeline
- Edit page fix (import from `vue-router`) confirmed working conceptually; not browser-tested this session
---
## Decisions Made This Session
- **Import `useRoute`/`useRouter` from `vue-router` directly, never use Nuxt auto-import** BECAUSE Nuxt's auto-imported `useRoute()` always returns `params: {}` inside IonRouterOutlet — confirmed by official docs at ionic.nuxtjs.org/get-started/watch-outs and by console.log showing `params: {}` at runtime — STATUS: CONFIRMED. Apply to all future param-reading pages.
- **Put `member_name` in the view via JOIN rather than lazy-fetching via `getUserNameById`** BECAUSE `getUserNameById` relies on a members query that is blocked by RLS for non-admin users (returns null → "Unknown"). `security_invoker=false` on the view makes the JOIN run as owner, bypassing RLS — STATUS: CONFIRMED.
- **Admins bypass all booking limits and cert checks in the edge function** BECAUSE admin-created bookings are operational overrides, consistent with how `admin/reservations.vue` (direct insert) already worked — STATUS: CONFIRMED. Overlap constraint still applies to everyone.
- **Always render member name row in slot blocks (even if empty string)** BECAUSE rendering it conditionally causes slots to have different heights — STATUS: CONFIRMED.
---
## Key Numbers Generated or Discovered This Session
- `reservation_slots` view now has 7 columns: `id, boat_id, user_id, start_time, end_time, status, member_name`
- US-12, US-13 added (member profiles) — total user stories now: 13
- Migration sequence: `...000002_prevent_past_reservation_updates.sql``...20260422000000_reservation_slots_add_user_id.sql`
---
## Files Created or Modified
| File Path | Action | Description |
|-----------|--------|-------------|
| `supabase/migrations/20260422000000_reservation_slots_add_user_id.sql` | Created | Drops and recreates `reservation_slots` view with `user_id` and `member_name` (LEFT JOIN members) |
| `app/types/supabase.ts` | Modified | Added `user_id: string` and `member_name: string \| null` to `reservation_slots` Row |
| `app/pages/schedule.vue` | Modified | Desktop interactivity; admin access; member names; confirmation dialog; slot display fix |
| `app/pages/reservations/edit/[id].vue` | Modified | Import `useRoute`/`useRouter` from `vue-router`; admin member selector |
| `app/pages/reservations/create.vue` | Modified | Admin member selector (step 2); passes `target_user_id` to edge function |
| `supabase/functions/create-reservation/index.ts` | Modified | `target_user_id` support; admin bypasses limits; deduped admin check |
| `CLAUDE.md` | Modified | Added "Ionic + Nuxt Watch-outs" section (6 rules from official docs) |
| `docs/planning/user-stories.md` | Created | US-01US-13 across Open Reservations, Notifications, Check-out/in, Member Profiles |
| `docs/summaries/00-project-brief.md` | Modified | Added planned features table linking to user stories |
---
## What the NEXT Session Should Do
1. **First**: Browser-test the edit page fix — navigate from home/schedule to an edit page and confirm reservation loads. Check console for any remaining `params: {}` warnings.
2. **Then**: Push migration `20260422000000` to dev Supabase (`supabase db push --linked` or via deploy pipeline).
3. **Consider**: Any other pages in the app that use `useRoute()` for params need the same `import { useRoute } from 'vue-router'` fix. Run a grep for auto-imported `useRoute` usage on param pages.
4. **Consider next feature**: Open Reservations (US-01US-07) or Member Profiles (US-12US-13) — ask Patrick which to tackle.
---
## Open Questions Requiring User Input
- [ ] US-07: What constitutes a "non-member" on a reservation? (name-only entry, or guest account?) — impacts crew schema design
- [ ] US-10: What fields are on the check-out/in forms? — impacts DB schema
- [ ] US-13: Full list of member-controlled privacy fields (is phone in schema? any other fields?) — impacts members table migration
- [ ] US-13: Privacy enforcement via RLS on `members`, or a separate `member_profiles` view? — impacts DB design
- [ ] US-08: Discord webhook URL/channel — confirm stored in Vault at `kv/oys/`?
---
## Assumptions That Need Validation
- ASSUMED: `import { useRoute } from 'vue-router'` fixes params for ALL IonRouterOutlet param pages, not just the edit page — validate by testing at least one other param route if more are added.
- ASSUMED: `security_invoker=false` on `reservation_slots` view is sufficient to expose member names to all authenticated users without violating privacy intent — validate with Patrick (member names may eventually be subject to US-13 privacy rules).
---
## Files to Load Next Session
- `docs/summaries/handoff-2026-04-22-schedule-admin-routing-fixes.md` — this file
- `docs/summaries/00-project-brief.md` — project state and open items
- `docs/planning/user-stories.md` — if working on any new feature
- `app/pages/schedule.vue` — if continuing schedule work
- `CLAUDE.md` — always (routing rules, data fetching pattern, Ionic watch-outs)