Phase 7: Add user onboarding tour and tenant setup wizard

Feature 1 - How-To Intro Tour (react-joyride):
- 8-step guided walkthrough highlighting Dashboard, Accounts, Assessments,
  Transactions, Budgets, Reports, and AI Investment Planning
- Runs automatically on first login, tracked via has_seen_intro flag on user
- Centralized step config in config/tourSteps.ts for easy text editing
- data-tour attributes on Sidebar nav items and Dashboard for targeting

Feature 2 - Tenant Onboarding Wizard:
- 3-step modal wizard: create operating account, assessment group + units,
  import budget CSV
- Runs after tour completes, tracked via onboardingComplete in org settings JSONB
- Reuses existing API endpoints (POST /accounts, /assessment-groups, /units,
  /budgets/:year/import)

Backend changes:
- Add has_seen_intro column to shared.users + migration
- Add PATCH /auth/intro-seen endpoint to mark tour complete
- Add PATCH /organizations/settings endpoint for org settings updates
- Include hasSeenIntro in login response, settings in switch-org response

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 09:47:45 -05:00
parent d1c40c633f
commit f1e66966f3
15 changed files with 4111 additions and 10 deletions

View File

@@ -0,0 +1,93 @@
import { useState, useCallback } from 'react';
import Joyride, { type CallBackProps, STATUS, ACTIONS, EVENTS } from 'react-joyride';
import { TOUR_STEPS } from '../../config/tourSteps';
import { useAuthStore } from '../../stores/authStore';
import api from '../../services/api';
interface AppTourProps {
run: boolean;
onComplete: () => void;
}
export function AppTour({ run, onComplete }: AppTourProps) {
const [stepIndex, setStepIndex] = useState(0);
const setUserIntroSeen = useAuthStore((s) => s.setUserIntroSeen);
const handleCallback = useCallback(
async (data: CallBackProps) => {
const { status, action, type } = data;
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
if (finishedStatuses.includes(status)) {
// Mark intro as seen on backend (fire-and-forget)
api.patch('/auth/intro-seen').catch(() => {});
setUserIntroSeen();
onComplete();
return;
}
// Handle step navigation
if (type === EVENTS.STEP_AFTER) {
setStepIndex((prev) =>
action === ACTIONS.PREV ? prev - 1 : prev + 1,
);
}
},
[onComplete, setUserIntroSeen],
);
if (!run) return null;
return (
<Joyride
steps={TOUR_STEPS}
run={run}
stepIndex={stepIndex}
continuous
showProgress
showSkipButton
scrollToFirstStep
disableOverlayClose
callback={handleCallback}
styles={{
options: {
primaryColor: '#228be6',
zIndex: 10000,
arrowColor: '#fff',
backgroundColor: '#fff',
textColor: '#333',
overlayColor: 'rgba(0, 0, 0, 0.5)',
},
tooltip: {
borderRadius: 8,
fontSize: 14,
padding: 20,
},
tooltipTitle: {
fontSize: 16,
fontWeight: 600,
},
buttonNext: {
borderRadius: 6,
fontSize: 14,
padding: '8px 16px',
},
buttonBack: {
borderRadius: 6,
fontSize: 14,
marginRight: 8,
},
buttonSkip: {
fontSize: 13,
},
}}
locale={{
back: 'Previous',
close: 'Close',
last: 'Finish Tour',
next: 'Next',
skip: 'Skip Tour',
}}
/>
);
}