Quality-of-life enhancements: CSV import/export, opening balances, interest rates, mobile UX
- CSV import/export for Units, Projects, and Vendors with match-on-name/number upsert - Cash Flow report toggle for Cash Only vs Cash + Investments - Per-account and bulk opening balance setting with as-of date - Interest rate field on normal accounts with estimated monthly/annual interest display - Mobile sidebar auto-close on navigation - Shared CSV parsing/export utility extracted to frontend/src/utils/csv.ts DB migration needed for existing tenants: ALTER TABLE accounts ADD COLUMN IF NOT EXISTS interest_rate DECIMAL(6,4); Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,11 +29,17 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get('cash-flow')
|
||||
getCashFlowStatement(@Query('from') from?: string, @Query('to') to?: string) {
|
||||
getCashFlowStatement(
|
||||
@Query('from') from?: string,
|
||||
@Query('to') to?: string,
|
||||
@Query('includeInvestments') includeInvestments?: string,
|
||||
) {
|
||||
const now = new Date();
|
||||
const defaultFrom = `${now.getFullYear()}-01-01`;
|
||||
const defaultTo = now.toISOString().split('T')[0];
|
||||
return this.reportsService.getCashFlowStatement(from || defaultFrom, to || defaultTo);
|
||||
return this.reportsService.getCashFlowStatement(
|
||||
from || defaultFrom, to || defaultTo, includeInvestments === 'true',
|
||||
);
|
||||
}
|
||||
|
||||
@Get('aging')
|
||||
|
||||
@@ -178,7 +178,7 @@ export class ReportsService {
|
||||
return { nodes, links, total_income: totalIncome, total_expenses: totalExpenses, net_cash_flow: netFlow };
|
||||
}
|
||||
|
||||
async getCashFlowStatement(from: string, to: string) {
|
||||
async getCashFlowStatement(from: string, to: string, includeInvestments = false) {
|
||||
// Operating activities: income minus expenses from journal entries
|
||||
const operating = await this.tenant.query(`
|
||||
SELECT a.name, a.account_type,
|
||||
@@ -222,6 +222,11 @@ export class ReportsService {
|
||||
ORDER BY a.name
|
||||
`, [from, to]);
|
||||
|
||||
// Asset filter: cash-only vs cash + investment accounts
|
||||
const assetFilter = includeInvestments
|
||||
? `a.account_type = 'asset'`
|
||||
: `a.account_type = 'asset' AND a.name LIKE '%Cash%'`;
|
||||
|
||||
// Cash beginning and ending balances
|
||||
const beginCash = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(sub.bal), 0) as balance FROM (
|
||||
@@ -231,7 +236,7 @@ export class ReportsService {
|
||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||
AND je.is_posted = true AND je.is_void = false
|
||||
AND je.entry_date < $1
|
||||
WHERE a.account_type = 'asset' AND a.name LIKE '%Cash%' AND a.is_active = true
|
||||
WHERE ${assetFilter} AND a.is_active = true
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
`, [from]);
|
||||
@@ -244,11 +249,20 @@ export class ReportsService {
|
||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||
AND je.is_posted = true AND je.is_void = false
|
||||
AND je.entry_date <= $1
|
||||
WHERE a.account_type = 'asset' AND a.name LIKE '%Cash%' AND a.is_active = true
|
||||
WHERE ${assetFilter} AND a.is_active = true
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
`, [to]);
|
||||
|
||||
// Include investment_accounts table balances when requested
|
||||
let investmentBalance = 0;
|
||||
if (includeInvestments) {
|
||||
const inv = await this.tenant.query(
|
||||
`SELECT COALESCE(SUM(current_value), 0) as total FROM investment_accounts WHERE is_active = true`,
|
||||
);
|
||||
investmentBalance = parseFloat(inv[0]?.total || '0');
|
||||
}
|
||||
|
||||
const operatingItems = operating.map((r: any) => ({
|
||||
name: r.name, type: r.account_type, amount: parseFloat(r.amount),
|
||||
}));
|
||||
@@ -258,11 +272,12 @@ export class ReportsService {
|
||||
|
||||
const totalOperating = operatingItems.reduce((s: number, r: any) => s + r.amount, 0);
|
||||
const totalReserve = reserveItems.reduce((s: number, r: any) => s + r.amount, 0);
|
||||
const beginningBalance = parseFloat(beginCash[0]?.balance || '0');
|
||||
const endingBalance = parseFloat(endCash[0]?.balance || '0');
|
||||
const beginningBalance = parseFloat(beginCash[0]?.balance || '0') + (includeInvestments ? investmentBalance : 0);
|
||||
const endingBalance = parseFloat(endCash[0]?.balance || '0') + investmentBalance;
|
||||
|
||||
return {
|
||||
from, to,
|
||||
include_investments: includeInvestments,
|
||||
operating_activities: operatingItems,
|
||||
reserve_activities: reserveItems,
|
||||
total_operating: totalOperating.toFixed(2),
|
||||
@@ -270,6 +285,7 @@ export class ReportsService {
|
||||
net_cash_change: (totalOperating + totalReserve).toFixed(2),
|
||||
beginning_cash: beginningBalance.toFixed(2),
|
||||
ending_cash: endingBalance.toFixed(2),
|
||||
investment_balance: investmentBalance.toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user