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(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(0); const [balanceDate, setBalanceDate] = useState(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(0); const [reserveBalanceDate, setReserveBalanceDate] = useState(new Date()); // ── Step 3: Assessment Group State ── const [groupCreated, setGroupCreated] = useState(false); const [groupName, setGroupName] = useState('Standard Assessment'); const [regularAssessment, setRegularAssessment] = useState(0); const [frequency, setFrequency] = useState('monthly'); const [unitCount, setUnitCount] = useState(0); const [units, setUnits] = useState([]); 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 ( {}} // Prevent closing without completing withCloseButton={false} size="xl" centered overlayProps={{ opacity: 0.6, blur: 3 }} styles={{ body: { padding: 0 }, }} > {/* Header */}
Set Up Your Organization Let's get the essentials configured so you can start managing your HOA finances.
} completedIcon={} /> } completedIcon={reserveSkipped ? : } /> } completedIcon={} /> {error && ( } color="red" mb="md" withCloseButton onClose={() => setError(null)}> {error} )} {/* ── Step 1: Create Operating Account ── */} {active === 0 && ( Create Your Primary Operating Account This is your HOA's main bank account for day-to-day operations. You can add more accounts later. {accountCreated ? ( } color="green" variant="light"> {accountName} created successfully! Initial balance: ${(typeof initialBalance === 'number' ? initialBalance : parseFloat(initialBalance as string) || 0).toLocaleString()} {balanceDate && ` as of ${balanceDate.toLocaleDateString()}`} ) : ( <> setAccountName(e.currentTarget.value)} required /> setAccountNumber(e.currentTarget.value)} required />