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,84 @@
import { useEffect, useState } from 'react';
import { WizardStep } from './wizard-step';
import { AiForm, emptyAiFormValues, type AiFormValues, type AiProvider } from '@/components/forms/ai-form';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import type { WizardStepComponentProps } from './wizard-step-types';
type ServerAiConfig = {
provider?: AiProvider;
model?: string;
temperature?: number;
systemPromptAddendum?: string;
};
// AI step — loads the current AI config, lets the admin pick provider and
// model, and saves. This is the last step, so on save we fire the finish
// flow instead of advancing.
export const WizardStepAi = (props: WizardStepComponentProps) => {
const [values, setValues] = useState<AiFormValues>(emptyAiFormValues);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
apiClient
.get<ServerAiConfig>('/api/config/ai', { silent: true })
.then((data) => {
setValues({
provider: data.provider ?? 'openai',
model: data.model ?? 'gpt-4o-mini',
temperature: data.temperature != null ? String(data.temperature) : '0.7',
systemPromptAddendum: data.systemPromptAddendum ?? '',
});
})
.catch(() => {
// non-fatal — defaults will do
})
.finally(() => setLoading(false));
}, []);
const handleSave = async () => {
if (!values.model.trim()) {
notify.error('Model is required');
return;
}
const temperature = Number(values.temperature);
setSaving(true);
try {
await apiClient.put('/api/config/ai', {
provider: values.provider,
model: values.model.trim(),
temperature: Number.isNaN(temperature) ? 0.7 : Math.min(2, Math.max(0, temperature)),
systemPromptAddendum: values.systemPromptAddendum,
});
notify.success('AI settings saved', 'Your assistant is ready.');
await props.onComplete('ai');
// Don't auto-advance — this is the last step, the WizardStep
// shell already renders a "Finish setup" button the admin taps
// themselves.
} catch (err) {
console.error('[wizard/ai] save failed', err);
} finally {
setSaving(false);
}
};
return (
<WizardStep
step="ai"
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 AI settings</p>
) : (
<AiForm value={values} onChange={setValues} />
)}
</WizardStep>
);
};