Files
helix-engage/src/components/setup/wizard-step-clinics.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

172 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};