Sprint 6: Monthly actuals input, reconciliation, and file attachments

Add spreadsheet-style Monthly Actuals page for entering monthly actuals
against budget with auto-generated journal entries and reconciliation flag.
Add file attachment support (PDF, images, spreadsheets) on journal entries
for receipts and invoices. Enhance Budget vs Actual report with month
filter dropdown. Add reconciled badge to Transactions page. Replace bcrypt
with bcryptjs to fix Docker cross-platform native binding issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 11:48:57 -05:00
parent ea49b91bb3
commit 84822474f8
20 changed files with 9868 additions and 22 deletions

View File

@@ -8,7 +8,8 @@ import { DateInput } from '@mantine/dates';
import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { IconPlus, IconEye, IconCheck, IconX, IconTrash } from '@tabler/icons-react';
import { IconPlus, IconEye, IconCheck, IconX, IconTrash, IconShieldCheck } from '@tabler/icons-react';
import { AttachmentPanel } from '../../components/attachments/AttachmentPanel';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../../services/api';
@@ -30,6 +31,7 @@ interface JournalEntry {
entry_type: string;
is_posted: boolean;
is_void: boolean;
is_reconciled?: boolean;
created_at: string;
lines?: JournalEntryLine[];
total_debit?: string;
@@ -190,13 +192,22 @@ export function TransactionsPage() {
<Table.Td ta="right" ff="monospace">{fmt(e.total_debit || '0')}</Table.Td>
<Table.Td ta="right" ff="monospace">{fmt(e.total_credit || '0')}</Table.Td>
<Table.Td>
{e.is_void ? (
<Badge color="red" variant="light" size="sm">Void</Badge>
) : e.is_posted ? (
<Badge color="green" variant="light" size="sm">Posted</Badge>
) : (
<Badge color="yellow" variant="light" size="sm">Draft</Badge>
)}
<Group gap={4}>
{e.is_void ? (
<Badge color="red" variant="light" size="sm">Void</Badge>
) : e.is_posted ? (
<Badge color="green" variant="light" size="sm">Posted</Badge>
) : (
<Badge color="yellow" variant="light" size="sm">Draft</Badge>
)}
{e.is_reconciled && (
<Tooltip label="Reconciled">
<Badge color="teal" variant="light" size="sm" leftSection={<IconShieldCheck size={12} />}>
Reconciled
</Badge>
</Tooltip>
)}
</Group>
</Table.Td>
<Table.Td>
<Group gap="xs">
@@ -353,6 +364,11 @@ export function TransactionsPage() {
</Group>
<Text><strong>Description:</strong> {viewEntry.description}</Text>
{viewEntry.reference_number && <Text><strong>Ref #:</strong> {viewEntry.reference_number}</Text>}
{viewEntry.is_reconciled && (
<Badge color="teal" variant="light" leftSection={<IconShieldCheck size={14} />}>
Reconciled
</Badge>
)}
<Table>
<Table.Thead>
<Table.Tr>
@@ -373,6 +389,10 @@ export function TransactionsPage() {
))}
</Table.Tbody>
</Table>
{/* Attachments */}
<Text fw={500} mt="md">Attachments</Text>
<AttachmentPanel journalEntryId={viewEntry.id} />
</Stack>
)}
</Modal>