chore: init from v0
This commit is contained in:
81
components/dashboard/accounts-summary.tsx
Normal file
81
components/dashboard/accounts-summary.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import type { BankingData } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Building2 } from "lucide-react"
|
||||
|
||||
interface AccountsSummaryProps {
|
||||
data: BankingData
|
||||
}
|
||||
|
||||
export function AccountsSummary({ data }: AccountsSummaryProps) {
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const totalPositive = data.accounts.filter((a) => a.balance > 0).reduce((sum, a) => sum + a.balance, 0)
|
||||
|
||||
if (data.accounts.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Mes Comptes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Building2 className="w-12 h-12 text-muted-foreground mb-3" />
|
||||
<p className="text-muted-foreground">Aucun compte</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Importez un fichier OFX pour ajouter un compte</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Mes Comptes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{data.accounts.map((account) => {
|
||||
const percentage = totalPositive > 0 ? Math.max(0, (account.balance / totalPositive) * 100) : 0
|
||||
|
||||
return (
|
||||
<div key={account.id} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Building2 className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-sm">{account.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{account.accountNumber.slice(-4).padStart(account.accountNumber.length, "*")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold tabular-nums",
|
||||
account.balance >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(account.balance)}
|
||||
</span>
|
||||
</div>
|
||||
{account.balance > 0 && <Progress value={percentage} className="h-1.5" />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
98
components/dashboard/category-breakdown.tsx
Normal file
98
components/dashboard/category-breakdown.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import type { BankingData } from "@/lib/types"
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
|
||||
|
||||
interface CategoryBreakdownProps {
|
||||
data: BankingData
|
||||
}
|
||||
|
||||
export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
|
||||
// Get current month expenses by category
|
||||
const thisMonth = new Date()
|
||||
thisMonth.setDate(1)
|
||||
const thisMonthStr = thisMonth.toISOString().slice(0, 7)
|
||||
|
||||
const monthExpenses = data.transactions.filter((t) => t.date.startsWith(thisMonthStr) && t.amount < 0)
|
||||
|
||||
const categoryTotals = new Map<string, number>()
|
||||
|
||||
monthExpenses.forEach((t) => {
|
||||
const catId = t.categoryId || "uncategorized"
|
||||
const current = categoryTotals.get(catId) || 0
|
||||
categoryTotals.set(catId, current + Math.abs(t.amount))
|
||||
})
|
||||
|
||||
const chartData = Array.from(categoryTotals.entries())
|
||||
.map(([categoryId, total]) => {
|
||||
const category = data.categories.find((c) => c.id === categoryId)
|
||||
return {
|
||||
name: category?.name || "Non catégorisé",
|
||||
value: total,
|
||||
color: category?.color || "#94a3b8",
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 6)
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dépenses par catégorie</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<p className="text-muted-foreground">Pas de données ce mois-ci</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dépenses par catégorie</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[250px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(var(--card))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Legend formatter={(value) => <span className="text-sm text-foreground">{value}</span>} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
96
components/dashboard/overview-cards.tsx
Normal file
96
components/dashboard/overview-cards.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { TrendingUp, TrendingDown, Wallet, CreditCard } from "lucide-react"
|
||||
import type { BankingData } from "@/lib/types"
|
||||
|
||||
interface OverviewCardsProps {
|
||||
data: BankingData
|
||||
}
|
||||
|
||||
export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
const totalBalance = data.accounts.reduce((sum, acc) => sum + acc.balance, 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 ? Math.round((reconciled / total) * 100) : 0
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Solde Total</CardTitle>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={cn("text-2xl font-bold", totalBalance >= 0 ? "text-emerald-600" : "text-red-600")}>
|
||||
{formatCurrency(totalBalance)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{data.accounts.length} compte{data.accounts.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Revenus du mois</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-emerald-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-emerald-600">{formatCurrency(income)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{monthTransactions.filter((t) => t.amount > 0).length} opération
|
||||
{monthTransactions.filter((t) => t.amount > 0).length > 1 ? "s" : ""}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Dépenses du mois</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{formatCurrency(expenses)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{monthTransactions.filter((t) => t.amount < 0).length} opération
|
||||
{monthTransactions.filter((t) => t.amount < 0).length > 1 ? "s" : ""}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Pointage</CardTitle>
|
||||
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{reconciledPercent}%</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{reconciled} / {total} opérations pointées
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
114
components/dashboard/recent-transactions.tsx
Normal file
114
components/dashboard/recent-transactions.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { CheckCircle2, Circle } from "lucide-react"
|
||||
import type { BankingData } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface RecentTransactionsProps {
|
||||
data: BankingData
|
||||
}
|
||||
|
||||
export function RecentTransactions({ data }: RecentTransactionsProps) {
|
||||
const recentTransactions = [...data.transactions]
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.slice(0, 10)
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
})
|
||||
}
|
||||
|
||||
const getCategory = (categoryId: string | null) => {
|
||||
if (!categoryId) return null
|
||||
return data.categories.find((c) => c.id === categoryId)
|
||||
}
|
||||
|
||||
const getAccount = (accountId: string) => {
|
||||
return data.accounts.find((a) => a.id === accountId)
|
||||
}
|
||||
|
||||
if (recentTransactions.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transactions récentes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<p className="text-muted-foreground">Aucune transaction</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Importez un fichier OFX pour commencer</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transactions récentes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentTransactions.map((transaction) => {
|
||||
const category = getCategory(transaction.categoryId)
|
||||
const account = getAccount(transaction.accountId)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={transaction.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-muted/50 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{transaction.isReconciled ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-600" />
|
||||
) : (
|
||||
<Circle className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{transaction.description}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-muted-foreground">{formatDate(transaction.date)}</span>
|
||||
{account && <span className="text-xs text-muted-foreground">• {account.name}</span>}
|
||||
{category && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
style={{ backgroundColor: `${category.color}20`, color: category.color }}
|
||||
>
|
||||
{category.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"font-semibold tabular-nums",
|
||||
transaction.amount >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
{formatCurrency(transaction.amount)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
81
components/dashboard/sidebar.tsx
Normal file
81
components/dashboard/sidebar.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Wallet,
|
||||
FolderTree,
|
||||
Tags,
|
||||
BarChart3,
|
||||
Upload,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
} from "lucide-react"
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", label: "Tableau de bord", icon: LayoutDashboard },
|
||||
{ href: "/accounts", label: "Comptes", icon: Wallet },
|
||||
{ href: "/folders", label: "Organisation", icon: FolderTree },
|
||||
{ href: "/transactions", label: "Transactions", icon: Upload },
|
||||
{ href: "/categories", label: "Catégories", icon: Tags },
|
||||
{ href: "/statistics", label: "Statistiques", icon: BarChart3 },
|
||||
]
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"flex flex-col h-screen bg-card border-r border-border transition-all duration-300",
|
||||
collapsed ? "w-16" : "w-64",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
{!collapsed && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
||||
<Wallet className="w-5 h-5 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="font-semibold text-foreground">FinTrack</span>
|
||||
</div>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" onClick={() => setCollapsed(!collapsed)} className="ml-auto">
|
||||
{collapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-2 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href
|
||||
return (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Button
|
||||
variant={isActive ? "secondary" : "ghost"}
|
||||
className={cn("w-full justify-start gap-3", collapsed && "justify-center px-2")}
|
||||
>
|
||||
<item.icon className="w-5 h-5 shrink-0" />
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-2 border-t border-border">
|
||||
<Link href="/settings">
|
||||
<Button variant="ghost" className={cn("w-full justify-start gap-3", collapsed && "justify-center px-2")}>
|
||||
<Settings className="w-5 h-5 shrink-0" />
|
||||
{!collapsed && <span>Paramètres</span>}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user