feat: appointment form uses master data endpoint for clinics, doctors, departments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 17:24:59 +05:30
parent 8da431a6cd
commit 951acf59c5

View File

@@ -44,22 +44,8 @@ const genderItems = [
{ id: 'other', label: 'Other' }, { id: 'other', label: 'Other' },
]; ];
const timeSlotItems = [ // Time slots are fetched from /api/masterdata/slots based on
{ id: '09:00', label: '9:00 AM' }, // doctor + date. No hardcoded times.
{ id: '09:30', label: '9:30 AM' },
{ id: '10:00', label: '10:00 AM' },
{ id: '10:30', label: '10:30 AM' },
{ id: '11:00', label: '11:00 AM' },
{ id: '11:30', label: '11:30 AM' },
{ id: '14:00', label: '2:00 PM' },
{ id: '14:30', label: '2:30 PM' },
{ id: '15:00', label: '3:00 PM' },
{ id: '15:30', label: '3:30 PM' },
{ id: '16:00', label: '4:00 PM' },
];
const formatDeptLabel = (dept: string) =>
dept.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
export const AppointmentForm = ({ export const AppointmentForm = ({
isOpen, isOpen,
@@ -111,6 +97,24 @@ export const AppointmentForm = ({
const [chiefComplaint, setChiefComplaint] = useState(existingAppointment?.reasonForVisit ?? ''); const [chiefComplaint, setChiefComplaint] = useState(existingAppointment?.reasonForVisit ?? '');
const [source, setSource] = useState('Inbound Call'); const [source, setSource] = useState('Inbound Call');
const [agentNotes, setAgentNotes] = useState(''); const [agentNotes, setAgentNotes] = useState('');
const [timeSlotItems, setTimeSlotItems] = useState<Array<{ id: string; label: string }>>([]);
// Fetch available time slots when doctor + date change
useEffect(() => {
if (!doctor || !date) {
setTimeSlotItems([]);
return;
}
apiClient.get<Array<{ time: string; label: string; clinicId: string; clinicName: string }>>(
`/api/masterdata/slots?doctorId=${doctor}&date=${date}`,
).then(slots => {
setTimeSlotItems(slots.map(s => ({ id: s.time, label: s.label })));
// Auto-select clinic from the slot's clinic
if (slots.length > 0 && !clinic) {
setClinic(slots[0].clinicId);
}
}).catch(() => setTimeSlotItems([]));
}, [doctor, date]);
// Availability state // Availability state
const [bookedSlots, setBookedSlots] = useState<string[]>([]); const [bookedSlots, setBookedSlots] = useState<string[]>([]);
@@ -121,55 +125,26 @@ export const AppointmentForm = ({
// Fetch doctors on mount. Doctors are hospital-wide — no single // Fetch doctors on mount. Doctors are hospital-wide — no single
// `clinic` field anymore. We pull the full visit-slot list via the // `clinic` field anymore. We pull the full visit-slot list via the
// doctorVisitSlots reverse relation so the agent can see which // Fetch clinics + doctors from the master data endpoint (Redis-cached).
// clinics + days this doctor covers in the picker. // This is faster than direct GraphQL and returns pre-formatted data.
// Fetch clinics from platform
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
apiClient.graphql<{ clinics: { edges: Array<{ node: { id: string; clinicName: string } }> } }>( apiClient.get<Array<{ id: string; name: string; phone: string; address: string }>>('/api/masterdata/clinics')
`{ clinics(first: 50) { edges { node { id clinicName } } } }`, .then(clinics => {
).then(data => { setClinicItems(clinics.map(c => ({ id: c.id, label: c.name || 'Unnamed Clinic' })));
setClinicItems(
data.clinics.edges.map(e => ({ id: e.node.id, label: e.node.clinicName || 'Unnamed Clinic' })),
);
}).catch(() => {}); }).catch(() => {});
}, [isOpen]); }, [isOpen]);
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
apiClient.graphql<{ doctors: { edges: Array<{ node: any }> } }>( apiClient.get<Array<{ id: string; name: string; department: string; qualifications: string }>>('/api/masterdata/doctors')
`{ doctors(first: 50) { edges { node { .then(docs => {
id name fullName { firstName lastName } department setDoctors(docs.map(d => ({
doctorVisitSlots(first: 50) { id: d.id,
edges { node { id clinic { id clinicName } dayOfWeek startTime endTime } } name: d.name,
} department: d.department,
} } } }`, clinic: '', // clinic assignment via visit slots, not on doctor directly
).then(data => { })));
const docs = data.doctors.edges.map(e => {
// Flatten the visit-slot list into a comma-separated
// clinic summary for display. Keep full slot data on
// the record in case future UX needs it (e.g., show
// only slots matching the selected date's weekday).
const slotEdges: Array<{ node: any }> = e.node.doctorVisitSlots?.edges ?? [];
const clinicNames = Array.from(
new Set(
slotEdges
.map((se) => se.node.clinic?.clinicName ?? se.node.clinicId)
.filter((n): n is string => !!n),
),
);
return {
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` here is a display-only summary: "Koramangala, Whitefield"
// or empty if the doctor has no slots yet.
clinic: clinicNames.join(', '),
};
});
setDoctors(docs);
}).catch(() => {}); }).catch(() => {});
}, [isOpen]); }, [isOpen]);
@@ -219,9 +194,18 @@ export const AppointmentForm = ({
setTimeSlot(null); setTimeSlot(null);
}, [doctor, date]); }, [doctor, date]);
// Derive department and doctor lists from fetched data // Departments from master data (or fallback to deriving from doctors)
const departmentItems = [...new Set(doctors.map(d => d.department).filter(Boolean))] const [departmentItems, setDepartmentItems] = useState<Array<{ id: string; label: string }>>([]);
.map(dept => ({ id: dept, label: formatDeptLabel(dept) })); useEffect(() => {
if (!isOpen) return;
apiClient.get<string[]>('/api/masterdata/departments')
.then(depts => setDepartmentItems(depts.map(d => ({ id: d, label: d }))))
.catch(() => {
// Fallback: derive from doctor list
const derived = [...new Set(doctors.map(d => d.department).filter(Boolean))];
setDepartmentItems(derived.map(d => ({ id: d, label: d })));
});
}, [isOpen, doctors]);
const filteredDoctors = department const filteredDoctors = department
? doctors.filter(d => d.department === department) ? doctors.filter(d => d.department === department)