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>
94 lines
2.3 KiB
TypeScript
94 lines
2.3 KiB
TypeScript
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',
|
|
}}
|
|
/>
|
|
);
|
|
}
|