Pawfect Groom - Product Requirements Document
1) Summary
Pawfect Groom is a booking app for a small dog-grooming business run by one groomer from one shop. Customers pick a service, choose a free time slot, enter their dog's details and book without an account; the groomer opens a private /admin page to see the day's appointments, confirm, complete, cancel or add bookings by hand, and set opening hours. Data lives in Supabase Postgres, because a customer on one device and the groomer on another must see the same calendar.
- Assumption: There is one groomer and one table, so at most one appointment occupies any given time; there are no staff or rooms.
- Assumption: "
Slot" means a start time on a 15-minute grid where the full service duration fits before closing and does not overlap another non-cancelled appointment, not a fixed pre-made time block. - Assumption: All times are the shop's local time,
Europe/London; a "day" is a local calendar date and dates are stored asYYYY-MM-DDplusHH:mmstrings. - Assumption: Customers have no accounts; a booking is identified by a 6-character
referenceand managed with the reference plus the phone number. - Assumption: A booking is accepted on request and starts as
pending; it blocks its slot immediately, and the groomer confirms it. - Assumption: Dog size sets duration and price:
smallunder 10 kg,medium10-25 kg,largeover 25 kg (see 4). - Assumption: Platform is a responsive browser app, phone-first and usable at 1280 px; it is not installable and sends no reminders.
- Assumption: No payments are taken online; the price is shown and paid at the shop.
2) Tech Stack
- Framework → Next.js 14 (App Router) → server routes keep the admin password and service key off the browser
- Language → TypeScript → typed booking and slot logic
- Database → Supabase Postgres → shared calendar for customers and groomer
- Auth → Single admin password, checked server-side, stored in an httpOnly cookie → the only privileged user is the groomer
- Styling → Tailwind CSS → fast mobile-first layout
- Dates →
date-fns→ slot arithmetic and formatting - Deployment → Vercel → zero-config Next.js hosting
3) Users & Screens
| Role | Can | Cannot |
|---|---|---|
| Customer (anonymous) | Browse services, see free slots, book, look up and cancel own booking | See other customers' data, change prices or hours |
| Groomer (admin) | View all bookings, change status, add and cancel bookings, edit opening hours, block days | Take payment, message customers |
Screens:
| Screen | Reached from | Shows | Empty / loading / error | Back / close goes to |
|---|---|---|---|---|
Home / | Direct | Shop name, service cards, Book now | Loading: skeleton cards; error: Couldn't load services. Try again. | none |
Book /book | Book now | 4-step form (6.1) | see 6.1 | Home |
Confirmation /booking/[reference] | Successful booking, lookup | Booking summary and status | Unknown reference: We couldn't find that booking. | Home |
Find booking /manage | Home footer link | Reference + phone form | see 6.3 | Home |
Admin login /admin/login | /admin when signed out | Password field | Wrong password. | Home |
Admin day view /admin | Login | Date picker, day timeline, list | see 6.4 | Login |
Admin hours /admin/hours | Admin header link | Weekly hours, closed dates | see 6.5 | Admin day view |
Navigation: customer pages have a header with the shop name linking Home; admin pages have a header with Today, Hours, Sign out.
4) Data Model & Logic
Service (seeded, not editable in V1): id, name, base_minutes (int), price_small, price_medium, price_large (int pence).
| Service | Minutes (small/medium/large) | Price (small/medium/large) |
|---|---|---|
Bath & Brush | 45 / 60 / 75 | £30 / £38 / £48 |
Full Groom | 75 / 90 / 120 | £48 / £60 / £78 |
Nail Trim | 15 / 15 / 15 | £10 / £10 / £10 |
Booking: id (uuid), reference (6 chars from ABCDEFGHJKLMNPQRSTUVWXYZ23456789, unique), service_id, size (small|medium|large), date (YYYY-MM-DD), start (HH:mm), end (HH:mm, computed), price (pence, copied at booking), dog_name (1-30 chars), breed (0-40 chars), owner_name (1-60), phone (7-15 digits, spaces and + allowed), notes (0-300), status, created_at.
OpeningHours: weekday (0 = Sunday … 6), open, close (HH:mm), is_closed (bool). Default: Mon-Fri 09:00-17:00, Sat 09:00-13:00, Sun closed.
ClosedDate: date, reason (0-60 chars).
Slot logic:
duration = service.minutes[size]
for t in range(open, close - duration, step 15 min):
slot free if no booking b (status != cancelled) on date with t < b.end and t+duration > b.start
and date not in ClosedDate and not weekday.is_closed
and (date > today or t >= now + 60 min)
Customers can book 1 to 60 days ahead (today counts only if a slot is ≥ 60 min away).
Worked example: Tuesday 14 May, hours 09:00-17:00. Existing: Rex (Full Groom, large) 09:00-11:00; Bella (Nail Trim) 11:00-11:15; Max (Bath & Brush, small) cancelled 13:00-13:45. A customer picks Full Groom, medium (90 min). Free starts: 11:15, 11:30 … up to 15:30 (ends 17:00). 09:00-10:45 are blocked by Rex; 11:00 overlaps Bella. Max's slot is free again because it is cancelled. Booking 11:15 gives end = 12:45, price = £60. A second customer now sees the earliest Nail Trim at 12:45. Edge: Full Groom large (120 min) on Saturday (09:00-13:00) has last start 11:00.
Status transitions:
| From | Event | To | Side effect |
|---|---|---|---|
| (new) | Customer submits | pending | Slot blocked |
pending | Groomer taps Confirm | confirmed | none |
pending/confirmed | Customer or groomer cancels | cancelled | Slot freed |
confirmed | Groomer taps Complete | completed | terminal |
pending/confirmed | Groomer taps No-show | no_show | terminal, slot stays in history |
cancelled | none | none | terminal |
Edits and edge cases: Slot availability is re-checked in a database transaction on submit; if taken meanwhile, the booking fails (6.1). Customers cancel only up to 2 hours before start. Changing opening hours never moves existing bookings; the admin day view flags bookings outside current hours with Outside hours. Two admins confirming at once is harmless (idempotent). Duplicate bookings (same phone, date, start) are rejected.
5) External Services
Supabase
Used for: Postgres storage of services, bookings, hours, closed dates.
Calls: supabase-js from('bookings').select/insert/update, and an RPC create_booking that runs the overlap check and insert in one transaction.
Fields read: all booking fields (admin), date,start,end,status only (public slot query via server route).
Limits: free tier 500 MB; 1 request per slot query is enough at this scale.
Failure: server routes return 503; UI shows Something went wrong. Please try again in a minute.
Keys: SUPABASE_SERVICE_ROLE_KEY server only; NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY browser-safe (anon key has no table access; all reads and writes go through server routes).
6) Core Features
6.1 Book an Appointment
User Flow:
- Home: tap
Book now./bookopens at step 1 of 4, with a progress labelStep 1 of 4. - Step 1
Choose a service: tap a card (name,from £30); then pickSmall (under 10 kg),Medium (10-25 kg)orLarge (over 25 kg).Nextstays disabled until both are chosen. The summary line shows duration and price, e.g.90 min · £60. - Step 2
Pick a day and time: a horizontal strip of the next 14 days (closed days greyed out and not tappable); tapping a day loads free slot chips (09:00,09:15…). Tap one to select it. - Step 3
About your dog: fieldsDog's name,Breed (optional),Your name,Phone number,Notes for the groomer (optional). - Step 4
Review: summary of all choices. TapRequest booking. The server runscreate_booking. - On success go to
/booking/[reference]showing the status and the reference.Backon any step keeps entered data.
UI Copy & Colors:
- Page title:
Book a grooming, text#0F172A - Selected card/chip: background
#0F766E, text#FFFFFF - Primary button:
Next/Request booking,#0F766Eon#FFFFFF - Confirmation heading:
Request received!withYour reference is {REF}. We'll confirm shortly.
Error & Empty States:
- No free slot that day →
No free times on this day. Try another day.Recovery: pick another day. - Slot taken at submit →
Sorry, that time was just booked. Please pick another.Recovery: returns to step 2 with refreshed slots, other data kept. - Invalid phone →
Enter a phone number with 7-15 digits. - Empty required field →
This field is required. - Server failure →
Something went wrong. Please try again in a minute.Recovery:Request bookingstays enabled.
Acceptance Checks:
- ✓
Full Groom,medium, Tue 14 May with Rex09:00-11:00and Bella11:00-11:15existing → first chip is11:15, last chip15:30. - ✓
Nail Trimselected on Saturday (09:00-13:00) → last chip12:45. - ✓ Choose
Bath & Brush,small→ summary shows45 min · £30. - ✓ Two customers submit
11:15simultaneously → exactly one getsRequest received!, the other seesSorry, that time was just booked. Please pick another.
6.2 Booking Confirmation Page
User Flow:
/booking/[reference]showsreference, status badge, service, dog name, date asEEEE d MMMM, timeHH:mm - HH:mm, price.- Shows the shop address line
12 High Street, AnytownandPay at the shop. - Button
Cancel bookingappears while status ispendingorconfirmedandstartis more than 2 h away. - Tapping
Cancel bookingopens a dialogCancel this booking?withKeep itandYes, cancel.Yes, cancelsetscancelled.
UI Copy & Colors:
- Badges:
Pending#B45309on#FEF3C7;Confirmed#166534on#DCFCE7;Cancelled#991B1Bon#FEE2E2;Completed#334155on#E2E8F0;No-show#334155on#E2E8F0.
Error & Empty States:
- Unknown reference →
We couldn't find that booking.Recovery: linkFind my booking. - Cancel inside 2 h → button hidden; text
Too late to cancel online. Please call the shop. - Already cancelled → page shows the
Cancelledbadge and no button.
Acceptance Checks:
- ✓ Booking
Fri 17 May 10:00viewed at07:30same day →Cancel bookingvisible; viewed at08:30→ hidden with the call text. - ✓ After
Yes, cancel, the badge readsCancelledand the10:00slot appears in the slot list. - ✓ Time shown as
10:00 - 11:30for a 90-min service starting10:00.
6.3 Find My Booking
User Flow:
/manageshows fieldsBooking referenceandPhone number, and buttonFind booking.- Reference is uppercased as typed. Phone is compared on digits only.
- A match navigates to
/booking/[reference]; the phone digits are held insessionStoragefor that visit so the page may show the owner's details.
UI Copy & Colors:
- Heading
Find your booking; button#0F766Eon#FFFFFF.
Error & Empty States:
- No match →
No booking matches that reference and phone number.Recovery: retype. - 5 failed attempts in 10 min from one IP →
Too many tries. Please wait 10 minutes.
Acceptance Checks:
- ✓ Reference
k7m2xqtyped lowercase with phone07700 900123for a booking stored+44 7700 900123: matches on digits7700900123suffix and opens the page. - ✓ Right reference, wrong phone →
No booking matches that reference and phone number. - ✓ 6th wrong try within 10 min →
Too many tries. Please wait 10 minutes.
6.4 Admin Day View
User Flow:
/admin/login: enter the password, tapSign in. A correct password sets an httpOnly cookie valid 12 h./adminopens on today. A date control has‹, a date picker and›, plusToday.- Below, a list of the day's non-cancelled bookings in time order; each row shows
HH:mm - HH:mm, dog name, breed, service, size, owner name, a tap-to-call phone link, notes and a status badge. - Row actions by status:
pending→Confirm,Cancel;confirmed→Complete,No-show,Cancel.CancelasksCancel this booking?withKeep it/Yes, cancel. Add bookingopens the same 4-step form as 6.1 with the notesBooked by phoneprefilled and status set directly toconfirmed; admin can book inside 60 min and on closed days after a warning.- A toggle
Show cancelledreveals cancelled rows struck through.
UI Copy & Colors:
- Row
Confirmbutton#0F766Eon#FFFFFF;Canceltext#DC2626; rowOutside hourstag#B45309on#FEF3C7.
Error & Empty States:
- No bookings →
Nothing booked for this day. - Wrong password →
Wrong password.Recovery: retry; 5 fails →Too many tries. Please wait 10 minutes. - Cookie expired → redirect to login with
Please sign in again. - Action fails → toast
Couldn't update the booking. Try again.
Acceptance Checks:
- ✓ A
pendingbooking tappedConfirmshowsConfirmedbadge and buttons change toComplete,No-show,Cancel. - ✓ Tapping
Completeon a booking removes its action buttons (terminal). - ✓ Admin adds
Nail Trimsmall at12:00on a day closed byClosedDateafter warning → row appears withConfirmed. - ✓ After a customer cancels
13:00, togglingShow cancelledshows that row struck through.
6.5 Opening Hours & Closed Days
User Flow:
/admin/hourslists Monday to Sunday; each row has anOpentoggle and two time inputsOpensandCloses(15-min steps).Save hoursstores all seven rows.- Below,
Closed days: a date input, aReasonfield andAdd closed day; existing entries list withRemove. - Saving shows
Hours saved.
UI Copy & Colors:
- Success toast
Hours saved.#166534on#DCFCE7.
Error & Empty States:
Closesnot afterOpens→Closing time must be after opening time.Save is blocked.- Closed day already has bookings →
{n} booking(s) already exist on this date. They are kept; contact the customers.Day is still closed to new bookings. - No closed days →
No closed days set.
Acceptance Checks:
- ✓ Setting Wednesday to
10:00-15:00and saving → a customer's Wednesday chips run from10:00to the last start that fits before15:00. - ✓ Adding Monday 27 May as closed → that day is greyed out in the customer date strip.
- ✓
Opens 17:00,Closes 09:00→Closing time must be after opening time.
7) UI/UX
Palette:
| Token | Hex | Used for |
|---|---|---|
| Primary | #0F766E | Buttons, selected chips |
| Primary dark | #115E59 | Pressed state |
| Accent | #F59E0B | Highlights, logo paw |
| Success | #16A34A | Confirmed, saved |
| Danger | #DC2626 | Cancel, errors |
| Text | #0F172A | Body |
| Muted | #64748B | Secondary text |
| Surface | #F8FAFC | Page background |
Typography: Inter, 16 px base; headings 24 px bold; step titles 20 px; reference code 28 px monospace; captions 13 px.
Layout: single column, mobile first, max width 480 px centred for customer pages; admin day view widens to 960 px with a two-column timeline above 1024 px. Sticky bottom bar holds Back / Next on the form.
Motion:
| Interaction | Duration | Easing |
|---|---|---|
| Step change slide | 200 ms | ease-out |
| Slot chip select | 120 ms | ease-in-out |
| Dialog open | 150 ms | ease-out |
| Toast in/out | 250 ms | ease-in-out |
Timestamp display: Dates EEEE d MMMM (Tuesday 14 May); times 24-hour HH:mm; stored times are shown unchanged, no conversion.
Accessibility: text contrast ≥ 4.5:1, tap targets ≥ 44×44 px, visible focus ring #0F766E 2 px, labels on all icon buttons, chips reachable by keyboard.
8) Infrastructure Notes
Environment variables:
- Server only:
SUPABASE_SERVICE_ROLE_KEY,ADMIN_PASSWORD,ADMIN_COOKIE_SECRET - Browser-safe:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY
Client vs server: the browser only calls Next.js route handlers; slot calculation, booking creation, status changes and rate limiting run on the server; row-level security denies all direct table access to the anon key.
Platform limits:
| Limit | Platforms affected | Design answer |
|---|---|---|
| No push or reminders without a service | All | No reminders in V1; confirmation page is the record |
| Vercel Hobby cron once per day | Vercel | No scheduled jobs needed |
| Cookie cleared by browser | All | Admin signs in again |
9) Out of Scope (V1)
- Online payments or deposits: tempting to cut no-shows, but it adds Stripe, refunds and legal text; price is paid at the shop.
- SMS or email reminders: the obvious next ask, but it needs a provider, consent and a scheduler.
- Customer accounts and saved dogs: tempting for repeat clients, but the reference-plus-phone lookup is enough for one groomer.
- Multiple groomers or rooms: the slot logic assumes one table; parallel capacity changes every overlap rule.
- Editable services and prices in the admin: services are seeded; changes are made in the database.
- Reschedule in place: customers cancel and rebook; moving a booking adds a second overlap path.
- Vaccination records, photos and groom history: useful for a groomer, but not needed to take a booking.
10) Success Criteria
- ✓ A customer completes
Book now→Request bookingand lands on a page showing a 6-character reference and statusPending. - ✓
Full Groommedium shows90 min · £60;Bath & Brushsmall shows45 min · £30;Nail Trimlarge shows15 min · £10. - ✓ With Rex
09:00-11:00booked, no chip earlier than11:00appears for any service lasting more than 0 min on that day. - ✓ A booking whose
endwould pass closing time never appears as a chip (SaturdayFull Groomlarge last chip is11:00). - ✓ Two simultaneous submissions for the same start produce one success and one
Sorry, that time was just booked. Please pick another. - ✓ Cancelling a booking more than 2 h before
startfrees its slot immediately; inside 2 h the button is hidden. - ✓
/adminredirects to/admin/loginwhen signed out, and a wrong password showsWrong password. - ✓
Confirmturns aPendingbadge intoConfirmed;Completeremoves the action buttons. - ✓ Marking Monday 27 May closed greys it out in the customer date strip.
- ✓ No console errors during the full booking flow from
Book nowto the confirmation page.