feat: add transaction statistics to TransactionsPage; implement reconciled and categorized percentage calculations, enhance card layout, and update UI components for improved data presentation
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m11s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m11s
This commit is contained in:
@@ -2,7 +2,15 @@
|
|||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { PageLayout, PageHeader } from "@/components/layout";
|
import { PageLayout, PageHeader } from "@/components/layout";
|
||||||
import { RefreshCw, Receipt, Euro, ChevronDown, ChevronUp } from "lucide-react";
|
import {
|
||||||
|
RefreshCw,
|
||||||
|
Receipt,
|
||||||
|
Euro,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
CheckCircle2,
|
||||||
|
Tag,
|
||||||
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
TransactionFilters,
|
TransactionFilters,
|
||||||
TransactionBulkActions,
|
TransactionBulkActions,
|
||||||
@@ -105,6 +113,7 @@ export default function TransactionsPage() {
|
|||||||
isLoading: isLoadingChart,
|
isLoading: isLoadingChart,
|
||||||
totalAmount: chartTotalAmount,
|
totalAmount: chartTotalAmount,
|
||||||
totalCount: chartTotalCount,
|
totalCount: chartTotalCount,
|
||||||
|
transactions: chartTransactions,
|
||||||
} = useTransactionsChartData({
|
} = useTransactionsChartData({
|
||||||
selectedAccounts,
|
selectedAccounts,
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
@@ -175,6 +184,23 @@ export default function TransactionsPage() {
|
|||||||
const totalAmount = chartTotalAmount ?? 0;
|
const totalAmount = chartTotalAmount ?? 0;
|
||||||
const displayTotalCount = chartTotalCount ?? totalTransactions;
|
const displayTotalCount = chartTotalCount ?? totalTransactions;
|
||||||
|
|
||||||
|
// Calculate percentages from chart transactions (all filtered transactions)
|
||||||
|
const reconciledPercent = useMemo(() => {
|
||||||
|
if (chartTransactions.length === 0) return "0.00";
|
||||||
|
const reconciledCount = chartTransactions.filter(
|
||||||
|
(t) => t.isReconciled
|
||||||
|
).length;
|
||||||
|
return ((reconciledCount / chartTransactions.length) * 100).toFixed(2);
|
||||||
|
}, [chartTransactions]);
|
||||||
|
|
||||||
|
const categorizedPercent = useMemo(() => {
|
||||||
|
if (chartTransactions.length === 0) return "0.00";
|
||||||
|
const categorizedCount = chartTransactions.filter(
|
||||||
|
(t) => t.categoryId !== null
|
||||||
|
).length;
|
||||||
|
return ((categorizedCount / chartTransactions.length) * 100).toFixed(2);
|
||||||
|
}, [chartTransactions]);
|
||||||
|
|
||||||
// Persist statistics collapsed state in localStorage
|
// Persist statistics collapsed state in localStorage
|
||||||
const [isStatsExpanded, setIsStatsExpanded] = useLocalStorage(
|
const [isStatsExpanded, setIsStatsExpanded] = useLocalStorage(
|
||||||
"transactions-stats-expanded",
|
"transactions-stats-expanded",
|
||||||
@@ -271,7 +297,7 @@ export default function TransactionsPage() {
|
|||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
{/* Summary cards */}
|
{/* Summary cards */}
|
||||||
{!isLoadingTransactions && (
|
{!isLoadingTransactions && (
|
||||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 mb-6">
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 mb-6">
|
||||||
<Card className="card-hover">
|
<Card className="card-hover">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -283,7 +309,10 @@ export default function TransactionsPage() {
|
|||||||
{displayTotalCount}
|
{displayTotalCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Receipt className="w-8 h-8 text-muted-foreground" />
|
<Receipt
|
||||||
|
className="w-8 h-8"
|
||||||
|
style={{ color: "var(--gray)" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -304,7 +333,49 @@ export default function TransactionsPage() {
|
|||||||
{formatCurrency(totalAmount)}
|
{formatCurrency(totalAmount)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Euro className="w-8 h-8 text-muted-foreground" />
|
<Euro
|
||||||
|
className={`w-8 h-8 ${
|
||||||
|
totalAmount >= 0
|
||||||
|
? "text-emerald-600"
|
||||||
|
: "text-red-600"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="card-hover">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
|
Pointé
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold mt-1 text-primary">
|
||||||
|
{reconciledPercent}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CheckCircle2 className="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="card-hover">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
|
Catégorisé
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="text-2xl font-bold mt-1"
|
||||||
|
style={{ color: "var(--blue)" }}
|
||||||
|
>
|
||||||
|
{categorizedPercent}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Tag
|
||||||
|
className="w-8 h-8"
|
||||||
|
style={{ color: "var(--blue)" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
|||||||
const reconciled = data.transactions.filter((t) => t.isReconciled).length;
|
const reconciled = data.transactions.filter((t) => t.isReconciled).length;
|
||||||
const total = data.transactions.length;
|
const total = data.transactions.length;
|
||||||
const reconciledPercent =
|
const reconciledPercent =
|
||||||
total > 0 ? Math.round((reconciled / total) * 100) : 0;
|
total > 0 ? ((reconciled / total) * 100).toFixed(2) : "0.00";
|
||||||
|
|
||||||
const categorized = data.transactions.filter(
|
const categorized = data.transactions.filter(
|
||||||
(t) => t.categoryId !== null
|
(t) => t.categoryId !== null
|
||||||
).length;
|
).length;
|
||||||
const categorizedPercent =
|
const categorizedPercent =
|
||||||
total > 0 ? Math.round((categorized / total) * 100) : 0;
|
total > 0 ? ((categorized / total) * 100).toFixed(2) : "0.00";
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number) => {
|
||||||
return new Intl.NumberFormat("fr-FR", {
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export function useTransactionsChartData({
|
|||||||
isLoading,
|
isLoading,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
totalCount,
|
totalCount,
|
||||||
|
transactions: transactionsData?.transactions || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user