- Add petSizeCategory and petCoatType dropdowns to booking wizard
(after breed field, optional but encouraged)
- Pass selected values to GET /availability as query params
- large/x-large pets add service.defaultBufferMinutes to slot calculation
and appointment end time (buffer never shown to client)
- POST /appointments saves size/coat to pet record
- Confirmation step shows total duration (service + buffer if applicable)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Without this, Vite sees VITE_API_URL as undefined (not empty string) at
build time. The ?? operator only replaces null/undefined, not a missing var,
so better-auth receives undefined — which it treats as a relative path and
prepends window.location.origin at build time, resulting in the UAT URL being
baked in.
Explicitly setting ARG VITE_API_URL= (empty string) in the Dockerfile makes
Vite see it as defined with empty value, so the || fallback fires at runtime.
Fixes GRO-1280.
Vite bakes VITE_* vars at build time, so hardcoding a URL in .env.production
breaks CI E2E which runs on localhost. Now falls back to the browser origin
at runtime, which works correctly since nginx reverse-proxies /api to the
local API container.
Fixes GRO-1280.
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>
- Wrap conversation mocks in { items, nextCursor } response shape
(loadConversations reads json.items, bare array caused undefined.length crash)
- Guard scrollIntoView with ?. (jsdom doesn't implement it)
- Use getAllByText for text appearing in both preview and thread
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Extract Conversation interface fields to match API response:
replace lastMessageBody with lastMessage object, externalNumber with
clientPhone, remove staffReadAt
- loadConversations(): extract json.items array instead of raw array
- loadMessages(): extract json.items and reverse() for chronological order
- Update test mocks to use { items, nextCursor } response shape
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- Added GET /portal/conversation and GET /portal/conversation/messages endpoints
- Created Communication.api.ts with typed fetchers and React hooks
- Rewired Communication.tsx to use real API, removed mock data
- Added composer-disabled bar with "Reply from your phone" tooltip
- Added conversation route tests to portal.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Backend refund endpoint: allow refunds on paid invoices without stripePaymentIntentId (manual refund path)
- Backend GET /invoices/🆔 inline fetch cardLast4 + paymentStatus from Stripe when stripePaymentIntentId present
- Frontend: show Refund button on all paid invoices for managers (not just Stripe-backed ones)
- Seed: add stripePaymentIntentId (pi_test_*) to ~20% of paid invoices for Stripe-path testing
cc @cpfarhood
- CustomerPortal.tsx: change main from overflow-x-hidden to overflow-hidden
to properly clip child overflow in both axes
- BillingPayments.tsx: add overflow-x-auto to tab button row so long
button labels scroll instead of causing page-level overflow
- PetProfiles.tsx: already has overflow-x-auto on tab row — no change needed
Discovered in UAT by Shedward (DEF-2 and DEF-3 on GRO-754).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The inline async onClick handler already calls the refund API directly. The
separate issueRefund function was defined but never called, causing
@typescript-eslint/no-unused-vars CI failure on PR #351.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Cherry-pick of 628ed34 to fix @typescript-eslint/no-unused-vars
error on PR #351 Lint & Typecheck.
The issueRefund function was defined but never called. This commit:
- Removes the inline async onClick handler that bypassed issueRefund
- Wires the Refund button to open setShowRefundDialog(true) instead
- Uses issueRefund function (with refundAmount/refundError/refunding state)
- Adds manager role check before showing refund button
- Shows "Refunded" badge when invoice.stripeRefundId is set
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
- Add stripePaymentIntentId to the GET /invoices list query so the refund button
renders when seed data includes a payment intent ID
- Wrap /api/invoices/stats/summary in try/catch so errors return 200 with zero
defaults instead of 5xx, preventing the Invoices page from crashing on
mount for groomer-role sessions
Parent: GRO-882
Grandparent: GRO-816
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(gro-609): add payment stats to admin dashboard (AppointmentsPage)
- Fetch /api/invoices/stats/summary on mount and display Revenue/Outstanding/Refunds
summary cards above the calendar view on /admin
- Mirrors the same stats section already on /admin/invoices
- Gracefully handles errors via try/catch on the stats endpoint
Parent: GRO-882
Grandparent: GRO-816
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
All logo S3 interactions are now server-proxied:
- GET /api/admin/settings/logo streams image bytes directly instead of
returning a presigned S3 URL to the browser
- Upload already went through POST /api/admin/settings/logo/upload
- Frontend uses relative /api/admin/settings/logo path as img src,
never a raw S3 URL
- Appends cache-buster query param (?t=Date.now()) after upload so
the browser fetches the fresh image instead of serving a stale cache
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- PetProfiles.tsx: update AppointmentsResponse interface to use flat
appointments[] array instead of { upcoming, past }
- PetProfiles.tsx: update petHistory filter to use appointments.appointments
with date filter for past-only appointments
- portal.ts: change /api/portal/appointments response to { appointments: [] }
instead of { upcoming: [], past: [] }
- portal.ts: change /api/portal/pets response field names to match frontend
Pet interface: weightKg→weight, dateOfBirth→birthDate, photoKey→photoUrl,
groomingNotes→notes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add GET /api/invoices/:id/stripe-details endpoint to fetch card last4 and
payment status from Stripe
- Add getPaymentIntentDetails() to payment service
- Fix stats summary query to filter by startOfMonth
- Add cardLast4, paymentStatus, stripeRefundId transient fields to Invoice type
- Display Stripe details (card last4, payment status, refund status) in modal
- Add stripeRefundId and paymentFailureReason to Invoice schema (was missing in dev types)
Ref: GRO-609
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Remove role="dialog" and aria-modal="true" from outer backdrop div
- Keep ARIA attributes only on inner dialog div (the actual modal)
- Restore useEffect focus management: auto-focus first element,
Tab cycle wrapping, Escape key handler, focus restore on close
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Update Modal component to accept title and titleStyle props
- Add role="dialog", aria-modal="true", and aria-labelledby attributes
- Use useId() to generate stable ID for title heading association
- Update all 4 Modal call sites (New/Edit Client, Add/Edit Pet,
Log Grooming Visit, Permanently Delete Client) with title props
- Delete modal passes titleStyle for red color on warning
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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>
* fix(GRO-766): prevent horizontal overflow on portal mobile pages
- Add overflow-x-hidden to main content area in CustomerPortal
- Add w-full overflow-hidden to content wrapper div
- Add flex-wrap to BillingPayments tab button row
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-769): proxy logo uploads through API server to fix mixed content
The pre-signed URL flow used an internal HTTP endpoint for S3 uploads,
which browsers blocked as mixed content on HTTPS pages. Instead of
generating a pre-signed URL that the browser uploads to directly,
the new /logo/upload endpoint receives the file via multipart POST
and streams it to S3 from the API server using the internal endpoint.
This resolves the mixed content error that was blocking logo uploads
on dev.groombook.dev.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Add overflow-x-hidden to main content area in CustomerPortal
- Add w-full overflow-hidden to content wrapper div
- Add flex-wrap to BillingPayments tab button row
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Extend updateInvoiceSchema to accept optional tipSplits array in PATCH body
- Validate tip splits sum to 100% (10000 bps) when marking paid with tipCents > 0
- Return 422 if tipSplits not provided and no existing splits in DB
- Save tip splits atomically in same DB transaction as invoice status update
- Update frontend markPaid() to send tipSplits in PATCH body instead of separate POST
- Remove non-atomic POST /tip-splits call from markPaid flow
Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Add role="dialog", aria-modal="true", focus trap, Escape-to-close,
and focus-restore-on-close to Invoices.tsx and Clients.tsx Modal
components, and to the two inline modals in BillingPayments.tsx.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
QA found test assertion failures - tests were asserting the old (incorrect)
Authorization: Bearer header instead of the correct X-Impersonation-Session-Id.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace Authorization: Bearer with X-Impersonation-Session-Id in all 5
mutation handlers in Appointments.tsx (confirm, cancel, save-notes,
reschedule, booking). The portal backend validates X-Impersonation-Session-Id
header, not Authorization Bearer.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Exclude image/svg+xml from the frontend allowlist since SVG poses greater
XSS risk due to its ability to contain scripts, even with proper Content-Type
validation. The server-side validation (commit 8182870) still accepts SVG
and validates magic bytes, but the frontend restrict to safer bitmap formats
as specified in the issue.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add ALLOWED_LOGO_TYPES allowlist check before constructing data URL from
user-controlled logoBase64 and logoMimeType fields. Only MIME types that
the API explicitly accepts (image/png, image/jpeg, image/gif, image/webp,
image/svg+xml) can be rendered as data URLs.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add pre-submit validation in markPaid() that checks tip split percentages
sum to 100% before allowing the payment to be processed. This addresses
Finding #7 from the frontend code quality review (GRO-628).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Updates playwright baseURL to the canonical dev.groombook.dev FQDN
per canonical infra targets.
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Add X-Content-Type-Options, X-Frame-Options, Referrer-Policy, X-XSS-Protection,
and Permissions-Policy headers to server block and static assets location.
Co-Authored-By: Paperclip <noreply@paperclip.ing>