Initial commit: HOA Financial Intelligence Platform MVP
Multi-tenant financial management platform for homeowner associations featuring: - NestJS backend with 16 modules (auth, accounts, transactions, budgets, units, invoices, payments, vendors, reserves, investments, capital projects, reports) - React + Mantine frontend with dashboard, CRUD pages, and financial reports - Schema-per-tenant PostgreSQL isolation with JWT-based tenant resolution - Docker Compose infrastructure (nginx, backend, frontend, postgres, redis) - Comprehensive seed data for Sunrise Valley HOA demo - 39 API endpoints with Swagger documentation - Double-entry bookkeeping with journal entries - Budget vs actual reporting and Sankey cash flow visualization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
81
frontend/src/components/layout/AppLayout.tsx
Normal file
81
frontend/src/components/layout/AppLayout.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
import { AppShell, Burger, Group, Title, Text, Menu, UnstyledButton, Avatar } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
IconLogout,
|
||||
IconSwitchHorizontal,
|
||||
IconChevronDown,
|
||||
} from '@tabler/icons-react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
export function AppLayout() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { user, currentOrg, logout } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{ width: 260, breakpoint: 'sm', collapsed: { mobile: !opened } }}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Title order={3} c="blue">HOA Financial Platform</Title>
|
||||
</Group>
|
||||
<Group>
|
||||
{currentOrg && (
|
||||
<Text size="sm" c="dimmed">{currentOrg.name}</Text>
|
||||
)}
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
<Group gap="xs">
|
||||
<Avatar size="sm" radius="xl" color="blue">
|
||||
{user?.firstName?.[0]}{user?.lastName?.[0]}
|
||||
</Avatar>
|
||||
<Text size="sm">{user?.firstName} {user?.lastName}</Text>
|
||||
<IconChevronDown size={14} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconSwitchHorizontal size={14} />}
|
||||
onClick={() => navigate('/select-org')}
|
||||
>
|
||||
Switch Organization
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
color="red"
|
||||
leftSection={<IconLogout size={14} />}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar>
|
||||
<Sidebar />
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>
|
||||
<Outlet />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
86
frontend/src/components/layout/Sidebar.tsx
Normal file
86
frontend/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NavLink, ScrollArea } from '@mantine/core';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
IconDashboard,
|
||||
IconListDetails,
|
||||
IconReceipt,
|
||||
IconHome,
|
||||
IconFileInvoice,
|
||||
IconCash,
|
||||
IconReportAnalytics,
|
||||
IconChartSankey,
|
||||
IconShieldCheck,
|
||||
IconPigMoney,
|
||||
IconBuildingBank,
|
||||
IconCalendarEvent,
|
||||
IconUsers,
|
||||
IconFileText,
|
||||
IconSettings,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', icon: IconDashboard, path: '/dashboard' },
|
||||
{ label: 'Chart of Accounts', icon: IconListDetails, path: '/accounts' },
|
||||
{ label: 'Transactions', icon: IconReceipt, path: '/transactions' },
|
||||
{ label: 'Units / Homeowners', icon: IconHome, path: '/units' },
|
||||
{ label: 'Invoices', icon: IconFileInvoice, path: '/invoices' },
|
||||
{ label: 'Payments', icon: IconCash, path: '/payments' },
|
||||
{ label: 'Budgets', icon: IconReportAnalytics, path: '/budgets/2026' },
|
||||
{
|
||||
label: 'Reports',
|
||||
icon: IconChartSankey,
|
||||
children: [
|
||||
{ label: 'Balance Sheet', path: '/reports/balance-sheet' },
|
||||
{ label: 'Income Statement', path: '/reports/income-statement' },
|
||||
{ label: 'Cash Flow', path: '/reports/cash-flow' },
|
||||
{ label: 'Budget vs Actual', path: '/reports/budget-vs-actual' },
|
||||
{ label: 'Aging Report', path: '/reports/aging' },
|
||||
{ label: 'Sankey Diagram', path: '/reports/sankey' },
|
||||
],
|
||||
},
|
||||
{ label: 'Reserves', icon: IconShieldCheck, path: '/reserves' },
|
||||
{ label: 'Investments', icon: IconPigMoney, path: '/investments' },
|
||||
{ label: 'Capital Projects', icon: IconBuildingBank, path: '/capital-projects' },
|
||||
{ label: 'Vendors', icon: IconUsers, path: '/vendors' },
|
||||
{ label: 'Year-End', icon: IconFileText, path: '/year-end' },
|
||||
{ label: 'Settings', icon: IconSettings, path: '/settings' },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<ScrollArea p="sm">
|
||||
{navItems.map((item) =>
|
||||
item.children ? (
|
||||
<NavLink
|
||||
key={item.label}
|
||||
label={item.label}
|
||||
leftSection={<item.icon size={18} />}
|
||||
defaultOpened={item.children.some((c) =>
|
||||
location.pathname.startsWith(c.path),
|
||||
)}
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<NavLink
|
||||
key={child.path}
|
||||
label={child.label}
|
||||
active={location.pathname === child.path}
|
||||
onClick={() => navigate(child.path)}
|
||||
/>
|
||||
))}
|
||||
</NavLink>
|
||||
) : (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
label={item.label}
|
||||
leftSection={<item.icon size={18} />}
|
||||
active={location.pathname === item.path}
|
||||
onClick={() => navigate(item.path!)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user