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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user