Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ee621e8d9 | |||
| 1480a37de1 | |||
| f235dcad81 | |||
| 661bd4f902 | |||
| fe565861b9 | |||
| 7ef270312c | |||
| f58a0e569b | |||
| 2a401a4584 | |||
| e93017b279 | |||
| 27c59113e2 | |||
| db11e5f2bd | |||
| 980615b8e6 | |||
| 95c688764b | |||
| f549101962 | |||
| 62dc85b560 | |||
| 5bb8fbcb7d | |||
| bc21d6de09 | |||
| 32ef3bca4d | |||
| 47c29ecbc2 | |||
| de7386e47a | |||
| fdff0977ad | |||
| ec29f71974 | |||
| bd2a0d9516 | |||
| 0e5e9d1f16 | |||
| 3b4d0f15f6 | |||
| 87939e5413 | |||
| 4e3a038bf3 | |||
| 2aad7cb6a0 | |||
| 8349ea00de | |||
| 0c41640f59 | |||
| 0306c7fbd9 | |||
| 93da2f1dd8 | |||
| 62cbfe4e43 | |||
| db6a2a1bbf | |||
| 032a3796ba | |||
| cac8fc947e | |||
| 592be1301c |
-10
@@ -6,13 +6,3 @@ dist/
|
|||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
*.log
|
*.log
|
||||||
# Agent runtime artifacts — never commit
|
|
||||||
.gh-token
|
|
||||||
*.gh-token
|
|
||||||
**/.gh-token
|
|
||||||
.config/gh/
|
|
||||||
**/.config/gh/
|
|
||||||
**/AGENT_HOME/**
|
|
||||||
$AGENT_HOME/**
|
|
||||||
.claude/
|
|
||||||
.codex/
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This repository (`groombook/web`) is part of the GroomBook application stack. The
|
||||||
|
authoritative process, quality bar, and safety rules live in the shared
|
||||||
|
[`groombook/org`](https://git.farh.net/groombook/org) skills repository. Read
|
||||||
|
those first; this file is only a pointer.
|
||||||
|
|
||||||
|
## Authoritative skills
|
||||||
|
|
||||||
|
- **SDLC (branching, PRs, phases, handoffs):**
|
||||||
|
[`groombook/org/skills/sdlc/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/sdlc/SKILL.md)
|
||||||
|
- **Coding standards (priority ordering, PR discipline, tests, no-hardcoded-values, CalVer):**
|
||||||
|
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md)
|
||||||
|
- **Safety (no plaintext secrets, no direct `kubectl apply` to `groombook`, no self-merge, board approval for destructive actions):**
|
||||||
|
[`groombook/org/skills/safety/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/safety/SKILL.md)
|
||||||
|
|
||||||
|
For human contributors and humans reviewing agent work, see
|
||||||
|
[`CONTRIBUTING.md`](./CONTRIBUTING.md) in this repo for the phase-by-phase PR
|
||||||
|
flow and the `uat→main` merge-gate policy summary.
|
||||||
|
|
||||||
|
## Non-negotiable operational rules
|
||||||
|
|
||||||
|
These mirror the org skills; they are restated here so any agent landing in
|
||||||
|
this repo sees them without a cross-repo fetch.
|
||||||
|
|
||||||
|
- **All changes go through a PR.** Never push directly to `dev`, `uat`, or `main`.
|
||||||
|
- **Branch strategy:** `feature/<name>` → `dev` → `uat` → `main`. Engineers
|
||||||
|
always target `dev` first.
|
||||||
|
- **No self-merge contract.** The engineer who opened a PR clicks merge only
|
||||||
|
after the named reviewer (CI / QA / UAT / Security / CTO per phase)
|
||||||
|
approves. Issue-thread QA / UAT / security approvals do **not** clear the
|
||||||
|
Gitea `required_approvals` gate on `uat→main` — only a Gitea **Approve**
|
||||||
|
click from a member of the `approvals_whitelist_username` does. On this
|
||||||
|
repo that whitelist is `["gb_flea", "gb_dogfather"]` (engineer team).
|
||||||
|
Board-level accounts cannot give the Approve click by policy.
|
||||||
|
- **Always include `cc @cpfarhood`** at the bottom of every PR body for
|
||||||
|
board visibility (not as a reviewer).
|
||||||
|
- **Secrets in code are forbidden.** Use Bitnami Sealed Secrets; never commit
|
||||||
|
plaintext. See the `safety` skill.
|
||||||
|
- **Production (`groombook` namespace) is Flux-managed.** Never
|
||||||
|
`kubectl apply` directly. Infrastructure changes go through PRs in
|
||||||
|
`groombook/infra`.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
See the repo's own README, package scripts, and CI workflow. The
|
||||||
|
authoritative pipeline (Gitea Actions, image build, deploy hooks) is the
|
||||||
|
shared `groombook/infra` overlay; do not reimplement it here.
|
||||||
|
|
||||||
|
## When uncertain
|
||||||
|
|
||||||
|
If a task conflicts with the org skills, **the org skills win**. Open an
|
||||||
|
issue in `groombook/org` to propose a change rather than encoding a local
|
||||||
|
exception.
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# Contributing to `groombook/web`
|
||||||
|
|
||||||
|
Thanks for contributing. This document is the human-facing companion to
|
||||||
|
[`AGENTS.md`](./AGENTS.md) and the authoritative
|
||||||
|
[`groombook/org`](https://git.farh.net/groombook/org) skills. The org skills
|
||||||
|
govern; this file is a quick-reference for the human/agent PR flow in this
|
||||||
|
repo.
|
||||||
|
|
||||||
|
## Branch strategy
|
||||||
|
|
||||||
|
Three long-lived branches; one PR per promotion step.
|
||||||
|
|
||||||
|
| Branch | Environment | Who merges | Prerequisites for merge |
|
||||||
|
|---------|-------------|-----------|-------------------------|
|
||||||
|
| `dev` | Dev | Engineer | CI passes |
|
||||||
|
| `uat` | UAT | Engineer | QA code review approval |
|
||||||
|
| `main` | Production | Engineer | UAT validation + CTO Gitea Approve when the `uat→main` merge-gate policy applies (see below) |
|
||||||
|
|
||||||
|
Engineers always target `dev` first. Feature branches: `<agent-name>/<short-description>`.
|
||||||
|
|
||||||
|
## Phase-by-phase PR flow
|
||||||
|
|
||||||
|
### Phase 1 — Dev
|
||||||
|
|
||||||
|
1. Branch from `dev`: `git checkout -b <name>/<short-description> origin/dev`.
|
||||||
|
2. Write code + tests. Run unit tests, type check, and lint locally (or rely on CI).
|
||||||
|
3. Open a PR against `dev`:
|
||||||
|
```bash
|
||||||
|
tea pr create --base dev --title "..." --body "..."
|
||||||
|
```
|
||||||
|
Include `cc @cpfarhood` at the bottom of the body for board visibility.
|
||||||
|
4. CI must pass. CI green → engineer self-merges.
|
||||||
|
5. CI builds and deploys to Dev automatically.
|
||||||
|
|
||||||
|
### Phase 2 — UAT promotion
|
||||||
|
|
||||||
|
1. Open a PR from `dev` to `uat`.
|
||||||
|
2. CI must pass.
|
||||||
|
3. **QA (Lint Roller)** reviews and approves on the Gitea PR.
|
||||||
|
4. QA approved → engineer self-merges.
|
||||||
|
5. CI builds and deploys to UAT automatically.
|
||||||
|
|
||||||
|
### Phase 3 — UAT regression + Security review
|
||||||
|
|
||||||
|
1. **UAT (Shedward Scissorhands)** runs full regression against UAT — every
|
||||||
|
feature, old and new, no exceptions.
|
||||||
|
2. **Security (Barkley Trimsworth)** reviews the changes.
|
||||||
|
3. Failures in either gate bounce back to Phase 1.
|
||||||
|
|
||||||
|
### Phase 4 — Production promotion (`uat → main`)
|
||||||
|
|
||||||
|
This is the gate the org PR
|
||||||
|
[`groombook/org#13`](https://git.farh.net/groombook/org/pulls/13) defines.
|
||||||
|
The full rule is in
|
||||||
|
[`groombook/org/skills/sdlc/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/sdlc/SKILL.md)
|
||||||
|
and
|
||||||
|
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md);
|
||||||
|
the summary is below.
|
||||||
|
|
||||||
|
**The CTO Gitea Approve click is NOT the default gate.** Once the four
|
||||||
|
pre-gates (QA, UAT deploy, UAT regression, security) are green, the engineer
|
||||||
|
self-merges.
|
||||||
|
|
||||||
|
**A CTO Gitea Approve click IS required** only for PRs in one of three
|
||||||
|
categories:
|
||||||
|
|
||||||
|
1. **Novel auth / session paths** — login, OIDC, OOBE, session middleware,
|
||||||
|
token issuance, password reset, MFA, new auth provider integrations.
|
||||||
|
Routine auth-gated UI (button styling, error messages, form layout) is
|
||||||
|
**not** in this category.
|
||||||
|
2. **Infra / prod-affecting merges** — deploys, infra manifests, secrets,
|
||||||
|
GitOps overlays, CI/CD, `main` branch protection, production
|
||||||
|
routing/ingress, prod state mutations. All Phase 5 infra overlay PRs in
|
||||||
|
`groombook/infra` require CTO Gitea Approve without exception.
|
||||||
|
3. **Risk-flagged merges** — `risk:cto-approve` label, or explicit CTO/CEO
|
||||||
|
sign-off request in the PR or issue thread.
|
||||||
|
|
||||||
|
The engineer opens the `uat→main` PR, classifies it against the three
|
||||||
|
categories above, and adds `cc @cpfarhood`. If the PR is in scope, the CTO
|
||||||
|
clicks Approve; once approved (and the four pre-gates are green), the
|
||||||
|
engineer merges.
|
||||||
|
|
||||||
|
### Phase 5 — Production deployment
|
||||||
|
|
||||||
|
A separate PR in `groombook/infra` bumps the overlay image tag for prod.
|
||||||
|
Handed to QA (Lint Roller) for review, then self-merged by the engineer.
|
||||||
|
|
||||||
|
## The four pre-gates (uat→main)
|
||||||
|
|
||||||
|
A `uat→main` PR is mergeable when **all four** are green:
|
||||||
|
|
||||||
|
1. **QA code review** — done on the dev→uat promotion PR.
|
||||||
|
2. **UAT deploy** — the UAT image built from the uat tip is live in UAT.
|
||||||
|
3. **UAT regression** — Shedward's full-feature UAT pass is green (no
|
||||||
|
pre-existing defects, no new defects).
|
||||||
|
4. **Security review** — Barkley's security code review is green.
|
||||||
|
|
||||||
|
Issue-thread QA / UAT / security approvals do **not** clear the Gitea
|
||||||
|
`required_approvals` gate. Only a Gitea **Approve** click from a member of
|
||||||
|
the `approvals_whitelist_username` for `main` clears it. In this repo that
|
||||||
|
whitelist is the engineer team (`gb_flea`, `gb_dogfather`).
|
||||||
|
|
||||||
|
## Style, tests, and quality bar
|
||||||
|
|
||||||
|
See
|
||||||
|
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md)
|
||||||
|
for the engineering priority ordering, test requirements, no-hardcoded-values
|
||||||
|
rules, CalVer versioning policy, and the `git.farh.net` container registry
|
||||||
|
policy.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
See
|
||||||
|
[`groombook/org/skills/safety/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/safety/SKILL.md)
|
||||||
|
for the non-negotiable rules: no plaintext secrets, no `kubectl apply` to
|
||||||
|
`groombook`, no self-merge, no direct `tofu` runs, board approval for
|
||||||
|
destructive actions, escalation protocol.
|
||||||
+3
-18
@@ -291,18 +291,12 @@ the seeded UAT customer (`uat-customer@groombook.dev`), not just unit-rendered.
|
|||||||
| TC-WEB-5.13.1 | Revenue charts | Navigate to Reports | Revenue charts display with data |
|
| TC-WEB-5.13.1 | Revenue charts | Navigate to Reports | Revenue charts display with data |
|
||||||
| TC-WEB-5.13.2 | Utilization graphs | View reports | Staff/resource utilization graphs visible |
|
| TC-WEB-5.13.2 | Utilization graphs | View reports | Staff/resource utilization graphs visible |
|
||||||
|
|
||||||
### 5.14 Settings UI (manager / super-user only — GRO-2513)
|
### 5.14 Settings UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-5.14.1 | Manager sees Settings tab | Sign in as `uat-manager`, go to `/admin` | **Settings** link is visible in the admin nav bar |
|
| TC-WEB-5.14.1 | Configuration page | Navigate to Settings | Settings page loads without errors |
|
||||||
| TC-WEB-5.14.2 | Manager loads Settings page (200, no 403) | Click **Settings** in the nav | Page loads with Branding & Appearance form; DevTools → Network shows `GET /api/admin/settings` → **200**. Zero 403 responses anywhere in the Network tab. |
|
| TC-WEB-5.14.2 | Form interactions | Modify settings, save | Settings saved successfully, changes reflected |
|
||||||
| TC-WEB-5.14.3 | Manager can save branding | Modify Business Name, click Save | `PATCH /api/admin/settings` → 200; success message shown |
|
|
||||||
| TC-WEB-5.14.4 | Super-user sees auth-provider section | Sign in as a super-user, navigate to Settings | Auth provider config section is visible below Branding |
|
|
||||||
| TC-WEB-5.14.5 | Groomer does NOT see Settings tab | Sign in as `uat-groomer`, go to `/admin` | **Settings** link is **absent** from the nav bar. Network panel shows zero requests to `/api/admin/settings`. |
|
|
||||||
| TC-WEB-5.14.6 | Groomer navigating directly to `/admin/settings` is redirected | While signed in as `uat-groomer`, navigate to `https://uat.groombook.dev/admin/settings` | Browser redirects to `/admin` (Appointments page). No 403 error in Network tab, no error UI. |
|
|
||||||
| TC-WEB-5.14.7 | Receptionist does NOT see Settings tab | Sign in as `uat-receptionist` (if seeded), go to `/admin` | **Settings** link is **absent** from the nav bar. Network panel shows zero requests to `/api/admin/settings`. |
|
|
||||||
| TC-WEB-5.14.8 | Shared staff endpoints still work for groomer | Sign in as `uat-groomer` and navigate through Appointments, Clients, Staff pages | All return 200. No 403 on any shared endpoint. |
|
|
||||||
|
|
||||||
### 5.15 Navigation
|
### 5.15 Navigation
|
||||||
|
|
||||||
@@ -320,15 +314,6 @@ the seeded UAT customer (`uat-customer@groombook.dev`), not just unit-rendered.
|
|||||||
| TC-WEB-5.16.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met |
|
| TC-WEB-5.16.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met |
|
||||||
| TC-WEB-5.16.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input |
|
| TC-WEB-5.16.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input |
|
||||||
|
|
||||||
#### 5.16a Portal Tab Rows — Mobile Overflow (GRO-730 / GRO-1026)
|
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
|
||||||
|---|----------|-------|----------|
|
|
||||||
| TC-WEB-5.16.4 | My Pets tab row — horizontal scroll, no visible scrollbar | Sign in as customer → My Pets. Set viewport to 390px. If 3+ pets are seeded, the pet-selector row overflows. | Pet selector row scrolls horizontally; native scrollbar is **not** visible (`scrollbar-width: none` / `scrollbar-hide` applied). |
|
|
||||||
| TC-WEB-5.16.5 | My Pets section tab row — no visible scrollbar | On the same My Pets view, observe the tabs row (Basic Info / Medical / Grooming / History). | Tabs row scrolls horizontally when needed; native scrollbar is not visible. |
|
|
||||||
| TC-WEB-5.16.6 | Billing/Payments tab row — no wrap, no visible scrollbar | Sign in as customer → Billing/Payments at 390px. | Tab row (Invoices / Payment Methods / Packages) does **not** wrap to a second line; scrolls horizontally if needed; native scrollbar not visible. |
|
|
||||||
| TC-WEB-5.16.7 | Desktop — no visual regression | Open My Pets and Billing/Payments at ≥1024px. | No layout change; tab rows display identically to before the fix. |
|
|
||||||
|
|
||||||
### 5.17 Error & Empty States
|
### 5.17 Error & Empty States
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|
|||||||
+2
-19
@@ -187,17 +187,6 @@ function AdminLayout() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { branding } = useBranding();
|
const { branding } = useBranding();
|
||||||
const [staffUser, setStaffUser] = useState<{ role: string; isSuperUser: boolean } | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("/api/staff/me")
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((u) => setStaffUser({ role: u.role, isSuperUser: !!u.isSuperUser }))
|
|
||||||
.catch(() => setStaffUser({ role: "", isSuperUser: false }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const canSettings = staffUser !== null && (staffUser.role === "manager" || staffUser.isSuperUser);
|
|
||||||
const visibleNavLinks = NAV_LINKS.filter(({ to }) => to !== "/admin/settings" || canSettings);
|
|
||||||
|
|
||||||
const logoSrc = branding.logoBase64 && branding.logoMimeType
|
const logoSrc = branding.logoBase64 && branding.logoMimeType
|
||||||
? `data:${branding.logoMimeType};base64,${branding.logoBase64}`
|
? `data:${branding.logoMimeType};base64,${branding.logoBase64}`
|
||||||
@@ -262,7 +251,7 @@ function AdminLayout() {
|
|||||||
>
|
>
|
||||||
Book
|
Book
|
||||||
</Link>
|
</Link>
|
||||||
{visibleNavLinks.map(({ to, label }) => {
|
{NAV_LINKS.map(({ to, label }) => {
|
||||||
const active =
|
const active =
|
||||||
to === "/admin"
|
to === "/admin"
|
||||||
? location.pathname === "/admin"
|
? location.pathname === "/admin"
|
||||||
@@ -319,13 +308,7 @@ function AdminLayout() {
|
|||||||
<Route path="/group-bookings" element={<GroupBookingPage />} />
|
<Route path="/group-bookings" element={<GroupBookingPage />} />
|
||||||
<Route path="/routes" element={<RoutesPage />} />
|
<Route path="/routes" element={<RoutesPage />} />
|
||||||
<Route path="/reports" element={<ReportsPage />} />
|
<Route path="/reports" element={<ReportsPage />} />
|
||||||
<Route path="/settings" element={
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
staffUser === null
|
|
||||||
? null
|
|
||||||
: canSettings
|
|
||||||
? <SettingsPage />
|
|
||||||
: <Navigate to="/admin" replace />
|
|
||||||
} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,15 +78,6 @@ input:focus, select:focus, textarea:focus {
|
|||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Scrollbar hide utility ─── */
|
|
||||||
.scrollbar-hide {
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Scrollbar polish ─── */
|
/* ─── Scrollbar polish ─── */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|||||||
+42
-57
@@ -86,66 +86,51 @@ export function SettingsPage() {
|
|||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Load user role first, then gate settings/auth-provider fetches on role
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/staff/me")
|
fetch("/api/admin/settings")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((u) => {
|
.then(async (data) => {
|
||||||
const user = u as CurrentUser;
|
// The logo is now proxied through the API server so the browser
|
||||||
setCurrentUser(user);
|
// never receives an S3 URL — use the proxy path directly as the src.
|
||||||
const isManager = user.role === "manager" || user.isSuperUser;
|
setForm({
|
||||||
|
businessName: data.businessName ?? "GroomBook",
|
||||||
if (isManager) {
|
primaryColor: data.primaryColor ?? "#4f8a6f",
|
||||||
fetch("/api/admin/settings")
|
accentColor: data.accentColor ?? "#8b7355",
|
||||||
.then((r) => r.json())
|
logoKey: data.logoKey ?? null,
|
||||||
.then((data) => {
|
logoUrl: data.logoKey ? "/api/admin/settings/logo" : null,
|
||||||
setForm({
|
logoBase64: data.logoBase64 ?? null,
|
||||||
businessName: data.businessName ?? "GroomBook",
|
logoMimeType: data.logoMimeType ?? null,
|
||||||
primaryColor: data.primaryColor ?? "#4f8a6f",
|
});
|
||||||
accentColor: data.accentColor ?? "#8b7355",
|
|
||||||
logoKey: data.logoKey ?? null,
|
|
||||||
logoUrl: data.logoKey ? "/api/admin/settings/logo" : null,
|
|
||||||
logoBase64: data.logoBase64 ?? null,
|
|
||||||
logoMimeType: data.logoMimeType ?? null,
|
|
||||||
});
|
|
||||||
setLoaded(true);
|
|
||||||
})
|
|
||||||
.catch(() => setLoaded(true));
|
|
||||||
} else {
|
|
||||||
setLoaded(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isSuperUser) {
|
|
||||||
fetch("/api/admin/auth-provider")
|
|
||||||
.then(async (r) => {
|
|
||||||
if (r.ok) return r.json();
|
|
||||||
if (r.status === 404) return null;
|
|
||||||
throw new Error(`HTTP ${r.status}`);
|
|
||||||
})
|
|
||||||
.then((auth) => {
|
|
||||||
if (auth) {
|
|
||||||
setAuthConfig(auth as AuthProviderConfig);
|
|
||||||
setAuthForm({
|
|
||||||
providerId: (auth as AuthProviderConfig).providerId,
|
|
||||||
displayName: (auth as AuthProviderConfig).displayName,
|
|
||||||
issuerUrl: (auth as AuthProviderConfig).issuerUrl,
|
|
||||||
internalBaseUrl: (auth as AuthProviderConfig).internalBaseUrl ?? "",
|
|
||||||
clientId: (auth as AuthProviderConfig).clientId,
|
|
||||||
clientSecret: (auth as AuthProviderConfig).clientSecret,
|
|
||||||
scopes: (auth as AuthProviderConfig).scopes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setAuthLoaded(true);
|
|
||||||
})
|
|
||||||
.catch(() => setAuthLoaded(true));
|
|
||||||
} else {
|
|
||||||
setAuthLoaded(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
setAuthLoaded(true);
|
})
|
||||||
});
|
.catch(() => setLoaded(true));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load current user (for isSuperUser check) and auth provider config
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
fetch("/api/staff/me").then((r) => r.json()).catch(() => null),
|
||||||
|
fetch("/api/admin/auth-provider").then(async (r) => {
|
||||||
|
if (r.ok) return r.json();
|
||||||
|
if (r.status === 404) return null;
|
||||||
|
throw new Error(`HTTP ${r.status}`);
|
||||||
|
}).catch(() => null),
|
||||||
|
]).then(([user, auth]) => {
|
||||||
|
setCurrentUser(user as CurrentUser | null);
|
||||||
|
if (auth) {
|
||||||
|
setAuthConfig(auth as AuthProviderConfig);
|
||||||
|
setAuthForm({
|
||||||
|
providerId: (auth as AuthProviderConfig).providerId,
|
||||||
|
displayName: (auth as AuthProviderConfig).displayName,
|
||||||
|
issuerUrl: (auth as AuthProviderConfig).issuerUrl,
|
||||||
|
internalBaseUrl: (auth as AuthProviderConfig).internalBaseUrl ?? "",
|
||||||
|
clientId: (auth as AuthProviderConfig).clientId,
|
||||||
|
clientSecret: (auth as AuthProviderConfig).clientSecret,
|
||||||
|
scopes: (auth as AuthProviderConfig).scopes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setAuthLoaded(true);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function BillingPaymentsInner({ sessionId, readOnly }: BillingPaymentsProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{([
|
{([
|
||||||
{ id: "invoices" as const, label: "Invoices", icon: DollarSign },
|
{ id: "invoices" as const, label: "Invoices", icon: DollarSign },
|
||||||
{ id: "payment" as const, label: "Payment Methods", icon: CreditCard },
|
{ id: "payment" as const, label: "Payment Methods", icon: CreditCard },
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Pet Selector */}
|
{/* Pet Selector */}
|
||||||
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
|
<div className="flex gap-3 overflow-x-auto pb-1">
|
||||||
{pets.map(p => (
|
{pets.map(p => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
@@ -191,7 +191,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-1 bg-white rounded-xl border border-stone-200 p-1 overflow-x-auto scrollbar-hide">
|
<div className="flex gap-1 bg-white rounded-xl border border-stone-200 p-1 overflow-x-auto">
|
||||||
{([
|
{([
|
||||||
{ id: "info", label: "Basic Info", icon: PawPrint },
|
{ id: "info", label: "Basic Info", icon: PawPrint },
|
||||||
{ id: "medical", label: "Medical", icon: Heart },
|
{ id: "medical", label: "Medical", icon: Heart },
|
||||||
|
|||||||
Reference in New Issue
Block a user