Two root causes fixed:
1. VITE_API_URL was empty in .env.production, so Better-Auth's client
had no baseURL and could not correctly route the OAuth callback.
2. OAuth callbackURL was window.location.origin (root path), causing
Better-Auth to redirect to / instead of /admin after login — since
unauthenticated users at / are redirected to /login, this created a
loop that appeared as 'session not persisting.'
With VITE_API_URL=https://uat.groombook.dev and callbackURL=/admin,
the callback lands on /admin which renders the admin layout and
correctly establishes the session cookie.
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Adds staff conversations API (GET /api/conversations, GET /api/conversations/:id/messages, POST /api/conversations/:id/messages) with auth scoping and cross-tenant protection
- Adds staffReadAt column to conversations table for unread tracking
- Adds staff Messages page with two-column inbox layout (thread list + conversation view + composer)
- Adds Messages entry to staff sidebar navigation
- Includes tests for the MessagesPage component
Part of GRO-106 (SMS/MMS integration) Phase 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Direct navigation to /admin/clients/{id} now:
- Fetches GET /api/clients/{id} on mount (unconditional)
- Fetches GET /api/pets?clientId= on mount
- Shows loading state while fetching
- Shows error state on failure (401/404/5xx)
- Preserves existing link-based navigation from ClientsPage
Added ClientDetailPage.tsx as a standalone route component.
Added 3 E2E tests covering direct nav, loading state, and error state.
Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Add flexShrink:0 to logo div to prevent shrinking
- Wrap Book + NAV_LINKS in scrollable div with overflow-x:auto, flex:1, minWidth:0
- Add flexShrink:0 to all nav links
- Move logout button outside scrollable div with flexShrink:0 instead of marginLeft:auto
- Keeps logout button always visible regardless of nav item count
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The OAuth callback was failing with "please_restart_the_process" because
Better-Auth's default DB-backed state (verification table) was unreliable —
the UAT hourly reset wipes all tables including verification records. Switch
to cookie-based state storage so the encrypted state survives in the browser
cookie across the redirect flow.
Also removes explicit redirectURI from socialProviders (Better-Auth derives
it from baseURL) and adds visible error feedback on the login page when
OAuth callbacks fail.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- auth.ts: add google/github social providers from better-auth/social-providers
- auth.ts: add getActiveProviders() to enumerate configured OAuth/social providers
- index.ts: add /api/auth/providers public endpoint for frontend
- App.tsx: update LoginPage to show Google/GitHub buttons based on /api/auth/providers response
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When SetupWizard completes POST /api/setup and navigates to /admin,
App.tsx still has needsSetup=true in React state, causing an immediate
redirect back to /setup. Pass onSetupComplete callback to SetupWizard
which clears the state before navigating, breaking the loop.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When a user was logged in as one client and switched to another via the
dev login selector, the portal pages (Home, My Pets, Appointments,
Billing) continued showing the original user's data.
Root cause: CustomerPortal was rendered unconditionally for all
non-/admin routes (including /login). Since CustomerPortal uses a
ref (initDone) to skip re-initialization on re-renders, navigating to
/login and back did not trigger session re-creation — the old session
remained in state.
Fix: make CustomerPortal conditional on pathname not being /login, so
it properly unmounts when the user switches. On re-navigation to /,
a fresh CustomerPortal mounts and creates a new session for the
selected dev user.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
cc @cpfarhood
Unauthenticated users saw a blank screen because the needsSetup null-guard
fired before the LoginPage render check. needsSetup stays null for
unauthenticated users since the setup-check effect early-returns when
!session. Now the login check runs first so users see the login page.
Co-authored-by: Flea Flicker <flea-flicker@groombook.io>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Scrubs McBarkley (CEO) <ceo-bot@groombook.farh.net>
## Changes
- Replace toNodeHandler with auth.handler(c.req.raw) sub-app mount for Hono compatibility
- Add /api/auth/ path skip in authMiddleware and resolveStaffMiddleware
- Add OIDC_INTERNAL_BASE env var for split-horizon (hairpin NAT) URL resolution
- Replace render-time signIn.social() with LoginPage component (fixes redirect loop)
- Change auth-client baseURL to relative (empty string) for deployed environments
- Add POST /api/portal/appointments/:id/reschedule endpoint with session auth
- Add RescheduleFlow modal, PetForm component, and wire Dashboard/Appointments UI
## CTO Note
Auth fix is P0-critical. Portal mock data (UAT blocker) predates this PR and is tracked separately in GRO-218.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(db): add Better-Auth schema tables (GRO-118)
Add user, session, account, and verification tables required by
Better-Auth's Drizzle adapter. Add nullable userId FK on staff to
link business identity to auth identity. Fix test fixtures and
factory to include the new column.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(api): mount Better-Auth handler at /api/auth/** (GRO-118)
- Import toNodeHandler from better-auth/node and auth from ./lib/auth.js
- Mount Better-Auth HTTP handler before auth middleware block
- Handles OAuth callbacks, sign-in/sign-out, session management
- Supports GET/POST/PUT/PATCH/DELETE/OPTIONS methods
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(api): replace JWT auth with Better-Auth session validation (GRO-118)
- Replace jose/jwtVerify with auth.api.getSession()
- Session token validated via cookie/header, DB-backed
- jwtPayload.sub now = Better-Auth user ID (not OIDC sub)
- Dev mode bypass preserved; production guard against AUTH_DISABLED preserved
- rbac.ts and tests updated in subsequent tasks
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(api): update resolveStaffMiddleware for Better-Auth userId (GRO-118)
- Remove JwtPayload import; use inline type in AppEnv
- Production and dev mode lookups now use staff.userId (not oidcSub)
- Backward compat: jwtPayload.sub now = Better-Auth user ID
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* chore(api): remove jose and openid-client deps (GRO-118)
- Remove unused jose and openid-client packages
- Regenerate pnpm lockfile
- Pre-existing Zod type errors resolved (1 remaining: JwtPayload in test)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(api): remove stale JwtPayload import from impersonation test (GRO-118)
auth.ts no longer exports JwtPayload — replace with inline type.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* test(api): update RBAC tests for Better-Auth userId (GRO-128)
- Add userId field to mock staff records (MANAGER, RECEPTIONIST, GROOMER)
- Update jwtPayload.sub to use userId instead of oidcSub in test helpers
- Update dev mode X-Dev-User-Id header to use userId
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* chore(api): upgrade zod to v4 with v3 compat layer (GRO-131)
- Bump zod from ^3.24.1 to ^4.3.6
- Bump @hono/zod-validator from ^0.4.3 to ^0.7.6
- Update all 12 route files to import from "zod/v3" compat layer
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(api): add Better-Auth configuration (GRO-118)
Exports the better-auth() instance configured with:
- Drizzle PG adapter
- genericOAuth plugin for Authentik OIDC
- 7-day session with 5-min cookie cache
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(web): install Better-Auth client and create config (GRO-118)
- Add better-auth to apps/web/package.json dependencies
- Create apps/web/src/lib/auth-client.ts with createAuthClient config
- Export signIn, signOut, useSession from the client
- Add vite-env.d.ts for Vite client types
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(web): use Better-Auth session state in App.tsx (GRO-126)
Add useSession hook to check Better-Auth session for production auth.
Redirect to Authentik sign-in when no session in production mode.
Dev mode flow (DevLoginSelector) preserved.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(web): scope devFetch interceptor to dev mode only (GRO-127)
* fix(api): validate BETTER_AUTH_SECRET and fix lockfile specifier (GRO-118)
- Add startup validation for BETTER_AUTH_SECRET when auth is enabled
- Fix pnpm-lock.yaml typescript specifier mismatch (^5.9.3 → ^5.7.3)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(web): mock authDisabled=true in App.test.tsx to fix CI failures
App.test.tsx "App navigation" tests were failing because the beforeEach
set authDisabled=false (production mode), which triggers the Better Auth
useSession() path. Since useSession() was not mocked in tests, the
component rendered null instead of the admin nav.
Now uses authDisabled=true + dev user in localStorage for those tests,
bypassing the Better Auth dependency while still testing the nav render.
Also removes duplicate App.test.js (compiled artifact).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(e2e): set authDisabled=true in fixtures to bypass Better Auth
The App.tsx production auth path calls signIn.social() when
authDisabled=false, causing E2E tests to render blank. The fixtures
must mock authDisabled=true so the dev login selector is used instead.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(e2e): add dev/config, dev/users, and branding mocks to navigation.spec.ts
Playwright matches routes in last-registered-first-served order, so the
catch-all /api/** handler was overwriting the authDisabled: true fixture.
Added specific handlers before the catch-all to ensure auth config,
user list, and branding responses are properly shaped.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(web): gate DevLoginSelector on API authDisabled, not import.meta.env.DEV
Move the DevLoginSelector rendering check from import.meta.env.DEV to the
API-driven authDisabled state, after the loading guard. Simplify the redirect
condition to remove the now-redundant pathname exception.
Fixes E2E login tests that were failing because DevLoginSelector was never
rendered in Docker production builds where import.meta.env.DEV is false.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(db): add missing migration journal entries 0012-0017
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(web): import App.tsx (not App.js) in App.test.tsx (#137)
* fix(web): mock /api/auth/get-session in Dev login selector test
The "redirects to /login when auth is disabled and no user selected" test
fails because useSession() from better-auth/react calls /api/auth/get-session
which wasn't mocked, causing sessionLoading to stay true indefinitely.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(web): import App.tsx (not App.js) in test to get authDisabled bypass
The Dev login selector test was importing the compiled App.js instead of
the source App.tsx. App.js has different logic (uses import.meta.env.DEV
instead of API-based authDisabled) and doesn't implement the
sessionLoading bypass needed for tests to pass.
Also applied the rawSession/rawSessionLoading refactor in App.tsx that
bypasses useSession result when authDisabled=true.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(web): use extensionless import for App in test
The `.tsx` extension in the import path is not allowed without
`allowImportingTsExtensions` (TS5097). Use extensionless `../App`
which resolves correctly via moduleResolution: "bundler".
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
* fix(auth): dev login resolve staff by id, not userId
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(rbac): fallback lookup for staff records predating Better-Auth userId (#140)
GRO-153: /api/staff returned 403 for all staff because resolveStaffMiddleware
looked up by staff.userId (Better-Auth ID) but dev login sent staff.id (PK),
and existing staff records had userId=NULL.
Changes:
- resolveStaffMiddleware: try userId first, fall back to staff.id (dev mode)
- resolveStaffMiddleware: try userId first, fall back to oidcSub (production)
- GET /api/dev/users: include userId field for DevLoginSelector
- DevLoginSelector: send userId (not staff.id) as X-Dev-User-Id
- Migration 0018: backfill userId for known demo staff
Co-authored-by: groombook-engineer[bot] <groombook-engineer@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Barkley Trimsworth <barkley@groombook.farh.net>
* fix(rbac): allow all staff roles to READ /api/staff
GRO-156 follow-up: RBAC middleware was blocking groomer/receptionist
from GET /api/staff. The QA review found 403 with "role groomer is not
permitted" after PR #140 deployment.
Fix: split the /staff/* guard — GET requests allow all roles
(groomer, receptionist, manager); write operations remain manager-only.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Flea Flicker <flea-flicker@paperclip.ing>
Co-authored-by: groombook-engineer[bot] <groombook-engineer@users.noreply.github.com>
Co-authored-by: Barkley Trimsworth <barkley@groombook.farh.net>
Add customer confirmation/cancellation flow for appointments:
- DB migration (0013): add confirmation_status, confirmed_at, cancelled_at,
confirmation_token to appointments table with index on token column
- schema.ts + factories.ts + types: expose new columns and ConfirmationStatus type
- GET /api/book/confirm/:token — tokenized confirm via email link (redirects)
- GET /api/book/cancel/:token — tokenized cancel via email link, single-use token
- POST /api/appointments/:id/confirm — portal/staff confirm endpoint
- POST /api/appointments/:id/cancel — portal/staff cancel endpoint
- Reminder emails now include Confirm/Cancel CTA buttons with tokenized links
- Reminder service generates confirmation token if missing before sending
- Staff calendar shows confirmation status indicator on appointment cards
and in the detail modal (confirmed ✓ / customer cancelled ✗)
- /booking/confirmed, /booking/cancelled, /booking/error redirect pages
- 23 new unit tests covering all new endpoints and edge cases
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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>
* 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>
* Improve admin UI visual design — polish look and feel
- Sticky nav bar with subtle shadow, branded GroomBook wordmark, green gradient Book button
- Consistent brand green (#4f8a6f) for primary buttons across all admin pages
- Tables wrapped in white cards with rounded corners and soft shadows
- Uppercase table headers with better spacing and hierarchy
- Input/button border-radius increased to 6px for softer feel
- Global CSS: button transitions, input focus states with brand green ring, subtle card shadows
- Background changed from plain white to light gray (#f0f2f5) for depth
- Reports: polished stat cards with shadows, refined section headers, card-wrapped tables
- Custom scrollbar styling for a cleaner look
Closesgroombook/groombook#58
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Fix test selectors for branded nav text
- Use regex /Groom\s*Book/ to match split-element brand text
- Use getByRole("link") for Book CTA to avoid matching brand <strong>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Fix brand text test to handle split-element rendering
The nav brand was changed to <span>Groom</span>Book for color styling,
but getByText with a regex can't match text split across child elements.
Use a custom text matcher that checks the STRONG element's textContent.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Fix E2E tests for split-element brand name
The brand is now <span>Groom</span>Book (no space), so Playwright's
getByText needs "GroomBook" instead of "Groom Book".
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Groom Book CTO <cto@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Groom Book CTO <cto@groombook.app>
* feat: flip routing — customer portal at /, admin at /admin
Move all admin dashboard routes under /admin prefix and mount the
customer portal at root (/). This gives customers clean, shareable
URLs while staff bookmark /admin.
- Admin routes: /admin, /admin/clients, /admin/services, etc.
- Customer portal: / (root)
- Admin nav "Customer Portal" link points to / for staff preview
- Updated tests for new route structure and fixed React 19 act compat
Closes#56
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(e2e): update tests for routing flip — admin at /admin, portal at /
All E2E tests now use /admin prefix for admin routes (clients, services,
staff, invoices, reports, book). Adds customer portal smoke test at /.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(e2e): use specific locator for customer portal test
getByText('Paws & Reflect') matched 3 elements causing strict mode
violation. Scope to navigation role for unique match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Groom Book CTO <cto@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
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>