- Onboarding wizard: add Reserve Account step between Operating and Assessments, redirect to Budget Planning on completion - Dashboard: health score pending state shows clickable links to set up missing items - Projects/Vendors: rich empty-state wizard screens with real-world examples and CTAs - Investment Planning: auto-refresh AI recommendations when empty or stale (>30 days) - Hide Invoices and Payments menus (see PARKING-LOT.md for re-enablement) - Send welcome email via Resend when new members are added to a tenant - Enforce 5-member limit for Starter/Standard/Professional plans (Enterprise unlimited) - Cash flow forecast: only mark months as "Actual" when journal entries exist, fixing the issue where months without data showed as actuals - Bump version to 2026.3.19 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
701 lines
27 KiB
TypeScript
701 lines
27 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Modal, Stepper, Button, Group, TextInput, NumberInput, Textarea,
|
|
Select, Stack, Text, Title, Alert, ActionIcon, Table,
|
|
Card, ThemeIcon, Divider, Badge, SimpleGrid, Box,
|
|
} from '@mantine/core';
|
|
import { DateInput } from '@mantine/dates';
|
|
import { notifications } from '@mantine/notifications';
|
|
import {
|
|
IconBuildingBank, IconUsers,
|
|
IconPlus, IconTrash, IconCheck, IconRocket,
|
|
IconAlertCircle, IconFileSpreadsheet, IconPigMoney, IconX,
|
|
} from '@tabler/icons-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import api from '../../services/api';
|
|
import { useAuthStore } from '../../stores/authStore';
|
|
|
|
interface OnboardingWizardProps {
|
|
opened: boolean;
|
|
onComplete: () => void;
|
|
}
|
|
|
|
interface UnitRow {
|
|
unitNumber: string;
|
|
ownerName: string;
|
|
ownerEmail: string;
|
|
}
|
|
|
|
export function OnboardingWizard({ opened, onComplete }: OnboardingWizardProps) {
|
|
const navigate = useNavigate();
|
|
const [active, setActive] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const setOrgSettings = useAuthStore((s) => s.setOrgSettings);
|
|
|
|
// ── Step 1: Operating Account State ──
|
|
const [accountCreated, setAccountCreated] = useState(false);
|
|
const [accountName, setAccountName] = useState('Operating Checking');
|
|
const [accountNumber, setAccountNumber] = useState('1000');
|
|
const [accountDescription, setAccountDescription] = useState('');
|
|
const [initialBalance, setInitialBalance] = useState<number | string>(0);
|
|
const [balanceDate, setBalanceDate] = useState<Date | null>(new Date());
|
|
|
|
// ── Step 2: Reserve Account State ──
|
|
const [reserveCreated, setReserveCreated] = useState(false);
|
|
const [reserveSkipped, setReserveSkipped] = useState(false);
|
|
const [reserveName, setReserveName] = useState('Reserve Savings');
|
|
const [reserveNumber, setReserveNumber] = useState('2000');
|
|
const [reserveDescription, setReserveDescription] = useState('');
|
|
const [reserveBalance, setReserveBalance] = useState<number | string>(0);
|
|
const [reserveBalanceDate, setReserveBalanceDate] = useState<Date | null>(new Date());
|
|
|
|
// ── Step 3: Assessment Group State ──
|
|
const [groupCreated, setGroupCreated] = useState(false);
|
|
const [groupName, setGroupName] = useState('Standard Assessment');
|
|
const [regularAssessment, setRegularAssessment] = useState<number | string>(0);
|
|
const [frequency, setFrequency] = useState('monthly');
|
|
const [unitCount, setUnitCount] = useState<number | string>(0);
|
|
const [units, setUnits] = useState<UnitRow[]>([]);
|
|
const [unitsCreated, setUnitsCreated] = useState(false);
|
|
|
|
// ── Step 1: Create Operating Account ──
|
|
const handleCreateAccount = async () => {
|
|
if (!accountName.trim()) {
|
|
setError('Account name is required');
|
|
return;
|
|
}
|
|
if (!accountNumber.trim()) {
|
|
setError('Account number is required');
|
|
return;
|
|
}
|
|
const balance = typeof initialBalance === 'string' ? parseFloat(initialBalance) : initialBalance;
|
|
if (isNaN(balance)) {
|
|
setError('Initial balance must be a valid number');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await api.post('/accounts', {
|
|
accountNumber: accountNumber.trim(),
|
|
name: accountName.trim(),
|
|
description: accountDescription.trim(),
|
|
accountType: 'asset',
|
|
fundType: 'operating',
|
|
initialBalance: balance,
|
|
initialBalanceDate: balanceDate ? balanceDate.toISOString().split('T')[0] : undefined,
|
|
});
|
|
setAccountCreated(true);
|
|
notifications.show({
|
|
title: 'Account Created',
|
|
message: `${accountName} has been created with an initial balance of $${balance.toLocaleString()}`,
|
|
color: 'green',
|
|
});
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.message || 'Failed to create account';
|
|
setError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// ── Step 2: Create Reserve Account ──
|
|
const handleCreateReserve = async () => {
|
|
if (!reserveName.trim()) {
|
|
setError('Account name is required');
|
|
return;
|
|
}
|
|
if (!reserveNumber.trim()) {
|
|
setError('Account number is required');
|
|
return;
|
|
}
|
|
const balance = typeof reserveBalance === 'string' ? parseFloat(reserveBalance) : reserveBalance;
|
|
if (isNaN(balance)) {
|
|
setError('Initial balance must be a valid number');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await api.post('/accounts', {
|
|
accountNumber: reserveNumber.trim(),
|
|
name: reserveName.trim(),
|
|
description: reserveDescription.trim(),
|
|
accountType: 'asset',
|
|
fundType: 'reserve',
|
|
initialBalance: balance,
|
|
initialBalanceDate: reserveBalanceDate ? reserveBalanceDate.toISOString().split('T')[0] : undefined,
|
|
});
|
|
setReserveCreated(true);
|
|
notifications.show({
|
|
title: 'Reserve Account Created',
|
|
message: `${reserveName} has been created with an initial balance of $${balance.toLocaleString()}`,
|
|
color: 'green',
|
|
});
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.message || 'Failed to create reserve account';
|
|
setError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSkipReserve = () => {
|
|
setReserveSkipped(true);
|
|
};
|
|
|
|
// ── Step 3: Create Assessment Group ──
|
|
const handleCreateGroup = async () => {
|
|
if (!groupName.trim()) {
|
|
setError('Group name is required');
|
|
return;
|
|
}
|
|
const assessment = typeof regularAssessment === 'string' ? parseFloat(regularAssessment) : regularAssessment;
|
|
if (isNaN(assessment) || assessment <= 0) {
|
|
setError('Assessment amount must be greater than zero');
|
|
return;
|
|
}
|
|
|
|
const count = typeof unitCount === 'string' ? parseInt(unitCount) : unitCount;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const { data: group } = await api.post('/assessment-groups', {
|
|
name: groupName.trim(),
|
|
regularAssessment: assessment,
|
|
frequency,
|
|
unitCount: isNaN(count) ? 0 : count,
|
|
isDefault: true,
|
|
});
|
|
setGroupCreated(true);
|
|
|
|
// Create units if any were added
|
|
if (units.length > 0) {
|
|
let created = 0;
|
|
for (const unit of units) {
|
|
if (!unit.unitNumber.trim()) continue;
|
|
try {
|
|
await api.post('/units', {
|
|
unitNumber: unit.unitNumber.trim(),
|
|
ownerName: unit.ownerName.trim() || null,
|
|
ownerEmail: unit.ownerEmail.trim() || null,
|
|
assessmentGroupId: group.id,
|
|
});
|
|
created++;
|
|
} catch {
|
|
// Continue even if a unit fails
|
|
}
|
|
}
|
|
setUnitsCreated(true);
|
|
notifications.show({
|
|
title: 'Assessment Group Created',
|
|
message: `${groupName} created with ${created} unit(s)`,
|
|
color: 'green',
|
|
});
|
|
} else {
|
|
notifications.show({
|
|
title: 'Assessment Group Created',
|
|
message: `${groupName} created successfully`,
|
|
color: 'green',
|
|
});
|
|
}
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.message || 'Failed to create assessment group';
|
|
setError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// ── Finish Wizard → Navigate to Budget Planning ──
|
|
const handleFinish = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await api.patch('/organizations/settings', { onboardingComplete: true });
|
|
setOrgSettings({ onboardingComplete: true });
|
|
onComplete();
|
|
// Navigate to Budget Planning so user can set up their budget immediately
|
|
navigate('/board-planning/budgets');
|
|
} catch {
|
|
// Even if API fails, close the wizard — onboarding data is already created
|
|
onComplete();
|
|
navigate('/board-planning/budgets');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// ── Unit Rows ──
|
|
const addUnit = () => {
|
|
setUnits([...units, { unitNumber: '', ownerName: '', ownerEmail: '' }]);
|
|
};
|
|
|
|
const updateUnit = (index: number, field: keyof UnitRow, value: string) => {
|
|
const updated = [...units];
|
|
updated[index] = { ...updated[index], [field]: value };
|
|
setUnits(updated);
|
|
};
|
|
|
|
const removeUnit = (index: number) => {
|
|
setUnits(units.filter((_, i) => i !== index));
|
|
};
|
|
|
|
// ── Navigation ──
|
|
const canGoNext = () => {
|
|
if (active === 0) return accountCreated;
|
|
if (active === 1) return reserveCreated || reserveSkipped;
|
|
if (active === 2) return groupCreated;
|
|
return false;
|
|
};
|
|
|
|
const nextStep = () => {
|
|
setError(null);
|
|
if (active < 3) setActive(active + 1);
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={() => {}} // Prevent closing without completing
|
|
withCloseButton={false}
|
|
size="xl"
|
|
centered
|
|
overlayProps={{ opacity: 0.6, blur: 3 }}
|
|
styles={{
|
|
body: { padding: 0 },
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<Box px="xl" pt="xl" pb="md" style={{ borderBottom: '1px solid var(--mantine-color-gray-2)' }}>
|
|
<Group>
|
|
<ThemeIcon size={44} radius="md" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }}>
|
|
<IconRocket size={24} />
|
|
</ThemeIcon>
|
|
<div>
|
|
<Title order={3}>Set Up Your Organization</Title>
|
|
<Text c="dimmed" size="sm">
|
|
Let's get the essentials configured so you can start managing your HOA finances.
|
|
</Text>
|
|
</div>
|
|
</Group>
|
|
</Box>
|
|
|
|
<Box px="xl" py="lg">
|
|
<Stepper active={active} size="sm" mb="xl">
|
|
<Stepper.Step
|
|
label="Operating Account"
|
|
description="Primary bank account"
|
|
icon={<IconBuildingBank size={18} />}
|
|
completedIcon={<IconCheck size={18} />}
|
|
/>
|
|
<Stepper.Step
|
|
label="Reserve Account"
|
|
description={reserveSkipped ? 'Skipped' : 'Savings account'}
|
|
icon={<IconPigMoney size={18} />}
|
|
completedIcon={reserveSkipped ? <IconX size={18} /> : <IconCheck size={18} />}
|
|
/>
|
|
<Stepper.Step
|
|
label="Assessment Group"
|
|
description="Define homeowner assessments"
|
|
icon={<IconUsers size={18} />}
|
|
completedIcon={<IconCheck size={18} />}
|
|
/>
|
|
</Stepper>
|
|
|
|
{error && (
|
|
<Alert icon={<IconAlertCircle size={16} />} color="red" mb="md" withCloseButton onClose={() => setError(null)}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* ── Step 1: Create Operating Account ── */}
|
|
{active === 0 && (
|
|
<Stack gap="md">
|
|
<Card withBorder p="lg">
|
|
<Text fw={600} mb="xs">Create Your Primary Operating Account</Text>
|
|
<Text size="sm" c="dimmed" mb="md">
|
|
This is your HOA's main bank account for day-to-day operations. You can add more accounts later.
|
|
</Text>
|
|
|
|
{accountCreated ? (
|
|
<Alert icon={<IconCheck size={16} />} color="green" variant="light">
|
|
<Text fw={500}>{accountName} created successfully!</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Initial balance: ${(typeof initialBalance === 'number' ? initialBalance : parseFloat(initialBalance as string) || 0).toLocaleString()}
|
|
{balanceDate && ` as of ${balanceDate.toLocaleDateString()}`}
|
|
</Text>
|
|
</Alert>
|
|
) : (
|
|
<>
|
|
<SimpleGrid cols={2} mb="md">
|
|
<TextInput
|
|
label="Account Name"
|
|
placeholder="e.g. Operating Checking"
|
|
value={accountName}
|
|
onChange={(e) => setAccountName(e.currentTarget.value)}
|
|
required
|
|
/>
|
|
<TextInput
|
|
label="Account Number"
|
|
placeholder="e.g. 1000"
|
|
value={accountNumber}
|
|
onChange={(e) => setAccountNumber(e.currentTarget.value)}
|
|
required
|
|
/>
|
|
</SimpleGrid>
|
|
<Textarea
|
|
label="Description"
|
|
placeholder="Optional description"
|
|
value={accountDescription}
|
|
onChange={(e) => setAccountDescription(e.currentTarget.value)}
|
|
mb="md"
|
|
autosize
|
|
minRows={2}
|
|
/>
|
|
<SimpleGrid cols={2} mb="md">
|
|
<NumberInput
|
|
label="Current Balance"
|
|
description="Enter the current balance of this bank account"
|
|
placeholder="0.00"
|
|
value={initialBalance}
|
|
onChange={setInitialBalance}
|
|
thousandSeparator=","
|
|
prefix="$"
|
|
decimalScale={2}
|
|
/>
|
|
<DateInput
|
|
label="Balance As-Of Date"
|
|
description="Date this balance was accurate (e.g. last statement date)"
|
|
value={balanceDate}
|
|
onChange={setBalanceDate}
|
|
maxDate={new Date()}
|
|
clearable={false}
|
|
/>
|
|
</SimpleGrid>
|
|
<Button
|
|
onClick={handleCreateAccount}
|
|
loading={loading}
|
|
leftSection={<IconBuildingBank size={16} />}
|
|
>
|
|
Create Account
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* ── Step 2: Reserve Account ── */}
|
|
{active === 1 && (
|
|
<Stack gap="md">
|
|
<Card withBorder p="lg">
|
|
<Text fw={600} mb="xs">Set Up a Reserve Savings Account</Text>
|
|
<Text size="sm" c="dimmed" mb="md">
|
|
Most HOAs maintain a reserve fund for long-term capital projects like roof replacements,
|
|
paving, and major repairs. Setting this up now gives you a more complete financial picture
|
|
from the start.
|
|
</Text>
|
|
|
|
{reserveCreated ? (
|
|
<Alert icon={<IconCheck size={16} />} color="green" variant="light">
|
|
<Text fw={500}>{reserveName} created successfully!</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Initial balance: ${(typeof reserveBalance === 'number' ? reserveBalance : parseFloat(reserveBalance as string) || 0).toLocaleString()}
|
|
{reserveBalanceDate && ` as of ${reserveBalanceDate.toLocaleDateString()}`}
|
|
</Text>
|
|
</Alert>
|
|
) : reserveSkipped ? (
|
|
<Alert icon={<IconX size={16} />} color="gray" variant="light">
|
|
<Text fw={500}>Reserve account skipped</Text>
|
|
<Text size="sm" c="dimmed">
|
|
You can always add a reserve account later from the Accounts page.
|
|
</Text>
|
|
</Alert>
|
|
) : (
|
|
<>
|
|
<SimpleGrid cols={2} mb="md">
|
|
<TextInput
|
|
label="Account Name"
|
|
placeholder="e.g. Reserve Savings"
|
|
value={reserveName}
|
|
onChange={(e) => setReserveName(e.currentTarget.value)}
|
|
required
|
|
/>
|
|
<TextInput
|
|
label="Account Number"
|
|
placeholder="e.g. 2000"
|
|
value={reserveNumber}
|
|
onChange={(e) => setReserveNumber(e.currentTarget.value)}
|
|
required
|
|
/>
|
|
</SimpleGrid>
|
|
<Textarea
|
|
label="Description"
|
|
placeholder="Optional description"
|
|
value={reserveDescription}
|
|
onChange={(e) => setReserveDescription(e.currentTarget.value)}
|
|
mb="md"
|
|
autosize
|
|
minRows={2}
|
|
/>
|
|
<SimpleGrid cols={2} mb="md">
|
|
<NumberInput
|
|
label="Current Balance"
|
|
description="Enter the current balance of this reserve account"
|
|
placeholder="0.00"
|
|
value={reserveBalance}
|
|
onChange={setReserveBalance}
|
|
thousandSeparator=","
|
|
prefix="$"
|
|
decimalScale={2}
|
|
/>
|
|
<DateInput
|
|
label="Balance As-Of Date"
|
|
description="Date this balance was accurate"
|
|
value={reserveBalanceDate}
|
|
onChange={setReserveBalanceDate}
|
|
maxDate={new Date()}
|
|
clearable={false}
|
|
/>
|
|
</SimpleGrid>
|
|
<Group>
|
|
<Button
|
|
onClick={handleCreateReserve}
|
|
loading={loading}
|
|
leftSection={<IconPigMoney size={16} />}
|
|
>
|
|
Create Reserve Account
|
|
</Button>
|
|
<Button
|
|
variant="subtle"
|
|
color="gray"
|
|
onClick={handleSkipReserve}
|
|
>
|
|
No Reserve Account
|
|
</Button>
|
|
</Group>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* ── Step 3: Assessment Group + Units ── */}
|
|
{active === 2 && (
|
|
<Stack gap="md">
|
|
<Card withBorder p="lg">
|
|
<Text fw={600} mb="xs">Create an Assessment Group</Text>
|
|
<Text size="sm" c="dimmed" mb="md">
|
|
Assessment groups define how much each homeowner pays and how often. You can create additional groups later for different unit types.
|
|
</Text>
|
|
|
|
{groupCreated ? (
|
|
<Alert icon={<IconCheck size={16} />} color="green" variant="light">
|
|
<Text fw={500}>{groupName} created successfully!</Text>
|
|
<Text size="sm" c="dimmed">
|
|
${(typeof regularAssessment === 'number' ? regularAssessment : parseFloat(regularAssessment as string) || 0).toLocaleString()} {frequency}
|
|
{unitsCreated && ` with ${units.length} unit(s)`}
|
|
</Text>
|
|
</Alert>
|
|
) : (
|
|
<>
|
|
<SimpleGrid cols={2} mb="md">
|
|
<TextInput
|
|
label="Group Name"
|
|
placeholder="e.g. Standard Assessment"
|
|
value={groupName}
|
|
onChange={(e) => setGroupName(e.currentTarget.value)}
|
|
required
|
|
/>
|
|
<NumberInput
|
|
label="Total Unit Count"
|
|
description="How many units/lots does your community have?"
|
|
placeholder="e.g. 50"
|
|
value={unitCount}
|
|
onChange={setUnitCount}
|
|
min={0}
|
|
required
|
|
/>
|
|
</SimpleGrid>
|
|
<SimpleGrid cols={2} mb="md">
|
|
<NumberInput
|
|
label="Assessment Amount"
|
|
placeholder="0.00"
|
|
value={regularAssessment}
|
|
onChange={setRegularAssessment}
|
|
thousandSeparator=","
|
|
prefix="$"
|
|
decimalScale={2}
|
|
required
|
|
/>
|
|
<Select
|
|
label="Frequency"
|
|
value={frequency}
|
|
onChange={(v) => setFrequency(v || 'monthly')}
|
|
data={[
|
|
{ value: 'monthly', label: 'Monthly' },
|
|
{ value: 'quarterly', label: 'Quarterly' },
|
|
{ value: 'annual', label: 'Annual' },
|
|
]}
|
|
/>
|
|
</SimpleGrid>
|
|
|
|
<Divider my="md" label="Add Homeowner Units (Optional)" labelPosition="center" />
|
|
|
|
{units.length > 0 && (
|
|
<Table mb="md" striped withTableBorder>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Unit Number</Table.Th>
|
|
<Table.Th>Owner Name</Table.Th>
|
|
<Table.Th>Owner Email</Table.Th>
|
|
<Table.Th w={40}></Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{units.map((unit, idx) => (
|
|
<Table.Tr key={idx}>
|
|
<Table.Td>
|
|
<TextInput
|
|
size="xs"
|
|
placeholder="e.g. 101"
|
|
value={unit.unitNumber}
|
|
onChange={(e) => updateUnit(idx, 'unitNumber', e.currentTarget.value)}
|
|
/>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<TextInput
|
|
size="xs"
|
|
placeholder="John Smith"
|
|
value={unit.ownerName}
|
|
onChange={(e) => updateUnit(idx, 'ownerName', e.currentTarget.value)}
|
|
/>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<TextInput
|
|
size="xs"
|
|
placeholder="john@example.com"
|
|
value={unit.ownerEmail}
|
|
onChange={(e) => updateUnit(idx, 'ownerEmail', e.currentTarget.value)}
|
|
/>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<ActionIcon color="red" variant="subtle" size="sm" onClick={() => removeUnit(idx)}>
|
|
<IconTrash size={14} />
|
|
</ActionIcon>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
)}
|
|
|
|
<Group mb="md">
|
|
<Button
|
|
variant="light"
|
|
size="xs"
|
|
leftSection={<IconPlus size={14} />}
|
|
onClick={addUnit}
|
|
>
|
|
Add Unit
|
|
</Button>
|
|
<Text size="xs" c="dimmed">You can also import units in bulk later from the Units page.</Text>
|
|
</Group>
|
|
|
|
<Button
|
|
onClick={handleCreateGroup}
|
|
loading={loading}
|
|
leftSection={<IconUsers size={16} />}
|
|
>
|
|
Create Assessment Group
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* ── Completion Screen ── */}
|
|
{active === 3 && (
|
|
<Card withBorder p="xl" style={{ textAlign: 'center' }}>
|
|
<ThemeIcon size={60} radius="xl" variant="gradient" gradient={{ from: 'green', to: 'teal' }} mx="auto" mb="md">
|
|
<IconCheck size={32} />
|
|
</ThemeIcon>
|
|
<Title order={3} mb="xs">You're All Set!</Title>
|
|
<Text c="dimmed" mb="lg" maw={400} mx="auto">
|
|
Your organization is configured and ready to go. The next step is to set up your annual
|
|
budget — we'll take you straight to Budget Planning.
|
|
</Text>
|
|
<SimpleGrid cols={4} mb="xl" maw={600} mx="auto">
|
|
<Card withBorder p="sm" style={{ textAlign: 'center' }}>
|
|
<ThemeIcon size={32} color="blue" variant="light" radius="xl" mx="auto" mb={4}>
|
|
<IconBuildingBank size={16} />
|
|
</ThemeIcon>
|
|
<Badge color="green" size="sm">Done</Badge>
|
|
<Text size="xs" mt={4}>Operating</Text>
|
|
</Card>
|
|
<Card withBorder p="sm" style={{ textAlign: 'center' }}>
|
|
<ThemeIcon size={32} color="violet" variant="light" radius="xl" mx="auto" mb={4}>
|
|
<IconPigMoney size={16} />
|
|
</ThemeIcon>
|
|
<Badge color={reserveSkipped ? 'gray' : 'green'} size="sm">
|
|
{reserveSkipped ? 'Skipped' : 'Done'}
|
|
</Badge>
|
|
<Text size="xs" mt={4}>Reserve</Text>
|
|
</Card>
|
|
<Card withBorder p="sm" style={{ textAlign: 'center' }}>
|
|
<ThemeIcon size={32} color="blue" variant="light" radius="xl" mx="auto" mb={4}>
|
|
<IconUsers size={16} />
|
|
</ThemeIcon>
|
|
<Badge color="green" size="sm">Done</Badge>
|
|
<Text size="xs" mt={4}>Assessments</Text>
|
|
</Card>
|
|
<Card withBorder p="sm" style={{ textAlign: 'center' }}>
|
|
<ThemeIcon size={32} color="cyan" variant="light" radius="xl" mx="auto" mb={4}>
|
|
<IconFileSpreadsheet size={16} />
|
|
</ThemeIcon>
|
|
<Badge color="cyan" size="sm">Up Next</Badge>
|
|
<Text size="xs" mt={4}>Budget</Text>
|
|
</Card>
|
|
</SimpleGrid>
|
|
<Alert icon={<IconFileSpreadsheet size={16} />} color="blue" variant="light" mb="lg" ta="left">
|
|
<Text size="sm" fw={500} mb={4}>Set Up Your Budget</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Your budget is critical for accurate financial health scores, cash flow forecasting,
|
|
and investment planning. Click below to go directly to Budget Planning where you can
|
|
download a CSV template, fill in your monthly amounts, and upload your budget.
|
|
</Text>
|
|
</Alert>
|
|
<Button
|
|
size="lg"
|
|
onClick={handleFinish}
|
|
loading={loading}
|
|
leftSection={<IconFileSpreadsheet size={18} />}
|
|
variant="gradient"
|
|
gradient={{ from: 'blue', to: 'cyan' }}
|
|
>
|
|
Set Up My Budget
|
|
</Button>
|
|
</Card>
|
|
)}
|
|
|
|
{/* ── Navigation Buttons ── */}
|
|
{active < 3 && (
|
|
<Group justify="flex-end" mt="xl">
|
|
<Button
|
|
onClick={nextStep}
|
|
disabled={!canGoNext()}
|
|
>
|
|
Next Step
|
|
</Button>
|
|
</Group>
|
|
)}
|
|
</Box>
|
|
</Modal>
|
|
);
|
|
}
|