diff --git a/README.md b/README.md index 785fa89..0ee3f6b 100644 --- a/README.md +++ b/README.md @@ -26,23 +26,26 @@ Application web moderne de gestion personnelle de comptes bancaires avec import ## 📋 Prérequis -- Node.js 18+ +- Node.js 18+ - pnpm (recommandé) ou npm/yarn ## 🔧 Installation 1. Clonez le dépôt : + ```bash git clone cd bank-account-management-app ``` 2. Installez les dépendances : + ```bash pnpm install ``` 3. Lancez le serveur de développement : + ```bash pnpm dev ``` @@ -99,6 +102,7 @@ L'application détecte automatiquement les doublons basés sur l'ID unique (FITI ### Catégories par défaut L'application inclut des catégories pré-configurées avec des mots-clés pour la catégorisation automatique : + - Alimentation - Transport - Logement @@ -132,6 +136,7 @@ Le thème sombre/clair peut être changé dans les paramètres. L'application d ### Structure des données Les données sont structurées comme suit : + - **Accounts** : Comptes bancaires avec solde et métadonnées - **Transactions** : Transactions avec montant, date, description, catégorie - **Folders** : Dossiers pour organiser les comptes @@ -149,4 +154,3 @@ Ce projet est en développement actif. Les suggestions et améliorations sont le --- Développé avec ❤️ en utilisant Next.js et React - diff --git a/app/accounts/page.tsx b/app/accounts/page.tsx index 17f48d6..c36b340 100644 --- a/app/accounts/page.tsx +++ b/app/accounts/page.tsx @@ -1,43 +1,68 @@ -"use client" +"use client"; -import { useState } from "react" -import { Sidebar } from "@/components/dashboard/sidebar" -import { useBankingData } from "@/lib/hooks" -import { updateAccount, deleteAccount } from "@/lib/store-db" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { MoreVertical, Pencil, Trash2, Building2, CreditCard, Wallet, PiggyBank, RefreshCw } from "lucide-react" -import type { Account } from "@/lib/types" -import { cn } from "@/lib/utils" +import { useState } from "react"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useBankingData } from "@/lib/hooks"; +import { updateAccount, deleteAccount } from "@/lib/store-db"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + MoreVertical, + Pencil, + Trash2, + Building2, + CreditCard, + Wallet, + PiggyBank, + RefreshCw, +} from "lucide-react"; +import type { Account } from "@/lib/types"; +import { cn } from "@/lib/utils"; const accountTypeIcons = { CHECKING: Wallet, SAVINGS: PiggyBank, CREDIT_CARD: CreditCard, OTHER: Building2, -} +}; const accountTypeLabels = { CHECKING: "Compte courant", SAVINGS: "Épargne", CREDIT_CARD: "Carte de crédit", OTHER: "Autre", -} +}; export default function AccountsPage() { - const { data, isLoading, refresh, update } = useBankingData() - const [editingAccount, setEditingAccount] = useState(null) - const [isDialogOpen, setIsDialogOpen] = useState(false) + const { data, isLoading, refresh, update } = useBankingData(); + const [editingAccount, setEditingAccount] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); const [formData, setFormData] = useState({ name: "", type: "CHECKING" as Account["type"], folderId: "folder-root", - }) + }); if (isLoading || !data) { return ( @@ -47,28 +72,28 @@ export default function AccountsPage() { - ) + ); } const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(amount) - } + }).format(amount); + }; const handleEdit = (account: Account) => { - setEditingAccount(account) + setEditingAccount(account); setFormData({ name: account.name, type: account.type, folderId: account.folderId || "folder-root", - }) - setIsDialogOpen(true) - } + }); + setIsDialogOpen(true); + }; const handleSave = async () => { - if (!editingAccount) return + if (!editingAccount) return; try { const updatedAccount = { @@ -76,34 +101,34 @@ export default function AccountsPage() { name: formData.name, type: formData.type, folderId: formData.folderId, - } - await updateAccount(updatedAccount) - refresh() - setIsDialogOpen(false) - setEditingAccount(null) + }; + await updateAccount(updatedAccount); + refresh(); + setIsDialogOpen(false); + setEditingAccount(null); } catch (error) { - console.error("Error updating account:", error) - alert("Erreur lors de la mise à jour du compte") + console.error("Error updating account:", error); + alert("Erreur lors de la mise à jour du compte"); } - } + }; const handleDelete = async (accountId: string) => { - if (!confirm("Supprimer ce compte et toutes ses transactions ?")) return + if (!confirm("Supprimer ce compte et toutes ses transactions ?")) return; try { - await deleteAccount(accountId) - refresh() + await deleteAccount(accountId); + refresh(); } catch (error) { - console.error("Error deleting account:", error) - alert("Erreur lors de la suppression du compte") + console.error("Error deleting account:", error); + alert("Erreur lors de la suppression du compte"); } - } + }; const getTransactionCount = (accountId: string) => { - return data.transactions.filter((t) => t.accountId === accountId).length - } + return data.transactions.filter((t) => t.accountId === accountId).length; + }; - const totalBalance = data.accounts.reduce((sum, a) => sum + a.balance, 0) + const totalBalance = data.accounts.reduce((sum, a) => sum + a.balance, 0); return (
@@ -113,11 +138,18 @@ export default function AccountsPage() {

Comptes

-

Gérez vos comptes bancaires

+

+ Gérez vos comptes bancaires +

Solde total

-

= 0 ? "text-emerald-600" : "text-red-600")}> +

= 0 ? "text-emerald-600" : "text-red-600", + )} + > {formatCurrency(totalBalance)}

