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' },
];
const timeSlotItems = [
{ id: '09:00', label: '9:00 AM' },
{ 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());
// Time slots are fetched from /api/masterdata/slots based on
// doctor + date. No hardcoded times.
export const AppointmentForm = ({
isOpen,
@@ -111,6 +97,24 @@ export const AppointmentForm = ({
const [chiefComplaint, setChiefComplaint] = useState(existingAppointment?.reasonForVisit ?? '');
const [source, setSource] = useState('Inbound Call');
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
const [bookedSlots, setBookedSlots] = useState<string[]>([]);
@@ -121,56 +125,27 @@ export const AppointmentForm = ({
// Fetch doctors on mount. Doctors are hospital-wide — no single
// `clinic` field anymore. We pull the full visit-slot list via the
// doctorVisitSlots reverse relation so the agent can see which
// clinics + days this doctor covers in the picker.
// Fetch clinics from platform
// Fetch clinics + doctors from the master data endpoint (Redis-cached).
// This is faster than direct GraphQL and returns pre-formatted data.
useEffect(() => {
if (!isOpen) return;
apiClient.graphql<{ clinics: { edges: Array<{ node: { id: string; clinicName: string } }> } }>(
`{ clinics(first: 50) { edges { node { id clinicName } } } }`,
).then(data => {
setClinicItems(
data.clinics.edges.map(e => ({ id: e.node.id, label: e.node.clinicName || 'Unnamed Clinic' })),
);
}).catch(() => {});
apiClient.get<Array<{ id: string; name: string; phone: string; address: string }>>('/api/masterdata/clinics')
.then(clinics => {
setClinicItems(clinics.map(c => ({ id: c.id, label: c.name || 'Unnamed Clinic' })));
}).catch(() => {});
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
apiClient.graphql<{ doctors: { edges: Array<{ node: any }> } }>(
`{ doctors(first: 50) { edges { node {
id name fullName { firstName lastName } department
doctorVisitSlots(first: 50) {
edges { node { id clinic { id clinicName } dayOfWeek startTime endTime } }
}
} } } }`,
).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(() => {});
apiClient.get<Array<{ id: string; name: string; department: string; qualifications: string }>>('/api/masterdata/doctors')
.then(docs => {
setDoctors(docs.map(d => ({
id: d.id,
name: d.name,
department: d.department,
clinic: '', // clinic assignment via visit slots, not on doctor directly
})));
}).catch(() => {});
}, [isOpen]);
// Fetch booked slots when doctor + date selected
@@ -219,9 +194,18 @@ export const AppointmentForm = ({
setTimeSlot(null);
}, [doctor, date]);
// Derive department and doctor lists from fetched data
const departmentItems = [...new Set(doctors.map(d => d.department).filter(Boolean))]
.map(dept => ({ id: dept, label: formatDeptLabel(dept) }));
// Departments from master data (or fallback to deriving from doctors)
const [departmentItems, setDepartmentItems] = useState<Array<{ id: string; label: string }>>([]);
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
? doctors.filter(d => d.department === department)