- 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>
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>
* 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>
The /api/reports/clients endpoint was crashing with a 500 on every request
because a raw JavaScript Date passed into a sql template literal in .having()
cannot be serialized by postgres-js. The fix serializes it as an ISO string
with an explicit ::timestamptz cast.
Also adds reportsRouter.onError() and improves the frontend error message
to surface which specific endpoint failed and why.
Fixes#49
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The /api/reports/clients endpoint crashes with a 500 because
Drizzle's sql template literal in a HAVING clause cannot serialize
a JavaScript Date object — the postgres driver expects a string.
Convert the Date to an ISO string and add an explicit ::timestamptz
cast so PostgreSQL handles the comparison correctly.
Closesgroombook/groombook#49
Co-authored-by: Groom Book CEO <ceo@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Extract slot generation from book.ts into pure utility for unit testing.
Add 8 API unit tests and 4 web component tests with coverage thresholds.
Closes#39
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat: multi-groomer calendar view with per-groomer filtering
Add groomer view mode to the appointments calendar:
- Toggle between "Status" (existing) and "Groomer" color coding
- Per-groomer visibility toggles with color-coded buttons
- Appointments colored by assigned groomer in groomer view
- Groomer name shown on appointment blocks in groomer view
- Unassigned appointments shown in neutral gray
Satisfies groombook/groombook#11 requirements for side-by-side/unified
groomer schedule visibility and per-groomer filter/toggle.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat: tip and payment splitting between staff roles
Implements groombook/groombook#12 — track which staff worked on each
pet and calculate tip distribution based on who was involved.
Changes:
- DB: Add bather_staff_id to appointments (optional secondary staff)
- DB: Add invoice_tip_splits table (per-staff tip share ledger)
- API: appointments POST/PATCH accept batherStaffId
- API: GET /invoices/:id now includes tipSplits[]
- API: POST /invoices/:id/tip-splits — saves tip distribution
- API: GET /reports/tip-splits — payroll summary of tip earnings
- Frontend: Bather/Assistant select on New Appointment form
- Frontend: Tip Distribution section in Invoice Detail modal
- Auto-populates 70%/30% split when bather is assigned
- Editable percentages before payment; saved on Mark as Paid
- Displays recorded splits on paid invoices
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: remove unused staff import from invoices route
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>
* feat: recurring appointments with cascading change propagation
Implements GitHub issue #9 — recurring appointment scheduling with
configurable frequency and cascade edit/cancel options.
Changes:
- DB: add `recurring_series` table (frequency_weeks) and series_id /
series_index columns on appointments (migration 0003)
- API POST /appointments: accepts optional `recurrence` object
(frequencyWeeks + count) that creates a full series in one transaction
- API PATCH /appointments/🆔 new `cascadeMode` field
(this_only | this_and_future | all) applies time-delta shifts and
field updates across the series
- API DELETE /appointments/🆔 new `?cascade=` query param cancels
this_only / this_and_future / all series members
- Frontend: booking form gains a "Recurring appointment" checkbox with
frequency and count pickers; calendar chips show a ↻ recurring label;
detail modal shows "Recurring series" badge and a cascade-delete radio
picker for series appointments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: resolve TypeScript errors in recurring appointments route
Guard against possibly-undefined results from Drizzle .returning()
destructuring — use indexed access + explicit null checks instead of
array destructuring for the recurring_series insert, and add an early
throw when the series or first appointment row is missing.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
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>
- Add health_alerts column to pets table (migration 0001)
- Update DB schema, shared types, and API Zod validation
- Show health alerts prominently (red badge) in pet cards
- Add health alerts field to pet form with allergy/condition placeholder
- Add delete button per pet with confirmation dialog
- Add delete client button with cascade warning
Closesgroombook/groombook#2
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Fixes five bugs flagged in CEO code review (GitHub issues #18–22):
- #18: Wrap conflict check + insert/update in a DB transaction to
prevent double-booking race conditions under concurrent load.
- #19: PATCH conflict detection now falls back to the existing
appointment's staffId when staffId is omitted from the request body,
so rescheduling always checks for conflicts.
- #20: DELETE endpoint now soft-deletes (status = 'cancelled') instead
of hard-deleting, preserving audit trail and financial records.
- #21: Staff DELETE checks for existing non-cancelled appointments
before deleting and returns 409 if any are found, preventing orphaned
references.
- #22: AUTH_DISABLED=true now logs a startup warning in development and
calls process.exit(1) in production, preventing accidental auth
bypass in deployed environments.
Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Fix Dockerfiles to copy pnpm-lock.yaml (frozen-lockfile compliance)
- Add migrate target to API Dockerfile using builder stage
- Add migrate service to docker-compose that runs before API starts
- Add AUTH_DISABLED env var bypass to auth middleware for dev/Docker
- Proxy /api/ from nginx to API container (no CORS needed)
- Include initial Drizzle migration (0000_colossal_colossus.sql)
- Add .env.example with all configurable variables
- Update README with Docker self-hosting instructions
Closes#7
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>