mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat(onboarding/phase-6): setup wizard polish, seed script alignment, doctor visit slots
- 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>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { WizardStep } from './wizard-step';
|
||||
import { DoctorsRightPane, type DoctorSummary } from './wizard-right-panes';
|
||||
import {
|
||||
DoctorForm,
|
||||
doctorFormToGraphQLInput,
|
||||
doctorCoreToGraphQLInput,
|
||||
visitSlotInputsFromForm,
|
||||
emptyDoctorFormValues,
|
||||
type DoctorFormValues,
|
||||
} from '@/components/forms/doctor-form';
|
||||
@@ -20,21 +22,40 @@ 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);
|
||||
|
||||
useEffect(() => {
|
||||
apiClient
|
||||
.graphql<{ clinics: { edges: { node: ClinicLite }[] } }>(
|
||||
`{ clinics(first: 100) { edges { node { id clinicName } } } }`,
|
||||
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 },
|
||||
)
|
||||
.then((data) => setClinics(data.clinics.edges.map((e) => e.node)))
|
||||
.catch(() => setClinics([]))
|
||||
.finally(() => setLoadingClinics(false));
|
||||
);
|
||||
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],
|
||||
@@ -47,16 +68,37 @@ export const WizardStepDoctors = (props: WizardStepComponentProps) => {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await apiClient.graphql(
|
||||
// 1. Core doctor record
|
||||
const res = await apiClient.graphql<{ createDoctor: { id: string } }>(
|
||||
`mutation CreateDoctor($data: DoctorCreateInput!) {
|
||||
createDoctor(data: $data) { id }
|
||||
}`,
|
||||
{ data: doctorFormToGraphQLInput(values) },
|
||||
{ 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 props.onComplete('doctors');
|
||||
await fetchData();
|
||||
if (!props.isCompleted) {
|
||||
await props.onComplete('doctors');
|
||||
}
|
||||
setValues(emptyDoctorFormValues());
|
||||
props.onAdvance();
|
||||
} catch (err) {
|
||||
console.error('[wizard/doctors] save failed', err);
|
||||
} finally {
|
||||
@@ -64,16 +106,19 @@ export const WizardStepDoctors = (props: WizardStepComponentProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const pretendCompleted = props.isCompleted || doctors.length > 0;
|
||||
|
||||
return (
|
||||
<WizardStep
|
||||
step="doctors"
|
||||
isCompleted={props.isCompleted}
|
||||
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>
|
||||
@@ -87,13 +132,17 @@ export const WizardStepDoctors = (props: WizardStepComponentProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{props.isCompleted && (
|
||||
<div className="mb-5 rounded-lg border border-secondary bg-secondary p-4 text-xs text-tertiary">
|
||||
You've already added at least one doctor. Fill the form again to add another, or
|
||||
click <b>Next</b> to continue.
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user