diff --git a/appointment-booking/app/api/service-locations.ts b/appointment-booking/app/api/service-locations.ts new file mode 100644 index 000000000..517f56438 --- /dev/null +++ b/appointment-booking/app/api/service-locations.ts @@ -0,0 +1,65 @@ +import { getApiBaseUrl } from '../runtime-config' +import type { Location } from './locations' + +// Location for the booking service-locations step (filtered offices + next appointment date). +// nextAppointmentDate is mapped now for a later UI step; this page does not display it yet. +export type ServiceLocation = Location & { + nextAppointmentDate: string | null +} + +// API response row from GET /api/v1/offices?service_id=. +type ApiOffice = { + office_id: number + office_name: string + civic_address: string | null + latitude: number | null + longitude: number | null + appointments_enabled_ind: number + online_status: string | null + deleted: string | null + office_appointment_message: string | null + next_appointment_date: string | null +} + +type ApiOfficesResponse = { + offices: ApiOffice[] +} + +// Fetch bookable offices for a selected service. +// Throws on fetch/HTTP/parse failure so the page can tell error apart from empty []. +export async function getServiceLocations(serviceId: number): Promise { + const url = `${await getApiBaseUrl()}/offices?service_id=${serviceId}` + + let body: ApiOfficesResponse + try { + const response = await fetch(url) + if (!response.ok) { + throw new Error('Failed to load locations') + } + body = (await response.json()) as ApiOfficesResponse + } catch { + throw new Error('Failed to load locations') + } + + const locations: ServiceLocation[] = [] + + for (const row of body.offices ?? []) { + const status = row.online_status ?? '' + const isShow = status === 'SHOW' || status.endsWith('.SHOW') + if (row.deleted || !row.appointments_enabled_ind || !isShow) { + continue + } + + locations.push({ + id: row.office_id, + name: row.office_name.trim(), + address: row.civic_address?.trim() || '', + latitude: row.latitude, + longitude: row.longitude, + appointmentMessage: row.office_appointment_message?.trim() || '', + nextAppointmentDate: row.next_appointment_date ?? null, + }) + } + + return locations +} diff --git a/appointment-booking/app/routes/service-locations.tsx b/appointment-booking/app/routes/service-locations.tsx index aa00e10ce..39c14332a 100644 --- a/appointment-booking/app/routes/service-locations.tsx +++ b/appointment-booking/app/routes/service-locations.tsx @@ -2,7 +2,8 @@ import { useEffect, useMemo, useState } from 'react' import { Callout, InlineAlert, Text } from '@bcgov/design-system-react-components' import { getDistance } from 'geolib' import { useNavigate } from 'react-router' -import { getBookingLocations, type Location } from '~/api/locations' +import type { Location } from '~/api/locations' +import { getServiceLocations, type ServiceLocation } from '~/api/service-locations' import type { Service } from '~/api/services' import { useBooking } from '~/booking/booking-context' import { BookingBackRow } from '~/components/BookingBackRow' @@ -79,8 +80,9 @@ export default function ServiceLocationsPage() { const { selectedService, selectedLocation, setSelectedLocation } = useBooking() const selectedId = selectedLocation ? String(selectedLocation.id) : '' - const [locations, setLocations] = useState([]) - const [isLoading, setIsLoading] = useState(true) + // ServiceLocation keeps nextAppointmentDate for a later UI step; unused on this page yet. + const [locations, setLocations] = useState([]) + const [isLoading, setIsLoading] = useState(() => !!selectedService) const [loadError, setLoadError] = useState(false) const [search, setSearch] = useState('') const [sortDirection, setSortDirection] = useState('asc') @@ -93,10 +95,16 @@ export default function ServiceLocationsPage() { toggleNearestSort, } = useNearestSort() + const serviceId = selectedService?.id + // Ignore late responses if the user leaves the page mid-fetch (SSR/client remounts). useEffect(() => { + if (serviceId == null) { + return + } + let cancelled = false - getBookingLocations() + getServiceLocations(serviceId) .then((loaded) => { if (!cancelled) { setLocations(loaded) @@ -116,7 +124,7 @@ export default function ServiceLocationsPage() { return () => { cancelled = true } - }, []) + }, [serviceId]) const visibleLocations = useMemo(() => { const tokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean)