feat: UX enhancements, member limits, forecast fix, and menu cleanup (v2026.3.19)

- Onboarding wizard: add Reserve Account step between Operating and Assessments,
  redirect to Budget Planning on completion
- Dashboard: health score pending state shows clickable links to set up missing items
- Projects/Vendors: rich empty-state wizard screens with real-world examples and CTAs
- Investment Planning: auto-refresh AI recommendations when empty or stale (>30 days)
- Hide Invoices and Payments menus (see PARKING-LOT.md for re-enablement)
- Send welcome email via Resend when new members are added to a tenant
- Enforce 5-member limit for Starter/Standard/Professional plans (Enterprise unlimited)
- Cash flow forecast: only mark months as "Actual" when journal entries exist,
  fixing the issue where months without data showed as actuals
- Bump version to 2026.3.19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 14:47:04 -04:00
parent db8b520009
commit 66e2f87a96
14 changed files with 482 additions and 41 deletions

View File

@@ -1,13 +1,13 @@
import { useState, useRef } from 'react';
import {
Title, Table, Group, Button, Stack, TextInput, Modal,
Switch, Badge, ActionIcon, Text, Loader, Center,
Switch, Badge, ActionIcon, Text, Loader, Center, Card, ThemeIcon, List,
} from '@mantine/core';
import { DateInput } from '@mantine/dates';
import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { IconPlus, IconEdit, IconSearch, IconUpload, IconDownload } from '@tabler/icons-react';
import { IconPlus, IconEdit, IconSearch, IconUpload, IconDownload, IconUsers, IconBulb, IconRocket } from '@tabler/icons-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../../services/api';
import { useIsReadOnly } from '../../stores/authStore';
@@ -153,7 +153,63 @@ export function VendorsPage() {
<Table.Td>{!isReadOnly && <ActionIcon variant="subtle" onClick={() => handleEdit(v)}><IconEdit size={16} /></ActionIcon>}</Table.Td>
</Table.Tr>
))}
{filtered.length === 0 && <Table.Tr><Table.Td colSpan={8}><Text ta="center" c="dimmed" py="lg">No vendors yet</Text></Table.Td></Table.Tr>}
{filtered.length === 0 && vendors.length === 0 && (
<Table.Tr>
<Table.Td colSpan={8} p={0}>
<Card p="xl" style={{ textAlign: 'center' }}>
<ThemeIcon size={60} radius="xl" variant="gradient" gradient={{ from: 'orange', to: 'yellow' }} mx="auto" mb="md">
<IconUsers size={32} />
</ThemeIcon>
<Title order={3} mb="xs">Vendor Management</Title>
<Text c="dimmed" maw={550} mx="auto" mb="lg">
Keep track of your HOA&apos;s service providers, contractors, and suppliers.
Having a centralized vendor directory helps with 1099 reporting, contract
renewal tracking, and comparing year-over-year spending.
</Text>
<Card withBorder p="md" maw={550} mx="auto" mb="lg" ta="left">
<Text fw={600} mb="xs">
<IconBulb size={16} style={{ verticalAlign: 'middle', marginRight: 6 }} />
Common HOA Vendors to Track
</Text>
<List size="sm" spacing="xs" c="dimmed">
<List.Item><Text span fw={500} c="dark">Landscaping Company</Text> Lawn care, tree trimming, seasonal planting</List.Item>
<List.Item><Text span fw={500} c="dark">Property Management</Text> Day-to-day management and tenant communications</List.Item>
<List.Item><Text span fw={500} c="dark">Insurance Provider</Text> Master policy for buildings and common areas</List.Item>
<List.Item><Text span fw={500} c="dark">Pool Maintenance</Text> Weekly chemical testing, cleaning, and equipment repair</List.Item>
<List.Item><Text span fw={500} c="dark">Snow Removal / Paving</Text> Winter plowing and parking lot maintenance</List.Item>
<List.Item><Text span fw={500} c="dark">Attorney / CPA</Text> Legal counsel and annual financial review</List.Item>
</List>
</Card>
<Group justify="center" gap="md">
{!isReadOnly && (
<>
<Button
size="md"
leftSection={<IconRocket size={18} />}
variant="gradient"
gradient={{ from: 'orange', to: 'yellow' }}
onClick={() => { setEditing(null); form.reset(); open(); }}
>
Add Your First Vendor
</Button>
<Button
size="md"
variant="light"
leftSection={<IconUpload size={16} />}
onClick={() => fileInputRef.current?.click()}
>
Import from CSV
</Button>
</>
)}
</Group>
</Card>
</Table.Td>
</Table.Tr>
)}
{filtered.length === 0 && vendors.length > 0 && (
<Table.Tr><Table.Td colSpan={8}><Text ta="center" c="dimmed" py="lg">No vendors match your search</Text></Table.Td></Table.Tr>
)}
</Table.Tbody>
</Table>
<Modal opened={opened} onClose={close} title={editing ? 'Edit Vendor' : 'New Vendor'}>