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>
This commit is contained in:
2026-04-10 08:37:34 +05:30
parent efe67dc28b
commit f57fbc1f24
25 changed files with 3461 additions and 706 deletions

View File

@@ -1,45 +1,76 @@
import type { ReactNode } from 'react';
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. Renders a left-side step navigator
// (with completed/active/upcoming visual states) and a right-side content
// pane fed by the parent. The header has a "Skip for now" affordance that
// dismisses the wizard for this workspace — once dismissed it never auto-shows
// 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;
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 (
<div className="min-h-screen bg-primary">
{/* header */}
<header className="border-b border-secondary bg-primary px-8 py-5">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-6">
<div>
<h1 className="text-xl font-bold text-primary">Set up your hospital</h1>
<p className="mt-1 text-sm text-tertiary">
{completedCount} of {totalSteps} steps complete · finish setup to start using your workspace
</p>
<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>
{/* progress bar */}
<div className="mx-auto mt-4 max-w-6xl">
<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"
@@ -49,9 +80,13 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
</div>
</header>
{/* body — step navigator + content */}
<div className="mx-auto flex max-w-6xl gap-8 px-8 py-8">
<nav className="w-72 shrink-0">
{/* 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];
@@ -64,7 +99,7 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
type="button"
onClick={() => onSelectStep(step)}
className={cx(
'group flex w-full items-start gap-3 rounded-lg border px-3 py-3 text-left transition',
'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',
@@ -75,7 +110,9 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
icon={isComplete ? faCircleCheck : faCircle}
className={cx(
'size-5',
isComplete ? 'text-success-primary' : 'text-quaternary',
isComplete
? 'text-success-primary'
: 'text-quaternary',
)}
/>
</span>
@@ -99,8 +136,24 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
</ol>
</nav>
<main className="min-w-0 flex-1">{children}</main>
{/* 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>
);
};