diff --git a/docs/superpowers/specs/2026-08-12-phase2b-admin-panel-design.md b/docs/superpowers/specs/2026-08-12-phase2b-admin-panel-design.md new file mode 100644 index 0000000..009f2a6 --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-phase2b-admin-panel-design.md @@ -0,0 +1,129 @@ +# Phase 2B: Admin Panel — Design + +**Status:** Approved +**Supersedes:** N/A. Builds directly on the Phase 2A Supabase backend +(`docs/superpowers/specs/2026-08-08-phase2-backend-foundation-supabase-design.md`), +which this spec assumes is complete and deployed. + +## Context + +Phase 2A shipped a Postgres schema (products, services, schedule, bookings, +inquiries), RLS policies gating writes to a single fixed admin UUID, and +notification Edge Functions — all live on a real Supabase project. That spec +explicitly deferred "the admin panel UI itself (login screen, product/service/ +availability management screens, bookings/inquiries dashboard)" to a future +sub-project, calling it Sub-project B. This spec is that sub-project. + +Mira (the salon owner) currently has no way to manage her own data. Products, +services, and the schedule are only editable by directly running SQL or the +seed script; bookings and inquiries (which land in RLS-protected, admin-only +tables) are only visible via the Supabase dashboard's Table Editor. This spec +gives her a real UI for all of that. + +## Scope + +In scope: +- Login screen for the admin account (already created in Phase 2A) +- CRUD for products and services, including image upload to the `salon-images` + Storage bucket (reserved but unused since Phase 2A) +- Weekly schedule template editing, plus one-off schedule exceptions + (holidays, special hours) +- Viewing bookings and inquiries, with the ability to change their status + (mark a booking cancelled/completed, mark an inquiry responded) + +Out of scope (explicitly deferred): +- The public site reading live data instead of the mock data in `src/data/` + — that's Sub-project C +- Any reporting/analytics beyond the raw bookings/inquiries lists +- Multi-admin support — the single fixed admin UUID model from Phase 2A is + unchanged; this is a one-user admin panel + +## Architecture + +Admin routes live under `/admin/*` in the existing `src/App.tsx` router, as a +`React.lazy`-loaded subtree (mirroring the existing lazy-loaded `Home` page) +so the public bundle's size is unaffected by admin code. + +A `ProtectedRoute` component wraps every admin route except `/admin/login`. +It checks `supabase.auth.getSession()` on mount, subscribes to +`onAuthStateChange()` for session expiry, and redirects to `/admin/login` +when there's no session. RLS already gates every admin write on `auth.uid()` +matching the fixed admin UUID (from Phase 2A), so no separate authorization +check is needed client-side — an authenticated non-admin (there shouldn't be +one; public signup is disabled) would simply get a `42501` from Postgres on +any write attempt, the same protection every other write path in this +backend already relies on. + +**No new frontend dependencies.** Data fetching and mutation go directly +through `supabase-js` calls in `useState`/`useEffect`, matching the pattern +already used throughout this codebase (e.g. `src/hooks/useBooking.ts`). This +project has never used a data-fetching library (React Query, SWR, etc.), and +nothing about this admin panel's scope — five sections, a single user, low +request volume — needs the caching/sync machinery those would add. + +## Data model changes + +No new tables. Two additions to what Phase 2A shipped: + +**1. `salon-images` Storage bucket policies.** The bucket exists (created +manually in Phase 2A's Task 8) but is private with no policies. This spec +adds: +- The bucket flips from private to public + (`update storage.buckets set public = true where id = 'salon-images'`), + so `getPublicUrl()` works without signed URLs — product/service photos are + marketing images, meant to be publicly visible on the site once Sub-project + C wires the public pages to live data. +- `select` on `storage.objects` for this bucket: public (`true`) +- `insert`/`update`/`delete` on `storage.objects` for this bucket: gated to + the admin UID, using the same pattern as every other admin-write RLS + policy in this backend (`(select auth.uid()) = ''`) + +**2. Booking status transitions.** A separate, already-in-progress follow-up +to Phase 2A is adding `after update of status` / `after delete` triggers on +`bookings` so that cancelling a booking frees its slot in the `booked_slots` +mirror table. This admin panel is the UI that calls +`update bookings set status = 'cancelled' where id = ...` — no schema change +needed here beyond consuming what that trigger already does once it lands. + +## Components & pages + +| Component | Purpose | +|---|---| +| `AdminLogin` | Email/password form via `supabase.auth.signInWithPassword()` | +| `ProtectedRoute` | Session check + redirect, wraps all routes below | +| `AdminLayout` | Sidebar nav + ``, sign-out action | +| `AdminProducts` | Table + create/edit form, delete with confirmation, image upload | +| `AdminServices` | Same shape as `AdminProducts`, service-specific fields (duration, price-from) | +| `AdminSchedule` | Weekly grid (7 rows: open/close time or closed) for `schedule_template`; a form below for one-off `schedule_exceptions` (date + closed-or-custom-hours) | +| `AdminBookings` | Table of bookings, most recent first, status change actions | +| `AdminInquiries` | Table of inquiries, status change actions (mark responded) | + +Visual style matches the public site's typography and color tokens +(`src/styles/tokens.css`) and uses `framer-motion` for the same transition +feel as the rest of the site, but with a dashboard layout (sidebar + content +area) rather than the public site's marketing-page layout. + +## Error handling + +Every mutation (create/update/delete/upload) shows an inline error message +on failure instead of failing silently — RLS denials (`42501`), network +failures, and Storage upload errors all surface to Mira in plain language. +Session expiry (an expired JWT) redirects to `/admin/login` automatically via +the `onAuthStateChange` listener, rather than presenting a raw 401. + +## Testing + +Vitest + React Testing Library for every page and for `ProtectedRoute`, +mocking `supabase-js` — consistent with how this codebase already tests +components that touch external state. No new pgTAP tests are needed for +table RLS (already covered in Phase 2A); the one new pgTAP addition is for +the `salon-images` Storage bucket policies described above, since those are +new database-enforced behavior this spec introduces. + +## Open questions / risks + +None outstanding — all scope questions were resolved during brainstorming +(bookings/inquiries get status-changing actions, not just viewing; image +upload is in scope now, not deferred; schedule editing covers both the +weekly template and one-off exceptions; visual style matches the public +site; no new frontend dependencies).