fix(GRO-765): include service name in portal appointments response

- 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 <noreply@paperclip.ing>
This commit is contained in:
Test User
2026-04-17 12:50:07 +00:00
parent 3c7820d785
commit 0ace23de53
4 changed files with 64 additions and 6 deletions
+5 -2
View File
@@ -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) => {
+22 -1
View File
@@ -123,7 +123,28 @@ export const AppointmentsSection: React.FC<AppointmentsSectionProps> = ({ sessio
if (response.ok) {
const data = await response.json();
const fetchedAppointments: Appointment[] = data.appointments || data || [];
const rawAppointments: Record<string, unknown>[] = 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));
+18 -1
View File
@@ -116,7 +116,24 @@ export function Dashboard({
const invoicesData = await invoicesRes.json();
const brandingData = await brandingRes.json();
setAppointments(appointmentsData.appointments || []);
const rawAppointments: Record<string, unknown>[] = 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")
+19 -2
View File
@@ -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<string, unknown>[] = 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);