189 lines
7.9 KiB
TypeScript
189 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Wallet,
|
|
CreditCard,
|
|
Tag,
|
|
} from "lucide-react";
|
|
import type { BankingData } from "@/lib/types";
|
|
import { getAccountBalance } from "@/lib/account-utils";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface OverviewCardsProps {
|
|
data: BankingData;
|
|
}
|
|
|
|
export function OverviewCards({ data }: OverviewCardsProps) {
|
|
const totalBalance = data.accounts.reduce(
|
|
(sum, acc) => sum + getAccountBalance(acc),
|
|
0,
|
|
);
|
|
|
|
const thisMonth = new Date();
|
|
thisMonth.setDate(1);
|
|
const thisMonthStr = thisMonth.toISOString().slice(0, 7);
|
|
|
|
const monthTransactions = data.transactions.filter((t) =>
|
|
t.date.startsWith(thisMonthStr),
|
|
);
|
|
|
|
const income = monthTransactions
|
|
.filter((t) => t.amount > 0)
|
|
.reduce((sum, t) => sum + t.amount, 0);
|
|
|
|
const expenses = monthTransactions
|
|
.filter((t) => t.amount < 0)
|
|
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
|
|
|
|
const reconciled = data.transactions.filter((t) => t.isReconciled).length;
|
|
const total = data.transactions.length;
|
|
const reconciledPercent =
|
|
total > 0 ? ((reconciled / total) * 100).toFixed(2) : "0.00";
|
|
|
|
const categorized = data.transactions.filter(
|
|
(t) => t.categoryId !== null,
|
|
).length;
|
|
const categorizedPercent =
|
|
total > 0 ? ((categorized / total) * 100).toFixed(2) : "0.00";
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
}).format(amount);
|
|
};
|
|
|
|
return (
|
|
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-5">
|
|
<Card className="stat-card-gradient-1 card-hover group relative overflow-hidden">
|
|
{/* Icône en arrière-plan */}
|
|
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
|
<Wallet
|
|
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-primary"
|
|
strokeWidth={1}
|
|
/>
|
|
</div>
|
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
|
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
|
Solde Total
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-5 pb-5 sm:px-6 sm:pb-6 pt-0 relative z-10">
|
|
<div
|
|
className={cn(
|
|
"text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight mb-3 leading-tight break-words",
|
|
totalBalance >= 0 ? "text-success" : "text-destructive",
|
|
)}
|
|
>
|
|
{formatCurrency(totalBalance)}
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground/70">
|
|
{data.accounts.length} compte{data.accounts.length > 1 ? "s" : ""}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="stat-card-gradient-2 card-hover group relative overflow-hidden">
|
|
{/* Icône en arrière-plan */}
|
|
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
|
<TrendingUp
|
|
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-success"
|
|
strokeWidth={1}
|
|
/>
|
|
</div>
|
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
|
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
|
Revenus du mois
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-5 pb-5 sm:px-6 sm:pb-6 pt-0 relative z-10">
|
|
<div className="text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight text-success mb-3 leading-tight break-words">
|
|
{formatCurrency(income)}
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground/70">
|
|
{monthTransactions.filter((t) => t.amount > 0).length} opération
|
|
{monthTransactions.filter((t) => t.amount > 0).length > 1
|
|
? "s"
|
|
: ""}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="stat-card-gradient-3 card-hover group relative overflow-hidden">
|
|
{/* Icône en arrière-plan */}
|
|
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
|
<TrendingDown
|
|
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-destructive"
|
|
strokeWidth={1}
|
|
/>
|
|
</div>
|
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
|
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
|
Dépenses du mois
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-5 pb-5 sm:px-6 sm:pb-6 pt-0 relative z-10">
|
|
<div className="text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight text-destructive mb-3 leading-tight break-words">
|
|
{formatCurrency(expenses)}
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground/70">
|
|
{monthTransactions.filter((t) => t.amount < 0).length} opération
|
|
{monthTransactions.filter((t) => t.amount < 0).length > 1
|
|
? "s"
|
|
: ""}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="stat-card-gradient-4 card-hover group relative overflow-hidden">
|
|
{/* Icône en arrière-plan */}
|
|
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
|
<CreditCard
|
|
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-4"
|
|
strokeWidth={1}
|
|
/>
|
|
</div>
|
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
|
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
|
Pointage
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-5 pb-5 sm:px-6 sm:pb-6 pt-0 relative z-10">
|
|
<div className="text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight mb-3 leading-tight break-words">
|
|
{reconciledPercent}%
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground/70">
|
|
{reconciled} / {total} opérations pointées
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="stat-card-gradient-5 card-hover group relative overflow-hidden">
|
|
{/* Icône en arrière-plan */}
|
|
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
|
<Tag
|
|
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-5"
|
|
strokeWidth={1}
|
|
/>
|
|
</div>
|
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
|
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
|
Catégorisation
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-5 pb-5 sm:px-6 sm:pb-6 pt-0 relative z-10">
|
|
<div className="text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight mb-3 leading-tight break-words">
|
|
{categorizedPercent}%
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground/70">
|
|
{categorized} / {total} opérations catégorisées
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|