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,102 @@
import { useEffect, useState } from 'react';
import { WizardStep } from './wizard-step';
import {
TelephonyForm,
emptyTelephonyFormValues,
type TelephonyFormValues,
} from '@/components/forms/telephony-form';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import type { WizardStepComponentProps } from './wizard-step-types';
// Telephony step — loads the existing masked config from the sidecar and
// lets the admin fill in the Ozonetel/SIP/Exotel credentials. On save, PUTs
// the full form (the backend treats '***masked***' as "no change") and
// marks the step complete.
//
// Unlike the entity steps, this is a single-doc config so we always load the
// current state rather than treating the form as "add new".
export const WizardStepTelephony = (props: WizardStepComponentProps) => {
const [values, setValues] = useState<TelephonyFormValues>(emptyTelephonyFormValues);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
apiClient
.get<TelephonyFormValues>('/api/config/telephony', { silent: true })
.then((data) => {
setValues({
ozonetel: {
agentId: data.ozonetel?.agentId ?? '',
agentPassword: data.ozonetel?.agentPassword ?? '',
did: data.ozonetel?.did ?? '',
sipId: data.ozonetel?.sipId ?? '',
campaignName: data.ozonetel?.campaignName ?? '',
},
sip: {
domain: data.sip?.domain ?? 'blr-pub-rtc4.ozonetel.com',
wsPort: data.sip?.wsPort ?? '444',
},
exotel: {
apiKey: data.exotel?.apiKey ?? '',
apiToken: data.exotel?.apiToken ?? '',
accountSid: data.exotel?.accountSid ?? '',
subdomain: data.exotel?.subdomain ?? 'api.exotel.com',
},
});
})
.catch(() => {
// If the endpoint is unreachable, fall back to defaults so the
// admin can at least fill out the form.
})
.finally(() => setLoading(false));
}, []);
const handleSave = async () => {
// Required fields for a working Ozonetel setup.
if (
!values.ozonetel.agentId.trim() ||
!values.ozonetel.did.trim() ||
!values.ozonetel.sipId.trim() ||
!values.ozonetel.campaignName.trim()
) {
notify.error('Missing required fields', 'Agent ID, DID, SIP ID, and campaign name are all required.');
return;
}
setSaving(true);
try {
await apiClient.put('/api/config/telephony', {
ozonetel: values.ozonetel,
sip: values.sip,
exotel: values.exotel,
});
notify.success('Telephony saved', 'Changes are live — no restart needed.');
await props.onComplete('telephony');
props.onAdvance();
} catch (err) {
console.error('[wizard/telephony] save failed', err);
} finally {
setSaving(false);
}
};
return (
<WizardStep
step="telephony"
isCompleted={props.isCompleted}
isLast={props.isLast}
onPrev={props.onPrev}
onNext={props.onNext}
onMarkComplete={handleSave}
onFinish={props.onFinish}
saving={saving}
>
{loading ? (
<p className="text-sm text-tertiary">Loading telephony settings</p>
) : (
<TelephonyForm value={values} onChange={setValues} />
)}
</WizardStep>
);
};