fix(GRO-2105): include serviceId in BookingFlow/RescheduleFlow availability call (#46)
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (push) Successful in 28s
CI / Test (pull_request) Successful in 25s
CI / Lint & Typecheck (pull_request) Successful in 27s
CI / Build & Push Docker Image (push) Successful in 11s
CI / Build & Push Docker Image (pull_request) Successful in 12s

This commit was merged in pull request #46.
This commit is contained in:
2026-06-02 19:06:15 +00:00
parent 4600dcf950
commit f0c58c193c
3 changed files with 118 additions and 25 deletions
+68 -18
View File
@@ -2,6 +2,35 @@ import React, { useState, useEffect } from 'react';
import { Calendar, Clock, Plus, ChevronRight, ChevronDown, Loader2 } from 'lucide-react';
import { ANALYTICS_EVENTS, fireAnalyticsEvent } from '../../lib/analytics';
// ─── Availability fetch helper ───────────────────────────────────────────────
// Returns ISO startTime strings for the given service/date, or an error message.
// Validates HTTP status and that the body is actually an array — the API
// responds with `{error: "..."}` on 4xx, and we must not treat that as slots.
const AVAILABILITY_ERROR_MESSAGE = 'Failed to load time slots';
async function fetchAvailability(
params: { serviceId: string; date: string },
sessionId: string | null,
): Promise<{ times: string[]; error: string | null }> {
const url = `/api/book/availability?${new URLSearchParams(params).toString()}`;
const headers: Record<string, string> = {};
if (sessionId) headers['X-Impersonation-Session-Id'] = sessionId;
try {
const res = await fetch(url, { headers });
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
return { times: [], error: body.error ?? `${AVAILABILITY_ERROR_MESSAGE} (HTTP ${res.status})` };
}
const data: unknown = await res.json();
if (!Array.isArray(data)) {
return { times: [], error: AVAILABILITY_ERROR_MESSAGE };
}
return { times: data as string[], error: null };
} catch {
return { times: [], error: AVAILABILITY_ERROR_MESSAGE };
}
}
export interface Appointment {
id: string;
petId: string;
@@ -595,19 +624,29 @@ export function RescheduleFlow({
useEffect(() => {
if (!selectedDate || !sessionId) {
setAvailableTimes([]);
setSlotsError(null);
return;
}
const params = new URLSearchParams({ date: selectedDate });
if (!appt.serviceId) {
setAvailableTimes([]);
setSlotsError('Failed to load time slots');
return;
}
let cancelled = false;
setSlotsLoading(true);
setSlotsError(null);
fetch(`/api/book/availability?${params.toString()}`, {
headers: { "X-Impersonation-Session-Id": sessionId ?? "" },
})
.then((r) => r.json() as Promise<string[]>)
.then(setAvailableTimes)
.catch(() => setSlotsError('Failed to load time slots'))
.finally(() => setSlotsLoading(false));
}, [selectedDate, sessionId]);
fetchAvailability({ serviceId: appt.serviceId, date: selectedDate }, sessionId).then(
({ times, error }) => {
if (cancelled) return;
setAvailableTimes(times);
setSlotsError(error);
setSlotsLoading(false);
},
);
return () => {
cancelled = true;
};
}, [selectedDate, sessionId, appt.serviceId]);
async function handleSubmit() {
if (!selectedDate || !selectedTime) return;
@@ -766,19 +805,30 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
useEffect(() => {
if (!selectedDate || !sessionId) {
setAvailableTimes([]);
setSlotsError(null);
return;
}
const params = new URLSearchParams({ date: selectedDate });
const serviceId = selectedServices[0]?.id;
if (!serviceId) {
setAvailableTimes([]);
setSlotsError('Failed to load time slots');
return;
}
let cancelled = false;
setSlotsLoading(true);
setSlotsError(null);
fetch(`/api/book/availability?${params.toString()}`, {
headers: { "X-Impersonation-Session-Id": sessionId ?? "" },
})
.then((r) => r.json() as Promise<string[]>)
.then(setAvailableTimes)
.catch(() => setSlotsError('Failed to load time slots'))
.finally(() => setSlotsLoading(false));
}, [selectedDate, sessionId]);
fetchAvailability({ serviceId, date: selectedDate }, sessionId).then(
({ times, error }) => {
if (cancelled) return;
setAvailableTimes(times);
setSlotsError(error);
setSlotsLoading(false);
},
);
return () => {
cancelled = true;
};
}, [selectedDate, sessionId, selectedServices]);
useEffect(() => {
const fetchData = async () => {