mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
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:
102
src/components/setup/wizard-step-telephony.tsx
Normal file
102
src/components/setup/wizard-step-telephony.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user