130 lines
6.5 KiB
Markdown
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).
|