From 0ace23de531dfdab0ede1af818b85a8751da1efa Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 17 Apr 2026 12:50:07 +0000 Subject: [PATCH] fix(GRO-765): include service name in portal appointments response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added service JOIN to /api/portal/appointments to include name, duration, and price fields in the service sub-object - Changed API response shape from { upcoming, past } to { appointments: [] } to match client expectations and simplify data flow - Updated Appointments, ReportCards, and Dashboard components to transform the new response format (startTime → date + time) and extract serviceName, petName, and groomerName from nested sub-objects Co-Authored-By: Paperclip --- apps/api/src/routes/portal.ts | 7 ++++-- apps/web/src/portal/sections/Appointments.tsx | 23 ++++++++++++++++++- apps/web/src/portal/sections/Dashboard.tsx | 19 ++++++++++++++- apps/web/src/portal/sections/ReportCards.tsx | 21 +++++++++++++++-- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/apps/api/src/routes/portal.ts b/apps/api/src/routes/portal.ts index d768bc8..2becab5 100644 --- a/apps/api/src/routes/portal.ts +++ b/apps/api/src/routes/portal.ts @@ -60,12 +60,15 @@ portalRouter.get("/appointments", async (c) => { const petIds = allAppts.map(a => a.petId).filter((id): id is string => id !== null); const staffIds = allAppts.map(a => a.staffId).filter((id): id is string => id !== null); + const serviceIds = allAppts.map(a => a.serviceId).filter((id): id is string => id !== null); const petRows = petIds.length ? await db.select().from(pets).where(inArray(pets.id, petIds)) : []; const staffRows = staffIds.length ? await db.select().from(staff).where(inArray(staff.id, staffIds)) : []; + const serviceRows = serviceIds.length ? await db.select().from(services).where(inArray(services.id, serviceIds)) : []; const petMap = Object.fromEntries(petRows.map(p => [p.id, p])); const staffMap = Object.fromEntries(staffRows.map(s => [s.id, s])); + const serviceMap = Object.fromEntries(serviceRows.map(s => [s.id, s])); const appts = allAppts.map(a => ({ id: a.id, @@ -76,14 +79,14 @@ portalRouter.get("/appointments", async (c) => { customerNotes: a.customerNotes, notes: a.notes, pet: a.petId ? { id: petMap[a.petId]?.id, name: petMap[a.petId]?.name, photo: petMap[a.petId]?.photoKey } : null, - service: a.serviceId ? { id: a.serviceId } : null, + service: a.serviceId ? { id: a.serviceId, name: serviceMap[a.serviceId]?.name, duration: serviceMap[a.serviceId]?.durationMinutes, price: serviceMap[a.serviceId]?.basePriceCents } : null, staff: a.staffId ? { id: staffMap[a.staffId]?.id, name: staffMap[a.staffId]?.name } : null, })); const upcoming = appts.filter(a => a.startTime > now && a.status !== "cancelled"); const past = appts.filter(a => a.startTime <= now || a.status === "cancelled"); - return c.json({ upcoming, past }); + return c.json({ appointments: appts }); }); portalRouter.get("/pets", async (c) => { diff --git a/apps/web/src/portal/sections/Appointments.tsx b/apps/web/src/portal/sections/Appointments.tsx index f5fad62..dacee0f 100644 --- a/apps/web/src/portal/sections/Appointments.tsx +++ b/apps/web/src/portal/sections/Appointments.tsx @@ -123,7 +123,28 @@ export const AppointmentsSection: React.FC = ({ sessio if (response.ok) { const data = await response.json(); - const fetchedAppointments: Appointment[] = data.appointments || data || []; + const rawAppointments: Record[] = data.appointments || data || []; + + // Transform API response (startTime) to client format (date + time) + const fetchedAppointments: Appointment[] = rawAppointments.map((a) => { + const start = new Date(a.startTime as string); + const dateStr = start.toISOString().split('T')[0]; + const hours = start.getHours(); + const minutes = start.getMinutes().toString().padStart(2, '0'); + const period = hours >= 12 ? 'PM' : 'AM'; + const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + const timeStr = `${hour12}:${minutes} ${period}`; + return { + ...a, + date: dateStr, + time: timeStr, + petName: (a.pet as { name?: string })?.name, + serviceName: (a.service as { name?: string })?.name, + groomerName: (a.staff as { name?: string })?.name, + duration: (a.service as { duration?: number })?.duration, + price: (a.service as { price?: number })?.price, + } as Appointment; + }); const upcoming = fetchedAppointments.filter((appt) => isUpcoming(appt)); const past = fetchedAppointments.filter((appt) => !isUpcoming(appt)); diff --git a/apps/web/src/portal/sections/Dashboard.tsx b/apps/web/src/portal/sections/Dashboard.tsx index 136a93a..3b836fd 100644 --- a/apps/web/src/portal/sections/Dashboard.tsx +++ b/apps/web/src/portal/sections/Dashboard.tsx @@ -116,7 +116,24 @@ export function Dashboard({ const invoicesData = await invoicesRes.json(); const brandingData = await brandingRes.json(); - setAppointments(appointmentsData.appointments || []); + const rawAppointments: Record[] = appointmentsData.appointments || []; + const transformedAppointments = rawAppointments.map((a) => { + const start = new Date(a.startTime as string); + const dateStr = start.toISOString().split('T')[0]; + const hours = start.getHours(); + const minutes = start.getMinutes().toString().padStart(2, '0'); + const period = hours >= 12 ? 'PM' : 'AM'; + const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + const timeStr = `${hour12}:${minutes} ${period}`; + return { + ...a, + date: dateStr, + time: timeStr, + petName: (a.pet as { name?: string })?.name ?? '', + serviceName: (a.service as { name?: string })?.name ?? '', + }; + }); + setAppointments(transformedAppointments as Appointment[]); setPets(petsData.pets || []); // Filter for pending invoices only (not "outstanding") diff --git a/apps/web/src/portal/sections/ReportCards.tsx b/apps/web/src/portal/sections/ReportCards.tsx index 018b376..d467e45 100644 --- a/apps/web/src/portal/sections/ReportCards.tsx +++ b/apps/web/src/portal/sections/ReportCards.tsx @@ -44,8 +44,25 @@ export function ReportCards({ sessionId }: Props) { if (response.ok) { const data = await response.json(); - const allAppointments: Appointment[] = data.appointments || data || []; - const reportCardAppointments = allAppointments.filter( + const rawAppointments: Record[] = data.appointments || data || []; + const transformed: Appointment[] = rawAppointments.map((a) => { + const start = new Date(a.startTime as string); + const dateStr = start.toISOString().split('T')[0]; + const hours = start.getHours(); + const minutes = start.getMinutes().toString().padStart(2, '0'); + const period = hours >= 12 ? 'PM' : 'AM'; + const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + const timeStr = `${hour12}:${minutes} ${period}`; + return { + ...a, + date: dateStr, + time: timeStr, + petName: (a.pet as { name?: string })?.name, + serviceName: (a.service as { name?: string })?.name, + groomerName: (a.staff as { name?: string })?.name, + } as Appointment; + }); + const reportCardAppointments = transformed.filter( (appt) => appt.reportCardId ); setAppointments(reportCardAppointments);