@@ -129,15 +161,18 @@ export default function AccountsPage() {

Aucun compte

- Importez un fichier OFX depuis le tableau de bord pour ajouter votre premier compte. + Importez un fichier OFX depuis le tableau de bord pour ajouter + votre premier compte.

) : (
{data.accounts.map((account) => { - const Icon = accountTypeIcons[account.type] - const folder = data.folders.find((f) => f.id === account.folderId) + const Icon = accountTypeIcons[account.type]; + const folder = data.folders.find( + (f) => f.id === account.folderId, + ); return ( @@ -148,22 +183,35 @@ export default function AccountsPage() {
- {account.name} -

{accountTypeLabels[account.type]}

+ + {account.name} + +

+ {accountTypeLabels[account.type]} +

- - handleEdit(account)}> + handleEdit(account)} + > Modifier - handleDelete(account.id)} className="text-red-600"> + handleDelete(account.id)} + className="text-red-600" + > Supprimer @@ -175,23 +223,30 @@ export default function AccountsPage() {
= 0 ? "text-emerald-600" : "text-red-600", + account.balance >= 0 + ? "text-emerald-600" + : "text-red-600", )} > {formatCurrency(account.balance)}
- {getTransactionCount(account.id)} transactions + + {getTransactionCount(account.id)} transactions + {folder && {folder.name}}
{account.lastImport && (

- Dernier import: {new Date(account.lastImport).toLocaleDateString("fr-FR")} + Dernier import:{" "} + {new Date(account.lastImport).toLocaleDateString( + "fr-FR", + )}

)} - ) + ); })}
)} @@ -206,13 +261,20 @@ export default function AccountsPage() {
- setFormData({ ...formData, name: e.target.value })} /> + + setFormData({ ...formData, name: e.target.value }) + } + />
setFormData({ ...formData, folderId: v })}> + - setFormData({ ...formData, parentId: value === "none" ? null : value }) + setFormData({ + ...formData, + parentId: value === "none" ? null : value, + }) } > - Aucune (catégorie principale) + + Aucune (catégorie principale) + {parentCategories .filter((p) => p.id !== editingCategory?.id) .map((parent) => ( @@ -495,7 +624,9 @@ export default function CategoriesPage() { setFormData({ ...formData, name: e.target.value })} + onChange={(e) => + setFormData({ ...formData, name: e.target.value }) + } placeholder="Ex: Alimentation" />
@@ -510,7 +641,8 @@ export default function CategoriesPage() { onClick={() => setFormData({ ...formData, color })} className={cn( "w-8 h-8 rounded-full transition-transform", - formData.color === color && "ring-2 ring-offset-2 ring-primary scale-110" + formData.color === color && + "ring-2 ring-offset-2 ring-primary scale-110", )} style={{ backgroundColor: color }} /> @@ -526,7 +658,9 @@ export default function CategoriesPage() { value={newKeyword} onChange={(e) => setNewKeyword(e.target.value)} placeholder="Ajouter un mot-clé" - onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addKeyword())} + onKeyDown={(e) => + e.key === "Enter" && (e.preventDefault(), addKeyword()) + } />
- ) + ); } diff --git a/app/folders/page.tsx b/app/folders/page.tsx index c0fe57a..47e6bd6 100644 --- a/app/folders/page.tsx +++ b/app/folders/page.tsx @@ -1,15 +1,31 @@ -"use client" +"use client"; -import { useState } from "react" -import { Sidebar } from "@/components/dashboard/sidebar" -import { useBankingData } from "@/lib/hooks" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { useState } from "react"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useBankingData } from "@/lib/hooks"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Plus, MoreVertical, @@ -21,10 +37,15 @@ import { ChevronDown, Building2, RefreshCw, -} from "lucide-react" -import { addFolder, updateFolder, deleteFolder, updateAccount } from "@/lib/store-db" -import type { Folder as FolderType, Account } from "@/lib/types" -import { cn } from "@/lib/utils" +} from "lucide-react"; +import { + addFolder, + updateFolder, + deleteFolder, + updateAccount, +} from "@/lib/store-db"; +import type { Folder as FolderType, Account } from "@/lib/types"; +import { cn } from "@/lib/utils"; const folderColors = [ { value: "#6366f1", label: "Indigo" }, @@ -33,17 +54,17 @@ const folderColors = [ { value: "#ec4899", label: "Rose" }, { value: "#3b82f6", label: "Bleu" }, { value: "#ef4444", label: "Rouge" }, -] +]; interface FolderTreeItemProps { - folder: FolderType - accounts: Account[] - allFolders: FolderType[] - level: number - onEdit: (folder: FolderType) => void - onDelete: (folderId: string) => void - onEditAccount: (account: Account) => void - formatCurrency: (amount: number) => string + folder: FolderType; + accounts: Account[]; + allFolders: FolderType[]; + level: number; + onEdit: (folder: FolderType) => void; + onDelete: (folderId: string) => void; + onEditAccount: (account: Account) => void; + formatCurrency: (amount: number) => string; } function FolderTreeItem({ @@ -56,20 +77,27 @@ function FolderTreeItem({ onEditAccount, formatCurrency, }: FolderTreeItemProps) { - const [isExpanded, setIsExpanded] = useState(true) + const [isExpanded, setIsExpanded] = useState(true); // Pour le dossier "Mes Comptes" (folder-root), inclure aussi les comptes sans dossier - const folderAccounts = accounts.filter((a) => - a.folderId === folder.id || (folder.id === "folder-root" && a.folderId === null) - ) - const childFolders = allFolders.filter((f) => f.parentId === folder.id) - const hasChildren = childFolders.length > 0 || folderAccounts.length > 0 + const folderAccounts = accounts.filter( + (a) => + a.folderId === folder.id || + (folder.id === "folder-root" && a.folderId === null), + ); + const childFolders = allFolders.filter((f) => f.parentId === folder.id); + const hasChildren = childFolders.length > 0 || folderAccounts.length > 0; - const folderTotal = folderAccounts.reduce((sum, a) => sum + a.balance, 0) + const folderTotal = folderAccounts.reduce((sum, a) => sum + a.balance, 0); return (
-
0 && "ml-6")}> +
0 && "ml-6", + )} + > @@ -119,7 +154,10 @@ function FolderTreeItem({ Modifier {folder.id !== "folder-root" && ( - onDelete(folder.id)} className="text-red-600"> + onDelete(folder.id)} + className="text-red-600" + > Supprimer @@ -131,15 +169,26 @@ function FolderTreeItem({ {isExpanded && (
{folderAccounts.map((account) => ( -
+
{account.name} - = 0 ? "text-emerald-600" : "text-red-600")}> + = 0 ? "text-emerald-600" : "text-red-600", + )} + > {formatCurrency(account.balance)} -
)}
- ) + ); } const accountTypeLabels = { @@ -172,26 +221,26 @@ const accountTypeLabels = { SAVINGS: "Épargne", CREDIT_CARD: "Carte de crédit", OTHER: "Autre", -} +}; export default function FoldersPage() { - const { data, isLoading, refresh } = useBankingData() - const [isDialogOpen, setIsDialogOpen] = useState(false) - const [editingFolder, setEditingFolder] = useState(null) + const { data, isLoading, refresh } = useBankingData(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingFolder, setEditingFolder] = useState(null); const [formData, setFormData] = useState({ name: "", parentId: "folder-root" as string | null, color: "#6366f1", - }) - + }); + // Account editing state - const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false) - const [editingAccount, setEditingAccount] = useState(null) + const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false); + const [editingAccount, setEditingAccount] = useState(null); const [accountFormData, setAccountFormData] = useState({ name: "", type: "CHECKING" as Account["type"], folderId: "folder-root", - }) + }); if (isLoading || !data) { return ( @@ -201,36 +250,37 @@ export default function FoldersPage() {
- ) + ); } const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(amount) - } + }).format(amount); + }; - const rootFolders = data.folders.filter((f) => f.parentId === null) + const rootFolders = data.folders.filter((f) => f.parentId === null); const handleNewFolder = () => { - setEditingFolder(null) - setFormData({ name: "", parentId: "folder-root", color: "#6366f1" }) - setIsDialogOpen(true) - } + setEditingFolder(null); + setFormData({ name: "", parentId: "folder-root", color: "#6366f1" }); + setIsDialogOpen(true); + }; const handleEdit = (folder: FolderType) => { - setEditingFolder(folder) + setEditingFolder(folder); setFormData({ name: folder.name, parentId: folder.parentId || "folder-root", color: folder.color, - }) - setIsDialogOpen(true) - } + }); + setIsDialogOpen(true); + }; const handleSave = async () => { - const parentId = formData.parentId === "folder-root" ? null : formData.parentId + const parentId = + formData.parentId === "folder-root" ? null : formData.parentId; try { if (editingFolder) { @@ -239,63 +289,71 @@ export default function FoldersPage() { name: formData.name, parentId, color: formData.color, - }) + }); } else { await addFolder({ name: formData.name, parentId, color: formData.color, icon: "folder", - }) + }); } - refresh() - setIsDialogOpen(false) + refresh(); + setIsDialogOpen(false); } catch (error) { - console.error("Error saving folder:", error) - alert("Erreur lors de la sauvegarde du dossier") + console.error("Error saving folder:", error); + alert("Erreur lors de la sauvegarde du dossier"); } - } + }; const handleDelete = async (folderId: string) => { - if (!confirm("Supprimer ce dossier ? Les comptes seront déplacés à la racine.")) return + if ( + !confirm( + "Supprimer ce dossier ? Les comptes seront déplacés à la racine.", + ) + ) + return; try { - await deleteFolder(folderId) - refresh() + await deleteFolder(folderId); + refresh(); } catch (error) { - console.error("Error deleting folder:", error) - alert("Erreur lors de la suppression du dossier") + console.error("Error deleting folder:", error); + alert("Erreur lors de la suppression du dossier"); } - } + }; const handleEditAccount = (account: Account) => { - setEditingAccount(account) + setEditingAccount(account); setAccountFormData({ name: account.name, type: account.type, folderId: account.folderId || "folder-root", - }) - setIsAccountDialogOpen(true) - } + }); + setIsAccountDialogOpen(true); + }; const handleSaveAccount = async () => { - if (!editingAccount) return + if (!editingAccount) return; try { await updateAccount({ ...editingAccount, name: accountFormData.name, type: accountFormData.type, - folderId: accountFormData.folderId === "folder-root" ? null : accountFormData.folderId, - }) - refresh() - setIsAccountDialogOpen(false) - setEditingAccount(null) + folderId: + accountFormData.folderId === "folder-root" + ? null + : accountFormData.folderId, + }); + refresh(); + setIsAccountDialogOpen(false); + setEditingAccount(null); } catch (error) { - console.error("Error updating account:", error) - alert("Erreur lors de la mise à jour du compte") + console.error("Error updating account:", error); + alert("Erreur lors de la mise à jour du compte"); } - } + }; return (
@@ -304,8 +362,12 @@ export default function FoldersPage() {
-

Organisation

-

Organisez vos comptes en dossiers

+

+ Organisation +

+

+ Organisez vos comptes en dossiers +

@@ -460,5 +547,5 @@ export default function FoldersPage() {
- ) + ); } diff --git a/app/globals.css b/app/globals.css index dc2aea1..840b33f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,5 @@ -@import 'tailwindcss'; -@import 'tw-animate-css'; +@import "tailwindcss"; +@import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); @@ -75,8 +75,8 @@ } @theme inline { - --font-sans: 'Geist', 'Geist Fallback'; - --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --font-sans: "Geist", "Geist Fallback"; + --font-mono: "Geist Mono", "Geist Mono Fallback"; --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); diff --git a/app/layout.tsx b/app/layout.tsx index 7291846..b12000a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,25 +1,26 @@ -import type React from "react" -import type { Metadata } from "next" -import { Geist, Geist_Mono } from "next/font/google" -import "./globals.css" +import type React from "react"; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; -const _geist = Geist({ subsets: ["latin"] }) -const _geistMono = Geist_Mono({ subsets: ["latin"] }) +const _geist = Geist({ subsets: ["latin"] }); +const _geistMono = Geist_Mono({ subsets: ["latin"] }); export const metadata: Metadata = { title: "FinTrack - Gestion de compte bancaire", - description: "Application de gestion personnelle de comptes bancaires avec import OFX", - generator: 'v0.app' -} + description: + "Application de gestion personnelle de comptes bancaires avec import OFX", + generator: "v0.app", +}; export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { return ( {children} - ) + ); } diff --git a/app/page.tsx b/app/page.tsx index 8cd3c20..2553b4f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,17 +1,17 @@ -"use client" +"use client"; -import { Sidebar } from "@/components/dashboard/sidebar" -import { OverviewCards } from "@/components/dashboard/overview-cards" -import { RecentTransactions } from "@/components/dashboard/recent-transactions" -import { AccountsSummary } from "@/components/dashboard/accounts-summary" -import { CategoryBreakdown } from "@/components/dashboard/category-breakdown" -import { OFXImportDialog } from "@/components/import/ofx-import-dialog" -import { useBankingData } from "@/lib/hooks" -import { Button } from "@/components/ui/button" -import { Upload, RefreshCw } from "lucide-react" +import { Sidebar } from "@/components/dashboard/sidebar"; +import { OverviewCards } from "@/components/dashboard/overview-cards"; +import { RecentTransactions } from "@/components/dashboard/recent-transactions"; +import { AccountsSummary } from "@/components/dashboard/accounts-summary"; +import { CategoryBreakdown } from "@/components/dashboard/category-breakdown"; +import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; +import { useBankingData } from "@/lib/hooks"; +import { Button } from "@/components/ui/button"; +import { Upload, RefreshCw } from "lucide-react"; export default function DashboardPage() { - const { data, isLoading, refresh } = useBankingData() + const { data, isLoading, refresh } = useBankingData(); if (isLoading || !data) { return ( @@ -21,7 +21,7 @@ export default function DashboardPage() {
- ) + ); } return ( @@ -31,8 +31,12 @@ export default function DashboardPage() {
-

Tableau de bord

-

Vue d'ensemble de vos finances

+

+ Tableau de bord +

+

+ Vue d'ensemble de vos finances +

- ) + ); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index d4b7ab6..7947fc2 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,10 +1,16 @@ -"use client" +"use client"; -import { useState } from "react" -import { Sidebar } from "@/components/dashboard/sidebar" -import { useBankingData } from "@/lib/hooks" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { useState } from "react"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useBankingData } from "@/lib/hooks"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { AlertDialog, AlertDialogAction, @@ -15,13 +21,21 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, -} from "@/components/ui/alert-dialog" -import { Download, Trash2, Upload, RefreshCw, Database, FileJson, Tags } from "lucide-react" -import type { BankingData } from "@/lib/types" +} from "@/components/ui/alert-dialog"; +import { + Download, + Trash2, + Upload, + RefreshCw, + Database, + FileJson, + Tags, +} from "lucide-react"; +import type { BankingData } from "@/lib/types"; export default function SettingsPage() { - const { data, isLoading, refresh, update } = useBankingData() - const [importing, setImporting] = useState(false) + const { data, isLoading, refresh, update } = useBankingData(); + const [importing, setImporting] = useState(false); if (isLoading || !data) { return ( @@ -31,72 +45,80 @@ export default function SettingsPage() {
- ) + ); } const exportData = () => { - const dataStr = JSON.stringify(data, null, 2) - const blob = new Blob([dataStr], { type: "application/json" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = `fintrack-backup-${new Date().toISOString().split("T")[0]}.json` - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - } + const dataStr = JSON.stringify(data, null, 2); + const blob = new Blob([dataStr], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `fintrack-backup-${new Date().toISOString().split("T")[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; const importData = () => { - const input = document.createElement("input") - input.type = "file" - input.accept = ".json" + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0] - if (!file) return + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; - setImporting(true) + setImporting(true); try { - const content = await file.text() - const importedData = JSON.parse(content) as BankingData + const content = await file.text(); + const importedData = JSON.parse(content) as BankingData; // Validate structure - if (!importedData.accounts || !importedData.transactions || !importedData.categories || !importedData.folders) { - alert("Format de fichier invalide") - return + if ( + !importedData.accounts || + !importedData.transactions || + !importedData.categories || + !importedData.folders + ) { + alert("Format de fichier invalide"); + return; } - update(importedData) - alert("Données importées avec succès") + update(importedData); + alert("Données importées avec succès"); } catch (error) { - alert("Erreur lors de l'import") + alert("Erreur lors de l'import"); } finally { - setImporting(false) + setImporting(false); } - } - input.click() - } + }; + input.click(); + }; const resetData = () => { - localStorage.removeItem("banking-app-data") - window.location.reload() - } + localStorage.removeItem("banking-app-data"); + window.location.reload(); + }; const clearAllCategories = async () => { try { - const response = await fetch("/api/banking/transactions/clear-categories", { - method: "POST", - }) - if (!response.ok) throw new Error("Erreur") - refresh() - alert("Catégories supprimées de toutes les opérations") + const response = await fetch( + "/api/banking/transactions/clear-categories", + { + method: "POST", + }, + ); + if (!response.ok) throw new Error("Erreur"); + refresh(); + alert("Catégories supprimées de toutes les opérations"); } catch (error) { - console.error(error) - alert("Erreur lors de la suppression des catégories") + console.error(error); + alert("Erreur lors de la suppression des catégories"); } - } + }; - const categorizedCount = data.transactions.filter((t) => t.categoryId).length + const categorizedCount = data.transactions.filter((t) => t.categoryId).length; return (
@@ -105,7 +127,9 @@ export default function SettingsPage() {

Paramètres

-

Gérez vos données et préférences

+

+ Gérez vos données et préférences +

@@ -114,25 +138,37 @@ export default function SettingsPage() { Données - Exportez ou importez vos données pour les sauvegarder ou les transférer + + Exportez ou importez vos données pour les sauvegarder ou les + transférer +

Statistiques

- {data.accounts.length} comptes, {data.transactions.length} transactions, {data.categories.length}{" "} - catégories + {data.accounts.length} comptes, {data.transactions.length}{" "} + transactions, {data.categories.length} catégories

- - @@ -146,31 +182,45 @@ export default function SettingsPage() { Zone dangereuse - Actions irréversibles - procédez avec prudence + + Actions irréversibles - procédez avec prudence + {/* Supprimer catégories des opérations */} - - Supprimer toutes les catégories ? + + Supprimer toutes les catégories ? + - Cette action va retirer la catégorie de {categorizedCount} opération{categorizedCount > 1 ? "s" : ""}. - Les catégories elles-mêmes ne seront pas supprimées, seulement leur affectation aux opérations. + Cette action va retirer la catégorie de {categorizedCount}{" "} + opération{categorizedCount > 1 ? "s" : ""}. Les catégories + elles-mêmes ne seront pas supprimées, seulement leur + affectation aux opérations. Annuler - + Supprimer les affectations @@ -180,7 +230,10 @@ export default function SettingsPage() { {/* Réinitialiser toutes les données */} - @@ -189,13 +242,17 @@ export default function SettingsPage() { Êtes-vous sûr ? - Cette action supprimera définitivement tous vos comptes, transactions, catégories et dossiers. - Cette action est irréversible. + Cette action supprimera définitivement tous vos comptes, + transactions, catégories et dossiers. Cette action est + irréversible. Annuler - + Supprimer tout @@ -210,17 +267,21 @@ export default function SettingsPage() { Format OFX - Informations sur l'import de fichiers + + Informations sur l'import de fichiers +

- L'application accepte les fichiers au format OFX (Open Financial Exchange) ou QFX. Ces fichiers sont - généralement disponibles depuis l'espace client de votre banque. + L'application accepte les fichiers au format OFX (Open + Financial Exchange) ou QFX. Ces fichiers sont généralement + disponibles depuis l'espace client de votre banque.

- Lors de l'import, les transactions sont automatiquement catégorisées selon les mots-clés définis. Les - doublons sont détectés et ignorés automatiquement. + Lors de l'import, les transactions sont automatiquement + catégorisées selon les mots-clés définis. Les doublons sont + détectés et ignorés automatiquement.

@@ -228,5 +289,5 @@ export default function SettingsPage() {
- ) + ); } diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 8500724..31e8678 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -1,12 +1,18 @@ -"use client" +"use client"; -import { useState, useMemo } from "react" -import { Sidebar } from "@/components/dashboard/sidebar" -import { useBankingData } from "@/lib/hooks" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { RefreshCw, TrendingUp, TrendingDown, ArrowRight } from "lucide-react" -import { CategoryIcon } from "@/components/ui/category-icon" +import { useState, useMemo } from "react"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useBankingData } from "@/lib/hooks"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { RefreshCw, TrendingUp, TrendingDown, ArrowRight } from "lucide-react"; +import { CategoryIcon } from "@/components/ui/category-icon"; import { BarChart, Bar, @@ -21,111 +27,130 @@ import { LineChart, Line, Legend, -} from "recharts" -import { cn } from "@/lib/utils" +} from "recharts"; +import { cn } from "@/lib/utils"; -type Period = "3months" | "6months" | "12months" | "all" +type Period = "3months" | "6months" | "12months" | "all"; export default function StatisticsPage() { - const { data, isLoading } = useBankingData() - const [period, setPeriod] = useState("6months") - const [selectedAccount, setSelectedAccount] = useState("all") + const { data, isLoading } = useBankingData(); + const [period, setPeriod] = useState("6months"); + const [selectedAccount, setSelectedAccount] = useState("all"); const stats = useMemo(() => { - if (!data) return null + if (!data) return null; - const now = new Date() - let startDate: Date + const now = new Date(); + let startDate: Date; switch (period) { case "3months": - startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1) - break + startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1); + break; case "6months": - startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1) - break + startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1); + break; case "12months": - startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1) - break + startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1); + break; default: - startDate = new Date(0) + startDate = new Date(0); } - let transactions = data.transactions.filter((t) => new Date(t.date) >= startDate) + let transactions = data.transactions.filter( + (t) => new Date(t.date) >= startDate, + ); if (selectedAccount !== "all") { - transactions = transactions.filter((t) => t.accountId === selectedAccount) + transactions = transactions.filter( + (t) => t.accountId === selectedAccount, + ); } // Monthly breakdown - const monthlyData = new Map() + const monthlyData = new Map(); transactions.forEach((t) => { - const monthKey = t.date.substring(0, 7) - const current = monthlyData.get(monthKey) || { income: 0, expenses: 0 } + const monthKey = t.date.substring(0, 7); + const current = monthlyData.get(monthKey) || { income: 0, expenses: 0 }; if (t.amount >= 0) { - current.income += t.amount + current.income += t.amount; } else { - current.expenses += Math.abs(t.amount) + current.expenses += Math.abs(t.amount); } - monthlyData.set(monthKey, current) - }) + monthlyData.set(monthKey, current); + }); const monthlyChartData = Array.from(monthlyData.entries()) .sort((a, b) => a[0].localeCompare(b[0])) .map(([month, values]) => ({ - month: new Date(month + "-01").toLocaleDateString("fr-FR", { month: "short", year: "2-digit" }), + month: new Date(month + "-01").toLocaleDateString("fr-FR", { + month: "short", + year: "2-digit", + }), revenus: Math.round(values.income), depenses: Math.round(values.expenses), solde: Math.round(values.income - values.expenses), - })) + })); // Category breakdown (expenses only) - const categoryTotals = new Map() + const categoryTotals = new Map(); transactions .filter((t) => t.amount < 0) .forEach((t) => { - const catId = t.categoryId || "uncategorized" - const current = categoryTotals.get(catId) || 0 - categoryTotals.set(catId, current + Math.abs(t.amount)) - }) + const catId = t.categoryId || "uncategorized"; + const current = categoryTotals.get(catId) || 0; + categoryTotals.set(catId, current + Math.abs(t.amount)); + }); const categoryChartData = Array.from(categoryTotals.entries()) .map(([categoryId, total]) => { - const category = data.categories.find((c) => c.id === categoryId) + const category = data.categories.find((c) => c.id === categoryId); return { name: category?.name || "Non catégorisé", value: Math.round(total), color: category?.color || "#94a3b8", - } + }; }) .sort((a, b) => b.value - a.value) - .slice(0, 8) + .slice(0, 8); // Top expenses const topExpenses = transactions .filter((t) => t.amount < 0) .sort((a, b) => a.amount - b.amount) - .slice(0, 5) + .slice(0, 5); // Summary - const totalIncome = transactions.filter((t) => t.amount >= 0).reduce((sum, t) => sum + t.amount, 0) - const totalExpenses = transactions.filter((t) => t.amount < 0).reduce((sum, t) => sum + Math.abs(t.amount), 0) - const avgMonthlyExpenses = monthlyData.size > 0 ? totalExpenses / monthlyData.size : 0 + const totalIncome = transactions + .filter((t) => t.amount >= 0) + .reduce((sum, t) => sum + t.amount, 0); + const totalExpenses = transactions + .filter((t) => t.amount < 0) + .reduce((sum, t) => sum + Math.abs(t.amount), 0); + const avgMonthlyExpenses = + monthlyData.size > 0 ? totalExpenses / monthlyData.size : 0; // Balance evolution - const sortedTransactions = [...transactions].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + const sortedTransactions = [...transactions].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); - let runningBalance = 0 - const balanceByDate = new Map() + let runningBalance = 0; + const balanceByDate = new Map(); sortedTransactions.forEach((t) => { - runningBalance += t.amount - balanceByDate.set(t.date, runningBalance) - }) + runningBalance += t.amount; + balanceByDate.set(t.date, runningBalance); + }); - const balanceChartData = Array.from(balanceByDate.entries()).map(([date, balance]) => ({ - date: new Date(date).toLocaleDateString("fr-FR", { day: "2-digit", month: "short" }), - solde: Math.round(balance), - })) + const balanceChartData = Array.from(balanceByDate.entries()).map( + ([date, balance]) => ({ + date: new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "short", + }), + solde: Math.round(balance), + }), + ); return { monthlyChartData, @@ -136,8 +161,8 @@ export default function StatisticsPage() { avgMonthlyExpenses, balanceChartData, transactionCount: transactions.length, - } - }, [data, period, selectedAccount]) + }; + }, [data, period, selectedAccount]); if (isLoading || !data || !stats) { return ( @@ -147,15 +172,15 @@ export default function StatisticsPage() {
- ) + ); } const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(amount) - } + }).format(amount); + }; return (
@@ -164,11 +189,18 @@ export default function StatisticsPage() {
-

Statistiques

-

Analysez vos dépenses et revenus

+

+ Statistiques +

+

+ Analysez vos dépenses et revenus +

- @@ -181,7 +213,10 @@ export default function StatisticsPage() { ))} - setPeriod(v as Period)} + > @@ -205,7 +240,9 @@ export default function StatisticsPage() { -
{formatCurrency(stats.totalIncome)}
+
+ {formatCurrency(stats.totalIncome)} +
@@ -217,7 +254,9 @@ export default function StatisticsPage() { -
{formatCurrency(stats.totalExpenses)}
+
+ {formatCurrency(stats.totalExpenses)} +
@@ -229,19 +268,25 @@ export default function StatisticsPage() { -
{formatCurrency(stats.avgMonthlyExpenses)}
+
+ {formatCurrency(stats.avgMonthlyExpenses)} +
- Économies + + Économies +
= 0 ? "text-emerald-600" : "text-red-600", + stats.totalIncome - stats.totalExpenses >= 0 + ? "text-emerald-600" + : "text-red-600", )} > {formatCurrency(stats.totalIncome - stats.totalExpenses)} @@ -262,9 +307,15 @@ export default function StatisticsPage() {
- + - `${v}€`} /> + `${v}€`} + /> formatCurrency(value)} contentStyle={{ @@ -274,8 +325,16 @@ export default function StatisticsPage() { }} /> - - + +
@@ -318,7 +377,13 @@ export default function StatisticsPage() { borderRadius: "8px", }} /> - {value}} /> + ( + + {value} + + )} + />
@@ -340,9 +405,19 @@ export default function StatisticsPage() {
- - - `${v}€`} /> + + + `${v}€`} + /> formatCurrency(value)} contentStyle={{ @@ -351,7 +426,13 @@ export default function StatisticsPage() { borderRadius: "8px", }} /> - +
@@ -372,24 +453,40 @@ export default function StatisticsPage() { {stats.topExpenses.length > 0 ? (
{stats.topExpenses.map((expense, index) => { - const category = data.categories.find((c) => c.id === expense.categoryId) + const category = data.categories.find( + (c) => c.id === expense.categoryId, + ); return ( -
+
{index + 1}
-

{expense.description}

+

+ {expense.description} +

- {new Date(expense.date).toLocaleDateString("fr-FR")} + {new Date(expense.date).toLocaleDateString( + "fr-FR", + )} {category && ( - + {category.name} )} @@ -399,7 +496,7 @@ export default function StatisticsPage() { {formatCurrency(expense.amount)}
- ) + ); })}
) : ( @@ -413,5 +510,5 @@ export default function StatisticsPage() {
- ) + ); } diff --git a/app/transactions/loading.tsx b/app/transactions/loading.tsx index f15322a..4349ac3 100644 --- a/app/transactions/loading.tsx +++ b/app/transactions/loading.tsx @@ -1,3 +1,3 @@ export default function Loading() { - return null + return null; } diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index a5b8d49..66499ba 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -1,91 +1,125 @@ -"use client" +"use client"; -import { useState, useMemo } from "react" -import { Sidebar } from "@/components/dashboard/sidebar" -import { useBankingData } from "@/lib/hooks" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Badge } from "@/components/ui/badge" -import { Checkbox } from "@/components/ui/checkbox" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { useState, useMemo } from "react"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useBankingData } from "@/lib/hooks"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu" -import { OFXImportDialog } from "@/components/import/ofx-import-dialog" -import { CategoryIcon } from "@/components/ui/category-icon" -import { Search, CheckCircle2, Circle, MoreVertical, Tags, Upload, RefreshCw, ArrowUpDown, Check } from "lucide-react" -import { cn } from "@/lib/utils" +} from "@/components/ui/dropdown-menu"; +import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import { + Search, + CheckCircle2, + Circle, + MoreVertical, + Tags, + Upload, + RefreshCw, + ArrowUpDown, + Check, +} from "lucide-react"; +import { cn } from "@/lib/utils"; -type SortField = "date" | "amount" | "description" -type SortOrder = "asc" | "desc" +type SortField = "date" | "amount" | "description"; +type SortOrder = "asc" | "desc"; export default function TransactionsPage() { - const { data, isLoading, refresh, update } = useBankingData() - const [searchQuery, setSearchQuery] = useState("") - const [selectedAccount, setSelectedAccount] = useState("all") - const [selectedCategory, setSelectedCategory] = useState("all") - const [showReconciled, setShowReconciled] = useState("all") - const [sortField, setSortField] = useState("date") - const [sortOrder, setSortOrder] = useState("desc") - const [selectedTransactions, setSelectedTransactions] = useState>(new Set()) + const { data, isLoading, refresh, update } = useBankingData(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedAccount, setSelectedAccount] = useState("all"); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [showReconciled, setShowReconciled] = useState("all"); + const [sortField, setSortField] = useState("date"); + const [sortOrder, setSortOrder] = useState("desc"); + const [selectedTransactions, setSelectedTransactions] = useState>( + new Set(), + ); const filteredTransactions = useMemo(() => { - if (!data) return [] + if (!data) return []; - let transactions = [...data.transactions] + let transactions = [...data.transactions]; // Filter by search if (searchQuery) { - const query = searchQuery.toLowerCase() + const query = searchQuery.toLowerCase(); transactions = transactions.filter( - (t) => t.description.toLowerCase().includes(query) || t.memo?.toLowerCase().includes(query), - ) + (t) => + t.description.toLowerCase().includes(query) || + t.memo?.toLowerCase().includes(query), + ); } // Filter by account if (selectedAccount !== "all") { - transactions = transactions.filter((t) => t.accountId === selectedAccount) + transactions = transactions.filter( + (t) => t.accountId === selectedAccount, + ); } // Filter by category if (selectedCategory !== "all") { if (selectedCategory === "uncategorized") { - transactions = transactions.filter((t) => !t.categoryId) + transactions = transactions.filter((t) => !t.categoryId); } else { - transactions = transactions.filter((t) => t.categoryId === selectedCategory) + transactions = transactions.filter( + (t) => t.categoryId === selectedCategory, + ); } } // Filter by reconciliation status if (showReconciled !== "all") { - const isReconciled = showReconciled === "reconciled" - transactions = transactions.filter((t) => t.isReconciled === isReconciled) + const isReconciled = showReconciled === "reconciled"; + transactions = transactions.filter( + (t) => t.isReconciled === isReconciled, + ); } // Sort transactions.sort((a, b) => { - let comparison = 0 + let comparison = 0; switch (sortField) { case "date": - comparison = new Date(a.date).getTime() - new Date(b.date).getTime() - break + comparison = new Date(a.date).getTime() - new Date(b.date).getTime(); + break; case "amount": - comparison = a.amount - b.amount - break + comparison = a.amount - b.amount; + break; case "description": - comparison = a.description.localeCompare(b.description) - break + comparison = a.description.localeCompare(b.description); + break; } - return sortOrder === "asc" ? comparison : -comparison - }) + return sortOrder === "asc" ? comparison : -comparison; + }); - return transactions - }, [data, searchQuery, selectedAccount, selectedCategory, showReconciled, sortField, sortOrder]) + return transactions; + }, [ + data, + searchQuery, + selectedAccount, + selectedCategory, + showReconciled, + sortField, + sortOrder, + ]); if (isLoading || !data) { return ( @@ -95,78 +129,80 @@ export default function TransactionsPage() {
- ) + ); } const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(amount) - } + }).format(amount); + }; const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric", - }) - } + }); + }; const toggleReconciled = (transactionId: string) => { const updatedTransactions = data.transactions.map((t) => t.id === transactionId ? { ...t, isReconciled: !t.isReconciled } : t, - ) - update({ ...data, transactions: updatedTransactions }) - } + ); + update({ ...data, transactions: updatedTransactions }); + }; const setCategory = (transactionId: string, categoryId: string | null) => { - const updatedTransactions = data.transactions.map((t) => (t.id === transactionId ? { ...t, categoryId } : t)) - update({ ...data, transactions: updatedTransactions }) - } + const updatedTransactions = data.transactions.map((t) => + t.id === transactionId ? { ...t, categoryId } : t, + ); + update({ ...data, transactions: updatedTransactions }); + }; const bulkReconcile = (reconciled: boolean) => { const updatedTransactions = data.transactions.map((t) => selectedTransactions.has(t.id) ? { ...t, isReconciled: reconciled } : t, - ) - update({ ...data, transactions: updatedTransactions }) - setSelectedTransactions(new Set()) - } + ); + update({ ...data, transactions: updatedTransactions }); + setSelectedTransactions(new Set()); + }; const bulkSetCategory = (categoryId: string | null) => { const updatedTransactions = data.transactions.map((t) => selectedTransactions.has(t.id) ? { ...t, categoryId } : t, - ) - update({ ...data, transactions: updatedTransactions }) - setSelectedTransactions(new Set()) - } + ); + update({ ...data, transactions: updatedTransactions }); + setSelectedTransactions(new Set()); + }; const toggleSelectAll = () => { if (selectedTransactions.size === filteredTransactions.length) { - setSelectedTransactions(new Set()) + setSelectedTransactions(new Set()); } else { - setSelectedTransactions(new Set(filteredTransactions.map((t) => t.id))) + setSelectedTransactions(new Set(filteredTransactions.map((t) => t.id))); } - } + }; const toggleSelectTransaction = (id: string) => { - const newSelected = new Set(selectedTransactions) + const newSelected = new Set(selectedTransactions); if (newSelected.has(id)) { - newSelected.delete(id) + newSelected.delete(id); } else { - newSelected.add(id) + newSelected.add(id); } - setSelectedTransactions(newSelected) - } + setSelectedTransactions(newSelected); + }; const getCategory = (categoryId: string | null) => { - if (!categoryId) return null - return data.categories.find((c) => c.id === categoryId) - } + if (!categoryId) return null; + return data.categories.find((c) => c.id === categoryId); + }; const getAccount = (accountId: string) => { - return data.accounts.find((a) => a.id === accountId) - } + return data.accounts.find((a) => a.id === accountId); + }; return (
@@ -175,9 +211,12 @@ export default function TransactionsPage() {
-

Transactions

+

+ Transactions +

- {filteredTransactions.length} transaction{filteredTransactions.length > 1 ? "s" : ""} + {filteredTransactions.length} transaction + {filteredTransactions.length > 1 ? "s" : ""}

@@ -204,7 +243,10 @@ export default function TransactionsPage() {
- @@ -218,13 +260,18 @@ export default function TransactionsPage() { - Toutes catégories - Non catégorisé + + Non catégorisé + {data.categories.map((category) => ( {category.name} @@ -233,7 +280,10 @@ export default function TransactionsPage() { - @@ -253,13 +303,22 @@ export default function TransactionsPage() {
- {selectedTransactions.size} sélectionnée{selectedTransactions.size > 1 ? "s" : ""} + {selectedTransactions.size} sélectionnée + {selectedTransactions.size > 1 ? "s" : ""} - - @@ -271,11 +330,21 @@ export default function TransactionsPage() { - bulkSetCategory(null)}>Aucune catégorie + bulkSetCategory(null)}> + Aucune catégorie + {data.categories.map((cat) => ( - bulkSetCategory(cat.id)}> - + bulkSetCategory(cat.id)} + > + {cat.name} ))} @@ -291,7 +360,9 @@ export default function TransactionsPage() { {filteredTransactions.length === 0 ? (
-

Aucune transaction trouvée

+

+ Aucune transaction trouvée +

) : (
@@ -301,7 +372,8 @@ export default function TransactionsPage() { 0 } onCheckedChange={toggleSelectAll} @@ -311,10 +383,12 @@ export default function TransactionsPage() { - Compte - Catégorie + + Compte + + + Catégorie + - Pointé + + Pointé + {filteredTransactions.map((transaction) => { - const category = getCategory(transaction.categoryId) - const account = getAccount(transaction.accountId) + const category = getCategory(transaction.categoryId); + const account = getAccount(transaction.accountId); return ( - + toggleSelectTransaction(transaction.id)} + checked={selectedTransactions.has( + transaction.id, + )} + onCheckedChange={() => + toggleSelectTransaction(transaction.id) + } /> {formatDate(transaction.date)} -

{transaction.description}

+

+ {transaction.description} +

{transaction.memo && (

{transaction.memo}

)} - {account?.name || "-"} + + {account?.name || "-"} + @@ -399,26 +494,49 @@ export default function TransactionsPage() { color: category.color, }} > - + {category.name} ) : ( - + Non catégorisé )} - setCategory(transaction.id, null)}> + + setCategory(transaction.id, null) + } + > Aucune catégorie {data.categories.map((cat) => ( - setCategory(transaction.id, cat.id)}> - + + setCategory(transaction.id, cat.id) + } + > + {cat.name} - {transaction.categoryId === cat.id && } + {transaction.categoryId === cat.id && ( + + )} ))} @@ -427,7 +545,9 @@ export default function TransactionsPage() { = 0 ? "text-emerald-600" : "text-red-600", + transaction.amount >= 0 + ? "text-emerald-600" + : "text-red-600", )} > {transaction.amount >= 0 ? "+" : ""} @@ -448,19 +568,29 @@ export default function TransactionsPage() { - - toggleReconciled(transaction.id)}> - {transaction.isReconciled ? "Dépointer" : "Pointer"} + + toggleReconciled(transaction.id) + } + > + {transaction.isReconciled + ? "Dépointer" + : "Pointer"} - ) + ); })} @@ -471,5 +601,5 @@ export default function TransactionsPage() {
- ) + ); } diff --git a/components/dashboard/accounts-summary.tsx b/components/dashboard/accounts-summary.tsx index d62a42e..e7d93c3 100644 --- a/components/dashboard/accounts-summary.tsx +++ b/components/dashboard/accounts-summary.tsx @@ -1,13 +1,13 @@ -"use client" +"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" +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 + data: BankingData; } export function AccountsSummary({ data }: AccountsSummaryProps) { @@ -15,10 +15,12 @@ export function AccountsSummary({ data }: AccountsSummaryProps) { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(amount) - } + }).format(amount); + }; - const totalPositive = data.accounts.filter((a) => a.balance > 0).reduce((sum, a) => sum + a.balance, 0) + const totalPositive = data.accounts + .filter((a) => a.balance > 0) + .reduce((sum, a) => sum + a.balance, 0); if (data.accounts.length === 0) { return ( @@ -30,11 +32,13 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {

Aucun compte

-

Importez un fichier OFX pour ajouter un compte

+

+ Importez un fichier OFX pour ajouter un compte +

- ) + ); } return ( @@ -45,7 +49,10 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
{data.accounts.map((account) => { - const percentage = totalPositive > 0 ? Math.max(0, (account.balance / totalPositive) * 100) : 0 + const percentage = + totalPositive > 0 + ? Math.max(0, (account.balance / totalPositive) * 100) + : 0; return (
@@ -57,25 +64,31 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {

{account.name}

- {account.accountNumber.slice(-4).padStart(account.accountNumber.length, "*")} + {account.accountNumber + .slice(-4) + .padStart(account.accountNumber.length, "*")}

= 0 ? "text-emerald-600" : "text-red-600", + account.balance >= 0 + ? "text-emerald-600" + : "text-red-600", )} > {formatCurrency(account.balance)}
- {account.balance > 0 && } + {account.balance > 0 && ( + + )}
- ) + ); })}
- ) + ); } diff --git a/components/dashboard/category-breakdown.tsx b/components/dashboard/category-breakdown.tsx index 1434a50..de41275 100644 --- a/components/dashboard/category-breakdown.tsx +++ b/components/dashboard/category-breakdown.tsx @@ -1,47 +1,56 @@ -"use client" +"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" +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 + 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 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 monthExpenses = data.transactions.filter( + (t) => t.date.startsWith(thisMonthStr) && t.amount < 0, + ); - const categoryTotals = new Map() + const categoryTotals = new Map(); monthExpenses.forEach((t) => { - const catId = t.categoryId || "uncategorized" - const current = categoryTotals.get(catId) || 0 - categoryTotals.set(catId, current + Math.abs(t.amount)) - }) + 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) + 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) + .slice(0, 6); const formatCurrency = (value: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(value) - } + }).format(value); + }; if (chartData.length === 0) { return ( @@ -55,7 +64,7 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
- ) + ); } return ( @@ -88,11 +97,15 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) { borderRadius: "8px", }} /> - {value}} /> + ( + {value} + )} + />
- ) + ); } diff --git a/components/dashboard/overview-cards.tsx b/components/dashboard/overview-cards.tsx index 57d74ae..42b6360 100644 --- a/components/dashboard/overview-cards.tsx +++ b/components/dashboard/overview-cards.tsx @@ -1,46 +1,60 @@ -"use client" +"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" +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 + data: BankingData; } export function OverviewCards({ data }: OverviewCardsProps) { - const totalBalance = data.accounts.reduce((sum, acc) => sum + acc.balance, 0) + 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 thisMonth = new Date(); + thisMonth.setDate(1); + const thisMonthStr = thisMonth.toISOString().slice(0, 7); - const monthTransactions = data.transactions.filter((t) => t.date.startsWith(thisMonthStr)) + 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 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 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 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) - } + }).format(amount); + }; return (
- Solde Total + + Solde Total + -
= 0 ? "text-emerald-600" : "text-red-600")}> +
= 0 ? "text-emerald-600" : "text-red-600", + )} + > {formatCurrency(totalBalance)}

@@ -51,35 +65,49 @@ export function OverviewCards({ data }: OverviewCardsProps) { - Revenus du mois + + Revenus du mois + -

{formatCurrency(income)}
+
+ {formatCurrency(income)} +

{monthTransactions.filter((t) => t.amount > 0).length} opération - {monthTransactions.filter((t) => t.amount > 0).length > 1 ? "s" : ""} + {monthTransactions.filter((t) => t.amount > 0).length > 1 + ? "s" + : ""}

- Dépenses du mois + + Dépenses du mois + -
{formatCurrency(expenses)}
+
+ {formatCurrency(expenses)} +

{monthTransactions.filter((t) => t.amount < 0).length} opération - {monthTransactions.filter((t) => t.amount < 0).length > 1 ? "s" : ""} + {monthTransactions.filter((t) => t.amount < 0).length > 1 + ? "s" + : ""}

- Pointage + + Pointage + @@ -90,7 +118,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
- ) + ); } -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; diff --git a/components/dashboard/recent-transactions.tsx b/components/dashboard/recent-transactions.tsx index ef5b792..649612a 100644 --- a/components/dashboard/recent-transactions.tsx +++ b/components/dashboard/recent-transactions.tsx @@ -1,43 +1,43 @@ -"use client" +"use client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { CheckCircle2, Circle } from "lucide-react" -import { CategoryIcon } from "@/components/ui/category-icon" -import type { BankingData } from "@/lib/types" -import { cn } from "@/lib/utils" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { CheckCircle2, Circle } from "lucide-react"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import type { BankingData } from "@/lib/types"; +import { cn } from "@/lib/utils"; interface RecentTransactionsProps { - data: BankingData + 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) + .slice(0, 10); const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", - }).format(amount) - } + }).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) - } + if (!categoryId) return null; + return data.categories.find((c) => c.id === categoryId); + }; const getAccount = (accountId: string) => { - return data.accounts.find((a) => a.id === accountId) - } + return data.accounts.find((a) => a.id === accountId); + }; if (recentTransactions.length === 0) { return ( @@ -48,11 +48,13 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {

Aucune transaction

-

Importez un fichier OFX pour commencer

+

+ Importez un fichier OFX pour commencer +

- ) + ); } return ( @@ -63,8 +65,8 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
{recentTransactions.map((transaction) => { - const category = getCategory(transaction.categoryId) - const account = getAccount(transaction.accountId) + const category = getCategory(transaction.categoryId); + const account = getAccount(transaction.accountId); return (
-

{transaction.description}

+

+ {transaction.description} +

- {formatDate(transaction.date)} - {account && • {account.name}} + + {formatDate(transaction.date)} + + {account && ( + + • {account.name} + + )} {category && ( - + {category.name} )} @@ -100,17 +117,19 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
= 0 ? "text-emerald-600" : "text-red-600", + transaction.amount >= 0 + ? "text-emerald-600" + : "text-red-600", )} > {transaction.amount >= 0 ? "+" : ""} {formatCurrency(transaction.amount)}
- ) + ); })}
- ) + ); } diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx index d9b60ef..09cfab0 100644 --- a/components/dashboard/sidebar.tsx +++ b/components/dashboard/sidebar.tsx @@ -1,10 +1,10 @@ -"use client" +"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 { 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, @@ -15,7 +15,7 @@ import { ChevronLeft, ChevronRight, Settings, -} from "lucide-react" +} from "lucide-react"; const navItems = [ { href: "/", label: "Tableau de bord", icon: LayoutDashboard }, @@ -24,11 +24,11 @@ const navItems = [ { 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) + const pathname = usePathname(); + const [collapsed, setCollapsed] = useState(false); return (
)} -
-
- ) + ); } diff --git a/components/import/ofx-import-dialog.tsx b/components/import/ofx-import-dialog.tsx index a994b7e..b8228d1 100644 --- a/components/import/ofx-import-dialog.tsx +++ b/components/import/ofx-import-dialog.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import type React from "react" +import type React from "react"; -import { useState, useCallback } from "react" -import { useDropzone } from "react-dropzone" +import { useState, useCallback } from "react"; +import { useDropzone } from "react-dropzone"; import { Dialog, DialogContent, @@ -11,78 +11,109 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Progress } from "@/components/ui/progress" -import { Upload, FileText, CheckCircle2, AlertCircle, Loader2 } from "lucide-react" -import { parseOFX } from "@/lib/ofx-parser" -import { loadData, addAccount, updateAccount, addTransactions, generateId, autoCategorize } from "@/lib/store-db" -import type { OFXAccount, Account, Transaction, Folder, BankingData } from "@/lib/types" -import { cn } from "@/lib/utils" +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Progress } from "@/components/ui/progress"; +import { + Upload, + FileText, + CheckCircle2, + AlertCircle, + Loader2, +} from "lucide-react"; +import { parseOFX } from "@/lib/ofx-parser"; +import { + loadData, + addAccount, + updateAccount, + addTransactions, + generateId, + autoCategorize, +} from "@/lib/store-db"; +import type { + OFXAccount, + Account, + Transaction, + Folder, + BankingData, +} from "@/lib/types"; +import { cn } from "@/lib/utils"; interface OFXImportDialogProps { - children: React.ReactNode - onImportComplete?: () => void + children: React.ReactNode; + onImportComplete?: () => void; } -type ImportStep = "upload" | "configure" | "importing" | "success" | "error" +type ImportStep = "upload" | "configure" | "importing" | "success" | "error"; interface ImportResult { - fileName: string - accountName: string - transactionsImported: number - isNew: boolean - error?: string + fileName: string; + accountName: string; + transactionsImported: number; + isNew: boolean; + error?: string; } -export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogProps) { - const [open, setOpen] = useState(false) - const [step, setStep] = useState("upload") - +export function OFXImportDialog({ + children, + onImportComplete, +}: OFXImportDialogProps) { + const [open, setOpen] = useState(false); + const [step, setStep] = useState("upload"); + // Single file mode - const [parsedData, setParsedData] = useState(null) - const [accountName, setAccountName] = useState("") - const [selectedFolder, setSelectedFolder] = useState("folder-root") - const [folders, setFolders] = useState([]) - const [existingAccountId, setExistingAccountId] = useState(null) - + const [parsedData, setParsedData] = useState(null); + const [accountName, setAccountName] = useState(""); + const [selectedFolder, setSelectedFolder] = useState("folder-root"); + const [folders, setFolders] = useState([]); + const [existingAccountId, setExistingAccountId] = useState( + null, + ); + // Multi-file mode - const [importResults, setImportResults] = useState([]) - const [importProgress, setImportProgress] = useState(0) - const [totalFiles, setTotalFiles] = useState(0) - - const [error, setError] = useState(null) + const [importResults, setImportResults] = useState([]); + const [importProgress, setImportProgress] = useState(0); + const [totalFiles, setTotalFiles] = useState(0); + + const [error, setError] = useState(null); // Import a single OFX file directly (for multi-file mode) const importOFXDirect = async ( parsed: OFXAccount, fileName: string, - data: BankingData + data: BankingData, ): Promise => { try { // Check if account already exists const existing = data.accounts.find( - (a) => a.accountNumber === parsed.accountId && a.bankId === parsed.bankId - ) + (a) => + a.accountNumber === parsed.accountId && a.bankId === parsed.bankId, + ); - let accountId: string - let accountName: string - let isNew = false + let accountId: string; + let accountName: string; + let isNew = false; if (existing) { - accountId = existing.id - accountName = existing.name + accountId = existing.id; + accountName = existing.name; await updateAccount({ ...existing, balance: parsed.balance, lastImport: new Date().toISOString(), - }) + }); } else { - isNew = true - accountName = `Compte ${parsed.accountId.slice(-4)}` + isNew = true; + accountName = `Compte ${parsed.accountId.slice(-4)}`; const newAccount = await addAccount({ name: accountName, bankId: parsed.bankId, @@ -92,14 +123,16 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP balance: parsed.balance, currency: parsed.currency, lastImport: new Date().toISOString(), - }) - accountId = newAccount.id + }); + accountId = newAccount.id; } // Add transactions with auto-categorization const existingFitIds = new Set( - data.transactions.filter((t) => t.accountId === accountId).map((t) => t.fitId) - ) + data.transactions + .filter((t) => t.accountId === accountId) + .map((t) => t.fitId), + ); const newTransactions: Transaction[] = parsed.transactions .filter((t) => !existingFitIds.has(t.fitId)) @@ -110,15 +143,18 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP amount: t.amount, description: t.name, type: t.amount >= 0 ? "CREDIT" : "DEBIT", - categoryId: autoCategorize(t.name + " " + (t.memo || ""), data.categories), + categoryId: autoCategorize( + t.name + " " + (t.memo || ""), + data.categories, + ), isReconciled: false, fitId: t.fitId, memo: t.memo, checkNum: t.checkNum, - })) + })); if (newTransactions.length > 0) { - await addTransactions(newTransactions) + await addTransactions(newTransactions); } return { @@ -126,7 +162,7 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP accountName, transactionsImported: newTransactions.length, isNew, - } + }; } catch (err) { return { fileName, @@ -134,85 +170,90 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP transactionsImported: 0, isNew: false, error: err instanceof Error ? err.message : "Erreur inconnue", - } + }; } - } + }; - const onDrop = useCallback(async (acceptedFiles: File[]) => { - if (acceptedFiles.length === 0) return + const onDrop = useCallback( + async (acceptedFiles: File[]) => { + if (acceptedFiles.length === 0) return; - // Multi-file mode: import directly - if (acceptedFiles.length > 1) { - setStep("importing") - setTotalFiles(acceptedFiles.length) - setImportProgress(0) - setImportResults([]) + // Multi-file mode: import directly + if (acceptedFiles.length > 1) { + setStep("importing"); + setTotalFiles(acceptedFiles.length); + setImportProgress(0); + setImportResults([]); - const data = await loadData() - const results: ImportResult[] = [] + const data = await loadData(); + const results: ImportResult[] = []; - for (let i = 0; i < acceptedFiles.length; i++) { - const file = acceptedFiles[i] - const content = await file.text() - const parsed = parseOFX(content) + for (let i = 0; i < acceptedFiles.length; i++) { + const file = acceptedFiles[i]; + const content = await file.text(); + const parsed = parseOFX(content); - if (parsed) { - // Reload data after each import to get updated accounts/transactions - const freshData = i === 0 ? data : await loadData() - const result = await importOFXDirect(parsed, file.name, freshData) - results.push(result) - } else { - results.push({ - fileName: file.name, - accountName: "Erreur", - transactionsImported: 0, - isNew: false, - error: "Format OFX invalide", - }) + if (parsed) { + // Reload data after each import to get updated accounts/transactions + const freshData = i === 0 ? data : await loadData(); + const result = await importOFXDirect(parsed, file.name, freshData); + results.push(result); + } else { + results.push({ + fileName: file.name, + accountName: "Erreur", + transactionsImported: 0, + isNew: false, + error: "Format OFX invalide", + }); + } + + setImportProgress(((i + 1) / acceptedFiles.length) * 100); + setImportResults([...results]); } - setImportProgress(((i + 1) / acceptedFiles.length) * 100) - setImportResults([...results]) + setStep("success"); + onImportComplete?.(); + return; } - setStep("success") - onImportComplete?.() - return - } + // Single file mode: show configuration + const file = acceptedFiles[0]; + const content = await file.text(); + const parsed = parseOFX(content); - // Single file mode: show configuration - const file = acceptedFiles[0] - const content = await file.text() - const parsed = parseOFX(content) + if (parsed) { + setParsedData(parsed); + setAccountName(`Compte ${parsed.accountId.slice(-4)}`); - if (parsed) { - setParsedData(parsed) - setAccountName(`Compte ${parsed.accountId.slice(-4)}`) + try { + const data = await loadData(); + setFolders(data.folders); - try { - const data = await loadData() - setFolders(data.folders) + const existing = data.accounts.find( + (a) => + a.accountNumber === parsed.accountId && + a.bankId === parsed.bankId, + ); + if (existing) { + setExistingAccountId(existing.id); + setAccountName(existing.name); + setSelectedFolder(existing.folderId || "folder-root"); + } - const existing = data.accounts.find( - (a) => a.accountNumber === parsed.accountId && a.bankId === parsed.bankId - ) - if (existing) { - setExistingAccountId(existing.id) - setAccountName(existing.name) - setSelectedFolder(existing.folderId || "folder-root") + setStep("configure"); + } catch (err) { + console.error("Error loading data:", err); + setError("Erreur lors du chargement des données"); + setStep("error"); } - - setStep("configure") - } catch (err) { - console.error("Error loading data:", err) - setError("Erreur lors du chargement des données") - setStep("error") + } else { + setError("Impossible de lire le fichier OFX. Vérifiez le format."); + setStep("error"); } - } else { - setError("Impossible de lire le fichier OFX. Vérifiez le format.") - setStep("error") - } - }, [onImportComplete]) + }, + [onImportComplete], + ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, @@ -222,20 +263,22 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP "text/plain": [".ofx", ".qfx"], }, // No maxFiles limit - accept multiple files - }) + }); const handleImport = async () => { - if (!parsedData) return + if (!parsedData) return; try { - setStep("importing") - const data = await loadData() + setStep("importing"); + const data = await loadData(); - let accountId: string + let accountId: string; if (existingAccountId) { - accountId = existingAccountId - const existingAccount = data.accounts.find((a) => a.id === existingAccountId) + accountId = existingAccountId; + const existingAccount = data.accounts.find( + (a) => a.id === existingAccountId, + ); if (existingAccount) { await updateAccount({ ...existingAccount, @@ -243,7 +286,7 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP folderId: selectedFolder, balance: parsedData.balance, lastImport: new Date().toISOString(), - }) + }); } } else { const newAccount = await addAccount({ @@ -255,13 +298,15 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP balance: parsedData.balance, currency: parsedData.currency, lastImport: new Date().toISOString(), - }) - accountId = newAccount.id + }); + accountId = newAccount.id; } const existingFitIds = new Set( - data.transactions.filter((t) => t.accountId === accountId).map((t) => t.fitId) - ) + data.transactions + .filter((t) => t.accountId === accountId) + .map((t) => t.fitId), + ); const newTransactions: Transaction[] = parsedData.transactions .filter((t) => !existingFitIds.has(t.fitId)) @@ -272,57 +317,65 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP amount: t.amount, description: t.name, type: t.amount >= 0 ? "CREDIT" : "DEBIT", - categoryId: autoCategorize(t.name + " " + (t.memo || ""), data.categories), + categoryId: autoCategorize( + t.name + " " + (t.memo || ""), + data.categories, + ), isReconciled: false, fitId: t.fitId, memo: t.memo, checkNum: t.checkNum, - })) + })); if (newTransactions.length > 0) { - await addTransactions(newTransactions) + await addTransactions(newTransactions); } - setImportResults([{ - fileName: "Import", - accountName, - transactionsImported: newTransactions.length, - isNew: !existingAccountId, - }]) - setStep("success") - onImportComplete?.() + setImportResults([ + { + fileName: "Import", + accountName, + transactionsImported: newTransactions.length, + isNew: !existingAccountId, + }, + ]); + setStep("success"); + onImportComplete?.(); } catch (err) { - console.error("Error importing:", err) - setError("Erreur lors de l'import") - setStep("error") + console.error("Error importing:", err); + setError("Erreur lors de l'import"); + setStep("error"); } - } + }; const handleClose = () => { - setOpen(false) + setOpen(false); setTimeout(() => { - setStep("upload") - setParsedData(null) - setAccountName("") - setSelectedFolder("folder-root") - setExistingAccountId(null) - setError(null) - setImportResults([]) - setImportProgress(0) - setTotalFiles(0) - }, 200) - } + setStep("upload"); + setParsedData(null); + setAccountName(""); + setSelectedFolder("folder-root"); + setExistingAccountId(null); + setError(null); + setImportResults([]); + setImportProgress(0); + setTotalFiles(0); + }, 200); + }; - const totalTransactions = importResults.reduce((sum, r) => sum + r.transactionsImported, 0) - const successCount = importResults.filter((r) => !r.error).length - const errorCount = importResults.filter((r) => r.error).length + const totalTransactions = importResults.reduce( + (sum, r) => sum + r.transactionsImported, + 0, + ); + const successCount = importResults.filter((r) => !r.error).length; + const errorCount = importResults.filter((r) => r.error).length; return ( { - if (!o) handleClose() - else setOpen(true) + if (!o) handleClose(); + else setOpen(true); }} > {children} @@ -336,14 +389,16 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP {step === "error" && "Erreur d'import"} - {step === "upload" && "Glissez-déposez vos fichiers OFX ou cliquez pour sélectionner"} - {step === "configure" && "Vérifiez les informations du compte avant l'import"} - {step === "importing" && `Import de ${totalFiles} fichier${totalFiles > 1 ? "s" : ""}...`} - {step === "success" && ( - importResults.length > 1 + {step === "upload" && + "Glissez-déposez vos fichiers OFX ou cliquez pour sélectionner"} + {step === "configure" && + "Vérifiez les informations du compte avant l'import"} + {step === "importing" && + `Import de ${totalFiles} fichier${totalFiles > 1 ? "s" : ""}...`} + {step === "success" && + (importResults.length > 1 ? `${successCount} fichier${successCount > 1 ? "s" : ""} importé${successCount > 1 ? "s" : ""}, ${totalTransactions} transactions` - : `${totalTransactions} nouvelles transactions importées` - )} + : `${totalTransactions} nouvelles transactions importées`)} {step === "error" && error} @@ -353,16 +408,21 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP {...getRootProps()} className={cn( "border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors", - isDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50", + isDragActive + ? "border-primary bg-primary/5" + : "border-muted-foreground/25 hover:border-primary/50", )} >

- {isDragActive ? "Déposez les fichiers ici..." : "Fichiers .ofx ou .qfx acceptés"} + {isDragActive + ? "Déposez les fichiers ici..." + : "Fichiers .ofx ou .qfx acceptés"}

- Un fichier = configuration manuelle • Plusieurs fichiers = import direct + Un fichier = configuration manuelle • Plusieurs fichiers = import + direct

)} @@ -372,12 +432,15 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP
-

{parsedData.transactions.length} transactions

+

+ {parsedData.transactions.length} transactions +

Solde:{" "} - {new Intl.NumberFormat("fr-FR", { style: "currency", currency: parsedData.currency }).format( - parsedData.balance, - )} + {new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: parsedData.currency, + }).format(parsedData.balance)}

@@ -410,7 +473,8 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP {existingAccountId && (

- Ce compte existe déjà. Les nouvelles transactions seront ajoutées. + Ce compte existe déjà. Les nouvelles transactions seront + ajoutées.

)} @@ -457,7 +521,7 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP {step === "success" && (
- + {importResults.length > 1 && (
{importResults.map((result, i) => ( @@ -468,14 +532,21 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP )}
-

{result.accountName}

-

{result.fileName}

+

+ {result.accountName} +

+

+ {result.fileName} +

{result.error ? ( - {result.error} + + {result.error} + ) : ( - {result.isNew ? "Nouveau" : "Mis à jour"} • +{result.transactionsImported} + {result.isNew ? "Nouveau" : "Mis à jour"} • + + {result.transactionsImported} )}
@@ -503,5 +574,5 @@ export function OFXImportDialog({ children, onImportComplete }: OFXImportDialogP )} - ) + ); } diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 55c2f6e..1a44ac3 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -1,11 +1,11 @@ -'use client' +"use client"; -import * as React from 'react' +import * as React from "react"; import { ThemeProvider as NextThemesProvider, type ThemeProviderProps, -} from 'next-themes' +} from "next-themes"; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} + return {children}; } diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx index e538a33..6d60d31 100644 --- a/components/ui/accordion.tsx +++ b/components/ui/accordion.tsx @@ -1,15 +1,15 @@ -'use client' +"use client"; -import * as React from 'react' -import * as AccordionPrimitive from '@radix-ui/react-accordion' -import { ChevronDownIcon } from 'lucide-react' +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function Accordion({ ...props }: React.ComponentProps) { - return + return ; } function AccordionItem({ @@ -19,10 +19,10 @@ function AccordionItem({ return ( - ) + ); } function AccordionTrigger({ @@ -35,7 +35,7 @@ function AccordionTrigger({ svg]:rotate-180', + "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", className, )} {...props} @@ -44,7 +44,7 @@ function AccordionTrigger({ - ) + ); } function AccordionContent({ @@ -58,9 +58,9 @@ function AccordionContent({ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" {...props} > -
{children}
+
{children}
- ) + ); } -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx index 9704452..c37e0cf 100644 --- a/components/ui/alert-dialog.tsx +++ b/components/ui/alert-dialog.tsx @@ -1,15 +1,15 @@ -'use client' +"use client"; -import * as React from 'react' -import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import { cn } from '@/lib/utils' -import { buttonVariants } from '@/components/ui/button' +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; function AlertDialog({ ...props }: React.ComponentProps) { - return + return ; } function AlertDialogTrigger({ @@ -17,7 +17,7 @@ function AlertDialogTrigger({ }: React.ComponentProps) { return ( - ) + ); } function AlertDialogPortal({ @@ -25,7 +25,7 @@ function AlertDialogPortal({ }: React.ComponentProps) { return ( - ) + ); } function AlertDialogOverlay({ @@ -36,12 +36,12 @@ function AlertDialogOverlay({ - ) + ); } function AlertDialogContent({ @@ -54,42 +54,42 @@ function AlertDialogContent({ - ) + ); } function AlertDialogHeader({ className, ...props -}: React.ComponentProps<'div'>) { +}: React.ComponentProps<"div">) { return (
- ) + ); } function AlertDialogFooter({ className, ...props -}: React.ComponentProps<'div'>) { +}: React.ComponentProps<"div">) { return (
- ) + ); } function AlertDialogTitle({ @@ -99,10 +99,10 @@ function AlertDialogTitle({ return ( - ) + ); } function AlertDialogDescription({ @@ -112,10 +112,10 @@ function AlertDialogDescription({ return ( - ) + ); } function AlertDialogAction({ @@ -127,7 +127,7 @@ function AlertDialogAction({ className={cn(buttonVariants(), className)} {...props} /> - ) + ); } function AlertDialogCancel({ @@ -136,10 +136,10 @@ function AlertDialogCancel({ }: React.ComponentProps) { return ( - ) + ); } export { @@ -154,4 +154,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx index e6751ab..aa7de24 100644 --- a/components/ui/alert.tsx +++ b/components/ui/alert.tsx @@ -1,29 +1,29 @@ -import * as React from 'react' -import { cva, type VariantProps } from 'class-variance-authority' +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; const alertVariants = cva( - 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { - default: 'bg-card text-card-foreground', + default: "bg-card text-card-foreground", destructive: - 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, }, -) +); function Alert({ className, variant, ...props -}: React.ComponentProps<'div'> & VariantProps) { +}: React.ComponentProps<"div"> & VariantProps) { return (
- ) + ); } -function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } function AlertDescription({ className, ...props -}: React.ComponentProps<'div'>) { +}: React.ComponentProps<"div">) { return (
- ) + ); } -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx index 40bb120..c16d6bc 100644 --- a/components/ui/aspect-ratio.tsx +++ b/components/ui/aspect-ratio.tsx @@ -1,11 +1,11 @@ -'use client' +"use client"; -import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; function AspectRatio({ ...props }: React.ComponentProps) { - return + return ; } -export { AspectRatio } +export { AspectRatio }; diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx index aa98465..c4475c2 100644 --- a/components/ui/avatar.tsx +++ b/components/ui/avatar.tsx @@ -1,9 +1,9 @@ -'use client' +"use client"; -import * as React from 'react' -import * as AvatarPrimitive from '@radix-ui/react-avatar' +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function Avatar({ className, @@ -13,12 +13,12 @@ function Avatar({ - ) + ); } function AvatarImage({ @@ -28,10 +28,10 @@ function AvatarImage({ return ( - ) + ); } function AvatarFallback({ @@ -42,12 +42,12 @@ function AvatarFallback({ - ) + ); } -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index fc4126b..46f988c 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -1,38 +1,38 @@ -import * as React from 'react' -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; const badgeVariants = cva( - 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", { variants: { variant: { default: - 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", secondary: - 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: - 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, }, -) +); function Badge({ className, variant, asChild = false, ...props -}: React.ComponentProps<'span'> & +}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : 'span' + const Comp = asChild ? Slot : "span"; return ( - ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx index 1750ff2..f63ae19 100644 --- a/components/ui/breadcrumb.tsx +++ b/components/ui/breadcrumb.tsx @@ -1,101 +1,101 @@ -import * as React from 'react' -import { Slot } from '@radix-ui/react-slot' -import { ChevronRight, MoreHorizontal } from 'lucide-react' +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; -function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { - return
- ) + ); } const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([, config]) => config.theme || config.color, - ) + ); if (!colorConfig.length) { - return null + return null; } return ( @@ -89,35 +89,35 @@ ${colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color - return color ? ` --color-${key}: ${color};` : null + itemConfig.color; + return color ? ` --color-${key}: ${color};` : null; }) - .join('\n')} + .join("\n")} } `, ) - .join('\n'), + .join("\n"), }} /> - ) -} + ); +}; -const ChartTooltip = RechartsPrimitive.Tooltip +const ChartTooltip = RechartsPrimitive.Tooltip; type TooltipPayloadItem = { - dataKey?: string | number - name?: string - value?: number | string - color?: string - payload?: Record & { fill?: string } - fill?: string -} + dataKey?: string | number; + name?: string; + value?: number | string; + color?: string; + payload?: Record & { fill?: string }; + fill?: string; +}; function ChartTooltipContent({ active, payload, className, - indicator = 'dot', + indicator = "dot", hideLabel = false, hideIndicator = false, label, @@ -127,44 +127,47 @@ function ChartTooltipContent({ color, nameKey, labelKey, -}: Omit, 'payload' | 'label'> & - React.ComponentProps<'div'> & { - hideLabel?: boolean - hideIndicator?: boolean - indicator?: 'line' | 'dot' | 'dashed' - nameKey?: string - labelKey?: string - payload?: TooltipPayloadItem[] - label?: string | number +}: Omit< + React.ComponentProps, + "payload" | "label" +> & + React.ComponentProps<"div"> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + payload?: TooltipPayloadItem[]; + label?: string | number; }) { - const { config } = useChart() + const { config } = useChart(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { - return null + return null; } - const [item] = payload - const key = `${labelKey || item?.dataKey || item?.name || 'value'}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) + const [item] = payload; + const key = `${labelKey || item?.dataKey || item?.name || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); const value = - !labelKey && typeof label === 'string' + !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label - : itemConfig?.label + : itemConfig?.label; if (labelFormatter) { return ( -
+
{labelFormatter(value, payload)}
- ) + ); } if (!value) { - return null + return null; } - return
{value}
+ return
{value}
; }, [ label, labelFormatter, @@ -173,38 +176,44 @@ function ChartTooltipContent({ labelClassName, config, labelKey, - ]) + ]); if (!active || !payload?.length) { - return null + return null; } - const nestLabel = payload.length === 1 && indicator !== 'dot' + const nestLabel = payload.length === 1 && indicator !== "dot"; return (
{!nestLabel ? tooltipLabel : null}
{payload.map((item: TooltipPayloadItem, index: number) => { - const key = `${nameKey || item.name || item.dataKey || 'value'}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) - const indicatorColor = color || item.payload?.fill || item.color + const key = `${nameKey || item.name || item.dataKey || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = color || item.payload?.fill || item.color; return (
svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5', - indicator === 'dot' && 'items-center', + "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", + indicator === "dot" && "items-center", )} > {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item as never, index, item.payload as never) + formatter( + item.value, + item.name, + item as never, + index, + item.payload as never, + ) ) : ( <> {itemConfig?.icon ? ( @@ -213,19 +222,19 @@ function ChartTooltipContent({ !hideIndicator && (
@@ -233,8 +242,8 @@ function ChartTooltipContent({ )}
@@ -252,56 +261,56 @@ function ChartTooltipContent({ )}
- ) + ); })}
- ) + ); } -const ChartLegend = RechartsPrimitive.Legend +const ChartLegend = RechartsPrimitive.Legend; type LegendPayloadItem = { - value?: string - dataKey?: string | number - color?: string -} + value?: string; + dataKey?: string | number; + color?: string; +}; function ChartLegendContent({ className, hideIcon = false, payload, - verticalAlign = 'bottom', + verticalAlign = "bottom", nameKey, -}: React.ComponentProps<'div'> & { - hideIcon?: boolean - nameKey?: string - payload?: LegendPayloadItem[] - verticalAlign?: 'top' | 'bottom' | 'middle' - }) { - const { config } = useChart() +}: React.ComponentProps<"div"> & { + hideIcon?: boolean; + nameKey?: string; + payload?: LegendPayloadItem[]; + verticalAlign?: "top" | "bottom" | "middle"; +}) { + const { config } = useChart(); if (!payload?.length) { - return null + return null; } return (
{payload.map((item: LegendPayloadItem) => { - const key = `${nameKey || item.dataKey || 'value'}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) + const key = `${nameKey || item.dataKey || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); return (
svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3' + "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3" } > {itemConfig?.icon && !hideIcon ? ( @@ -316,10 +325,10 @@ function ChartLegendContent({ )} {itemConfig?.label}
- ) + ); })}
- ) + ); } // Helper to extract item config from a payload. @@ -328,37 +337,37 @@ function getPayloadConfigFromPayload( payload: unknown, key: string, ) { - if (typeof payload !== 'object' || payload === null) { - return undefined + if (typeof payload !== "object" || payload === null) { + return undefined; } const payloadPayload = - 'payload' in payload && - typeof payload.payload === 'object' && + "payload" in payload && + typeof payload.payload === "object" && payload.payload !== null ? payload.payload - : undefined + : undefined; - let configLabelKey: string = key + let configLabelKey: string = key; if ( key in payload && - typeof payload[key as keyof typeof payload] === 'string' + typeof payload[key as keyof typeof payload] === "string" ) { - configLabelKey = payload[key as keyof typeof payload] as string + configLabelKey = payload[key as keyof typeof payload] as string; } else if ( payloadPayload && key in payloadPayload && - typeof payloadPayload[key as keyof typeof payloadPayload] === 'string' + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" ) { configLabelKey = payloadPayload[ key as keyof typeof payloadPayload - ] as string + ] as string; } return configLabelKey in config ? config[configLabelKey] - : config[key as keyof typeof config] + : config[key as keyof typeof config]; } export { @@ -368,4 +377,4 @@ export { ChartLegend, ChartLegendContent, ChartStyle, -} +}; diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx index 37d340f..ae02cf5 100644 --- a/components/ui/checkbox.tsx +++ b/components/ui/checkbox.tsx @@ -1,10 +1,10 @@ -'use client' +"use client"; -import * as React from 'react' -import * as CheckboxPrimitive from '@radix-ui/react-checkbox' -import { CheckIcon } from 'lucide-react' +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function Checkbox({ className, @@ -14,7 +14,7 @@ function Checkbox({ - ) + ); } -export { Checkbox } +export { Checkbox }; diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx index 3cbdff6..90935c6 100644 --- a/components/ui/collapsible.tsx +++ b/components/ui/collapsible.tsx @@ -1,11 +1,11 @@ -'use client' +"use client"; -import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; function Collapsible({ ...props }: React.ComponentProps) { - return + return ; } function CollapsibleTrigger({ @@ -16,7 +16,7 @@ function CollapsibleTrigger({ data-slot="collapsible-trigger" {...props} /> - ) + ); } function CollapsibleContent({ @@ -27,7 +27,7 @@ function CollapsibleContent({ data-slot="collapsible-content" {...props} /> - ) + ); } -export { Collapsible, CollapsibleTrigger, CollapsibleContent } +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/components/ui/command.tsx b/components/ui/command.tsx index 4833ca8..ee7450a 100644 --- a/components/ui/command.tsx +++ b/components/ui/command.tsx @@ -1,17 +1,17 @@ -'use client' +"use client"; -import * as React from 'react' -import { Command as CommandPrimitive } from 'cmdk' -import { SearchIcon } from 'lucide-react' +import * as React from "react"; +import { Command as CommandPrimitive } from "cmdk"; +import { SearchIcon } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, -} from '@/components/ui/dialog' +} from "@/components/ui/dialog"; function Command({ className, @@ -21,26 +21,26 @@ function Command({ - ) + ); } function CommandDialog({ - title = 'Command Palette', - description = 'Search for a command to run...', + title = "Command Palette", + description = "Search for a command to run...", children, className, showCloseButton = true, ...props }: React.ComponentProps & { - title?: string - description?: string - className?: string - showCloseButton?: boolean + title?: string; + description?: string; + className?: string; + showCloseButton?: boolean; }) { return ( @@ -49,7 +49,7 @@ function CommandDialog({ {description} @@ -57,7 +57,7 @@ function CommandDialog({ - ) + ); } function CommandInput({ @@ -73,13 +73,13 @@ function CommandInput({
- ) + ); } function CommandList({ @@ -90,12 +90,12 @@ function CommandList({ - ) + ); } function CommandEmpty({ @@ -107,7 +107,7 @@ function CommandEmpty({ className="py-6 text-center text-sm" {...props} /> - ) + ); } function CommandGroup({ @@ -118,12 +118,12 @@ function CommandGroup({ - ) + ); } function CommandSeparator({ @@ -133,10 +133,10 @@ function CommandSeparator({ return ( - ) + ); } function CommandItem({ @@ -152,23 +152,23 @@ function CommandItem({ )} {...props} /> - ) + ); } function CommandShortcut({ className, ...props -}: React.ComponentProps<'span'>) { +}: React.ComponentProps<"span">) { return ( - ) + ); } export { @@ -181,4 +181,4 @@ export { CommandItem, CommandShortcut, CommandSeparator, -} +}; diff --git a/components/ui/context-menu.tsx b/components/ui/context-menu.tsx index 9e536f2..ab7a5d0 100644 --- a/components/ui/context-menu.tsx +++ b/components/ui/context-menu.tsx @@ -1,15 +1,15 @@ -'use client' +"use client"; -import * as React from 'react' -import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' -import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react' +import * as React from "react"; +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function ContextMenu({ ...props }: React.ComponentProps) { - return + return ; } function ContextMenuTrigger({ @@ -17,7 +17,7 @@ function ContextMenuTrigger({ }: React.ComponentProps) { return ( - ) + ); } function ContextMenuGroup({ @@ -25,7 +25,7 @@ function ContextMenuGroup({ }: React.ComponentProps) { return ( - ) + ); } function ContextMenuPortal({ @@ -33,13 +33,13 @@ function ContextMenuPortal({ }: React.ComponentProps) { return ( - ) + ); } function ContextMenuSub({ ...props }: React.ComponentProps) { - return + return ; } function ContextMenuRadioGroup({ @@ -50,7 +50,7 @@ function ContextMenuRadioGroup({ data-slot="context-menu-radio-group" {...props} /> - ) + ); } function ContextMenuSubTrigger({ @@ -59,7 +59,7 @@ function ContextMenuSubTrigger({ children, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - ) + ); } function ContextMenuSubContent({ @@ -85,12 +85,12 @@ function ContextMenuSubContent({ - ) + ); } function ContextMenuContent({ @@ -102,23 +102,23 @@ function ContextMenuContent({ - ) + ); } function ContextMenuItem({ className, inset, - variant = 'default', + variant = "default", ...props }: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' + inset?: boolean; + variant?: "default" | "destructive"; }) { return ( - ) + ); } function ContextMenuCheckboxItem({ @@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({ {children} - ) + ); } function ContextMenuRadioItem({ @@ -181,7 +181,7 @@ function ContextMenuRadioItem({ {children} - ) + ); } function ContextMenuLabel({ @@ -189,19 +189,19 @@ function ContextMenuLabel({ inset, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - ) + ); } function ContextMenuSeparator({ @@ -211,26 +211,26 @@ function ContextMenuSeparator({ return ( - ) + ); } function ContextMenuShortcut({ className, ...props -}: React.ComponentProps<'span'>) { +}: React.ComponentProps<"span">) { return ( - ) + ); } export { @@ -249,4 +249,4 @@ export { ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup, -} +}; diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 243fb19..7d60dd3 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,33 +1,33 @@ -'use client' +"use client"; -import * as React from 'react' -import * as DialogPrimitive from '@radix-ui/react-dialog' -import { XIcon } from 'lucide-react' +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function Dialog({ ...props }: React.ComponentProps) { - return + return ; } function DialogTrigger({ ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ @@ -38,12 +38,12 @@ function DialogOverlay({ - ) + ); } function DialogContent({ @@ -52,7 +52,7 @@ function DialogContent({ showCloseButton = true, ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { return ( @@ -60,7 +60,7 @@ function DialogContent({ - ) + ); } -function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } -function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } function DialogTitle({ @@ -110,10 +110,10 @@ function DialogTitle({ return ( - ) + ); } function DialogDescription({ @@ -123,10 +123,10 @@ function DialogDescription({ return ( - ) + ); } export { @@ -140,4 +140,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} +}; diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx index 307bdce..8848866 100644 --- a/components/ui/drawer.tsx +++ b/components/ui/drawer.tsx @@ -1,32 +1,32 @@ -'use client' +"use client"; -import * as React from 'react' -import { Drawer as DrawerPrimitive } from 'vaul' +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function Drawer({ ...props }: React.ComponentProps) { - return + return ; } function DrawerTrigger({ ...props }: React.ComponentProps) { - return + return ; } function DrawerPortal({ ...props }: React.ComponentProps) { - return + return ; } function DrawerClose({ ...props }: React.ComponentProps) { - return + return ; } function DrawerOverlay({ @@ -37,12 +37,12 @@ function DrawerOverlay({ - ) + ); } function DrawerContent({ @@ -56,11 +56,11 @@ function DrawerContent({ - ) + ); } -function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } -function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } function DrawerTitle({ @@ -102,10 +102,10 @@ function DrawerTitle({ return ( - ) + ); } function DrawerDescription({ @@ -115,10 +115,10 @@ function DrawerDescription({ return ( - ) + ); } export { @@ -132,4 +132,4 @@ export { DrawerFooter, DrawerTitle, DrawerDescription, -} +}; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index a2096fa..0329b9b 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -1,15 +1,15 @@ -'use client' +"use client"; -import * as React from 'react' -import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' -import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react' +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; function DropdownMenu({ ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuPortal({ @@ -17,7 +17,7 @@ function DropdownMenuPortal({ }: React.ComponentProps) { return ( - ) + ); } function DropdownMenuTrigger({ @@ -28,7 +28,7 @@ function DropdownMenuTrigger({ data-slot="dropdown-menu-trigger" {...props} /> - ) + ); } function DropdownMenuContent({ @@ -42,13 +42,13 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", className, )} {...props} /> - ) + ); } function DropdownMenuGroup({ @@ -56,17 +56,17 @@ function DropdownMenuGroup({ }: React.ComponentProps) { return ( - ) + ); } function DropdownMenuItem({ className, inset, - variant = 'default', + variant = "default", ...props }: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' + inset?: boolean; + variant?: "default" | "destructive"; }) { return ( - ) + ); } function DropdownMenuCheckboxItem({ @@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({ {children} - ) + ); } function DropdownMenuRadioGroup({ @@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({ data-slot="dropdown-menu-radio-group" {...props} /> - ) + ); } function DropdownMenuRadioItem({ @@ -140,7 +140,7 @@ function DropdownMenuRadioItem({ {children} - ) + ); } function DropdownMenuLabel({ @@ -148,19 +148,19 @@ function DropdownMenuLabel({ inset, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - ) + ); } function DropdownMenuSeparator({ @@ -170,32 +170,32 @@ function DropdownMenuSeparator({ return ( - ) + ); } function DropdownMenuShortcut({ className, ...props -}: React.ComponentProps<'span'>) { +}: React.ComponentProps<"span">) { return ( - ) + ); } function DropdownMenuSub({ ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuSubTrigger({ @@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({ children, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - ) + ); } function DropdownMenuSubContent({ @@ -230,12 +230,12 @@ function DropdownMenuSubContent({ - ) + ); } export { @@ -254,4 +254,4 @@ export { DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, -} +}; diff --git a/components/ui/empty.tsx b/components/ui/empty.tsx index 2c57e94..e4a0754 100644 --- a/components/ui/empty.tsx +++ b/components/ui/empty.tsx @@ -1,53 +1,53 @@ -import { cva, type VariantProps } from 'class-variance-authority' +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; -function Empty({ className, ...props }: React.ComponentProps<'div'>) { +function Empty({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } -function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) { +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } const emptyMediaVariants = cva( - 'flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0', + "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { - default: 'bg-transparent', + default: "bg-transparent", icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, }, -) +); function EmptyMedia({ className, - variant = 'default', + variant = "default", ...props -}: React.ComponentProps<'div'> & VariantProps) { +}: React.ComponentProps<"div"> & VariantProps) { return (
- ) + ); } -function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) { +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } -function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) { +function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { return (
a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4', + "text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", className, )} {...props} /> - ) + ); } -function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) { +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } export { @@ -101,4 +101,4 @@ export { EmptyDescription, EmptyContent, EmptyMedia, -} +}; diff --git a/components/ui/field.tsx b/components/ui/field.tsx index f4c2f21..9802658 100644 --- a/components/ui/field.tsx +++ b/components/ui/field.tsx @@ -1,88 +1,88 @@ -'use client' +"use client"; -import { useMemo } from 'react' -import { cva, type VariantProps } from 'class-variance-authority' +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from '@/lib/utils' -import { Label } from '@/components/ui/label' -import { Separator } from '@/components/ui/separator' +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; -function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) { +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return (
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3', + "flex flex-col gap-6", + "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", className, )} {...props} /> - ) + ); } function FieldLegend({ className, - variant = 'legend', + variant = "legend", ...props -}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) { +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { return ( - ) + ); } -function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) { +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { return (
[data-slot=field-group]]:gap-4', + "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", className, )} {...props} /> - ) + ); } const fieldVariants = cva( - 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive', + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", { variants: { orientation: { - vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'], + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], horizontal: [ - 'flex-row items-center', - '[&>[data-slot=field-label]]:flex-auto', - 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", ], responsive: [ - 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto', - '@md/field-group:[&>[data-slot=field-label]]:flex-auto', - '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", ], }, }, defaultVariants: { - orientation: 'vertical', + orientation: "vertical", }, }, -) +); function Field({ className, - orientation = 'vertical', + orientation = "vertical", ...props -}: React.ComponentProps<'div'> & VariantProps) { +}: React.ComponentProps<"div"> & VariantProps) { return (
- ) + ); } -function FieldContent({ className, ...props }: React.ComponentProps<'div'>) { +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { return (
- ) + ); } function FieldLabel({ @@ -115,57 +115,57 @@ function FieldLabel({