# Appointment Availability, Edit & Cancel — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add doctor availability checking (grey out booked slots), appointment editing, and appointment cancellation to the inline appointment form. **Architecture:** The form queries the platform for real doctors (replacing hardcoded list) and existing appointments for the selected doctor+date. Booked time slots are greyed out. After booking, the agent can edit or cancel via the same inline form. All queries go through the sidecar's GraphQL proxy. **Tech Stack:** React 19 + Jotai, platform GraphQL API (Doctor + Appointment entities) **Platform entities (relevant fields):** - **Doctor**: `id`, `fullName { firstName lastName }`, `department` (SELECT), `specialty`, `visitingHours`, `active`, `clinic { id clinicName }` - **Appointment**: `id`, `scheduledAt` (DATE_TIME), `durationMin` (NUMBER, default 30), `appointmentType` (SELECT), `appointmentStatus` (SELECT: SCHEDULED/CONFIRMED/CANCELLED/...), `doctorName` (TEXT), `department` (TEXT), `reasonForVisit` (TEXT), `patientId` (relation), `doctorId` (relation) **GraphQL field name note:** SDK field `durationMinutes` maps to `durationMin` in GraphQL. SDK field `isActive` maps to `active` in GraphQL. --- ## File Map | File | Responsibility | Action | |------|---------------|--------| | `src/components/call-desk/appointment-form.tsx` | Inline appointment form | Modify: fetch real doctors, check availability, support edit/cancel mode | | `src/lib/queries.ts` | GraphQL queries | Modify: add appointment-by-doctor-date query | Only 2 files. The form is self-contained — all logic stays in the component. --- ## Task 1: Fetch real doctors from platform Replace hardcoded `doctorItems` and `departmentItems` with real data from the platform. **Files:** - Modify: `helix-engage/src/components/call-desk/appointment-form.tsx` - [ ] **Step 1: Add doctor fetching on mount** Import `apiClient` at top level (remove the dynamic import in handleSave). Add state for doctors and a useEffect to fetch them: ```typescript import { useState, useEffect } from 'react'; import { apiClient } from '@/lib/api-client'; // Inside the component: const [doctors, setDoctors] = useState>([]); useEffect(() => { apiClient.graphql<{ doctors: { edges: Array<{ node: any }> } }>( `{ doctors(first: 50, filter: { active: { eq: true } }) { edges { node { id name fullName { firstName lastName } department clinic { id name clinicName } } } } }`, ).then(data => { const docs = data.doctors.edges.map(e => ({ id: e.node.id, name: e.node.fullName ? `Dr. ${e.node.fullName.firstName} ${e.node.fullName.lastName}`.trim() : e.node.name, department: e.node.department ?? '', clinic: e.node.clinic?.clinicName ?? e.node.clinic?.name ?? '', })); setDoctors(docs); }).catch(() => {}); }, []); ``` - [ ] **Step 2: Derive department and doctor lists from fetched data** Replace hardcoded `departmentItems` and `doctorItems` with derived lists: ```typescript const departmentItems = [...new Set(doctors.map(d => d.department).filter(Boolean))] .map(dept => ({ id: dept, label: dept.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) })); // Filter doctors by selected department const filteredDoctors = department ? doctors.filter(d => d.department === department) : doctors; const doctorSelectItems = filteredDoctors.map(d => ({ id: d.id, label: d.name })); ``` - [ ] **Step 3: Update Select components to use dynamic data** The Department select uses `departmentItems` (derived). The Doctor select uses `doctorSelectItems` (filtered by department). Reset doctor selection when department changes. - [ ] **Step 4: Update handleSave to use doctor ID** Instead of sending `doctorName` as a text label, send the `doctorId` relation: ```typescript const selectedDoctor = doctors.find(d => d.id === doctor); await apiClient.graphql( `mutation CreateAppointment($data: AppointmentCreateInput!) { createAppointment(data: $data) { id } }`, { data: { scheduledAt, durationMin: 30, appointmentType: 'CONSULTATION', appointmentStatus: 'SCHEDULED', doctorName: selectedDoctor?.name ?? '', department: selectedDoctor?.department ?? '', reasonForVisit: chiefComplaint || null, doctorId: doctor, ...(leadId ? { patientId: leadId } : {}), }, }, ); ``` - [ ] **Step 5: Remove hardcoded doctorItems and departmentItems arrays** Delete the `const doctorItems = [...]` and `const departmentItems = [...]` arrays from the top of the file. - [ ] **Step 6: Commit** ``` feat: fetch real doctors from platform for appointment form ``` --- ## Task 2: Add slot availability checking When the agent selects a doctor and date, query existing appointments and grey out booked slots. **Files:** - Modify: `helix-engage/src/components/call-desk/appointment-form.tsx` - [ ] **Step 1: Add booked slots state and fetch logic** ```typescript const [bookedSlots, setBookedSlots] = useState([]); const [loadingSlots, setLoadingSlots] = useState(false); useEffect(() => { if (!doctor || !date) { setBookedSlots([]); return; } setLoadingSlots(true); const dayStart = `${date}T00:00:00`; const dayEnd = `${date}T23:59:59`; apiClient.graphql<{ appointments: { edges: Array<{ node: any }> } }>( `{ appointments(filter: { doctorId: { eq: "${doctor}" }, scheduledAt: { gte: "${dayStart}", lte: "${dayEnd}" }, appointmentStatus: { in: [SCHEDULED, CONFIRMED, CHECKED_IN, IN_PROGRESS] } }) { edges { node { id scheduledAt durationMin } } } }`, ).then(data => { const slots = data.appointments.edges.map(e => { const dt = new Date(e.node.scheduledAt); return `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}`; }); setBookedSlots(slots); }).catch(() => { setBookedSlots([]); }).finally(() => setLoadingSlots(false)); }, [doctor, date]); ``` - [ ] **Step 2: Grey out booked slots in the time slot Select** Mark booked slots as disabled in the Select items: ```typescript const timeSlotSelectItems = timeSlotItems.map(slot => ({ ...slot, isDisabled: bookedSlots.includes(slot.id), label: bookedSlots.includes(slot.id) ? `${slot.label} (Booked)` : slot.label, })); ``` Use `timeSlotSelectItems` in the Time Slot Select component. Clear selected timeSlot when doctor or date changes if the slot becomes booked. - [ ] **Step 3: Add double-check before save** In `handleSave`, before creating the appointment, re-query to confirm the slot is still available: ```typescript // Double-check slot availability before booking const dayStart = `${date}T00:00:00`; const dayEnd = `${date}T23:59:59`; const checkResult = await apiClient.graphql<{ appointments: { totalCount: number } }>( `{ appointments(filter: { doctorId: { eq: "${doctor}" }, scheduledAt: { gte: "${dayStart}T${timeSlot}:00", lte: "${dayStart}T${timeSlot}:00" }, appointmentStatus: { in: [SCHEDULED, CONFIRMED] } }) { totalCount } }`, ); if (checkResult.appointments.totalCount > 0) { setError('This slot was just booked by someone else. Please select a different time.'); return; } ``` - [ ] **Step 4: Commit** ``` feat: check doctor availability and grey out booked time slots ``` --- ## Task 3: Add edit and cancel support After an appointment is booked (via the disposition flow's APPOINTMENT_BOOKED path), allow the agent to edit or cancel it. **Files:** - Modify: `helix-engage/src/components/call-desk/appointment-form.tsx` - [ ] **Step 1: Add optional appointment prop for edit mode** Extend the props type: ```typescript type AppointmentFormProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; callerNumber?: string | null; leadName?: string | null; leadId?: string | null; onSaved?: () => void; existingAppointment?: { id: string; scheduledAt: string; doctorName: string; doctorId?: string; department: string; reasonForVisit?: string; appointmentStatus: string; } | null; }; ``` When `existingAppointment` is provided, pre-fill the form fields from it. Change the header to "Edit Appointment" and the save button to "Update Appointment". - [ ] **Step 2: Initialize form state from existing appointment** ```typescript const isEditMode = !!existingAppointment; // Initialize state from existing appointment if in edit mode const [date, setDate] = useState(() => { if (existingAppointment?.scheduledAt) { return existingAppointment.scheduledAt.split('T')[0]; } return ''; }); const [timeSlot, setTimeSlot] = useState(() => { if (existingAppointment?.scheduledAt) { const dt = new Date(existingAppointment.scheduledAt); return `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}`; } return null; }); const [doctor, setDoctor] = useState(existingAppointment?.doctorId ?? null); const [department, setDepartment] = useState(existingAppointment?.department ?? null); const [chiefComplaint, setChiefComplaint] = useState(existingAppointment?.reasonForVisit ?? ''); ``` - [ ] **Step 3: Add update mutation to handleSave** In `handleSave`, if `isEditMode`, use `updateAppointment` instead of `createAppointment`: ```typescript if (isEditMode && existingAppointment) { await apiClient.graphql( `mutation UpdateAppointment($id: ID!, $data: AppointmentUpdateInput!) { updateAppointment(id: $id, data: $data) { id } }`, { id: existingAppointment.id, data: { scheduledAt, doctorName: selectedDoctor?.name ?? '', department: selectedDoctor?.department ?? '', doctorId: doctor, reasonForVisit: chiefComplaint || null, }, }, ); } else { // existing create logic } ``` - [ ] **Step 4: Add Cancel Appointment button** In edit mode, add a "Cancel Appointment" button in the footer: ```typescript {isEditMode && ( )} ``` The handler: ```typescript const handleCancel = async () => { if (!existingAppointment) return; setIsSaving(true); try { await apiClient.graphql( `mutation CancelAppointment($id: ID!, $data: AppointmentUpdateInput!) { updateAppointment(id: $id, data: $data) { id } }`, { id: existingAppointment.id, data: { appointmentStatus: 'CANCELLED' }, }, ); notify.success('Appointment Cancelled'); onSaved?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to cancel appointment'); } finally { setIsSaving(false); } }; ``` Import `notify` from `@/lib/toast`. - [ ] **Step 5: Commit** ``` feat: add appointment edit and cancel support ``` --- ## Task 4: Deploy and verify - [ ] **Step 1: Type check** ```bash cd helix-engage && npx tsc --noEmit ``` - [ ] **Step 2: Build and deploy frontend** ```bash VITE_API_URL=https://engage-api.srv1477139.hstgr.cloud \ VITE_SIP_URI=sip:523590@blr-pub-rtc4.ozonetel.com \ VITE_SIP_PASSWORD=523590 \ VITE_SIP_WS_SERVER=wss://blr-pub-rtc4.ozonetel.com:444 \ npm run build ``` - [ ] **Step 3: Test availability checking** 1. During a call, click "Book Appt" 2. Select a department → doctor list filters 3. Select a doctor + date → booked slots show as "(Booked)" and are disabled 4. Select an available slot → "Book Appointment" succeeds 5. Select the same doctor + date again → the slot you just booked should now be greyed out - [ ] **Step 4: Test edit/cancel** This requires passing `existingAppointment` prop from the parent component. For now, verify the edit/cancel code compiles and the props interface is correct. Wiring the edit flow from the call history or lead 360 panel can be done as a follow-up. --- ## Notes - **Hardcoded clinicItems and genderItems stay** — clinics aren't changing often and gender is static. Only doctors and departments come from the platform. - **timeSlotItems stay hardcoded** — these represent the hospital's standard appointment slots (30-min increments, 9am-4pm). The availability check determines which ones are booked. - **The `appointmentStatus` filter uses `in: [SCHEDULED, CONFIRMED, CHECKED_IN, IN_PROGRESS]`** — cancelled and completed appointments don't block the slot. - **The `doctorId` relation** links the appointment to the Doctor entity on the platform. We also keep `doctorName` as a denormalized text field for display.