feat(onboarding/phase-5): wire real forms into the setup wizard

Replaces the Phase 2 StepPlaceholder with six dedicated wizard step
components, each wrapping the corresponding Phase 3/4 form. The parent
setup-wizard.tsx is now a thin dispatcher that owns shell state +
markSetupStepComplete wiring; each step owns its own data load, form
state, validation, and save action.

- src/components/setup/wizard-step-types.ts — shared
  WizardStepComponentProps shape
- src/components/setup/wizard-step-identity.tsx — minimal brand form
  (hospital name + logo URL) hitting /api/config/theme, links out to
  /branding for full customisation
- src/components/setup/wizard-step-clinics.tsx — ClinicForm + createClinic
  mutation, always presents an empty "add new" form
- src/components/setup/wizard-step-doctors.tsx — DoctorForm with clinic
  dropdown, blocks with an inline warning when no clinics exist yet
- src/components/setup/wizard-step-team.tsx — InviteMemberForm with real
  roles fetched from getRoles, sends invitations via sendInvitations
- src/components/setup/wizard-step-telephony.tsx — loads masked config
  from /api/config/telephony, validates required Ozonetel fields on save
- src/components/setup/wizard-step-ai.tsx — loads AI config, clamps
  temperature 0..2, doesn't auto-advance (last step, admin taps Finish)
- src/pages/setup-wizard.tsx — dispatches to the right step component
  based on activeStep, passes a WizardStepComponentProps bundle

Each step calls onComplete(step) after a successful save, which updates
the shared SetupState so the left-nav badges reflect the new status
immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 07:54:35 +05:30
parent a7b2fd7fbe
commit a287a97fe4
8 changed files with 646 additions and 54 deletions

View File

@@ -0,0 +1,101 @@
import { useEffect, useMemo, useState } from 'react';
import { WizardStep } from './wizard-step';
import {
DoctorForm,
doctorFormToGraphQLInput,
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 [loadingClinics, setLoadingClinics] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
apiClient
.graphql<{ clinics: { edges: { node: ClinicLite }[] } }>(
`{ clinics(first: 100) { edges { node { id clinicName } } } }`,
undefined,
{ silent: true },
)
.then((data) => setClinics(data.clinics.edges.map((e) => e.node)))
.catch(() => setClinics([]))
.finally(() => setLoadingClinics(false));
}, []);
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 {
await apiClient.graphql(
`mutation CreateDoctor($data: DoctorCreateInput!) {
createDoctor(data: $data) { id }
}`,
{ data: doctorFormToGraphQLInput(values) },
);
notify.success('Doctor added', `Dr. ${values.firstName} ${values.lastName}`);
await props.onComplete('doctors');
setValues(emptyDoctorFormValues());
props.onAdvance();
} catch (err) {
console.error('[wizard/doctors] save failed', err);
} finally {
setSaving(false);
}
};
return (
<WizardStep
step="doctors"
isCompleted={props.isCompleted}
isLast={props.isLast}
onPrev={props.onPrev}
onNext={props.onNext}
onMarkComplete={handleSave}
onFinish={props.onFinish}
saving={saving}
>
{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>
) : (
<>
{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} />
</>
)}
</WizardStep>
);
};