mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user