fix: assessment scenarios UX tweaks and projection improvements

- Reorder sidebar: Assessment Scenarios now directly under Budget Planning
- Simplify special assessment form: remove Total Amount, keep Per Unit only
- Replace Duration field from free-text installments to dropdown (one-time/quarterly/6mo/annual)
- Update Change column display to show total per-unit with duration label
- Fix Reserve Coverage to use planned capital project costs instead of budget expenses
- Include capital_projects table in projection engine alongside projects table
- Replace actions dropdown menu with inline Edit/Remove icon buttons
- Remove Refresh Projection button (projection auto-refreshes on changes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:28:33 -04:00
parent 1d1073cba1
commit a98a7192bb
4 changed files with 78 additions and 58 deletions

View File

@@ -67,8 +67,8 @@ const navSections = [
label: 'Board Planning',
items: [
{ label: 'Budget Planning', icon: IconReportAnalytics, path: '/board-planning/budgets' },
{ label: 'Investment Scenarios', icon: IconScale, path: '/board-planning/investments' },
{ label: 'Assessment Scenarios', icon: IconCalculator, path: '/board-planning/assessments' },
{ label: 'Investment Scenarios', icon: IconScale, path: '/board-planning/investments' },
{ label: 'Compare Scenarios', icon: IconGitCompare, path: '/board-planning/compare' },
],
},

View File

@@ -1,10 +1,10 @@
import { useState } from 'react';
import {
Title, Text, Stack, Group, Button, Table, Badge, Card, ActionIcon,
Loader, Center, Menu, Select, SimpleGrid,
Loader, Center, Select, SimpleGrid, Tooltip,
} from '@mantine/core';
import {
IconPlus, IconArrowLeft, IconDots, IconTrash, IconEdit, IconRefresh,
IconPlus, IconArrowLeft, IconTrash, IconEdit,
} from '@tabler/icons-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
@@ -92,14 +92,6 @@ export function AssessmentScenarioDetailPage() {
},
});
const refreshProjection = useMutation({
mutationFn: () => api.post(`/board-planning/scenarios/${id}/projection/refresh`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['board-planning-projection', id] });
notifications.show({ message: 'Projection refreshed', color: 'green' });
},
});
if (isLoading) return <Center h={400}><Loader size="lg" /></Center>;
if (!scenario) return <Center h={400}><Text>Scenario not found</Text></Center>;
@@ -132,9 +124,6 @@ export function AssessmentScenarioDetailPage() {
{ value: 'approved', label: 'Approved' },
]}
/>
<Button size="sm" variant="light" leftSection={<IconRefresh size={16} />} onClick={() => refreshProjection.mutate()} loading={refreshProjection.isPending}>
Refresh Projection
</Button>
<Button size="sm" leftSection={<IconPlus size={16} />} onClick={() => setAddOpen(true)}>
Add Change
</Button>
@@ -163,8 +152,13 @@ export function AssessmentScenarioDetailPage() {
<Card withBorder p="md">
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>Reserve Coverage</Text>
<Text fw={700} size="xl" ff="monospace">
{(projection.summary.reserve_coverage_months || 0).toFixed(1)} mo
{projection.summary.reserve_coverage_months > 0
? `${projection.summary.reserve_coverage_months.toFixed(1)} mo`
: 'N/A'}
</Text>
{projection.summary.reserve_coverage_months <= 0 && (
<Text size="xs" c="dimmed">No planned capital projects</Text>
)}
</Card>
</SimpleGrid>
)}
@@ -201,7 +195,13 @@ export function AssessmentScenarioDetailPage() {
</Table.Td>
<Table.Td ta="right" ff="monospace">
{a.change_type === 'special_assessment'
? `${fmt(parseFloat(a.special_per_unit) || 0)}/unit x ${a.special_installments || 1} mo`
? `${fmt(parseFloat(a.special_per_unit) || 0)}/unit${(() => {
const inst = parseInt(a.special_installments) || 1;
if (inst === 1) return ', one-time';
if (inst === 3) return ', quarterly';
if (inst === 12) return ', annual';
return `, ${inst} mo`;
})()}`
: a.percentage_change
? `${parseFloat(a.percentage_change).toFixed(1)}%`
: a.flat_amount_change
@@ -211,15 +211,18 @@ export function AssessmentScenarioDetailPage() {
<Table.Td>{a.effective_date ? new Date(a.effective_date).toLocaleDateString() : '-'}</Table.Td>
<Table.Td>{a.end_date ? new Date(a.end_date).toLocaleDateString() : 'Ongoing'}</Table.Td>
<Table.Td>
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="subtle" color="gray"><IconDots size={16} /></ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconEdit size={14} />} onClick={() => setEditAsmt(a)}>Edit</Menu.Item>
<Menu.Item leftSection={<IconTrash size={14} />} color="red" onClick={() => removeMutation.mutate(a.id)}>Remove</Menu.Item>
</Menu.Dropdown>
</Menu>
<Group gap={4} wrap="nowrap">
<Tooltip label="Edit">
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => setEditAsmt(a)}>
<IconEdit size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Remove">
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => removeMutation.mutate(a.id)}>
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
))}

View File

@@ -117,32 +117,27 @@ export function AssessmentChangeForm({ opened, onClose, onSubmit, initialData, l
{isSpecial && (
<>
<Group grow>
<NumberInput
label="Total Amount"
value={form.specialTotal}
onChange={(v) => setForm({ ...form, specialTotal: Number(v) || 0 })}
min={0}
decimalScale={2}
thousandSeparator=","
prefix="$"
/>
<NumberInput
label="Per Unit Amount"
value={form.specialPerUnit}
onChange={(v) => setForm({ ...form, specialPerUnit: Number(v) || 0 })}
min={0}
decimalScale={2}
prefix="$"
/>
</Group>
<NumberInput
label="Installments"
description="1 = one-time lump sum, 6 = spread over 6 months, etc."
value={form.specialInstallments}
label="Per Unit Amount"
description="Total amount each unit will be assessed"
value={form.specialPerUnit}
onChange={(v) => setForm({ ...form, specialPerUnit: Number(v) || 0 })}
min={0}
decimalScale={2}
thousandSeparator=","
prefix="$"
/>
<Select
label="Duration"
description="How the assessment is collected"
value={String(form.specialInstallments)}
onChange={(v) => setForm({ ...form, specialInstallments: Number(v) || 1 })}
min={1}
max={60}
data={[
{ value: '1', label: 'One-time (lump sum)' },
{ value: '3', label: 'Quarterly (3 monthly payments)' },
{ value: '6', label: '6 months' },
{ value: '12', label: 'Annual (12 monthly payments)' },
]}
/>
</>
)}