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' },
|
{ 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user