From 1e89413ec60da233ad949bd880113028997ff4ce Mon Sep 17 00:00:00 2001 From: defiQUG Date: Fri, 23 Jan 2026 17:11:43 -0800 Subject: [PATCH] Complete Treasury page implementation with all features - Full treasury and subledger account management - Account creation modals (treasury and subledger) - Inter-subledger transfer functionality - Account details panel with balance display - Posting history view - Subledger report generation and display - Tabbed interface (Accounts, Transfers, Reports, Postings) - Real-time balance updates - Account hierarchy visualization - Error handling and loading states --- apps/web/src/pages/TreasuryPage.tsx | 960 +++++++++++++++++++++++++++- 1 file changed, 957 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/TreasuryPage.tsx b/apps/web/src/pages/TreasuryPage.tsx index b1b3984..9f7f333 100644 --- a/apps/web/src/pages/TreasuryPage.tsx +++ b/apps/web/src/pages/TreasuryPage.tsx @@ -1,10 +1,964 @@ -import React from 'react'; +import React, { useState, useMemo } from 'react'; +import { + getAccountStore, + createTreasuryAccount, + createSubledgerAccount, + executeSubledgerTransfer, + generateSubledgerReport, + getPostingStore, +} from '@brazil-swift-ops/treasury'; +import type { Account, TreasuryAccount, SubledgerAccount, SubledgerReport } from '@brazil-swift-ops/types'; +import LoadingSpinner from '../components/LoadingSpinner'; export default function TreasuryPage() { + const accountStore = getAccountStore(); + const postingStore = getPostingStore(); + + const [selectedAccount, setSelectedAccount] = useState(null); + const [showCreateTreasury, setShowCreateTreasury] = useState(false); + const [showCreateSubledger, setShowCreateSubledger] = useState(false); + const [showTransfer, setShowTransfer] = useState(false); + const [showReport, setShowReport] = useState(false); + const [activeTab, setActiveTab] = useState<'accounts' | 'transfers' | 'reports' | 'postings'>('accounts'); + const [reportData, setReportData] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + + // Get all accounts + const treasuryAccounts = useMemo(() => { + return accountStore.getAll().filter((acc) => acc.type === 'treasury') as TreasuryAccount[]; + }, [accountStore]); + + const subledgerAccounts = useMemo(() => { + return accountStore.getAll().filter((acc) => acc.type === 'subledger') as SubledgerAccount[]; + }, [accountStore]); + + // Get subledgers for selected treasury account + const subledgersForSelected = useMemo(() => { + if (!selectedAccount || selectedAccount.type !== 'treasury') return []; + return accountStore.getByParent(selectedAccount.id); + }, [selectedAccount, accountStore]); + + // Get postings for selected account + const accountPostings = useMemo(() => { + if (!selectedAccount) return []; + return postingStore.getByAccount(selectedAccount.id); + }, [selectedAccount, postingStore]); + + const handleCreateTreasury = (accountNumber: string, name: string, currency: string) => { + try { + const account = createTreasuryAccount(accountNumber, name, currency); + accountStore.add(account); + setShowCreateTreasury(false); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create treasury account'); + } + }; + + const handleCreateSubledger = (accountNumber: string, name: string, currency: string, parentId: string) => { + try { + const account = createSubledgerAccount(accountNumber, name, currency, parentId); + accountStore.add(account); + setShowCreateSubledger(false); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create subledger account'); + } + }; + + const handleTransfer = async (fromId: string, toId: string, amount: number, currency: string, description?: string) => { + setIsProcessing(true); + setError(null); + try { + executeSubledgerTransfer(fromId, toId, amount, currency, description); + setShowTransfer(false); + } catch (err) { + setError(err instanceof Error ? err.message : 'Transfer failed'); + } finally { + setIsProcessing(false); + } + }; + + const handleGenerateReport = (subledgerId: string, startDate: Date, endDate: Date) => { + try { + const report = generateSubledgerReport(subledgerId, startDate, endDate); + setReportData(report); + setShowReport(true); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to generate report'); + } + }; + return (
-

TreasuryPage

-

TreasuryPage interface

+
+

Treasury Management

+
+ + +
+
+ + {error && ( +
+ {error} +
+ )} + + {/* Tabs */} +
+ +
+ +
+ {/* Left Column: Account Lists */} +
+ {activeTab === 'accounts' && ( + <> + {/* Treasury Accounts */} +
+
+

Treasury Accounts

+
+
+ + + + + + + + + + + + {treasuryAccounts.length === 0 ? ( + + + + ) : ( + treasuryAccounts.map((account) => ( + setSelectedAccount(account)} + className={`cursor-pointer hover:bg-gray-50 ${ + selectedAccount?.id === account.id ? 'bg-blue-50' : '' + }`} + > + + + + + + + )) + )} + +
AccountNameCurrencyBalanceStatus
+ No treasury accounts yet +
+ {account.accountNumber} + {account.name}{account.currency} + {account.balance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + + {account.status} + +
+
+
+ + {/* Subledger Accounts */} +
+
+

Subledger Accounts

+
+
+ + + + + + + + + + + + + {subledgerAccounts.length === 0 ? ( + + + + ) : ( + subledgerAccounts.map((account) => { + const parent = accountStore.get(account.parentAccountId); + return ( + setSelectedAccount(account)} + className={`cursor-pointer hover:bg-gray-50 ${ + selectedAccount?.id === account.id ? 'bg-blue-50' : '' + }`} + > + + + + + + + + ); + }) + )} + +
AccountNameParentCurrencyBalanceStatus
+ No subledger accounts yet +
+ {account.accountNumber} + {account.name} + {parent?.name || account.parentAccountId} + {account.currency} + {account.balance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + + {account.status} + +
+
+
+ + )} + + {activeTab === 'transfers' && ( +
+

Inter-Subledger Transfers

+ + {/* Transfer history would go here */} +
+ )} + + {activeTab === 'reports' && ( +
+

Subledger Reports

+ {reportData && ( +
+

Report Summary

+
+
+

Opening Balance

+

+ {reportData.openingBalance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {reportData.currency} +

+
+
+

Closing Balance

+

+ {reportData.closingBalance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {reportData.currency} +

+
+
+

Total Debits

+

+ {reportData.totalDebits.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {reportData.currency} +

+
+
+

Total Credits

+

+ {reportData.totalCredits.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {reportData.currency} +

+
+
+

Net Position

+

+ {reportData.netPosition.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {reportData.currency} +

+
+
+

Transaction Count

+

{reportData.transactionCount}

+
+
+
+ )} +
+ )} + + {activeTab === 'postings' && ( +
+
+

Posting History

+
+
+ + + + + + + + + + + + + {accountPostings.length === 0 ? ( + + + + ) : ( + accountPostings.map((posting) => ( + + + + + + + + + )) + )} + +
DateTypeAmountBalance BeforeBalance AfterDescription
+ {selectedAccount ? 'No postings for this account' : 'Select an account to view postings'} +
+ {new Date(posting.postedAt).toLocaleString()} + + + {posting.postingType} + + + {posting.amount.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {posting.currency} + + {posting.balanceBefore.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + {posting.balanceAfter.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + {posting.description}
+
+
+ )} +
+ + {/* Right Column: Account Details */} +
+ {selectedAccount ? ( +
+

Account Details

+
+
+

Account Number

+

{selectedAccount.accountNumber}

+
+
+

Name

+

{selectedAccount.name}

+
+
+

Type

+

{selectedAccount.type}

+
+
+

Currency

+

{selectedAccount.currency}

+
+
+

Balance

+

+ {selectedAccount.balance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {selectedAccount.currency} +

+
+
+

Available Balance

+

+ {selectedAccount.availableBalance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}{' '} + {selectedAccount.currency} +

+
+
+

Status

+ + {selectedAccount.status} + +
+ {selectedAccount.type === 'subledger' && ( +
+

Parent Account

+

+ {accountStore.get(selectedAccount.parentAccountId)?.name || selectedAccount.parentAccountId} +

+
+ )} + {selectedAccount.type === 'treasury' && subledgersForSelected.length > 0 && ( +
+

Subledgers ({subledgersForSelected.length})

+
    + {subledgersForSelected.map((sub) => ( +
  • + {sub.name} ({sub.accountNumber}) +
  • + ))} +
+
+ )} + {selectedAccount.type === 'subledger' && ( + + )} +
+
+ ) : ( +
+

Select an account to view details

+
+ )} +
+
+ + {/* Modals */} + {showCreateTreasury && ( + setShowCreateTreasury(false)} + onSubmit={handleCreateTreasury} + /> + )} + + {showCreateSubledger && ( + setShowCreateSubledger(false)} + onSubmit={handleCreateSubledger} + treasuryAccounts={treasuryAccounts} + /> + )} + + {showTransfer && ( + setShowTransfer(false)} + onSubmit={handleTransfer} + accounts={subledgerAccounts} + isProcessing={isProcessing} + /> + )} + + {showReport && reportData && ( + setShowReport(false)} report={reportData} /> + )} +
+ ); +} + +// Modal Components +function CreateTreasuryModal({ + onClose, + onSubmit, +}: { + onClose: () => void; + onSubmit: (accountNumber: string, name: string, currency: string) => void; +}) { + const [accountNumber, setAccountNumber] = useState(''); + const [name, setName] = useState(''); + const [currency, setCurrency] = useState('USD'); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (accountNumber && name) { + onSubmit(accountNumber, name, currency); + } + }; + + return ( +
+
+

Create Treasury Account

+
+
+ + setAccountNumber(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2" + required + /> +
+
+ + setName(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2" + required + /> +
+
+ + +
+
+ + +
+
+
+
+ ); +} + +function CreateSubledgerModal({ + onClose, + onSubmit, + treasuryAccounts, +}: { + onClose: () => void; + onSubmit: (accountNumber: string, name: string, currency: string, parentId: string) => void; + treasuryAccounts: TreasuryAccount[]; +}) { + const [accountNumber, setAccountNumber] = useState(''); + const [name, setName] = useState(''); + const [currency, setCurrency] = useState('USD'); + const [parentId, setParentId] = useState(treasuryAccounts[0]?.id || ''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (accountNumber && name && parentId) { + onSubmit(accountNumber, name, currency, parentId); + } + }; + + return ( +
+
+

Create Subledger Account

+
+
+ + setAccountNumber(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2" + required + /> +
+
+ + setName(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2" + required + /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ); +} + +function TransferModal({ + onClose, + onSubmit, + accounts, + isProcessing, +}: { + onClose: () => void; + onSubmit: (fromId: string, toId: string, amount: number, currency: string, description?: string) => void; + accounts: SubledgerAccount[]; + isProcessing: boolean; +}) { + const [fromId, setFromId] = useState(''); + const [toId, setToId] = useState(''); + const [amount, setAmount] = useState(''); + const [currency, setCurrency] = useState('USD'); + const [description, setDescription] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (fromId && toId && amount && fromId !== toId) { + onSubmit(fromId, toId, parseFloat(amount), currency, description || undefined); + } + }; + + return ( +
+
+

Inter-Subledger Transfer

+
+
+ + +
+
+ + +
+
+ + setAmount(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2" + required + /> +
+
+ + +
+
+ +