Files
helix-engage/src/components/setup/wizard-step-doctors.tsx
saridsa2 f57fbc1f24 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>
2026-04-10 08:37:34 +05:30

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>
);
};