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:
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar, Alert, Button } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
@@ -9,17 +10,51 @@ import {
|
||||
IconUsersGroup,
|
||||
IconEyeOff,
|
||||
} from '@tabler/icons-react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { AppTour } from '../onboarding/AppTour';
|
||||
import { OnboardingWizard } from '../onboarding/OnboardingWizard';
|
||||
import logoSrc from '../../assets/logo.svg';
|
||||
|
||||
export function AppLayout() {
|
||||
const [opened, { toggle, close }] = useDisclosure();
|
||||
const { user, currentOrg, logout, impersonationOriginal, stopImpersonation } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isImpersonating = !!impersonationOriginal;
|
||||
|
||||
// ── Onboarding State ──
|
||||
const [showTour, setShowTour] = useState(false);
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only run for non-impersonating users with an org selected, on dashboard
|
||||
if (isImpersonating || !currentOrg || !user) return;
|
||||
if (!location.pathname.startsWith('/dashboard')) return;
|
||||
|
||||
if (user.hasSeenIntro === false || user.hasSeenIntro === undefined) {
|
||||
// Delay to ensure DOM elements are rendered for tour targeting
|
||||
const timer = setTimeout(() => setShowTour(true), 800);
|
||||
return () => clearTimeout(timer);
|
||||
} else if (currentOrg.settings?.onboardingComplete !== true) {
|
||||
setShowWizard(true);
|
||||
}
|
||||
}, [user?.hasSeenIntro, currentOrg?.id, currentOrg?.settings?.onboardingComplete, isImpersonating, location.pathname]);
|
||||
|
||||
const handleTourComplete = () => {
|
||||
setShowTour(false);
|
||||
// After tour, check if onboarding wizard should run
|
||||
if (currentOrg && currentOrg.settings?.onboardingComplete !== true) {
|
||||
// Small delay before showing wizard
|
||||
setTimeout(() => setShowWizard(true), 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWizardComplete = () => {
|
||||
setShowWizard(false);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
@@ -145,6 +180,10 @@ export function AppLayout() {
|
||||
<AppShell.Main>
|
||||
<Outlet />
|
||||
</AppShell.Main>
|
||||
|
||||
{/* ── Onboarding Components ── */}
|
||||
<AppTour run={showTour} onComplete={handleTourComplete} />
|
||||
<OnboardingWizard opened={showWizard} onComplete={handleWizardComplete} />
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user