Compare commits

...

19 Commits

Author SHA1 Message Date
Barcode Betty f6e6c3d75f fix(GRO-1633): add buildx network=host and provenance:false to web CI
CI / Test (pull_request) Successful in 20s
CI / Lint & Typecheck (pull_request) Successful in 25s
CI / Build & Push Docker Image (pull_request) Successful in 34s
GRO-1672 follow-up: mirror groombook/api CI fixes in groombook/web
- docker/setup-buildx-action: driver-opts network=host (DinD DNS fix)
- docker/build-push-action: provenance: false (registry push compat)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:15:14 +00:00
The Dogfather c83214cf42 Merge pull request 'fix(GRO-1414): update pet size value from x-large to xlarge' (#12) from fix/gro-1414-pet-size-enum into dev
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 21s
CI / Build & Push Docker Image (push) Failing after 2m20s
fix(GRO-1414): update pet size value from x-large to xlarge (#12)
2026-05-23 18:31:05 +00:00
The Dogfather 80101fc37c Merge pull request 'fix(GRO-1592): fallback auth baseURL to window.location.origin' (#15) from fix/gro-1592-sso-session-cookie into dev
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Image (push) Failing after 39s
2026-05-23 14:13:01 +00:00
Flea Flicker 8ee58471b2 docs(UAT_PLAYBOOK): add TC-AUTH-5.3.4 — SSO cookie after Authentik callback
CI / Test (pull_request) Successful in 14s
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Build & Push Docker Image (pull_request) Failing after 38s
Documents the acceptance criteria for GRO-1592: after completing
Authentik SSO login without VITE_API_URL set, the
__Secure-better-auth.session_token cookie must be present in the
browser and sent with subsequent /api/* calls.

Updated: UAT_PLAYBOOK.md §5.3

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 14:02:16 +00:00
Flea Flicker 35d31a984d fix(GRO-1592): fallback auth baseURL to window.location.origin
CI / Test (pull_request) Successful in 18s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Build & Push Docker Image (pull_request) Failing after 38s
When VITE_API_URL is not set (e.g. in Docker/container deployments
where the env var was never injected), fallback to
window.location.origin so the auth client uses relative URLs and
cookies are sent to the correct origin.

Previously the fallback was empty string "", which caused the auth
client to default to http://localhost:3000 — the nginx sub_filter
workaround only handles strings baked into the JS bundle at build
time, not runtime-constructed URLs.

Fixes: SSO session cookie not set in browser after Authentik callback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 13:57:47 +00:00
The Dogfather f62c0b112d Merge pull request 'feat(GRO-1173): admin UI buffer rules, service default buffer, pet size/coat' (#13) from flea-flicker/pet-profile-editor into dev
CI / Test (push) Successful in 15s
CI / Lint & Typecheck (push) Successful in 17s
CI / Build & Push Docker Image (push) Successful in 10s
CI / Test (pull_request) Successful in 15s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Image (pull_request) Successful in 10s
feat(GRO-1173): admin UI buffer rules, service default buffer, pet size/coat (#13)

Merged-By: The Dogfather (CTO)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 19:46:01 +00:00
Flea Flicker 034f4ab295 docs(GRO-1470): add UAT test cases for pet profile API persistence
CI / Test (pull_request) Successful in 13s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Image (pull_request) Successful in 20s
Add §5.23 covering:
- API persistence (page reload verification)
- Save error state (form stays open on failure)
- Saving indicator (spinner while in-flight)

Updated UAT_PLAYBOOK.md §5.23

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:32:35 +00:00
Flea Flicker f958dbdb4f Persist pet profile changes via PATCH /api/portal/pets/{petId}
CI / Test (pull_request) Successful in 15s
CI / Lint & Typecheck (pull_request) Successful in 18s
CI / Build & Push Docker Image (pull_request) Successful in 34s
- handlePetSave is now async; calls PATCH before updating local state
- API response used as source of truth for local state update
- Error state shown on API failure; edit form NOT cleared on failure
- Loading/saving indicator in PetForm while API call in flight

Refs: GRO-1470
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 19:17:12 +00:00
Flea Flicker 837b5f6d8a docs(GRO-1173): add UAT test cases for buffer rules, service default buffer, pet size/coat
CI / Test (pull_request) Successful in 16s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Image (pull_request) Successful in 17s
- §5.7: add TC-WEB-5.7.5 through TC-WEB-5.7.7 for pet size/coat in admin UI
- §5.8.1 (new): add Buffer Rules Management test cases
  - TC-WEB-5.8.2 through TC-WEB-5.8.7: create/edit/delete buffer rules, size/coat filtering, empty state
- §5.9: add TC-WEB-5.9.4 and TC-WEB-5.9.5 for service default buffer minutes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 14:52:42 +00:00
Flea Flicker 889e1e26ae feat(GRO-1173): add sizeCategory and coatType dropdowns to admin pet form
CI / Test (pull_request) Successful in 16s
CI / Lint & Typecheck (pull_request) Successful in 20s
CI / Build & Push Docker Image (pull_request) Successful in 33s
- PetForm interface: add sizeCategory and coatType fields
- EMPTY_PET: initialise new fields as empty strings
- openEditPet: pre-populate from pet.petSizeCategory and pet.coatType
- submitPet body: include petSizeCategory and coatType in POST/PATCH
- Pet form UI: add Size Category and Coat Type dropdowns after Breed field
  - Size: Small / Medium / Large / X-Large (maps to enum values)
  - Coat: Smooth / Double / Curly / Wire / Long / Hairless (maps to CoatType union)
  - Both optional — blank "Not set" option matches API optional semantics

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 14:44:55 +00:00
Flea Flicker f1bb7c4fa6 fix(GRO-1414): update pet size value from x-large to xlarge
CI / Test (pull_request) Successful in 14s
CI / Lint & Typecheck (pull_request) Successful in 18s
CI / Build & Push Docker Image (pull_request) Successful in 35s
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 06:58:56 +00:00
The Dogfather 56b11befe9 Merge pull request 'feat(GRO-1174): add pet size/coat dropdowns to booking wizard' (#8) from flea-flicker/pet-profile-editor into dev
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 17s
CI / Build & Push Docker Image (push) Successful in 37s
test merge
2026-05-21 00:43:10 +00:00
Scrubs McBarkley f70dd96c65 Merge pull request 'feat: extract groombook/web from monorepo (GRO-903)' (#1) from dev into main
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Image (push) Successful in 14s
feat: extract groombook/web from monorepo (GRO-903)

Bootstrap exception: dev → main

QA: Lint Roller (#2753)
CTO: The Dogfather (#2764)
CI: Lint & Typecheck ✓, Tests ✓, Docker Build ✓
UAT_PLAYBOOK.md: present
2026-05-20 15:26:27 +00:00
Chris Farhood 42f3e3211a fix(GRO-903): resolve CI/CD blockers on groombook/web PR #1
CI / Test (push) Successful in 15s
CI / Test (pull_request) Successful in 15s
CI / Lint & Typecheck (push) Successful in 17s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Image (push) Successful in 10s
CI / Build & Push Docker Image (pull_request) Successful in 11s
- Move CI workflow from .github/workflows/ to .gitea/workflows/
- Add uat branch to CI triggers (push and pull_request)
- Fix Dockerfile HEALTHCHECK to use wget instead of curl

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:01:46 +00:00
Chris Farhood 465db89ab4 fix(GRO-1361): remove unused X import and delete corrupted demo-pet images
CI / Test (push) Successful in 16s
CI / Test (pull_request) Successful in 15s
CI / Lint & Typecheck (push) Successful in 19s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Build & Push Docker Image (pull_request) Successful in 41s
CI / Build & Push Docker Image (push) Successful in 2m55s
- Remove unused 'X' import from lucide-react in PetProfiles.tsx
- Delete 10 corrupted demo-pet PNG files that contain Alibaba AccessDenied XML

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:09:20 +00:00
The Dogfather ee7fc2e9bf Merge pull request 'chore: add Renovate config (GRO-1081)' (#4) from add-renovate-config into dev
CI / Test (push) Successful in 15s
CI / Lint & Typecheck (push) Failing after 17s
CI / Build & Push Docker Image (push) Has been skipped
CI / Test (pull_request) Successful in 14s
CI / Lint & Typecheck (pull_request) Failing after 18s
CI / Build & Push Docker Image (pull_request) Has been skipped
chore: add Renovate config (GRO-1081)

Merge PR #4: add-renovate-config → dev
Approved by QA (Lint Roller) and CTO (The Dogfather).
2026-05-20 12:41:45 +00:00
The Dogfather c8610ec28d Merge pull request 'fix(ci): use Gitea registry for Docker push' (#9) from fix/ci-registry-auth into dev
CI / Test (push) Successful in 15s
CI / Lint & Typecheck (push) Failing after 17s
CI / Build & Push Docker Image (push) Has been skipped
CI / Lint & Typecheck (pull_request) Failing after 17s
CI / Test (pull_request) Successful in 13s
CI / Build & Push Docker Image (pull_request) Has been skipped
fix(ci): use Gitea registry for Docker push (#9)

GRO-1348

- Change Docker login from ghcr.io/GITHUB_TOKEN to git.farh.net/REGISTRY_TOKEN
- Update image tags from ghcr.io/groombook/web to git.farh.net/groombook/web
- Replace GitHub Actions cache with registry cache
2026-05-20 11:17:01 +00:00
Chris Farhood a582bd04b7 fix(ci): use Gitea registry for Docker push
CI / Lint & Typecheck (pull_request) Failing after 18s
CI / Test (pull_request) Successful in 15s
CI / Build & Push Docker Image (pull_request) Has been skipped
- Change Docker login from ghcr.io/GITHUB_TOKEN to git.farh.net/REGISTRY_TOKEN
- Update image tags from ghcr.io/groombook/web to git.farh.net/groombook/web
- Replace GitHub Actions cache (type=gha) with registry cache
- Remove GitHub Actions-specific permissions block
- GRO-1348

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-20 10:57:02 +00:00
Chris Farhood b8a9e8cc09 chore: add Renovate config
GRO-1081: add renovate.json to successor repos

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-14 17:42:22 +00:00
19 changed files with 115 additions and 105 deletions
@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [main, dev]
branches: [main, dev, uat]
pull_request:
branches: [main, dev]
branches: [main, dev, uat]
workflow_dispatch:
inputs:
ref:
@@ -78,6 +78,8 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
@@ -92,6 +94,7 @@ jobs:
context: .
file: Dockerfile
push: true
provenance: false
tags: |
git.farh.net/groombook/web:${{ steps.version.outputs.tag }}
${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/web:latest' || '' }}
+1 -1
View File
@@ -18,4 +18,4 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/ || exit 1
CMD wget --spider -q http://localhost:80/ || exit 1
+25
View File
@@ -69,6 +69,7 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
| TC-AUTH-5.3.1 | Auth client falls back to window.location.origin | Do not set `VITE_API_URL`, load app | Auth client uses `window.location.origin` as base URL |
| TC-AUTH-5.3.2 | Sign-in on localhost | Load app without `VITE_API_URL` on localhost:3000 | Auth client uses `http://localhost:3000` as base URL |
| TC-AUTH-5.3.3 | Sign-in on dev environment | Load app without `VITE_API_URL` on `https://dev.groombook.dev` | Auth client uses `https://dev.groombook.dev` as base URL |
| TC-AUTH-5.3.4 | SSO cookie set after Authentik callback (GRO-1592) | Complete Authentik SSO login on UAT without `VITE_API_URL` set | `__Secure-better-auth.session_token` cookie is present in browser; subsequent `/api/*` calls include the cookie and return 200 |
### 5.4 Session Persistence
@@ -103,6 +104,20 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
| TC-WEB-5.7.2 | Add pet | Click "Add Pet", fill form, submit | Pet created and linked to client |
| TC-WEB-5.7.3 | Edit pet details | Click on pet, modify details, save | Pet updated successfully |
| TC-WEB-5.7.4 | Grooming history view | View pet profile | Past appointments/grooming sessions displayed |
| TC-WEB-5.7.5 | Add pet with size/coat | Create pet with Size Category and Coat Type filled | Size and coat type persisted, visible on pet profile |
| TC-WEB-5.7.6 | Edit pet size/coat | Edit existing pet, change size/coat dropdowns | Updated values saved to pet record |
| TC-WEB-5.7.7 | Size/coat optional | Create pet without selecting size or coat | Pet created successfully, fields remain unset |
### 5.8.1 Buffer Rules Management UI (GRO-1173)
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.8.2 | Buffer rules section visible | Navigate to Settings | "Buffer Rules" section shown with description |
| TC-WEB-5.8.3 | Create buffer rule | Click "+ Add Rule", select service and buffer minutes, submit | Rule appears in list, matches service/size/coat |
| TC-WEB-5.8.4 | Edit buffer minutes inline | Click Edit on a rule, change minutes, click Save | New buffer value reflected in list |
| TC-WEB-5.8.5 | Delete buffer rule | Click Delete, confirm | Rule removed from list |
| TC-WEB-5.8.6 | Create rule with size/coat | Create rule with Size Category or Coat Type specified | Rule shows size/coat tags in list |
| TC-WEB-5.8.7 | Empty state | Navigate to Settings with no rules | "No buffer rules configured yet" message shown |
### 5.8 Appointment Scheduling UI
@@ -121,6 +136,8 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
| TC-WEB-5.9.1 | Service catalog loads | Navigate to Services | List of available services displayed |
| TC-WEB-5.9.2 | Create service | Click "New Service", fill form, submit | Service created successfully |
| TC-WEB-5.9.3 | Edit service | Click on service, modify details, save | Service updated successfully |
| TC-WEB-5.9.4 | Create service with default buffer | Create service with "Default buffer time" filled | Buffer shown in service list and form after save |
| TC-WEB-5.9.5 | Edit service buffer | Open existing service, change default buffer minutes | Updated value persisted after save |
### 5.10 Staff Management UI
@@ -259,6 +276,14 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
| TC-WEB-5.22.6 | Size and coat persisted | Save pet with size + coat, edit again | Both fields retain their selected values |
| TC-WEB-5.22.7 | Clear size | Select size, then clear back to default | Size cleared on save |
### 5.23 Pet Profile — API Persistence & Save UX (GRO-1470)
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.23.1 | Save pet — API persistence | Edit a pet, change a field (e.g. coat type), click Save, reload the page | Changed field retained after reload (proves PATCH round-trip to server) |
| TC-WEB-5.23.2 | Save pet — error state | Trigger an API save failure (e.g. network error) | Error message displayed; edit form stays open; no data cleared |
| TC-WEB-5.23.3 | Save pet — saving indicator | Click Save | Spinner/indicator shown while request is in flight; form controls disabled |
## 6. Pass/Fail Criteria
**Pass:**
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96C853FAECD363909C4A0</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96CFC84D7A9333708F278</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96D48D7892E37386B9ACB</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96C25663D703833F23607</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96D89851C843332073968</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96C9C5A03D33730C61AD8</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96BEB91911B30317E3BE8</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96BFB7B92D33535D6D90D</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96B8BDF4B473630A2E120</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>You have no right to access this object because of bucket acl.</Message>
<RequestId>69D96D78BFFCAD343037C27C</RequestId>
<HostId>hailuo-image-algeng-data-us.oss-us-east-1.aliyuncs.com</HostId>
<EC>0003-00000001</EC>
<RecommendDoc>https://api.alibabacloud.com/troubleshoot?q=0003-00000001</RecommendDoc>
</Error>
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":pinAllExceptPeerDependencies", "helpers:pinGitHubActionDigests"],
"labels": ["dependencies"],
"prConcurrentLimit": 5,
"packageRules": [
{"matchUpdateTypes": ["minor", "patch"], "groupName": "minor and patch dependencies", "automerge": false},
{"matchDepTypes": ["devDependencies"], "matchUpdateTypes": ["minor", "patch"], "automerge": true, "automergeType": "pr"}
]
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL ?? "",
baseURL: import.meta.env.VITE_API_URL || (typeof window !== "undefined" ? window.location.origin : ""),
});
export const { signIn, signOut, useSession, changePassword } = authClient;
+1 -1
View File
@@ -519,7 +519,7 @@ export function BookPage() {
<option value="small">Small (under 15 lbs)</option>
<option value="medium">Medium (1540 lbs)</option>
<option value="large">Large (4080 lbs)</option>
<option value="x-large">X-Large (over 80 lbs)</option>
<option value="xlarge">X-Large (over 80 lbs)</option>
</select>
</div>
<div>
+35
View File
@@ -25,6 +25,8 @@ interface PetForm {
cutStyle: string;
shampooPreference: string;
specialCareNotes: string;
coatType: string;
sizeCategory: string;
}
interface VisitLogForm {
@@ -38,6 +40,7 @@ const EMPTY_CLIENT: ClientForm = { name: "", email: "", phone: "", address: "",
const EMPTY_PET: PetForm = {
name: "", species: "Dog", breed: "", weightStr: "", dob: "",
healthAlerts: "", groomingNotes: "", cutStyle: "", shampooPreference: "", specialCareNotes: "",
coatType: "", sizeCategory: "",
};
const EMPTY_VISIT_LOG: VisitLogForm = { cutStyle: "", productsUsed: "", notes: "", groomedAt: "" };
@@ -209,6 +212,8 @@ export function ClientsPage() {
cutStyle: p.cutStyle ?? "",
shampooPreference: p.shampooPreference ?? "",
specialCareNotes: p.specialCareNotes ?? "",
coatType: p.coatType ?? "",
sizeCategory: p.petSizeCategory ?? "",
});
setPetFormError(null);
setShowPetForm(true);
@@ -315,6 +320,8 @@ export function ClientsPage() {
cutStyle: petForm.cutStyle || undefined,
shampooPreference: petForm.shampooPreference || undefined,
specialCareNotes: petForm.specialCareNotes || undefined,
coatType: petForm.coatType || undefined,
petSizeCategory: petForm.sizeCategory || undefined,
};
const res = editingPet
? await fetch(`/api/pets/${editingPet.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })
@@ -690,6 +697,34 @@ export function ClientsPage() {
<Field label="Breed (optional)">
<input value={petForm.breed} onChange={(e) => setPetForm((f) => ({ ...f, breed: e.target.value }))} style={inputStyle} />
</Field>
<Field label="Size Category (optional)">
<select
value={petForm.sizeCategory}
onChange={(e) => setPetForm((f) => ({ ...f, sizeCategory: e.target.value }))}
style={inputStyle}
>
<option value="">Not set</option>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
<option value="xlarge">X-Large</option>
</select>
</Field>
<Field label="Coat Type (optional)">
<select
value={petForm.coatType}
onChange={(e) => setPetForm((f) => ({ ...f, coatType: e.target.value }))}
style={inputStyle}
>
<option value="">Not set</option>
<option value="smooth">Smooth</option>
<option value="double">Double</option>
<option value="curly">Curly</option>
<option value="wire">Wire</option>
<option value="long">Long</option>
<option value="hairless">Hairless</option>
</select>
</Field>
<Field label="Weight kg (optional)">
<input type="number" step="0.1" min="0" value={petForm.weightStr} onChange={(e) => setPetForm((f) => ({ ...f, weightStr: e.target.value }))} style={inputStyle} />
</Field>
+14 -7
View File
@@ -1,5 +1,5 @@
import { useState } from "react";
import { X, Save, Plus, Star } from "lucide-react";
import { X, Save, Plus, Star, Loader2 } from "lucide-react";
import type { Pet, MedicalAlert, CoatType, AlertSeverity } from "@groombook/types";
const COAT_TYPES: CoatType[] = ["double", "wire", "curly", "smooth", "long", "hairless"];
@@ -9,15 +9,17 @@ type SizeOption = typeof SIZE_OPTIONS[number];
interface Props {
pet?: Pet;
onSave: (pet: Pet) => void;
onSave: (pet: Pet) => void | Promise<void>;
onCancel: () => void;
saving?: boolean;
saveError?: string | null;
}
function newAlert(): Omit<MedicalAlert, "id"> {
return { type: "", description: "", severity: "low" };
}
export function PetForm({ pet, onSave, onCancel }: Props) {
export function PetForm({ pet, onSave, onCancel, saving, saveError }: Props) {
const [name, setName] = useState(pet?.name ?? "");
const [breed, setBreed] = useState(pet?.breed ?? "");
const [weight, setWeight] = useState(pet?.weightKg ?? 0);
@@ -305,18 +307,23 @@ export function PetForm({ pet, onSave, onCancel }: Props) {
<button
type="button"
onClick={onCancel}
className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm text-stone-600 hover:bg-stone-50"
disabled={saving}
className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm text-stone-600 hover:bg-stone-50 disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)"
disabled={saving}
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover) disabled:opacity-50"
>
<Save size={14} />
Save
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
{saving ? "Saving…" : "Save"}
</button>
</div>
{saveError && (
<p className="text-sm text-red-500 text-center">{saveError}</p>
)}
</form>
</div>
);
+23 -3
View File
@@ -83,9 +83,27 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
const petHistory = appointments.appointments.filter(a => a.pet?.id === selectedPetId && new Date(a.startTime) <= new Date());
const editingPet = editingPetId ? pets.find(p => p.id === editingPetId) ?? null : null;
function handlePetSave(updatedPet: Pet) {
setPets(prev => prev.map(p => p.id === updatedPet.id ? updatedPet : p));
setEditingPetId(null);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
async function handlePetSave(updatedPet: Pet) {
setSaving(true);
setSaveError(null);
try {
const res = await fetch(`/api/portal/pets/${updatedPet.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", ...buildHeaders(sessionId) },
body: JSON.stringify(updatedPet),
});
if (!res.ok) throw new Error("Failed to save pet");
const saved: Pet = await res.json();
setPets(prev => prev.map(p => p.id === saved.id ? saved : p));
setEditingPetId(null);
} catch (e) {
setSaveError(e instanceof Error ? e.message : "Failed to save pet");
} finally {
setSaving(false);
}
}
if (editingPet) {
@@ -94,6 +112,8 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
pet={editingPet}
onSave={handlePetSave}
onCancel={() => setEditingPetId(null)}
saving={saving}
saveError={saveError}
/>
);
}