Files
helix-engage/src/pages/setup-wizard.tsx
saridsa2 f57fbc1f24 feat(onboarding/phase-6): setup wizard polish, seed script alignment, doctor visit slots
- 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>
2026-04-10 08:37:34 +05:30

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>
);
};