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:
93
frontend/src/components/onboarding/AppTour.tsx
Normal file
93
frontend/src/components/onboarding/AppTour.tsx
Normal 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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user