Fix bugs: monthly actuals month filter, unit assessments, project funding logic, UI cleanup

- Fix monthly actuals showing same data for all months (SQL JOIN excluded
  month filter from SUM — added je.id IS NOT NULL guard)
- Fix units displaying $0 assessment by reading from assessment group
  instead of stale unit field; add special assessment column
- Replace proportional project funding with priority-based sequential
  allocation — near-term items get fully funded first; add is_funding_locked
  flag so users can manually lock a project's fund balance
- Remove post-creation opening balance UI (keep only initial balance on
  account creation); remove redundant Fund filter dropdown from Accounts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 14:05:07 -05:00
parent 45a267d787
commit e0c956859b
7 changed files with 148 additions and 353 deletions

View File

@@ -2,13 +2,13 @@ import { useState, useRef } from 'react';
import {
Title, Table, Group, Button, Stack, Text, Modal, TextInput,
NumberInput, Select, Textarea, Badge, ActionIcon, Loader, Center,
Card, SimpleGrid, Progress,
Card, SimpleGrid, Progress, Switch, Tooltip,
} 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, IconUpload, IconDownload } from '@tabler/icons-react';
import { IconPlus, IconEdit, IconUpload, IconDownload, IconLock, IconLockOpen } from '@tabler/icons-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../../services/api';
import { parseCSV, downloadBlob } from '../../utils/csv';
@@ -41,6 +41,7 @@ interface Project {
account_id: string;
notes: string;
is_active: boolean;
is_funding_locked: boolean;
}
const FUTURE_YEAR = 9999;
@@ -151,6 +152,7 @@ export function ProjectsPage() {
target_year: currentYear,
target_month: 6,
notes: '',
is_funding_locked: false,
},
validate: {
name: (v) => (v.length > 0 ? null : 'Required'),
@@ -258,6 +260,7 @@ export function ProjectsPage() {
target_year: p.target_year || currentYear,
target_month: p.target_month || 6,
notes: p.notes || '',
is_funding_locked: p.is_funding_locked || false,
});
open();
};
@@ -300,9 +303,16 @@ export function ProjectsPage() {
const pct = cost > 0 ? (funded / cost) * 100 : 0;
const color = pct >= 70 ? 'green' : pct >= 40 ? 'yellow' : 'red';
return (
<Text span c={color} ff="monospace">
{pct.toFixed(0)}%
</Text>
<Group gap={4} justify="flex-end">
{project.is_funding_locked && (
<Tooltip label="Funding manually locked">
<IconLock size={14} style={{ color: 'var(--mantine-color-blue-5)' }} />
</Tooltip>
)}
<Text span c={color} ff="monospace">
{pct.toFixed(0)}%
</Text>
</Group>
);
};
@@ -540,12 +550,18 @@ export function ProjectsPage() {
{/* Row 5: Conditional reserve fields */}
{form.values.fund_source === 'reserve' && (
<>
<Switch
label="Lock Funding"
description="When locked, the fund balance and percentage you set here will be used as-is instead of being auto-calculated from the reserve balance"
{...form.getInputProps('is_funding_locked', { type: 'checkbox' })}
/>
<Group grow>
<NumberInput
label="Current Fund Balance"
prefix="$"
decimalScale={2}
min={0}
disabled={!form.values.is_funding_locked}
{...form.getInputProps('current_fund_balance')}
/>
<NumberInput
@@ -561,6 +577,7 @@ export function ProjectsPage() {
decimalScale={1}
min={0}
max={100}
disabled={!form.values.is_funding_locked}
{...form.getInputProps('funded_percentage')}
/>
</Group>