Phase 3: Optimize & clean up — unified projects, account enhancements, new tenant fix

- Unify reserve_components + capital_projects into single projects model with
  full CRUD backend and new Projects page frontend
- Rewrite Capital Planning to read from unified projects/planning endpoint;
  add empty state directing users to Projects page when no planning items exist
- Add default designation to assessment groups with auto-set on first creation;
  units now require an assessment group (pre-populated with default)
- Add primary account designation (one per fund type) and balance adjustment
  via journal entries against equity offset accounts (3000/3100)
- Add computed investment fields (interest earned, maturity value, days remaining)
  with PostgreSQL date arithmetic fix for DATE - DATE integer result
- Restructure sidebar: investments in Accounts tab, Year-End under Reports,
  Planning section with Projects and Capital Planning
- Fix new tenant creation seeding unwanted default chart of accounts —
  new tenants now start with a blank slate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:32:35 -05:00
parent 17fdacc0f2
commit 301f8a7bde
20 changed files with 1760 additions and 145 deletions

View File

@@ -1,12 +1,12 @@
import { useState } from 'react';
import {
Title, Table, Group, Button, Stack, TextInput, Modal,
NumberInput, Select, Badge, ActionIcon, Text, Loader, Center, Tooltip,
NumberInput, Select, Badge, ActionIcon, Text, Loader, Center, Tooltip, Alert,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { IconPlus, IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconPlus, IconEdit, IconSearch, IconTrash, IconInfoCircle } from '@tabler/icons-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../../services/api';
@@ -30,6 +30,7 @@ interface AssessmentGroup {
name: string;
regular_assessment: string;
frequency: string;
is_default: boolean;
}
export function UnitsPage() {
@@ -49,13 +50,19 @@ export function UnitsPage() {
queryFn: async () => { const { data } = await api.get('/assessment-groups'); return data; },
});
const defaultGroup = assessmentGroups.find(g => g.is_default);
const hasGroups = assessmentGroups.length > 0;
const form = useForm({
initialValues: {
unit_number: '', address_line1: '', city: '', state: '', zip_code: '',
owner_name: '', owner_email: '', owner_phone: '', monthly_assessment: 0,
assessment_group_id: '' as string | null,
},
validate: { unit_number: (v) => (v.length > 0 ? null : 'Required') },
validate: {
unit_number: (v) => (v.length > 0 ? null : 'Required'),
assessment_group_id: (v) => (v && v.length > 0 ? null : 'Assessment group is required'),
},
});
const saveMutation = useMutation({
@@ -95,6 +102,17 @@ export function UnitsPage() {
open();
};
const handleNew = () => {
setEditing(null);
form.reset();
// Pre-populate with default group
if (defaultGroup) {
form.setFieldValue('assessment_group_id', defaultGroup.id);
form.setFieldValue('monthly_assessment', parseFloat(defaultGroup.regular_assessment || '0'));
}
open();
};
const handleGroupChange = (groupId: string | null) => {
form.setFieldValue('assessment_group_id', groupId);
if (groupId) {
@@ -116,8 +134,21 @@ export function UnitsPage() {
<Stack>
<Group justify="space-between">
<Title order={2}>Units / Homeowners</Title>
<Button leftSection={<IconPlus size={16} />} onClick={() => { setEditing(null); form.reset(); open(); }}>Add Unit</Button>
{hasGroups ? (
<Button leftSection={<IconPlus size={16} />} onClick={handleNew}>Add Unit</Button>
) : (
<Tooltip label="Create an assessment group first">
<Button leftSection={<IconPlus size={16} />} disabled>Add Unit</Button>
</Tooltip>
)}
</Group>
{!hasGroups && (
<Alert icon={<IconInfoCircle size={16} />} color="yellow" variant="light">
You must create at least one assessment group before adding units. Go to Assessment Groups to create one.
</Alert>
)}
<TextInput placeholder="Search units..." leftSection={<IconSearch size={16} />} value={search} onChange={(e) => setSearch(e.currentTarget.value)} />
<Table striped highlightOnHover>
<Table.Thead>
@@ -182,14 +213,15 @@ export function UnitsPage() {
<TextInput label="Owner Phone" {...form.getInputProps('owner_phone')} />
<Select
label="Assessment Group"
placeholder="Select a group (optional)"
description="Required — all units must belong to an assessment group"
required
data={assessmentGroups.map(g => ({
value: g.id,
label: `${g.name}$${parseFloat(g.regular_assessment || '0').toFixed(2)}/${g.frequency || 'mo'}`,
label: `${g.name}${g.is_default ? ' (Default)' : ''}$${parseFloat(g.regular_assessment || '0').toFixed(2)}/${g.frequency || 'mo'}`,
}))}
value={form.values.assessment_group_id}
onChange={handleGroupChange}
clearable
error={form.errors.assessment_group_id}
/>
<NumberInput label="Monthly Assessment" prefix="$" decimalScale={2} min={0} {...form.getInputProps('monthly_assessment')} />
<Button type="submit" loading={saveMutation.isPending}>{editing ? 'Update' : 'Create'}</Button>