feat: display both net total and turnover in transactions footer (#53)

- Add calcNetTotalPerCurrency function to calculate signed totals (income positive, expenses negative)
- Update transaction list footer to display both net total and turnover
- Use semantic HTML markup (<dl>, <dt>, <dd>) for better accessibility
- Add color coding: green for positive net, red for negative net
- Maintain turnover calculation for total transaction volume

Fixes issue where transaction totals did not respect transaction type (income/expense)
This commit is contained in:
Artem Sushchev
2025-10-22 09:28:19 +02:00
committed by GitHub
parent 07e05aabe7
commit 3223d5026b
2 changed files with 50 additions and 8 deletions

View File

@@ -4,7 +4,7 @@ import { BulkActionsMenu } from "@/components/transactions/bulk-actions"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { calcTotalPerCurrency, isTransactionIncomplete } from "@/lib/stats" import { calcNetTotalPerCurrency, calcTotalPerCurrency, isTransactionIncomplete } from "@/lib/stats"
import { cn, formatCurrency } from "@/lib/utils" import { cn, formatCurrency } from "@/lib/utils"
import { Category, Field, Project, Transaction } from "@/prisma/client" import { Category, Field, Project, Transaction } from "@/prisma/client"
import { formatDate } from "date-fns" import { formatDate } from "date-fns"
@@ -112,14 +112,30 @@ export const standardFieldRenderers: Record<string, FieldRenderer> = {
</div> </div>
), ),
footerValue: (transactions: Transaction[]) => { footerValue: (transactions: Transaction[]) => {
const totalPerCurrency = calcTotalPerCurrency(transactions) const netTotalPerCurrency = calcNetTotalPerCurrency(transactions)
const turnoverPerCurrency = calcTotalPerCurrency(transactions)
return ( return (
<div className="flex flex-col"> <div className="flex flex-col gap-3 text-right">
{Object.entries(totalPerCurrency).map(([currency, total]) => ( <dl className="space-y-1">
<div key={currency} className="text-sm first:text-base"> <dt className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Net Total</dt>
{formatCurrency(total, currency)} {Object.entries(netTotalPerCurrency).map(([currency, total]) => (
</div> <dd
))} key={`net-${currency}`}
className={cn("text-sm first:text-base font-medium", total >= 0 ? "text-green-600" : "text-red-600")}
>
{formatCurrency(total, currency)}
</dd>
))}
</dl>
<dl className="space-y-1">
<dt className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Turnover</dt>
{Object.entries(turnoverPerCurrency).map(([currency, total]) => (
<dd key={`turnover-${currency}`} className="text-sm text-muted-foreground">
{formatCurrency(total, currency)}
</dd>
))}
</dl>
</div> </div>
) )
}, },

View File

@@ -16,6 +16,32 @@ export function calcTotalPerCurrency(transactions: Transaction[]): Record<string
) )
} }
export function calcNetTotalPerCurrency(transactions: Transaction[]): Record<string, number> {
return transactions.reduce(
(acc, transaction) => {
let amount = 0
let currency: string | undefined
if (
transaction.convertedTotal !== null &&
transaction.convertedTotal !== undefined &&
transaction.convertedCurrencyCode
) {
amount = transaction.convertedTotal
currency = transaction.convertedCurrencyCode.toUpperCase()
} else if (transaction.total !== null && transaction.total !== undefined && transaction.currencyCode) {
amount = transaction.total
currency = transaction.currencyCode.toUpperCase()
}
if (currency && amount !== 0) {
const sign = transaction.type === "expense" ? -1 : 1
acc[currency] = (acc[currency] || 0) + amount * sign
}
return acc
},
{} as Record<string, number>
)
}
export const isTransactionIncomplete = (fields: Field[], transaction: Transaction): boolean => { export const isTransactionIncomplete = (fields: Field[], transaction: Transaction): boolean => {
const incompleteFields = incompleteTransactionFields(fields, transaction) const incompleteFields = incompleteTransactionFields(fields, transaction)