Compare commits

...

2 Commits

Author SHA1 Message Date
Savannah Savings 9a30b4bd44 fix(GRO-2180): portal Appointments handles ISO startTime shape
CI / Test (pull_request) Failing after 14m48s
CI / Lint & Typecheck (pull_request) Failing after 14m48s
CI / Build & Push Docker Image (pull_request) Has been skipped
The /api/portal/appointments contract returns ISO `startTime`/`endTime`
and no `date`/`time` fields. `isUpcoming()` read `appt.date`/`appt.time`
and called `parseTimeTo24Hour(undefined)` → `undefined.split(' ')` →
TypeError. The throw was swallowed by the fetch `try/catch`, surfacing
"Failed to load appointments" and making "Book New" unreachable for
every signed-in customer.

- Add `getAppointmentStart()` helper: prefers ISO `startTime`, falls
  back to legacy `date` + `time`, returns null on missing/unparseable
  input so callers never throw.
- Rewrite `isUpcoming()` on top of the helper.
- Add `formatAppointmentDate()` / `formatAppointmentTime()` and use them
  at all date/time display sites (list row + RescheduleFlow header).
- Guard `parseTimeTo24Hour(undefined)`.
- Mark `date`/`time` optional and add `startTime`/`endTime` to the
  `Appointment` type to match the API contract.
- Tests: API-shape fixtures + regression guards (no throw on startTime
  shape, undefined-safe parse, helper resolution/formatting).
