mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
- Setup wizard: 3-pane layout with right-side live previews, resume banner, edit/copy icons on team step, AI prompt configuration - Forms: employee-create replaces invite-member (no email invites), clinic form with address/hours/payment, doctor form with visit slots - Seed script: aligned to current SDK schema — doctors created as workspace members (HelixEngage Manager role), visitingHours replaced by doctorVisitSlot entity, clinics seeded, portalUserId linked dynamically, SUB/ORIGIN/GQL configurable via env vars - Pages: clinics + doctors CRUD updated for new schema, team settings with temp password + role assignment - New components: time-picker, day-selector, wizard-right-panes, wizard-layout-context, resume-setup-banner - Removed: invite-member-form (replaced by employee-create-form per no-email-invites rule) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
151 lines
6.0 KiB
TypeScript
151 lines
6.0 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { WizardStep } from './wizard-step';
|
|
import { DoctorsRightPane, type DoctorSummary } from './wizard-right-panes';
|
|
import {
|
|
DoctorForm,
|
|
doctorCoreToGraphQLInput,
|
|
visitSlotInputsFromForm,
|
|
emptyDoctorFormValues,
|
|
type DoctorFormValues,
|
|
} from '@/components/forms/doctor-form';
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { notify } from '@/lib/toast';
|
|
import type { WizardStepComponentProps } from './wizard-step-types';
|
|
|
|
// Doctor step — mirrors the clinics step but also fetches the clinic list
|
|
// for the DoctorForm's clinic dropdown. If there are no clinics yet we let
|
|
// the admin know they need to complete step 2 first (the wizard doesn't
|
|
// force ordering, but a doctor without a clinic is useless).
|
|
|
|
type ClinicLite = { id: string; clinicName: string | null };
|
|
|
|
export const WizardStepDoctors = (props: WizardStepComponentProps) => {
|
|
const [values, setValues] = useState<DoctorFormValues>(emptyDoctorFormValues);
|
|
const [clinics, setClinics] = useState<ClinicLite[]>([]);
|
|
const [doctors, setDoctors] = useState<DoctorSummary[]>([]);
|
|
const [loadingClinics, setLoadingClinics] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
try {
|
|
const data = await apiClient.graphql<{
|
|
clinics: { edges: { node: ClinicLite }[] };
|
|
doctors: { edges: { node: DoctorSummary }[] };
|
|
}>(
|
|
`{
|
|
clinics(first: 100) { edges { node { id clinicName } } }
|
|
doctors(first: 100, orderBy: { createdAt: DescNullsLast }) {
|
|
edges { node { id fullName { firstName lastName } department specialty } }
|
|
}
|
|
}`,
|
|
undefined,
|
|
{ silent: true },
|
|
);
|
|
setClinics(data.clinics.edges.map((e) => e.node));
|
|
setDoctors(data.doctors.edges.map((e) => e.node));
|
|
} catch (err) {
|
|
console.error('[wizard/doctors] fetch failed', err);
|
|
setClinics([]);
|
|
setDoctors([]);
|
|
} finally {
|
|
setLoadingClinics(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const clinicOptions = useMemo(
|
|
() => clinics.map((c) => ({ id: c.id, label: c.clinicName ?? 'Unnamed clinic' })),
|
|
[clinics],
|
|
);
|
|
|
|
const handleSave = async () => {
|
|
if (!values.firstName.trim() || !values.lastName.trim()) {
|
|
notify.error('First and last name are required');
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
// 1. Core doctor record
|
|
const res = await apiClient.graphql<{ createDoctor: { id: string } }>(
|
|
`mutation CreateDoctor($data: DoctorCreateInput!) {
|
|
createDoctor(data: $data) { id }
|
|
}`,
|
|
{ data: doctorCoreToGraphQLInput(values) },
|
|
);
|
|
const doctorId = res.createDoctor.id;
|
|
|
|
// 2. Visit slots (doctor can be at multiple clinics on
|
|
// multiple days with different times each).
|
|
const slotInputs = visitSlotInputsFromForm(values, doctorId);
|
|
if (slotInputs.length > 0) {
|
|
await Promise.all(
|
|
slotInputs.map((data) =>
|
|
apiClient.graphql(
|
|
`mutation CreateDoctorVisitSlot($data: DoctorVisitSlotCreateInput!) {
|
|
createDoctorVisitSlot(data: $data) { id }
|
|
}`,
|
|
{ data },
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
notify.success('Doctor added', `Dr. ${values.firstName} ${values.lastName}`);
|
|
await fetchData();
|
|
if (!props.isCompleted) {
|
|
await props.onComplete('doctors');
|
|
}
|
|
setValues(emptyDoctorFormValues());
|
|
} catch (err) {
|
|
console.error('[wizard/doctors] save failed', err);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const pretendCompleted = props.isCompleted || doctors.length > 0;
|
|
|
|
return (
|
|
<WizardStep
|
|
step="doctors"
|
|
isCompleted={pretendCompleted}
|
|
isLast={props.isLast}
|
|
onPrev={props.onPrev}
|
|
onNext={props.onNext}
|
|
onMarkComplete={handleSave}
|
|
onFinish={props.onFinish}
|
|
saving={saving}
|
|
rightPane={<DoctorsRightPane doctors={doctors} />}
|
|
>
|
|
{loadingClinics ? (
|
|
<p className="text-sm text-tertiary">Loading clinics…</p>
|
|
) : clinics.length === 0 ? (
|
|
<div className="rounded-lg border border-dashed border-warning bg-warning-primary p-4">
|
|
<p className="text-sm font-semibold text-warning-primary">Add a clinic first</p>
|
|
<p className="mt-1 text-xs text-tertiary">
|
|
You need at least one clinic before you can assign doctors. Go back to the{' '}
|
|
<b>Clinics</b> step and add a branch first.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<DoctorForm value={values} onChange={setValues} clinics={clinicOptions} />
|
|
<div className="mt-6 flex justify-end">
|
|
<button
|
|
type="button"
|
|
disabled={saving}
|
|
onClick={handleSave}
|
|
className="inline-flex items-center gap-2 rounded-lg bg-brand-solid px-4 py-2 text-sm font-semibold text-primary_on-brand shadow-xs transition hover:bg-brand-solid_hover disabled:opacity-60"
|
|
>
|
|
{saving ? 'Adding…' : 'Add doctor'}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</WizardStep>
|
|
);
|
|
};
|