Files
braidsbymira/docs/superpowers/specs/2026-08-12-phase2b-admin-panel-design.md

130 lines
6.5 KiB
Markdown

# 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()) = '<admin-uuid>'`)
**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 + `<Outlet />`, 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).