- Update UAT_PLAYBOOK.md §5.12 (customer portal appointments).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 02:59:31 +00:00
groombook-engineer[bot] 90f6a30b74 docs(GRO-1289): add UAT_PLAYBOOK.md with auth base URL test cases (#5)
* docs(GRO-1289): add UAT_PLAYBOOK.md with auth base URL test cases

Add UAT_PLAYBOOK.md covering VITE_API_URL auth resolution:
- TC-AUTH-4.1.x: Tests for when VITE_API_URL is set
- TC-AUTH-4.2.x: Tests for when VITE_API_URL is unset (window.location.origin fallback)
- TC-AUTH-4.3.x: Session persistence tests

Updated UAT_PLAYBOOK.md §4 — auth base URL resolution test cases.

GRO-1289

* docs(GRO-1289): restore full UAT_PLAYBOOK with auth base URL test cases

- Restored Pre-conditions section (§3)
- Restored original §5.1 Authentication UI test cases
- Inserted new auth base URL resolution test cases (§5.2–§5.4):
  - TC-AUTH-5.2.x: VITE_API_URL set scenarios
  - TC-AUTH-5.3.x: VITE_API_URL unset fallback scenarios
  - TC-AUTH-5.4.x: Session persistence scenarios
- Restored all other feature test sections (§5.5–§5.17)
- Restored broader Update Policy (§7)

Updated UAT_PLAYBOOK.md §5.2–§5.4 — auth base URL resolution test cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-14 21:10:35 +00:00
3 changed files with 246 additions and 74 deletions
+114 -64
View File
@@ -20,125 +20,175 @@ GroomBook Web is the React 19 PWA frontend for the GroomBook pet grooming manage
- GroomBook API service is running and healthy
- Required test data exists (clients, pets, appointments, services, staff)
## 4. Test Cases
## 4. Auth Base URL Resolution
### 4.1 Authentication UI
The auth client resolves its API base URL based on the `VITE_API_URL` environment variable:
- **When `VITE_API_URL` is set:** Uses the configured URL as the auth base URL.
- **When `VITE_API_URL` is unset:** Falls back to `window.location.origin`.
This allows the app to work correctly in both:
- **Dev/PR deployments:** Where `VITE_API_URL` is explicitly set to the deployed API endpoint.
- **Local development:** Where `VITE_API_URL` is not set, using the same origin as the web app.
### Auth Client Configuration (src/lib/auth-client.ts)
```typescript
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL ?? "",
});
export const { signIn, signOut, useSession, changePassword } = authClient;
```
## 5. Test Cases
### 5.1 Authentication UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.1.1 | Login page loads | Navigate to UAT URL | Login form is displayed with OIDC provider button(s) |
| TC-WEB-4.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
| TC-WEB-4.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
| TC-WEB-4.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
| TC-WEB-5.1.1 | Login page loads | Navigate to UAT URL | Login form is displayed with OIDC provider button(s) |
| TC-WEB-5.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
| TC-WEB-5.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
| TC-WEB-5.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
### 4.2 Dashboard
### 5.2 Authentication — VITE_API_URL Set
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.2.1 | Dashboard loads after login | Complete authentication | Dashboard page loads without errors |
| TC-WEB-4.2.2 | Key metrics visible | View dashboard | Revenue, appointments, clients, and other key metrics displayed |
| TC-WEB-4.2.3 | No blank state | On fresh login | Dashboard shows meaningful data, not empty/blank state |
| TC-AUTH-5.2.1 | Auth client uses configured API URL | Configure `VITE_API_URL=https://api.example.com`, load app | Auth client sends requests to `https://api.example.com` |
| TC-AUTH-5.2.2 | Sign-in flow with configured API | Sign in when `VITE_API_URL` is set | Auth requests go to configured URL |
| TC-AUTH-5.2.3 | Sign-out flow with configured API | Sign out when `VITE_API_URL` is set | Auth requests go to configured URL |
### 4.3 Client Management UI
### 5.3 Authentication — VITE_API_URL Unset (Fallback)
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.3.1 | Client list loads | Navigate to Clients section | List of clients is displayed |
| TC-WEB-4.3.2 | Create client | Click "New Client", fill form, submit | Client created successfully, appears in list |
| TC-WEB-4.3.3 | Edit client | Click on client, modify details, save | Client updated successfully |
| TC-WEB-4.3.4 | Search clients | Enter search term in search box | List filters to matching clients |
| TC-WEB-4.3.5 | Archive client | Click archive on client record | Client marked as archived, removed from active list |
| 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 |
### 4.4 Pet Management UI
### 5.4 Session Persistence
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.4.1 | Pet profiles visible | Open client details | All pets for client displayed with basic info |
| TC-WEB-4.4.2 | Add pet | Click "Add Pet", fill form, submit | Pet created and linked to client |
| TC-WEB-4.4.3 | Edit pet details | Click on pet, modify details, save | Pet updated successfully |
| TC-WEB-4.4.4 | Grooming history view | View pet profile | Past appointments/grooming sessions displayed |
| TC-AUTH-5.4.1 | Session persists across page reload | Sign in, reload page | Session remains active |
| TC-AUTH-5.4.2 | Session clears on sign-out | Sign in, sign out | User is logged out, redirected to login |
### 4.5 Appointment Scheduling UI
### 5.5 Dashboard
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.5.1 | Calendar view loads | Navigate to Appointments | Calendar view displays appointments |
| TC-WEB-4.5.2 | Create booking | Click "New Appointment", fill details, submit | Appointment created and appears on calendar |
| TC-WEB-4.5.3 | Modify appointment | Click on appointment, change details, save | Appointment updated successfully |
| TC-WEB-4.5.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
| TC-WEB-4.5.5 | Appointment groups | View grouped appointments | Related appointments display as group |
| TC-WEB-5.5.1 | Dashboard loads after login | Complete authentication | Dashboard page loads without errors |
| TC-WEB-5.5.2 | Key metrics visible | View dashboard | Revenue, appointments, clients, and other key metrics displayed |
| TC-WEB-5.5.3 | No blank state | On fresh login | Dashboard shows meaningful data, not empty/blank state |
### 4.6 Service Management UI
### 5.6 Client Management UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.6.1 | Service catalog loads | Navigate to Services | List of available services displayed |
| TC-WEB-4.6.2 | Create service | Click "New Service", fill form, submit | Service created successfully |
| TC-WEB-4.6.3 | Edit service | Click on service, modify details, save | Service updated successfully |
| TC-WEB-5.6.1 | Client list loads | Navigate to Clients section | List of clients is displayed |
| TC-WEB-5.6.2 | Create client | Click "New Client", fill form, submit | Client created successfully, appears in list |
| TC-WEB-5.6.3 | Edit client | Click on client, modify details, save | Client updated successfully |
| TC-WEB-5.6.4 | Search clients | Enter search term in search box | List filters to matching clients |
| TC-WEB-5.6.5 | Archive client | Click archive on client record | Client marked as archived, removed from active list |
### 4.7 Staff Management UI
### 5.7 Pet Management UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.7.1 | Staff list loads | Navigate to Staff | List of staff members displayed |
| TC-WEB-4.7.2 | Role display | View staff member | Staff role/permissions clearly visible |
| TC-WEB-5.7.1 | Pet profiles visible | Open client details | All pets for client displayed with basic info |
| 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 |
### 4.8 Invoicing & Payments UI
### 5.8 Appointment Scheduling UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.8.1 | Invoice list loads | Navigate to Invoices | List of invoices displayed with status |
| TC-WEB-4.8.2 | Payment flow | Click "Pay" on unpaid invoice, complete payment | Payment processed, invoice marked as paid |
| TC-WEB-4.8.3 | Receipts view | View paid invoice | Receipt/payment details displayed |
| TC-WEB-5.8.1 | Calendar view loads | Navigate to Appointments | Calendar view displays appointments |
| TC-WEB-5.8.2 | Create booking | Click "New Appointment", fill details, submit | Appointment created and appears on calendar |
| TC-WEB-5.8.3 | Modify appointment | Click on appointment, change details, save | Appointment updated successfully |
| TC-WEB-5.8.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
| TC-WEB-5.8.5 | Appointment groups | View grouped appointments | Related appointments display as group |
### 4.9 Customer Portal UI
### 5.9 Service Management UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.9.1 | Client-facing view | Log in as client persona | Customer portal UI displayed |
| TC-WEB-4.9.2 | Appointment list | View client portal appointments | List of client's appointments visible |
| TC-WEB-4.9.3 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
| TC-WEB-4.9.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
| 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 |
### 4.10 Reports UI
### 5.10 Staff Management UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.10.1 | Revenue charts | Navigate to Reports | Revenue charts display with data |
| TC-WEB-4.10.2 | Utilization graphs | View reports | Staff/resource utilization graphs visible |
| TC-WEB-5.10.1 | Staff list loads | Navigate to Staff | List of staff members displayed |
| TC-WEB-5.10.2 | Role display | View staff member | Staff role/permissions clearly visible |
### 4.11 Settings UI
### 5.11 Invoicing & Payments UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.11.1 | Configuration page | Navigate to Settings | Settings page loads without errors |
| TC-WEB-4.11.2 | Form interactions | Modify settings, save | Settings saved successfully, changes reflected |
| TC-WEB-5.11.1 | Invoice list loads | Navigate to Invoices | List of invoices displayed with status |
| TC-WEB-5.11.2 | Payment flow | Click "Pay" on unpaid invoice, complete payment | Payment processed, invoice marked as paid |
| TC-WEB-5.11.3 | Receipts view | View paid invoice | Receipt/payment details displayed |
### 4.12 Navigation
### 5.12 Customer Portal UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.12.1 | Sidebar/menu links | Click navigation items | Each section loads correctly |
| TC-WEB-4.12.2 | All sections reachable | Navigate through all menu items | All sections accessible, no 404 errors |
| TC-WEB-4.12.3 | No broken links | Test all navigation paths | All links work, no broken routes |
| TC-WEB-5.12.1 | Client-facing view | Log in as client persona | Customer portal UI displayed |
| TC-WEB-5.12.2 | Appointment list loads | Sign in as `uat-customer@groombook.dev`, open **Appointments** | List of the customer's appointments renders. **No** "Failed to load appointments" error and **no** Retry button. (GRO-2180) |
| TC-WEB-5.12.3 | Date/time display | Inspect each appointment card | Each card shows a human-readable date and time derived from the API `startTime` (e.g. "Mon, Jun 1, 2026" / "10:00 AM"); no `undefined` or blank date/time. (GRO-2180) |
| TC-WEB-5.12.4 | Book New reachable | On the loaded Appointments view (non-readonly), look for the **Book New** button | "Book New" button is visible and opens the booking modal. (GRO-2180) |
| TC-WEB-5.12.5 | Upcoming/Past split | Toggle the **Upcoming** and **Past** tabs | Future appointments appear under Upcoming; completed/cancelled/past appear under Past. (GRO-2180) |
| TC-WEB-5.12.6 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
| TC-WEB-5.12.7 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
| TC-WEB-5.12.8 | Reschedule display | Open **Reschedule** on an upcoming appointment | Summary header shows the current appointment's date and time (from `startTime`); no `undefined`. (GRO-2180) |
### 4.13 Mobile / PWA
### 5.13 Reports UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.13.1 | Responsive at 390x844 | Resize viewport to mobile dimensions | Layout adapts correctly, no horizontal scroll |
| TC-WEB-4.13.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met |
| TC-WEB-4.13.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input |
| 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 |
### 4.14 Error & Empty States
### 5.14 Settings UI
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-4.14.1 | Form validation | Submit form with invalid data | Appropriate validation errors displayed |
| TC-WEB-4.14.2 | Missing data | Navigate to section with no data | Empty state message displayed, not blank page |
| TC-WEB-4.14.3 | Error boundaries | Trigger error condition | Friendly error message displayed, app doesn't crash |
| TC-WEB-5.14.1 | Configuration page | Navigate to Settings | Settings page loads without errors |
| TC-WEB-5.14.2 | Form interactions | Modify settings, save | Settings saved successfully, changes reflected |
## 5. Pass/Fail Criteria
### 5.15 Navigation
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.15.1 | Sidebar/menu links | Click navigation items | Each section loads correctly |
| TC-WEB-5.15.2 | All sections reachable | Navigate through all menu items | All sections accessible, no 404 errors |
| TC-WEB-5.15.3 | No broken links | Test all navigation paths | All links work, no broken routes |
### 5.16 Mobile / PWA
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.16.1 | Responsive at 390x844 | Resize viewport to mobile dimensions | Layout adapts correctly, no horizontal scroll |
| 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 |
### 5.17 Error & Empty States
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.17.1 | Form validation | Submit form with invalid data | Appropriate validation errors displayed |
| TC-WEB-5.17.2 | Missing data | Navigate to section with no data | Empty state message displayed, not blank page |
| TC-WEB-5.17.3 | Error boundaries | Trigger error condition | Friendly error message displayed, app doesn't crash |
## 6. Pass/Fail Criteria
**Pass:**
- All test cases execute without errors
@@ -152,7 +202,7 @@ GroomBook Web is the React 19 PWA frontend for the GroomBook pet grooming manage
- Screenshot or screen recording of failure
- Error details from browser console or network tab
## 6. Update Policy
## 7. Update Policy
**Any PR that changes user-facing behaviour MUST update this file.**
@@ -161,4 +211,4 @@ When modifying the GroomBook Web application in ways that affect the user interf
2. Add new test cases for new features or flows
3. Modify existing test cases if behaviour changes
4. Remove test cases for deprecated features
5. Reference the updated section(s) in the PR description (e.g., "Updated UAT_PLAYBOOK.md §4.5 — new appointment group feature")
5. Reference the updated section(s) in the PR description (e.g., "Updated UAT_PLAYBOOK.md §5.5 — new appointment group feature")
+85 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { parseTimeTo24Hour, isUpcoming, CustomerNotesSection, ConfirmationSection } from "../portal/sections/Appointments.tsx";
import { parseTimeTo24Hour, isUpcoming, getAppointmentStart, formatAppointmentDate, formatAppointmentTime, CustomerNotesSection, ConfirmationSection } from "../portal/sections/Appointments.tsx";
const UPCOMING_APPT = {
id: "appt-1",
@@ -29,6 +29,26 @@ const PAST_APPT = {
status: "completed" as const,
};
// GRO-2180: the /api/portal/appointments contract returns ISO startTime/endTime
// and no date/time fields. These fixtures mirror that shape exactly.
const API_UPCOMING_APPT = {
id: "appt-api-1",
petId: "pet-1",
serviceId: "service-1",
groomerId: null,
startTime: "2099-01-01T10:00:00.000Z",
endTime: "2099-01-01T10:45:00.000Z",
status: "confirmed" as const,
};
const API_PAST_APPT = {
...API_UPCOMING_APPT,
id: "appt-api-2",
startTime: "2020-01-01T10:00:00.000Z",
endTime: "2020-01-01T10:45:00.000Z",
status: "completed" as const,
};
describe("parseTimeTo24Hour", () => {
it("converts AM times correctly", () => {
expect(parseTimeTo24Hour("9:00 AM")).toBe("09:00:00");
@@ -42,6 +62,13 @@ describe("parseTimeTo24Hour", () => {
expect(parseTimeTo24Hour("11:00 PM")).toBe("23:00:00");
expect(parseTimeTo24Hour("12:00 PM")).toBe("12:00:00");
});
// GRO-2180 regression: must not throw on undefined/empty input.
it("returns a safe default for missing input", () => {
expect(() => parseTimeTo24Hour(undefined)).not.toThrow();
expect(parseTimeTo24Hour(undefined)).toBe("00:00:00");
expect(parseTimeTo24Hour("")).toBe("00:00:00");
});
});
describe("isUpcoming", () => {
@@ -60,6 +87,63 @@ describe("isUpcoming", () => {
it("returns false for completed appointments", () => {
expect(isUpcoming({ ...UPCOMING_APPT, status: "completed" })).toBe(false);
});
// GRO-2180 regression: the API contract uses ISO startTime with no date/time.
// Previously isUpcoming threw a TypeError on this shape, breaking the page.
it("does not throw on the API startTime/endTime shape", () => {
expect(() => isUpcoming(API_UPCOMING_APPT)).not.toThrow();
expect(() => isUpcoming(API_PAST_APPT)).not.toThrow();
});
it("returns true for future appointments using startTime", () => {
expect(isUpcoming(API_UPCOMING_APPT)).toBe(true);
});
it("returns false for past appointments using startTime", () => {
expect(isUpcoming(API_PAST_APPT)).toBe(false);
});
it("returns false (not throw) when neither startTime nor date is present", () => {
const { startTime, endTime, ...noDate } = API_UPCOMING_APPT;
void startTime;
void endTime;
expect(() => isUpcoming(noDate)).not.toThrow();
expect(isUpcoming(noDate)).toBe(false);
});
});
describe("getAppointmentStart / display helpers (GRO-2180)", () => {
it("resolves the start instant from ISO startTime", () => {
const start = getAppointmentStart(API_UPCOMING_APPT);
expect(start).not.toBeNull();
expect(start?.toISOString()).toBe("2099-01-01T10:00:00.000Z");
});
it("falls back to legacy date + time when startTime is absent", () => {
const start = getAppointmentStart(UPCOMING_APPT);
expect(start).not.toBeNull();
});
it("returns null when there is no usable date", () => {
const { startTime, endTime, ...noDate } = API_UPCOMING_APPT;
void startTime;
void endTime;
expect(getAppointmentStart(noDate)).toBeNull();
});
it("formats date/time without throwing on the API shape", () => {
expect(() => formatAppointmentDate(API_UPCOMING_APPT)).not.toThrow();
expect(() => formatAppointmentTime(API_UPCOMING_APPT)).not.toThrow();
expect(formatAppointmentDate(API_UPCOMING_APPT)).not.toBe("");
expect(formatAppointmentTime(API_UPCOMING_APPT)).not.toBe("");
});
it("returns empty display strings when there is no usable date", () => {
const { startTime, endTime, ...noDate } = API_UPCOMING_APPT;
void startTime;
void endTime;
expect(formatAppointmentDate(noDate)).toBe("");
});
});
describe("CustomerNotesSection", () => {
+47 -9
View File
@@ -6,8 +6,14 @@ export interface Appointment {
petId: string;
serviceId: string;
groomerId: string | null;
date: string;
time: string;
// The /api/portal/appointments contract returns ISO `startTime`/`endTime`.
// `date`/`time` are the legacy display shape, still produced locally by some
// flows (e.g. test fixtures), so both shapes are optional and code reads
// `startTime` first, falling back to `date` + `time`.
startTime?: string;
endTime?: string;
date?: string;
time?: string;
status: 'scheduled' | 'confirmed' | 'pending' | 'waitlisted' | 'completed' | 'cancelled' | 'no-show';
petName?: string;
serviceName?: string;
@@ -61,7 +67,8 @@ export function formatDate(dateStr: string): string {
});
}
export function parseTimeTo24Hour(time: string): string {
export function parseTimeTo24Hour(time: string | null | undefined): string {
if (!time) return '00:00:00';
const parts = time.split(' ');
const hoursMinutes = parts[0] ?? '';
const period = parts[1] ?? '';
@@ -74,10 +81,41 @@ export function parseTimeTo24Hour(time: string): string {
return `${hours24.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
}
/**
* Resolve an appointment's start instant from either the API contract shape
* (ISO `startTime`) or the legacy `date` + `time` shape. Returns null when no
* usable date is present or the value is unparseable, so callers never throw.
*/
export function getAppointmentStart(appt: Appointment): Date | null {
const raw = appt.startTime
? appt.startTime
: appt.date
? `${appt.date}T${parseTimeTo24Hour(appt.time)}`
: null;
if (!raw) return null;
const parsed = new Date(raw);
return isNaN(parsed.getTime()) ? null : parsed;
}
export function isUpcoming(appt: Appointment): boolean {
const now = new Date();
const apptDate = new Date(`${appt.date}T${parseTimeTo24Hour(appt.time)}`);
return apptDate > now && appt.status !== 'cancelled' && appt.status !== 'completed';
const start = getAppointmentStart(appt);
if (!start) return false;
return start > new Date() && appt.status !== 'cancelled' && appt.status !== 'completed';
}
/** Display date string, preferring the ISO `startTime` contract shape. */
export function formatAppointmentDate(appt: Appointment): string {
const start = getAppointmentStart(appt);
return start ? formatDate(start.toISOString()) : '';
}
/** Display time string, preferring the ISO `startTime` contract shape. */
export function formatAppointmentTime(appt: Appointment): string {
const start = getAppointmentStart(appt);
if (start) {
return start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
return appt.time ?? '';
}
const STATUS_COLORS: Record<string, string> = {
@@ -288,11 +326,11 @@ function AppointmentCard({
<div className="flex items-center gap-3 text-xs text-stone-500 mt-0.5">
<span className="flex items-center gap-1">
<Calendar size={12} />
{formatDate(appt.date)}
{formatAppointmentDate(appt)}
</span>
<span className="flex items-center gap-1">
<Clock size={12} />
{appt.time}
{formatAppointmentTime(appt)}
</span>
<span>with {appt.groomerName || 'First Available'}</span>
</div>
@@ -646,7 +684,7 @@ export function RescheduleFlow({
{appt.petName || 'Pet'} {appt.serviceName || 'Service'}
</p>
<p className="text-stone-500 mt-0.5">
{formatDate(appt.date)} at {appt.time} with{' '}
{formatAppointmentDate(appt)} at {formatAppointmentTime(appt)} with{' '}
{appt.groomerName || 'First Available'}
</p>
</div>