Phase 2 tweaks: admin tenant creation, unit delete, frequency, UI overhaul
- Admin panel: create tenants with org + first user, manage org status (active/suspended/archived), contract number and plan level fields - Units: delete with invoice check, assessment group dropdown binding - Assessment groups: frequency field (monthly/quarterly/annual) with income calculations normalized to monthly equivalents - Sidebar: grouped nav sections (Financials, Assessments, Transactions, Planning, Reports, Admin), renamed Chart of Accounts to Accounts - Header: replaced text with SVG logo - Capital projects: Kanban as default view, table-only PDF export, Future category (beyond 5-year plan) - Auth: block login for suspended/archived organizations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { AppShell, Burger, Group, Title, Text, Menu, UnstyledButton, Avatar } from '@mantine/core';
|
||||
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
IconLogout,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import logoSrc from '../../assets/logo.svg';
|
||||
|
||||
export function AppLayout() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
@@ -30,7 +30,7 @@ export function AppLayout() {
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Title order={3} c="blue">HOA LedgerIQ</Title>
|
||||
<img src={logoSrc} alt="HOA LedgerIQ" style={{ height: 40 }} />
|
||||
</Group>
|
||||
<Group>
|
||||
{currentOrg && (
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
IconShieldCheck,
|
||||
IconPigMoney,
|
||||
IconBuildingBank,
|
||||
IconCalendarEvent,
|
||||
IconUsers,
|
||||
IconFileText,
|
||||
IconSettings,
|
||||
@@ -21,33 +20,67 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
|
||||
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: 'Assessment Groups', icon: IconCategory, path: '/assessment-groups' },
|
||||
{ label: 'Invoices', icon: IconFileInvoice, path: '/invoices' },
|
||||
{ label: 'Payments', icon: IconCash, path: '/payments' },
|
||||
{ label: 'Budgets', icon: IconReportAnalytics, path: '/budgets/2026' },
|
||||
const navSections = [
|
||||
{
|
||||
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' },
|
||||
items: [
|
||||
{ label: 'Dashboard', icon: IconDashboard, path: '/dashboard' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Financials',
|
||||
items: [
|
||||
{ label: 'Accounts', icon: IconListDetails, path: '/accounts' },
|
||||
{ label: 'Budgets', icon: IconReportAnalytics, path: '/budgets/2026' },
|
||||
{ label: 'Investments', icon: IconPigMoney, path: '/investments' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Assessments',
|
||||
items: [
|
||||
{ label: 'Units / Homeowners', icon: IconHome, path: '/units' },
|
||||
{ label: 'Assessment Groups', icon: IconCategory, path: '/assessment-groups' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Transactions',
|
||||
items: [
|
||||
{ label: 'Transactions', icon: IconReceipt, path: '/transactions' },
|
||||
{ label: 'Invoices', icon: IconFileInvoice, path: '/invoices' },
|
||||
{ label: 'Payments', icon: IconCash, path: '/payments' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Planning',
|
||||
items: [
|
||||
{ label: 'Capital Projects', icon: IconBuildingBank, path: '/capital-projects' },
|
||||
{ label: 'Reserves', icon: IconShieldCheck, path: '/reserves' },
|
||||
{ label: 'Vendors', icon: IconUsers, path: '/vendors' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reports',
|
||||
items: [
|
||||
{
|
||||
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: 'Admin',
|
||||
items: [
|
||||
{ label: 'Year-End', icon: IconFileText, path: '/year-end' },
|
||||
{ label: 'Settings', icon: IconSettings, path: '/settings' },
|
||||
],
|
||||
},
|
||||
{ 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() {
|
||||
@@ -57,35 +90,47 @@ export function Sidebar() {
|
||||
|
||||
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) => (
|
||||
{navSections.map((section, sIdx) => (
|
||||
<div key={sIdx}>
|
||||
{section.label && (
|
||||
<>
|
||||
{sIdx > 0 && <Divider my={6} />}
|
||||
<Text size="xs" c="dimmed" fw={700} tt="uppercase" px="sm" pb={2} pt={sIdx > 0 ? 4 : 0}>
|
||||
{section.label}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{section.items.map((item: any) =>
|
||||
item.children ? (
|
||||
<NavLink
|
||||
key={child.path}
|
||||
label={child.label}
|
||||
active={location.pathname === child.path}
|
||||
onClick={() => navigate(child.path)}
|
||||
key={item.label}
|
||||
label={item.label}
|
||||
leftSection={<item.icon size={18} />}
|
||||
defaultOpened={item.children.some((c: any) =>
|
||||
location.pathname.startsWith(c.path),
|
||||
)}
|
||||
>
|
||||
{item.children.map((child: any) => (
|
||||
<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!)}
|
||||
/>
|
||||
))}
|
||||
</NavLink>
|
||||
) : (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
label={item.label}
|
||||
leftSection={<item.icon size={18} />}
|
||||
active={location.pathname === item.path}
|
||||
onClick={() => navigate(item.path!)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{user?.isSuperadmin && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user