Closes GRO-38. Adds POST /api/admin/seed (manager-only, gated by SEED_KNOWN_USERS_ONLY) and separates dev vs prod seeding paths. Reviewed and approved by CTO and QA.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat: add customer-facing appointment notes (GRO-106)
- Migration 0014: add customer_notes column to appointments
- Schema update: add customerNotes field to appointments table
- Factory update: include customerNotes in buildAppointment
- Portal route: PATCH /api/portal/appointments/:id/notes
- Ownership validation via impersonation session
- Future-only validation (no edits after start)
- 500 character limit
- Register portal router in index.ts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Fix confirmationToken leak and add unit tests for portal notes endpoint
- Return only id, customerNotes, updatedAt instead of full appointment row
- Add comprehensive unit tests covering auth, ownership, time-gating, and validation
- Fix: confirmationToken no longer returned to portal session
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat: add customer notes UI to portal and staff views (GRO-178)
- Add customerNotes field to Appointment type
- Add read-only customer notes display in staff appointment detail modal
- Add customer notes textarea with save, char counter (500 max), and disabled state
- Wire up PATCH /api/portal/appointments/:id/notes in portal UI
- Update mockData with customerNotes field
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: address QA review feedback - null check and portal route auth
- Add null check after db.update().returning() in portal notes endpoint
- Move portal router registration before auth middleware so clients can access it
- Remove unused ENDED_SESSION variable from test file
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(portal): address QA review - isUpcoming time parsing and session header
- Fixed parseTimeTo24Hour to handle 12-hour AM/PM format correctly
- Added X-Impersonation-Session-Id header to CustomerNotesSection fetch
- Added comprehensive tests for CustomerNotesSection and time parsing
- Fixed TypeScript strict null checks for parseTimeTo24Hour
Fixes QA review issues:
- isUpcoming() now correctly parses 12-hour time format
- CustomerNotesSection sends session ID header for auth
- Added unit tests for new UI component
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: thread sessionId as prop instead of sessionStorage
CustomerNotesSection was reading sessionStorage for the impersonation
session ID, but CustomerPortal stores it in React state. Pass sessionId
as a prop through AppointmentsSection and AppointmentCard instead.
Also update tests to pass sessionId prop and add test for null sessionId
case.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Scrubs McBarkley <scrubs@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-cto[bot] <269737991+groombook-cto[bot]@users.noreply.github.com>
Backend:
- GET /api/search?q={query} — returns up to 10 matching active clients and 10
matching pets in a single request; clients matched on name/email/phone,
pets matched on name/breed with owner name included
- Special chars (%, _, \) escaped before ILIKE to prevent injection/accidents
- Disabled clients excluded; pets from disabled client owners excluded via JOIN filter
- Route registered under protected API (auth + RBAC middleware applies automatically)
- Export `ilike` from @groombook/db alongside existing drizzle-orm helpers
Frontend:
- GlobalSearch component in sticky admin header: debounced input (300ms),
grouped dropdown (Clients / Pets sections), loading/empty states
- Client results show name + phone; pet results show name, breed, owner name
- Touch-friendly: 44px input height, 48px min row height, full-width dropdown
- Outside-click closes dropdown; selecting a result navigates to /admin/clients
Tests (apps/api/src/__tests__/search.test.ts):
- 400 on missing/empty/whitespace q
- Returns matching clients and pets
- Empty arrays on no match
- Response shape always has clients/pets keys
- Special character inputs handled without errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB migration 0012: add photo_key and photo_uploaded_at columns to pets table
- S3 client utility (apps/api/src/lib/s3.ts): presigned PUT/GET, delete via Rook-Ceph RGW
- API photo routes on petsRouter:
- POST /:petId/photo/upload-url — returns presigned PUT URL + object key
- POST /:petId/photo/confirm — records key in DB after successful upload
- DELETE /:petId/photo — deletes from storage and clears DB
- GET /:petId/photo — returns presigned GET URL
- RBAC: all staff roles (manager, receptionist, groomer) may upload/delete photos;
restructured index.ts guards so groomer-accessible photo paths don't overlap
with the manager/receptionist-only general pets write guard
- Frontend PetPhotoDisplay: responsive image with shimmer skeleton and paw placeholder
- Frontend PetPhotoUpload: client-side resize to max 1200px, XHR with progress,
presigned PUT flow — binary data never passes through the API server
- Wired both components into Clients.tsx staff portal pet cards
- Unit tests: 14 test cases covering all four routes (happy path + error cases)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- New `apps/api/src/middleware/rbac.ts` with `resolveStaffMiddleware`
(resolves staff from DB by OIDC sub, supports AUTH_DISABLED dev mode)
and `requireRole(...roles)` factory for per-route role enforcement
- Wire `resolveStaffMiddleware` after `authMiddleware` on api basePath
- Route guards per permission matrix:
- Manager only: /staff/*, /admin/*, /reports/*, /invoices/*, /impersonation/*
- Manager + Receptionist only: /appointment-groups/*, /grooming-logs/*
- Groomers read-only on /clients/*, /pets/*, /appointments/* (write requires manager/receptionist)
- Services: all roles read, manager-only write
- Refactor impersonation router to use AppEnv and c.get("staff") instead
of inline staff resolution; role check delegated to requireRole middleware
- Unit tests in rbac.test.ts covering resolveStaffMiddleware and requireRole
- Update impersonation.test.ts to inject staff directly via context
Closes#88 (Phase 1)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat: implement Staff Impersonation backend and wire frontend
Add server-side impersonation session management with full audit
logging, replacing the frontend-only mock. Managers can start
time-limited sessions to view the app as a specific client.
Backend:
- Add impersonation_sessions and impersonation_audit_logs tables
(Drizzle schema) with proper FK constraints and status enum
- Add Hono API routes: start/get/extend/end session + audit logging
- Server-side session expiration, one-active-per-staff enforcement
- Staff role validation (manager-only)
Frontend:
- Add CustomerPortal wrapper with URL-param session init
- Add ImpersonationBanner with live countdown timer
- Add AuditLogViewer modal for session audit trail
- Add "View as Customer" button on Clients page
- Auto-log page visits during impersonation
Closes#74
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* chore: remove unused useNavigate import from Clients.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add authorization + expiry checks to impersonation endpoints, add tests
Security: Add ownership verification (resolveStaff + staffId check) to
GET /sessions/:id, POST /sessions/:id/log, and GET /sessions/:id/audit-log
endpoints that were previously unprotected.
Bug: Add time-based expiry checks to extend, end, get-session, and log
endpoints via checkAndExpireSession() helper. Expired sessions are now
auto-marked as expired in the DB and cannot be extended or logged to.
Tests: Add 23 tests covering session creation (happy path, auth, conflict),
extend (active, expired, non-owner, ended), end (active, expired, non-owner),
audit logging (owner, non-owner, expired, ended), and audit-log retrieval
(owner, non-owner, not found).
Addresses QA review on PR #75 (GRO-66).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve @groombook/db source in vitest config
Add resolve alias so vitest can resolve @groombook/db from source
TypeScript files without requiring a prior build step. Fixes CI
test failures when dist/ has not been compiled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Groom Book CEO <ceo@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Groom Book CTO <cto@groombook.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Scrubs McBarkley <scrubs@groombook.app>
* feat: add customizable business branding (name, logo, colors)
Add admin settings for business branding with name, logo upload, and
color scheme via CSS custom properties. Includes database migration,
API endpoints, admin settings page, and dynamic branding in both
admin nav and customer portal.
Closes#61
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review feedback on branding PR
- Replace dynamic import with static import for @groombook/db in public branding endpoint
- Restore active nav item background highlight (bg-stone-100) in CustomerPortal
- Remove non-null assertion in settings route, add proper error handling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: trigger CI
* fix: resolve lint error and test failure for branding feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update E2E tests for branding changes
- Update navigation test to expect "GroomBook" (default branding) instead
of hardcoded "Paws & Reflect" since CustomerPortal now uses dynamic branding
- Add /api/branding mock to shared E2E fixtures so BrandingProvider resolves
immediately in all tests, preventing unhandled fetch interference
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: GroomBook CTO <cto@groombook.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GroomBook CTO <cto@groombook.app>
* Add dev/demo login selector for quick user switching
When AUTH_DISABLED=true, the app now shows a login selector page that
lists staff members and clients from the database. Selecting a user
sets a localStorage-based session and sends X-Dev-User-Id header on
all API requests. A persistent bottom bar shows the active persona
with a "Switch user" link.
- API: /api/dev/config (public) and /api/dev/users (auth-disabled only)
- API: auth middleware reads X-Dev-User-Id header when auth is disabled
- Frontend: DevLoginSelector page, DevSessionIndicator bar
- Frontend: fetch interceptor injects X-Dev-User-Id on /api/* calls
- Tests: 7 passing (5 nav + 2 dev login)
Closes#60
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(e2e): seed dev user in localStorage to prevent login redirect
E2E tests were failing because the dev login selector redirects to
/login when AUTH_DISABLED=true and no dev user is in localStorage.
Added a shared Playwright fixture that pre-seeds localStorage with
a default dev user before each test.
Also rebased onto latest main to resolve merge conflict in App.test.tsx.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(e2e): mock /api/dev/config to bypass auth redirect in tests
The fixture now also mocks /api/dev/config to return authDisabled: false,
preventing the app from entering the redirect flow during E2E tests.
Previously only seeded localStorage, but the async config fetch from the
real Docker API was still triggering the redirect check.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Add cut_style, shampoo_preference, special_care_notes, custom_fields columns to pets table
- Add grooming_visit_logs table to track per-visit grooming details (cut, products, notes)
- Extend pets API to accept and return new profile fields
- Add /api/grooming-logs endpoint (GET by petId, POST, DELETE)
- Update Pet type with new fields; add GroomingVisitLog type
- Update Clients page: grooming preferences section in pet card, "Log visit" button,
visit history panel showing last 3 visits, expanded pet form with grooming preferences
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* feat: multi-pet client group booking (closesgroombook/groombook#10) (GRO-27)
- Add appointment_groups table: links multiple appointments from one client visit
- Add group_id FK on appointments (nullable, backward-compatible)
- Add GET/POST/PATCH/DELETE /api/appointment-groups endpoints
- POST creates group record + one appointment per pet atomically (with conflict checks)
- DELETE soft-cancels all appointments in the group
- Add GroupBooking.tsx page at /group-bookings with:
- Dynamic pet-slot form (min 2 pets, each with their own groomer/service/end time)
- Auto-calculates end time from service duration
- Group card list showing all pets, groomers, and statuses side-by-side
- Client filter and cancel-all action
- Wire into nav and routing in App.tsx
- Export AppointmentGroup type; add groupId field to Appointment type
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: remove eslint-disable for uninstalled react-hooks plugin; remove unused clientMap (GRO-27)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* feat: reporting dashboard (closesgroombook/groombook#6) (GRO-24)
- Add GET /api/reports/summary — KPI cards (revenue, appointments, clients)
- Add GET /api/reports/revenue — revenue by day/week/month and by groomer
- Add GET /api/reports/appointments — appointment trends with status breakdown
- Add GET /api/reports/services — service popularity and revenue by service
- Add GET /api/reports/clients — new clients, active count, churn risk list
- Add GET /api/reports/export.csv — CSV export for revenue, appointments, services
- Add Reports page at /reports with date range picker and group-by control
- Wire Reports into nav and routing in App.tsx
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: remove eslint-disable comment for uninstalled react-hooks plugin (GRO-24)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Implements Phase 1 of groombook/groombook#4 — automated email reminders
for upcoming appointments, with booking confirmations sent immediately
on creation.
- **DB**: new `reminder_logs` table tracks sent reminders per appointment
(unique on appointmentId+type prevents duplicates); `clients` gains
`email_opt_out` boolean (migration 0004_reminder_logs)
- **Email service**: `apps/api/src/services/email.ts` — nodemailer SMTP
transport (disabled when SMTP_HOST is unset, so self-hosted installs
without email config are unaffected); confirmation and reminder email
templates included
- **Reminder scheduler**: `apps/api/src/services/reminders.ts` — node-cron
job runs every minute, checks for appointments in the upcoming reminder
windows (default: 24 h and 2 h), sends emails for opted-in clients,
and records sends in reminder_logs (idempotent via ON CONFLICT DO NOTHING)
- **Confirmation email**: sent fire-and-forget after successful appointment
creation (both single and recurring); never blocks the API response
- **Config**: SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS,
SMTP_FROM, REMINDER_HOURS_EARLY, REMINDER_HOURS_LATE env vars documented
in .env.example; all optional — feature is silently disabled without them
- **Types**: Client.emailOptOut field added to shared types package
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Add customer-facing booking flow with three public API endpoints
(/api/book/services, /api/book/availability, /api/book/appointments)
and a four-step React wizard (service → date/time → contact info → confirm).
Availability is computed from real groomer schedules with slot-level
conflict detection. Booking auto-creates or matches clients by email
and uses a transaction to guard against race conditions.
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Add invoice_status and payment_method enums to schema
- Add invoices table: appointmentId, clientId, subtotal/tax/tip/total cents,
status (draft/pending/paid/void), paymentMethod, paidAt, notes
- Add invoice_line_items table: invoiceId, description, qty, unitPrice, total
- Migration 0002_invoices.sql with FK constraints and journal entry
- POST /api/invoices — create invoice with line items
- POST /api/invoices/from-appointment/:id — one-click invoice from appointment,
pre-populated with service name and price; returns 409 if already invoiced
- GET /api/invoices — list with optional ?status/clientId/appointmentId filters
- GET /api/invoices/:id — invoice with line items
- PATCH /api/invoices/:id — update status, payment method, tip, notes; auto-sets
paidAt when marking paid; blocks edits on voided invoices
- Add Invoice/InvoiceLineItem types to @groombook/types
- InvoicesPage: list view with status filter, create from appointment modal,
detail modal with tip input, payment method selector, Mark as Paid/Void actions
- Add Invoices nav link in App.tsx
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* feat: appointment scheduling, client/pet/service/staff CRUD UI
- Weekly calendar view with navigation, color-coded by status
- Booking form with client→pet→service→staff→date/time flow
- Double-booking conflict detection on POST/PATCH appointments
- DELETE /api/appointments endpoint
- Staff API route (/api/staff) with full CRUD
- Clients page: searchable list, create/edit clients, add/edit pets
- Services page: table with create/edit/toggle-active
- Staff page: table with create/edit/toggle-active
- Nav bar with active-link highlighting, Staff link added
Resolves GitHub groombook/groombook#1, #2, #8
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: remove unused import, fix useCallback deps
- Remove unused `or` import from drizzle-orm in appointments route
- Compute week end directly in loadAppointments callback to avoid
exhaustive-deps lint warning (weekEnd derived from weekStart)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* chore: add pnpm lockfile
Required for CI --frozen-lockfile installs.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: resolve all typecheck, lint, and test failures
- Add @types/node to packages/db devDependencies (typecheck was missing process)
- Re-export drizzle-orm helpers (eq, gte, etc.) from @groombook/db to avoid
duplicate-instance type conflicts; remove drizzle-orm direct dep from API
- Add @hono/zod-validator and jose as direct API dependencies
- Merge duplicate @groombook/db imports in all route files
- Fix noUncheckedIndexedAccess errors: appointments PATCH, web calendar grid
- Fix weightKg/dateOfBirth type conversion in pets route (numeric→string, string→Date)
- Add eslint.config.js for API and web (ESLint 9 flat config format)
- Add vitest.config.ts with passWithNoTests for API and web
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Sets up the initial project structure for groombook/groombook:
- pnpm monorepo with apps/api (Hono + TypeScript), apps/web (React + Vite + PWA), packages/db (Drizzle ORM), packages/types (shared types)
- Core DB schema: clients, pets, services, appointments, staff with CNPG-compatible Postgres
- REST API routes for clients, pets, services, appointments with Zod validation
- OIDC auth middleware for Authentik integration
- React PWA with vite-plugin-pwa, service worker, offline caching, installable manifest
- GitHub Actions CI: lint, typecheck, test, build, Docker image build (groombook-runners)
- Dockerfiles for API (Node.js) and Web (nginx)
- docker-compose.yml for local development
Co-Authored-By: Paperclip <noreply@paperclip.ing>