mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
- 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>
135 lines
5.0 KiB
TypeScript
135 lines
5.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router';
|
|
import { WizardShell } from '@/components/setup/wizard-shell';
|
|
import { WizardStepIdentity } from '@/components/setup/wizard-step-identity';
|
|
import { WizardStepClinics } from '@/components/setup/wizard-step-clinics';
|
|
import { WizardStepDoctors } from '@/components/setup/wizard-step-doctors';
|
|
import { WizardStepTeam } from '@/components/setup/wizard-step-team';
|
|
import { WizardStepTelephony } from '@/components/setup/wizard-step-telephony';
|
|
import { WizardStepAi } from '@/components/setup/wizard-step-ai';
|
|
import type { WizardStepComponentProps } from '@/components/setup/wizard-step-types';
|
|
import {
|
|
SETUP_STEP_NAMES,
|
|
type SetupState,
|
|
type SetupStepName,
|
|
getSetupState,
|
|
markSetupStepComplete,
|
|
dismissSetupWizard,
|
|
} from '@/lib/setup-state';
|
|
import { notify } from '@/lib/toast';
|
|
import { useAuth } from '@/providers/auth-provider';
|
|
|
|
// Top-level onboarding wizard. Auto-shown by login.tsx redirect when the
|
|
// workspace has incomplete setup steps.
|
|
//
|
|
// Phase 5: each step is now backed by a real form component. The parent
|
|
// owns the shell navigation + per-step completion state, and passes a
|
|
// WizardStepComponentProps bundle to the dispatched child so the child
|
|
// can trigger save + mark-complete + advance from its own Save action.
|
|
|
|
const STEP_COMPONENTS: Record<SetupStepName, (p: WizardStepComponentProps) => React.ReactElement> = {
|
|
identity: WizardStepIdentity,
|
|
clinics: WizardStepClinics,
|
|
doctors: WizardStepDoctors,
|
|
team: WizardStepTeam,
|
|
telephony: WizardStepTelephony,
|
|
ai: WizardStepAi,
|
|
};
|
|
|
|
export const SetupWizardPage = () => {
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const [state, setState] = useState<SetupState | null>(null);
|
|
const [activeStep, setActiveStep] = useState<SetupStepName>('identity');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
getSetupState()
|
|
.then((s) => {
|
|
if (cancelled) return;
|
|
setState(s);
|
|
// Land on the first incomplete step so the operator picks up
|
|
// where they left off.
|
|
const firstIncomplete = SETUP_STEP_NAMES.find((name) => !s.steps[name].completed);
|
|
if (firstIncomplete) setActiveStep(firstIncomplete);
|
|
})
|
|
.catch((err) => {
|
|
console.error('Failed to load setup state', err);
|
|
notify.error('Setup', 'Could not load setup state. Please reload.');
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
if (loading || !state) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-primary">
|
|
<p className="text-sm text-tertiary">Loading setup…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const activeIndex = SETUP_STEP_NAMES.indexOf(activeStep);
|
|
const isLastStep = activeIndex === SETUP_STEP_NAMES.length - 1;
|
|
const onPrev = activeIndex > 0 ? () => setActiveStep(SETUP_STEP_NAMES[activeIndex - 1]) : null;
|
|
const onNext = !isLastStep ? () => setActiveStep(SETUP_STEP_NAMES[activeIndex + 1]) : null;
|
|
|
|
const handleComplete = async (step: SetupStepName) => {
|
|
// No try/catch here — if the setup-state PUT fails, we WANT
|
|
// the error to propagate up to the step's handleSave so the
|
|
// agent sees a toast AND the advance flow pauses until the
|
|
// issue is resolved. Silent swallowing hid the real failure
|
|
// mode during the Ramaiah local test.
|
|
const updated = await markSetupStepComplete(step, user?.email);
|
|
setState(updated);
|
|
};
|
|
|
|
const handleAdvance = () => {
|
|
if (!isLastStep) {
|
|
setActiveStep(SETUP_STEP_NAMES[activeIndex + 1]);
|
|
}
|
|
};
|
|
|
|
const handleFinish = () => {
|
|
notify.success('Setup complete', 'Welcome to your workspace!');
|
|
navigate('/', { replace: true });
|
|
};
|
|
|
|
const handleDismiss = async () => {
|
|
try {
|
|
await dismissSetupWizard();
|
|
notify.success('Setup dismissed', 'You can finish setup later from Settings.');
|
|
navigate('/', { replace: true });
|
|
} catch {
|
|
notify.error('Setup', 'Could not dismiss the wizard. Please try again.');
|
|
}
|
|
};
|
|
|
|
const StepComponent = STEP_COMPONENTS[activeStep];
|
|
const stepProps: WizardStepComponentProps = {
|
|
isCompleted: state.steps[activeStep].completed,
|
|
isLast: isLastStep,
|
|
onPrev,
|
|
onNext,
|
|
onComplete: handleComplete,
|
|
onAdvance: handleAdvance,
|
|
onFinish: handleFinish,
|
|
};
|
|
|
|
return (
|
|
<WizardShell
|
|
state={state}
|
|
activeStep={activeStep}
|
|
onSelectStep={setActiveStep}
|
|
onDismiss={handleDismiss}
|
|
>
|
|
<StepComponent {...stepProps} />
|
|
</WizardShell>
|
|
);
|
|
};
|