mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
fix(appointments): preload clinic + keep saved time on edit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
The appointment-edit form opened with clinic/time blank even for
well-formed appointments because the pipeline never carried clinicId
end-to-end. Four-layer audit + fix:
1. APPOINTMENTS_QUERY now fetches clinicId + clinic { id clinicName }
+ doctor.fullName (was only doctor.id).
2. transformAppointments populates real clinicId + clinicName from the
relation instead of faking clinicName=department.
3. Appointment type gets clinicId: string | null.
4. context-panel passes clinicId through to AppointmentForm's
existingAppointment prop; form initial-states clinic from it.
Also on edit: if the saved timeSlot isn't in the fresh slot list
(past-slot filter, schedule change, clinic mismatch) we inject it as
"HH:MM (current)" so the dropdown displays the existing value instead
of looking cleared.
Historical appointments with clinicId=null on the platform still fall
through to the auto-select-from-slot logic; a maint backfill for those
is a separate task.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ type ExistingAppointment = {
|
|||||||
doctorName: string;
|
doctorName: string;
|
||||||
doctorId?: string;
|
doctorId?: string;
|
||||||
department: string;
|
department: string;
|
||||||
|
clinicId?: string;
|
||||||
reasonForVisit?: string;
|
reasonForVisit?: string;
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
@@ -82,7 +83,11 @@ export const AppointmentForm = ({
|
|||||||
const [patientPhone, setPatientPhone] = useState(callerNumber ?? '');
|
const [patientPhone, setPatientPhone] = useState(callerNumber ?? '');
|
||||||
const [age, setAge] = useState('');
|
const [age, setAge] = useState('');
|
||||||
const [gender, setGender] = useState<string | null>(null);
|
const [gender, setGender] = useState<string | null>(null);
|
||||||
const [clinic, setClinic] = useState<string | null>(null);
|
// Preload clinic from the existing appointment when editing — so the
|
||||||
|
// select lands on the right branch instead of being empty and forcing
|
||||||
|
// the agent to re-pick. Only historical rows that predate clinicId
|
||||||
|
// persistence will fall through to the auto-select-from-slot logic.
|
||||||
|
const [clinic, setClinic] = useState<string | null>(existingAppointment?.clinicId ?? null);
|
||||||
const [clinicItems, setClinicItems] = useState<Array<{ id: string; label: string }>>([]);
|
const [clinicItems, setClinicItems] = useState<Array<{ id: string; label: string }>>([]);
|
||||||
const [department, setDepartment] = useState<string | null>(existingAppointment?.department ?? null);
|
const [department, setDepartment] = useState<string | null>(existingAppointment?.department ?? null);
|
||||||
const [doctor, setDoctor] = useState<string | null>(existingAppointment?.doctorId ?? null);
|
const [doctor, setDoctor] = useState<string | null>(existingAppointment?.doctorId ?? null);
|
||||||
@@ -113,14 +118,29 @@ export const AppointmentForm = ({
|
|||||||
).then(slots => {
|
).then(slots => {
|
||||||
// Filter by selected clinic — doctor may visit multiple branches
|
// Filter by selected clinic — doctor may visit multiple branches
|
||||||
const filtered = clinic ? slots.filter(s => s.clinicId === clinic) : slots;
|
const filtered = clinic ? slots.filter(s => s.clinicId === clinic) : slots;
|
||||||
setTimeSlotItems(filtered.map(s => ({ id: s.time, label: s.label })));
|
let items = filtered.map(s => ({ id: s.time, label: s.label }));
|
||||||
|
|
||||||
|
// In edit mode, the saved timeSlot may have been filtered out
|
||||||
|
// (past-slot filter, schedule change, clinic mismatch). Inject
|
||||||
|
// it as a synthetic option so the dropdown still shows the
|
||||||
|
// existing value — otherwise the agent sees a cleared field
|
||||||
|
// and assumes the save-time was lost.
|
||||||
|
if (timeSlot && !items.some(i => i.id === timeSlot)) {
|
||||||
|
items = [{ id: timeSlot, label: `${timeSlot} (current)` }, ...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeSlotItems(items);
|
||||||
// Auto-select clinic from the slot's clinic only if no clinic chosen
|
// Auto-select clinic from the slot's clinic only if no clinic chosen
|
||||||
if (filtered.length === 0 && slots.length > 0 && !clinic) {
|
if (filtered.length === 0 && slots.length > 0 && !clinic) {
|
||||||
setClinic(slots[0].clinicId);
|
setClinic(slots[0].clinicId);
|
||||||
setTimeSlotItems(slots.filter(s => s.clinicId === slots[0].clinicId).map(s => ({ id: s.time, label: s.label })));
|
const autoItems = slots.filter(s => s.clinicId === slots[0].clinicId).map(s => ({ id: s.time, label: s.label }));
|
||||||
|
if (timeSlot && !autoItems.some(i => i.id === timeSlot)) {
|
||||||
|
autoItems.unshift({ id: timeSlot, label: `${timeSlot} (current)` });
|
||||||
|
}
|
||||||
|
setTimeSlotItems(autoItems);
|
||||||
}
|
}
|
||||||
}).catch(() => setTimeSlotItems([]));
|
}).catch(() => setTimeSlotItems([]));
|
||||||
}, [doctor, date, clinic]);
|
}, [doctor, date, clinic, timeSlot]);
|
||||||
|
|
||||||
// Availability state
|
// Availability state
|
||||||
const [bookedSlots, setBookedSlots] = useState<string[]>([]);
|
const [bookedSlots, setBookedSlots] = useState<string[]>([]);
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export const ContextPanel = ({ selectedLead, activities, calls, followUps, appoi
|
|||||||
doctorName: editingAppointment.doctorName ?? '',
|
doctorName: editingAppointment.doctorName ?? '',
|
||||||
doctorId: editingAppointment.doctorId ?? undefined,
|
doctorId: editingAppointment.doctorId ?? undefined,
|
||||||
department: editingAppointment.department ?? '',
|
department: editingAppointment.department ?? '',
|
||||||
|
clinicId: editingAppointment.clinicId ?? undefined,
|
||||||
reasonForVisit: editingAppointment.reasonForVisit ?? undefined,
|
reasonForVisit: editingAppointment.reasonForVisit ?? undefined,
|
||||||
status: editingAppointment.appointmentStatus ?? 'SCHEDULED',
|
status: editingAppointment.appointmentStatus ?? 'SCHEDULED',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ export const APPOINTMENTS_QUERY = `{ appointments(first: 100, orderBy: [{ schedu
|
|||||||
scheduledAt durationMin appointmentType status
|
scheduledAt durationMin appointmentType status
|
||||||
doctorName department reasonForVisit
|
doctorName department reasonForVisit
|
||||||
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
||||||
doctor { id }
|
doctor { id fullName { firstName lastName } }
|
||||||
|
clinicId clinic { id clinicName }
|
||||||
} } } }`;
|
} } } }`;
|
||||||
|
|
||||||
export const PATIENTS_QUERY = `{ patients(first: 50) { edges { node {
|
export const PATIENTS_QUERY = `{ patients(first: 50) { edges { node {
|
||||||
|
|||||||
@@ -158,22 +158,31 @@ export function transformCalls(data: any): Call[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function transformAppointments(data: any): Appointment[] {
|
export function transformAppointments(data: any): Appointment[] {
|
||||||
return extractEdges(data, 'appointments').map((n) => ({
|
return extractEdges(data, 'appointments').map((n) => {
|
||||||
id: n.id,
|
// Doctor name: prefer the relation's fullName (authoritative — pulled
|
||||||
createdAt: n.createdAt,
|
// from the Doctor entity). Fall back to the denormalized doctorName
|
||||||
scheduledAt: n.scheduledAt,
|
// field for legacy rows that predate the doctor relation being fetched.
|
||||||
durationMinutes: n.durationMin ?? 30,
|
const doctorFullName = n.doctor?.fullName
|
||||||
appointmentType: n.appointmentType,
|
? `${n.doctor.fullName.firstName ?? ''} ${n.doctor.fullName.lastName ?? ''}`.trim()
|
||||||
appointmentStatus: n.status,
|
: '';
|
||||||
doctorName: n.doctorName,
|
return {
|
||||||
doctorId: n.doctor?.id ?? null,
|
id: n.id,
|
||||||
department: n.department,
|
createdAt: n.createdAt,
|
||||||
reasonForVisit: n.reasonForVisit,
|
scheduledAt: n.scheduledAt,
|
||||||
patientId: n.patient?.id ?? null,
|
durationMinutes: n.durationMin ?? 30,
|
||||||
patientName: n.patient?.fullName ? `${n.patient.fullName.firstName} ${n.patient.fullName.lastName}`.trim() : null,
|
appointmentType: n.appointmentType,
|
||||||
patientPhone: n.patient?.phones?.primaryPhoneNumber ?? null,
|
appointmentStatus: n.status,
|
||||||
clinicName: n.department ?? null,
|
doctorName: doctorFullName || n.doctorName || null,
|
||||||
}));
|
doctorId: n.doctor?.id ?? null,
|
||||||
|
department: n.department,
|
||||||
|
reasonForVisit: n.reasonForVisit,
|
||||||
|
patientId: n.patient?.id ?? null,
|
||||||
|
patientName: n.patient?.fullName ? `${n.patient.fullName.firstName} ${n.patient.fullName.lastName}`.trim() : null,
|
||||||
|
patientPhone: n.patient?.phones?.primaryPhoneNumber ?? null,
|
||||||
|
clinicId: n.clinicId ?? n.clinic?.id ?? null,
|
||||||
|
clinicName: n.clinic?.clinicName ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformPatients(data: any): Patient[] {
|
export function transformPatients(data: any): Patient[] {
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ export type Appointment = {
|
|||||||
patientId: string | null;
|
patientId: string | null;
|
||||||
patientName: string | null;
|
patientName: string | null;
|
||||||
patientPhone: string | null;
|
patientPhone: string | null;
|
||||||
|
clinicId: string | null;
|
||||||
clinicName: string | null;
|
clinicName: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user