diff --git a/apps/Backend/src/routes/appointments-procedures.ts b/apps/Backend/src/routes/appointments-procedures.ts index 7e980ef..82e3209 100644 --- a/apps/Backend/src/routes/appointments-procedures.ts +++ b/apps/Backend/src/routes/appointments-procedures.ts @@ -1,6 +1,5 @@ import { Router, Request, Response } from "express"; import { storage } from "../storage"; -import { prisma } from "@repo/db/client"; import { insertAppointmentProcedureSchema, updateAppointmentProcedureSchema, diff --git a/apps/Backend/src/routes/appointments.ts b/apps/Backend/src/routes/appointments.ts index 2dd8ae0..558f036 100644 --- a/apps/Backend/src/routes/appointments.ts +++ b/apps/Backend/src/routes/appointments.ts @@ -49,7 +49,7 @@ router.get("/day", async (req: Request, res: Response): Promise => { // dedupe patient ids referenced by those appointments const patientIds = Array.from( - new Set(appointments.map((a) => a.patientId).filter(Boolean)) + new Set(appointments.map((a) => a.patientId).filter(Boolean)), ); const patients = patientIds.length @@ -76,6 +76,43 @@ router.get("/recent", async (req: Request, res: Response) => { } }); +/** + * GET /api/appointments/:id/procedure-notes + */ +router.get("/:id/procedure-notes", async (req: Request, res: Response) => { + const appointmentId = Number(req.params.id); + if (isNaN(appointmentId)) { + return res.status(400).json({ message: "Invalid appointment ID" }); + } + + const appointment = await storage.getAppointment(appointmentId); + if (!appointment) { + return res.status(404).json({ message: "Appointment not found" }); + } + + return res.json({ + procedureNotes: appointment.procedureCodeNotes ?? "", + }); +}); + +/** + * PUT /api/appointments/:id/procedure-notes + */ +router.put("/:id/procedure-notes", async (req: Request, res: Response) => { + const appointmentId = Number(req.params.id); + if (isNaN(appointmentId)) { + return res.status(400).json({ message: "Invalid appointment ID" }); + } + + const { procedureNotes } = req.body as { procedureNotes?: string }; + + const updated = await storage.updateAppointment(appointmentId, { + procedureCodeNotes: procedureNotes ?? null, + }); + + return res.json(updated); +}); + // Get a single appointment by ID router.get( "/:id", @@ -101,7 +138,7 @@ router.get( } catch (error) { res.status(500).json({ message: "Failed to retrieve appointment" }); } - } + }, ); // Get all appointments for a specific patient @@ -128,7 +165,7 @@ router.get( } catch (err) { res.status(500).json({ message: "Failed to get patient appointments" }); } - } + }, ); /** @@ -162,7 +199,7 @@ router.get( .status(500) .json({ message: "Failed to retrieve patient for appointment" }); } - } + }, ); // Create a new appointment @@ -202,7 +239,7 @@ router.post( await storage.getPatientAppointmentByDateTime( appointmentData.patientId, appointmentData.date, - currentStartTime + currentStartTime, ); // Check staff conflict at this time @@ -210,7 +247,7 @@ router.post( appointmentData.staffId, appointmentData.date, currentStartTime, - sameDayAppointment?.id // Ignore self if updating + sameDayAppointment?.id, // Ignore self if updating ); if (!staffConflict) { @@ -231,7 +268,7 @@ router.post( if (sameDayAppointment?.id !== undefined) { const updated = await storage.updateAppointment( sameDayAppointment.id, - payload + payload, ); responseData = { ...updated, @@ -276,7 +313,7 @@ router.post( if (error instanceof z.ZodError) { console.log( "Validation error details:", - JSON.stringify(error.format(), null, 2) + JSON.stringify(error.format(), null, 2), ); return res.status(400).json({ message: "Validation error", @@ -289,7 +326,7 @@ router.post( error: error instanceof Error ? error.message : String(error), }); } - } + }, ); // Update an existing appointment @@ -343,7 +380,7 @@ router.put( existingAppointment.patientId, date, startTime, - appointmentId + appointmentId, ); if (patientConflict) { @@ -356,7 +393,7 @@ router.put( staffId, date, startTime, - appointmentId + appointmentId, ); if (staffConflict) { @@ -381,7 +418,7 @@ router.put( // Update appointment const updatedAppointment = await storage.updateAppointment( appointmentId, - updatePayload + updatePayload, ); return res.json(updatedAppointment); } catch (error) { @@ -390,7 +427,7 @@ router.put( if (error instanceof z.ZodError) { console.log( "Validation error details:", - JSON.stringify(error.format(), null, 2) + JSON.stringify(error.format(), null, 2), ); return res.status(400).json({ message: "Validation error", @@ -403,7 +440,7 @@ router.put( error: error instanceof Error ? error.message : String(error), }); } - } + }, ); // Delete an appointment diff --git a/apps/Frontend/src/components/appointment-procedures/appointment-procedure-notes.tsx b/apps/Frontend/src/components/appointment-procedures/appointment-procedure-notes.tsx new file mode 100644 index 0000000..aab7995 --- /dev/null +++ b/apps/Frontend/src/components/appointment-procedures/appointment-procedure-notes.tsx @@ -0,0 +1,156 @@ +import { useEffect, useState, useRef } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import { Save, Pencil, X } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; + +interface Props { + appointmentId: number; + enabled: boolean; +} + +export function AppointmentProcedureNotes({ + appointmentId, + enabled, +}: Props) { + const { toast } = useToast(); + + const [notes, setNotes] = useState(""); + const [originalNotes, setOriginalNotes] = useState(""); + const [isEditing, setIsEditing] = useState(false); + + const textareaRef = useRef(null); + + // ------------------------- + // Load procedure notes + // ------------------------- + useQuery({ + queryKey: ["appointment-procedure-notes", appointmentId], + enabled: enabled && !!appointmentId, + queryFn: async () => { + const res = await apiRequest( + "GET", + `/api/appointments/${appointmentId}/procedure-notes` + ); + if (!res.ok) throw new Error("Failed to load notes"); + + const data = await res.json(); + const value = data.procedureNotes ?? ""; + + setNotes(value); + setOriginalNotes(value); + setIsEditing(false); + + return data; + }, + }); + + // ------------------------- + // Save procedure notes + // ------------------------- + const saveMutation = useMutation({ + mutationFn: async () => { + const res = await apiRequest( + "PUT", + `/api/appointments/${appointmentId}/procedure-notes`, + { procedureNotes: notes } + ); + if (!res.ok) throw new Error("Failed to save notes"); + }, + onSuccess: () => { + toast({ title: "Procedure notes saved" }); + setOriginalNotes(notes); + setIsEditing(false); + + queryClient.invalidateQueries({ + queryKey: ["appointment-procedure-notes", appointmentId], + }); + }, + onError: (err: any) => { + toast({ + title: "Error", + description: err.message ?? "Failed to save notes", + variant: "destructive", + }); + }, + }); + + // Autofocus textarea when editing + useEffect(() => { + if (isEditing) { + textareaRef.current?.focus(); + } + }, [isEditing]); + + const handleCancel = () => { + setNotes(originalNotes); + setIsEditing(false); + }; + + const hasChanges = notes !== originalNotes; + + return ( +
+ {/* Header */} +
+
Procedure Notes
+ + {!isEditing ? ( + + ) : ( +
+ + + +
+ )} +
+ + {/* Textarea */} +