- Remove redundant Settings link from sidebar (accessible via user menu) - Move Transactions section below Board Reference for better grouping - Promote Investment Scenarios to its own top-level sidebar item - Add Compact View preference with tighter spacing theme - Wire compact theme into MantineProvider with dynamic switching - Enable Compact View toggle in both Preferences and Settings pages - Install missing @simplewebauthn/browser package (lock file update) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
236 lines
7.6 KiB
TypeScript
236 lines
7.6 KiB
TypeScript
import { NavLink, ScrollArea, Divider, Text } from '@mantine/core';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import {
|
|
IconDashboard,
|
|
IconListDetails,
|
|
IconReceipt,
|
|
IconHome,
|
|
IconFileInvoice,
|
|
IconCash,
|
|
IconReportAnalytics,
|
|
IconChartSankey,
|
|
IconShieldCheck,
|
|
IconBuildingBank,
|
|
IconUsers,
|
|
IconCrown,
|
|
IconCategory,
|
|
IconChartAreaLine,
|
|
IconClipboardCheck,
|
|
IconSparkles,
|
|
IconCalculator,
|
|
IconGitCompare,
|
|
IconScale,
|
|
} from '@tabler/icons-react';
|
|
import { useAuthStore } from '../../stores/authStore';
|
|
|
|
const navSections = [
|
|
{
|
|
items: [
|
|
{ label: 'Dashboard', icon: IconDashboard, path: '/dashboard' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Financials',
|
|
items: [
|
|
{ label: 'Accounts', icon: IconListDetails, path: '/accounts', tourId: 'nav-accounts' },
|
|
{ label: 'Cash Flow', icon: IconChartAreaLine, path: '/cash-flow' },
|
|
{ label: 'Monthly Actuals', icon: IconClipboardCheck, path: '/monthly-actuals' },
|
|
{ label: 'Budgets', icon: IconReportAnalytics, path: '/budgets/2026', tourId: 'nav-budgets' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Assessments',
|
|
items: [
|
|
{ label: 'Units / Homeowners', icon: IconHome, path: '/units' },
|
|
{ label: 'Assessment Groups', icon: IconCategory, path: '/assessment-groups', tourId: 'nav-assessment-groups' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Board Planning',
|
|
items: [
|
|
{ label: 'Budget Planning', icon: IconReportAnalytics, path: '/board-planning/budgets' },
|
|
{
|
|
label: 'Projects', icon: IconShieldCheck, path: '/projects',
|
|
children: [
|
|
{ label: 'Capital Planning', path: '/capital-projects' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Assessment Scenarios', icon: IconCalculator, path: '/board-planning/assessments',
|
|
},
|
|
{ label: 'Investment Planning', icon: IconSparkles, path: '/investment-planning', tourId: 'nav-investment-planning' },
|
|
{ label: 'Investment Scenarios', icon: IconScale, path: '/board-planning/investments' },
|
|
{ label: 'Compare Scenarios', icon: IconGitCompare, path: '/board-planning/compare' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Board Reference',
|
|
items: [
|
|
{ label: 'Vendors', icon: IconUsers, path: '/vendors' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Transactions',
|
|
items: [
|
|
{ label: 'Transactions', icon: IconReceipt, path: '/transactions', tourId: 'nav-transactions' },
|
|
{ label: 'Invoices', icon: IconFileInvoice, path: '/invoices' },
|
|
{ label: 'Payments', icon: IconCash, path: '/payments' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Reports',
|
|
items: [
|
|
{
|
|
label: 'Reports',
|
|
icon: IconChartSankey,
|
|
tourId: 'nav-reports',
|
|
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: 'Year-End', path: '/reports/year-end' },
|
|
{ label: 'Quarterly Financial', path: '/reports/quarterly' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
interface SidebarProps {
|
|
onNavigate?: () => void;
|
|
}
|
|
|
|
export function Sidebar({ onNavigate }: SidebarProps) {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const user = useAuthStore((s) => s.user);
|
|
const currentOrg = useAuthStore((s) => s.currentOrg);
|
|
const organizations = useAuthStore((s) => s.organizations);
|
|
const isAdminOnly = location.pathname.startsWith('/admin') && !currentOrg;
|
|
|
|
const go = (path: string) => {
|
|
navigate(path);
|
|
onNavigate?.();
|
|
};
|
|
|
|
// When on admin route with no org selected, show admin-only sidebar
|
|
if (isAdminOnly && user?.isSuperadmin) {
|
|
return (
|
|
<ScrollArea p="sm">
|
|
<Text size="xs" c="dimmed" fw={700} tt="uppercase" px="sm" pb={4}>
|
|
Platform Administration
|
|
</Text>
|
|
<NavLink
|
|
label="Admin Panel"
|
|
leftSection={<IconCrown size={18} />}
|
|
active={location.pathname === '/admin'}
|
|
onClick={() => go('/admin')}
|
|
color="red"
|
|
/>
|
|
{organizations && organizations.length > 0 && (
|
|
<>
|
|
<Divider my="sm" />
|
|
<NavLink
|
|
label="Switch to Tenant"
|
|
leftSection={<IconBuildingBank size={18} />}
|
|
onClick={() => go('/select-org')}
|
|
variant="subtle"
|
|
/>
|
|
</>
|
|
)}
|
|
</ScrollArea>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollArea p="sm" data-tour="sidebar-nav">
|
|
{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 && !item.path ? (
|
|
// Collapsible group without a parent route (e.g. Reports)
|
|
<NavLink
|
|
key={item.label}
|
|
label={item.label}
|
|
leftSection={<item.icon size={18} />}
|
|
defaultOpened={item.children.some((c: any) =>
|
|
location.pathname.startsWith(c.path),
|
|
)}
|
|
data-tour={item.tourId || undefined}
|
|
>
|
|
{item.children.map((child: any) => (
|
|
<NavLink
|
|
key={child.path}
|
|
label={child.label}
|
|
active={location.pathname === child.path}
|
|
onClick={() => go(child.path)}
|
|
/>
|
|
))}
|
|
</NavLink>
|
|
) : item.children && item.path ? (
|
|
// Parent with its own route + nested children (e.g. Projects > Capital Planning)
|
|
<NavLink
|
|
key={item.path}
|
|
label={item.label}
|
|
leftSection={<item.icon size={18} />}
|
|
defaultOpened={
|
|
location.pathname === item.path ||
|
|
item.children.some((c: any) => location.pathname.startsWith(c.path))
|
|
}
|
|
data-tour={item.tourId || undefined}
|
|
active={location.pathname === item.path}
|
|
onClick={() => go(item.path!)}
|
|
>
|
|
{item.children.map((child: any) => (
|
|
<NavLink
|
|
key={child.path}
|
|
label={child.label}
|
|
active={location.pathname === child.path}
|
|
onClick={(e: React.MouseEvent) => { e.stopPropagation(); go(child.path); }}
|
|
/>
|
|
))}
|
|
</NavLink>
|
|
) : (
|
|
<NavLink
|
|
key={item.path}
|
|
label={item.label}
|
|
leftSection={<item.icon size={18} />}
|
|
active={location.pathname === item.path}
|
|
onClick={() => go(item.path!)}
|
|
data-tour={item.tourId || undefined}
|
|
/>
|
|
),
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{user?.isSuperadmin && (
|
|
<>
|
|
<Divider my="sm" />
|
|
<Text size="xs" c="dimmed" fw={700} tt="uppercase" px="sm" pb={4}>
|
|
Platform Admin
|
|
</Text>
|
|
<NavLink
|
|
label="Admin Panel"
|
|
leftSection={<IconCrown size={18} />}
|
|
active={location.pathname === '/admin'}
|
|
onClick={() => go('/admin')}
|
|
color="red"
|
|
/>
|
|
</>
|
|
)}
|
|
</ScrollArea>
|
|
);
|
|
}
|