mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +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>
172 lines
6.8 KiB
TypeScript
172 lines
6.8 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
||
import { WizardStep } from './wizard-step';
|
||
import { ClinicsRightPane, type ClinicSummary } from './wizard-right-panes';
|
||
import {
|
||
ClinicForm,
|
||
clinicCoreToGraphQLInput,
|
||
holidayInputsFromForm,
|
||
requiredDocInputsFromForm,
|
||
emptyClinicFormValues,
|
||
type ClinicFormValues,
|
||
} from '@/components/forms/clinic-form';
|
||
import { apiClient } from '@/lib/api-client';
|
||
import { notify } from '@/lib/toast';
|
||
import type { WizardStepComponentProps } from './wizard-step-types';
|
||
|
||
// Clinic step — presents a single-clinic form. On save the wizard runs
|
||
// a three-stage create chain:
|
||
// 1. createClinic (main record → get id)
|
||
// 2. createHoliday × N (one per holiday entry)
|
||
// 3. createClinicRequiredDocument × N (one per required doc type)
|
||
//
|
||
// This mirrors what the /settings/clinics list page does, minus the
|
||
// delete-old-first step (wizard is always creating, never updating).
|
||
// Failures inside the chain throw up through onComplete so the user
|
||
// sees the error loud, and the wizard stays on the current step.
|
||
export const WizardStepClinics = (props: WizardStepComponentProps) => {
|
||
const [values, setValues] = useState<ClinicFormValues>(emptyClinicFormValues);
|
||
const [saving, setSaving] = useState(false);
|
||
const [clinics, setClinics] = useState<ClinicSummary[]>([]);
|
||
|
||
const fetchClinics = useCallback(async () => {
|
||
try {
|
||
// Field names match what the platform actually exposes:
|
||
// - the SDK ADDRESS field is named "address" but the
|
||
// platform mounts it as `addressCustom` (composite type
|
||
// with addressCity / addressStreet / etc.)
|
||
// - the SDK SELECT field labelled "Status" lands as plain
|
||
// `status: ClinicStatusEnum`, NOT `clinicStatus`
|
||
// Verified via __type introspection — keep this query
|
||
// pinned to the actual schema to avoid silent empty fetches.
|
||
type ClinicNode = {
|
||
id: string;
|
||
clinicName: string | null;
|
||
addressCustom: { addressCity: string | null } | null;
|
||
status: string | null;
|
||
};
|
||
const data = await apiClient.graphql<{
|
||
clinics: { edges: { node: ClinicNode }[] };
|
||
}>(
|
||
`{ clinics(first: 100, orderBy: { createdAt: DescNullsLast }) {
|
||
edges { node {
|
||
id clinicName
|
||
addressCustom { addressCity }
|
||
status
|
||
} }
|
||
} }`,
|
||
undefined,
|
||
{ silent: true },
|
||
);
|
||
// Flatten into the shape ClinicsRightPane expects.
|
||
setClinics(
|
||
data.clinics.edges.map((e) => ({
|
||
id: e.node.id,
|
||
clinicName: e.node.clinicName,
|
||
addressCity: e.node.addressCustom?.addressCity ?? null,
|
||
clinicStatus: e.node.status,
|
||
})),
|
||
);
|
||
} catch (err) {
|
||
console.error('[wizard/clinics] fetch failed', err);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchClinics();
|
||
}, [fetchClinics]);
|
||
|
||
const handleSave = async () => {
|
||
if (!values.clinicName.trim()) {
|
||
notify.error('Clinic name is required');
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
// 1. Core clinic record
|
||
const res = await apiClient.graphql<{ createClinic: { id: string } }>(
|
||
`mutation CreateClinic($data: ClinicCreateInput!) {
|
||
createClinic(data: $data) { id }
|
||
}`,
|
||
{ data: clinicCoreToGraphQLInput(values) },
|
||
);
|
||
const clinicId = res.createClinic.id;
|
||
|
||
// 2. Holidays
|
||
if (values.holidays.length > 0) {
|
||
const holidayInputs = holidayInputsFromForm(values, clinicId);
|
||
await Promise.all(
|
||
holidayInputs.map((data) =>
|
||
apiClient.graphql(
|
||
`mutation CreateHoliday($data: HolidayCreateInput!) {
|
||
createHoliday(data: $data) { id }
|
||
}`,
|
||
{ data },
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 3. Required documents
|
||
if (values.requiredDocumentTypes.length > 0) {
|
||
const docInputs = requiredDocInputsFromForm(values, clinicId);
|
||
await Promise.all(
|
||
docInputs.map((data) =>
|
||
apiClient.graphql(
|
||
`mutation CreateClinicRequiredDocument($data: ClinicRequiredDocumentCreateInput!) {
|
||
createClinicRequiredDocument(data: $data) { id }
|
||
}`,
|
||
{ data },
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
notify.success('Clinic added', values.clinicName);
|
||
await fetchClinics();
|
||
// Mark complete on first successful create. Don't auto-advance —
|
||
// admins typically add multiple clinics in one sitting; the
|
||
// Continue button on the wizard nav handles forward motion.
|
||
if (!props.isCompleted) {
|
||
await props.onComplete('clinics');
|
||
}
|
||
setValues(emptyClinicFormValues());
|
||
} catch (err) {
|
||
console.error('[wizard/clinics] save failed', err);
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
// Same trick as the Team step: once at least one clinic exists,
|
||
// flip isCompleted=true so the WizardStep renders the "Continue"
|
||
// button as the primary action — the form stays open below for
|
||
// adding more clinics.
|
||
const pretendCompleted = props.isCompleted || clinics.length > 0;
|
||
|
||
return (
|
||
<WizardStep
|
||
step="clinics"
|
||
isCompleted={pretendCompleted}
|
||
isLast={props.isLast}
|
||
onPrev={props.onPrev}
|
||
onNext={props.onNext}
|
||
onMarkComplete={handleSave}
|
||
onFinish={props.onFinish}
|
||
saving={saving}
|
||
rightPane={<ClinicsRightPane clinics={clinics} />}
|
||
>
|
||
<ClinicForm value={values} onChange={setValues} />
|
||
<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 clinic'}
|
||
</button>
|
||
</div>
|
||
</WizardStep>
|
||
);
|
||
};
|