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>
160 lines
8.2 KiB
TypeScript
160 lines
8.2 KiB
TypeScript
import { useState, type ReactNode } from 'react';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { faCircleCheck, faCircle } from '@fortawesome/pro-duotone-svg-icons';
|
|
import { Button } from '@/components/base/buttons/button';
|
|
import { cx } from '@/utils/cx';
|
|
import { SETUP_STEP_NAMES, SETUP_STEP_LABELS, type SetupStepName, type SetupState } from '@/lib/setup-state';
|
|
import { WizardLayoutContext } from './wizard-layout-context';
|
|
|
|
type WizardShellProps = {
|
|
state: SetupState;
|
|
activeStep: SetupStepName;
|
|
onSelectStep: (step: SetupStepName) => void;
|
|
onDismiss: () => void;
|
|
// Form column (middle pane). The active step component renders
|
|
// its form into this slot. The right pane is filled via the
|
|
// WizardLayoutContext + a portal — see wizard-step.tsx.
|
|
children: ReactNode;
|
|
};
|
|
|
|
// Layout shell for the onboarding wizard. Three-pane layout:
|
|
// left — step navigator (fixed width)
|
|
// middle — form (flexible, the focus column)
|
|
// right — preview pane fed by the active step component (sticky,
|
|
// hides below xl breakpoint)
|
|
//
|
|
// The whole shell is `fixed inset-0` so the document body cannot
|
|
// scroll while the wizard is mounted — fixes the double-scrollbar
|
|
// bug where the body was rendered taller than the viewport and
|
|
// scrolled alongside the form column. The form and preview columns
|
|
// each scroll independently inside the shell.
|
|
//
|
|
// The header has a "Skip for now" affordance that dismisses the
|
|
// wizard for this workspace; once dismissed it never auto-shows
|
|
// again on login.
|
|
export const WizardShell = ({
|
|
state,
|
|
activeStep,
|
|
onSelectStep,
|
|
onDismiss,
|
|
children,
|
|
}: WizardShellProps) => {
|
|
const completedCount = SETUP_STEP_NAMES.filter((s) => state.steps[s].completed).length;
|
|
const totalSteps = SETUP_STEP_NAMES.length;
|
|
const progressPct = Math.round((completedCount / totalSteps) * 100);
|
|
|
|
// Callback ref → state — guarantees that consumers re-render once
|
|
// the aside is mounted (a plain useRef would not propagate the
|
|
// attached node back through the context). The element is also
|
|
// updated to null on unmount so the context is always honest about
|
|
// whether the slot is currently available for portals.
|
|
const [rightPaneEl, setRightPaneEl] = useState<HTMLElement | null>(null);
|
|
|
|
return (
|
|
<WizardLayoutContext.Provider value={{ rightPaneEl }}>
|
|
<div className="fixed inset-0 z-50 flex flex-col bg-primary">
|
|
{/* Header — pinned. Progress bar always visible (grey
|
|
track when 0%), sits flush under the title row. */}
|
|
<header className="shrink-0 border-b border-secondary bg-primary">
|
|
<div className="mx-auto flex w-full max-w-screen-2xl items-center justify-between gap-6 px-8 pt-4 pb-3">
|
|
<div className="flex items-center gap-4">
|
|
<div>
|
|
<h1 className="text-lg font-bold text-primary">Set up your hospital</h1>
|
|
<p className="text-xs text-tertiary">
|
|
{completedCount} of {totalSteps} steps complete · finish setup to start
|
|
using your workspace
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button color="link-gray" size="sm" onClick={onDismiss}>
|
|
Skip for now
|
|
</Button>
|
|
</div>
|
|
<div className="mx-auto w-full max-w-screen-2xl px-8 pb-3">
|
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-secondary">
|
|
<div
|
|
className="h-full rounded-full bg-brand-solid transition-all duration-300"
|
|
style={{ width: `${progressPct}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Body — three columns inside a fixed-height flex row.
|
|
min-h-0 on the row + each column lets the inner
|
|
overflow-y-auto actually take effect. */}
|
|
<div className="mx-auto flex min-h-0 w-full max-w-screen-2xl flex-1 gap-6 px-8 py-6">
|
|
{/* Left — step navigator. Scrolls if it overflows on
|
|
very short viewports, but in practice it fits. */}
|
|
<nav className="w-60 shrink-0 overflow-y-auto">
|
|
<ol className="flex flex-col gap-1">
|
|
{SETUP_STEP_NAMES.map((step, idx) => {
|
|
const meta = SETUP_STEP_LABELS[step];
|
|
const status = state.steps[step];
|
|
const isActive = step === activeStep;
|
|
const isComplete = status.completed;
|
|
return (
|
|
<li key={step}>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSelectStep(step)}
|
|
className={cx(
|
|
'group flex w-full items-start gap-3 rounded-lg border px-3 py-2.5 text-left transition',
|
|
isActive
|
|
? 'border-brand bg-brand-primary'
|
|
: 'border-transparent hover:bg-secondary',
|
|
)}
|
|
>
|
|
<span className="mt-0.5 shrink-0">
|
|
<FontAwesomeIcon
|
|
icon={isComplete ? faCircleCheck : faCircle}
|
|
className={cx(
|
|
'size-5',
|
|
isComplete
|
|
? 'text-success-primary'
|
|
: 'text-quaternary',
|
|
)}
|
|
/>
|
|
</span>
|
|
<span className="flex-1">
|
|
<span className="block text-xs font-medium text-tertiary">
|
|
Step {idx + 1}
|
|
</span>
|
|
<span
|
|
className={cx(
|
|
'block text-sm font-semibold',
|
|
isActive ? 'text-brand-primary' : 'text-primary',
|
|
)}
|
|
>
|
|
{meta.title}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</nav>
|
|
|
|
{/* Middle — form column. min-w-0 prevents children from
|
|
forcing the column wider than its flex basis (long
|
|
inputs, etc.). overflow-y-auto so it scrolls
|
|
independently of the right pane. */}
|
|
<main className="flex min-w-0 flex-1 flex-col overflow-y-auto">{children}</main>
|
|
|
|
{/* Right — preview pane. Always rendered as a stable
|
|
portal target (so the active step's WizardStep can
|
|
createPortal into it via WizardLayoutContext).
|
|
Hidden below xl breakpoint (1280px) so the wizard
|
|
collapses cleanly to two columns on smaller screens.
|
|
Independent scroll. */}
|
|
<aside
|
|
ref={setRightPaneEl}
|
|
className="hidden w-80 shrink-0 overflow-y-auto xl:block"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</WizardLayoutContext.Provider>
|
|
);
|
|
};
|