import { useState } from 'react'; import { Modal, Stepper, Button, Group, TextInput, NumberInput, Textarea, Select, Stack, Text, Title, Alert, ActionIcon, Table, FileInput, Card, ThemeIcon, Divider, Loader, Badge, SimpleGrid, Box, } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import { IconBuildingBank, IconUsers, IconFileSpreadsheet, IconPlus, IconTrash, IconDownload, IconCheck, IconRocket, IconAlertCircle, } from '@tabler/icons-react'; import api from '../../services/api'; import { useAuthStore } from '../../stores/authStore'; interface OnboardingWizardProps { opened: boolean; onComplete: () => void; } interface UnitRow { unitNumber: string; ownerName: string; ownerEmail: string; } // ── CSV Parsing (reused from BudgetsPage pattern) ── function parseCSV(text: string): Record[] { const lines = text.split('\n').filter((l) => l.trim()); if (lines.length < 2) return []; const headers = lines[0].split(',').map((h) => h.trim().replace(/^"|"$/g, '')); return lines.slice(1).map((line) => { const values: string[] = []; let current = ''; let inQuotes = false; for (const char of line) { if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { values.push(current.trim()); current = ''; } else { current += char; } } values.push(current.trim()); const row: Record = {}; headers.forEach((h, i) => { row[h] = values[i] || ''; }); return row; }); } export function OnboardingWizard({ opened, onComplete }: OnboardingWizardProps) { const [active, setActive] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const setOrgSettings = useAuthStore((s) => s.setOrgSettings); // ── Step 1: 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); // ── Step 2: 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 [units, setUnits] = useState([]); const [unitsCreated, setUnitsCreated] = useState(false); // ── Step 3: Budget State ── const [budgetFile, setBudgetFile] = useState(null); const [budgetUploaded, setBudgetUploaded] = useState(false); const [budgetImportResult, setBudgetImportResult] = useState(null); const currentYear = new Date().getFullYear(); // ── Step 1: Create 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, }); 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 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; } setLoading(true); setError(null); try { const { data: group } = await api.post('/assessment-groups', { name: groupName.trim(), regularAssessment: assessment, frequency, 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); } }; // ── Step 3: Budget Import ── const handleDownloadTemplate = async () => { try { const response = await api.get(`/budgets/${currentYear}/template`, { responseType: 'blob', }); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `budget_template_${currentYear}.csv`); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch { notifications.show({ title: 'Error', message: 'Failed to download template', color: 'red', }); } }; const handleUploadBudget = async () => { if (!budgetFile) { setError('Please select a CSV file'); return; } setLoading(true); setError(null); try { const text = await budgetFile.text(); const rows = parseCSV(text); if (rows.length === 0) { setError('CSV file appears to be empty or invalid'); setLoading(false); return; } const { data } = await api.post(`/budgets/${currentYear}/import`, { rows }); setBudgetUploaded(true); setBudgetImportResult(data); notifications.show({ title: 'Budget Imported', message: `Imported ${data.imported || rows.length} budget line(s) for ${currentYear}`, color: 'green', }); } catch (err: any) { const msg = err.response?.data?.message || 'Failed to import budget'; setError(typeof msg === 'string' ? msg : JSON.stringify(msg)); } finally { setLoading(false); } }; // ── Finish Wizard ── const handleFinish = async () => { setLoading(true); try { await api.patch('/organizations/settings', { onboardingComplete: true }); setOrgSettings({ onboardingComplete: true }); onComplete(); } catch { // Even if API fails, close the wizard — onboarding data is already created onComplete(); } 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 groupCreated; if (active === 2) return true; // Budget is optional 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={} /> } 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()} ) : ( <> setAccountName(e.currentTarget.value)} required /> setAccountNumber(e.currentTarget.value)} required />