refactor: standardize quotation marks across all files and improve code consistency

This commit is contained in:
Julien Froidefond
2025-11-27 11:40:30 +01:00
parent cc1e8c20a6
commit b2efade4d5
107 changed files with 9471 additions and 5952 deletions

View File

@@ -1,3 +1,3 @@
export default function Loading() {
return null
return null;
}

View File

@@ -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<string>("all")
const [selectedCategory, setSelectedCategory] = useState<string>("all")
const [showReconciled, setShowReconciled] = useState<string>("all")
const [sortField, setSortField] = useState<SortField>("date")
const [sortOrder, setSortOrder] = useState<SortOrder>("desc")
const [selectedTransactions, setSelectedTransactions] = useState<Set<string>>(new Set())
const { data, isLoading, refresh, update } = useBankingData();
const [searchQuery, setSearchQuery] = useState("");
const [selectedAccount, setSelectedAccount] = useState<string>("all");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [showReconciled, setShowReconciled] = useState<string>("all");
const [sortField, setSortField] = useState<SortField>("date");
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
const [selectedTransactions, setSelectedTransactions] = useState<Set<string>>(
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() {
<RefreshCw className="w-8 h-8 animate-spin text-muted-foreground" />
</main>
</div>
)
);
}
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 (
<div className="flex h-screen bg-background">
@@ -175,9 +211,12 @@ export default function TransactionsPage() {
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">Transactions</h1>
<h1 className="text-2xl font-bold text-foreground">
Transactions
</h1>
<p className="text-muted-foreground">
{filteredTransactions.length} transaction{filteredTransactions.length > 1 ? "s" : ""}
{filteredTransactions.length} transaction
{filteredTransactions.length > 1 ? "s" : ""}
</p>
</div>
<OFXImportDialog onImportComplete={refresh}>
@@ -204,7 +243,10 @@ export default function TransactionsPage() {
</div>
</div>
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
<Select
value={selectedAccount}
onValueChange={setSelectedAccount}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Compte" />
</SelectTrigger>
@@ -218,13 +260,18 @@ export default function TransactionsPage() {
</SelectContent>
</Select>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<Select
value={selectedCategory}
onValueChange={setSelectedCategory}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Catégorie" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes catégories</SelectItem>
<SelectItem value="uncategorized">Non catégorisé</SelectItem>
<SelectItem value="uncategorized">
Non catégorisé
</SelectItem>
{data.categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
@@ -233,7 +280,10 @@ export default function TransactionsPage() {
</SelectContent>
</Select>
<Select value={showReconciled} onValueChange={setShowReconciled}>
<Select
value={showReconciled}
onValueChange={setShowReconciled}
>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="Pointage" />
</SelectTrigger>
@@ -253,13 +303,22 @@ export default function TransactionsPage() {
<CardContent className="py-3">
<div className="flex items-center gap-4">
<span className="text-sm font-medium">
{selectedTransactions.size} sélectionnée{selectedTransactions.size > 1 ? "s" : ""}
{selectedTransactions.size} sélectionnée
{selectedTransactions.size > 1 ? "s" : ""}
</span>
<Button size="sm" variant="outline" onClick={() => bulkReconcile(true)}>
<Button
size="sm"
variant="outline"
onClick={() => bulkReconcile(true)}
>
<CheckCircle2 className="w-4 h-4 mr-1" />
Pointer
</Button>
<Button size="sm" variant="outline" onClick={() => bulkReconcile(false)}>
<Button
size="sm"
variant="outline"
onClick={() => bulkReconcile(false)}
>
<Circle className="w-4 h-4 mr-1" />
Dépointer
</Button>
@@ -271,11 +330,21 @@ export default function TransactionsPage() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => bulkSetCategory(null)}>Aucune catégorie</DropdownMenuItem>
<DropdownMenuItem onClick={() => bulkSetCategory(null)}>
Aucune catégorie
</DropdownMenuItem>
<DropdownMenuSeparator />
{data.categories.map((cat) => (
<DropdownMenuItem key={cat.id} onClick={() => bulkSetCategory(cat.id)}>
<CategoryIcon icon={cat.icon} color={cat.color} size={14} className="mr-2" />
<DropdownMenuItem
key={cat.id}
onClick={() => bulkSetCategory(cat.id)}
>
<CategoryIcon
icon={cat.icon}
color={cat.color}
size={14}
className="mr-2"
/>
{cat.name}
</DropdownMenuItem>
))}
@@ -291,7 +360,9 @@ export default function TransactionsPage() {
<CardContent className="p-0">
{filteredTransactions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<p className="text-muted-foreground">Aucune transaction trouvée</p>
<p className="text-muted-foreground">
Aucune transaction trouvée
</p>
</div>
) : (
<div className="overflow-x-auto">
@@ -301,7 +372,8 @@ export default function TransactionsPage() {
<th className="p-3 text-left">
<Checkbox
checked={
selectedTransactions.size === filteredTransactions.length &&
selectedTransactions.size ===
filteredTransactions.length &&
filteredTransactions.length > 0
}
onCheckedChange={toggleSelectAll}
@@ -311,10 +383,12 @@ export default function TransactionsPage() {
<button
onClick={() => {
if (sortField === "date") {
setSortOrder(sortOrder === "asc" ? "desc" : "asc")
setSortOrder(
sortOrder === "asc" ? "desc" : "asc",
);
} else {
setSortField("date")
setSortOrder("desc")
setSortField("date");
setSortOrder("desc");
}
}}
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground"
@@ -327,10 +401,12 @@ export default function TransactionsPage() {
<button
onClick={() => {
if (sortField === "description") {
setSortOrder(sortOrder === "asc" ? "desc" : "asc")
setSortOrder(
sortOrder === "asc" ? "desc" : "asc",
);
} else {
setSortField("description")
setSortOrder("asc")
setSortField("description");
setSortOrder("asc");
}
}}
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground"
@@ -339,16 +415,22 @@ export default function TransactionsPage() {
<ArrowUpDown className="w-3 h-3" />
</button>
</th>
<th className="p-3 text-left text-sm font-medium text-muted-foreground">Compte</th>
<th className="p-3 text-left text-sm font-medium text-muted-foreground">Catégorie</th>
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
Compte
</th>
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
Catégorie
</th>
<th className="p-3 text-right">
<button
onClick={() => {
if (sortField === "amount") {
setSortOrder(sortOrder === "asc" ? "desc" : "asc")
setSortOrder(
sortOrder === "asc" ? "desc" : "asc",
);
} else {
setSortField("amount")
setSortOrder("desc")
setSortField("amount");
setSortOrder("desc");
}
}}
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground ml-auto"
@@ -357,35 +439,48 @@ export default function TransactionsPage() {
<ArrowUpDown className="w-3 h-3" />
</button>
</th>
<th className="p-3 text-center text-sm font-medium text-muted-foreground">Pointé</th>
<th className="p-3 text-center text-sm font-medium text-muted-foreground">
Pointé
</th>
<th className="p-3"></th>
</tr>
</thead>
<tbody>
{filteredTransactions.map((transaction) => {
const category = getCategory(transaction.categoryId)
const account = getAccount(transaction.accountId)
const category = getCategory(transaction.categoryId);
const account = getAccount(transaction.accountId);
return (
<tr key={transaction.id} className="border-b border-border last:border-0 hover:bg-muted/50">
<tr
key={transaction.id}
className="border-b border-border last:border-0 hover:bg-muted/50"
>
<td className="p-3">
<Checkbox
checked={selectedTransactions.has(transaction.id)}
onCheckedChange={() => toggleSelectTransaction(transaction.id)}
checked={selectedTransactions.has(
transaction.id,
)}
onCheckedChange={() =>
toggleSelectTransaction(transaction.id)
}
/>
</td>
<td className="p-3 text-sm text-muted-foreground whitespace-nowrap">
{formatDate(transaction.date)}
</td>
<td className="p-3">
<p className="font-medium text-sm">{transaction.description}</p>
<p className="font-medium text-sm">
{transaction.description}
</p>
{transaction.memo && (
<p className="text-xs text-muted-foreground truncate max-w-[300px]">
{transaction.memo}
</p>
)}
</td>
<td className="p-3 text-sm text-muted-foreground">{account?.name || "-"}</td>
<td className="p-3 text-sm text-muted-foreground">
{account?.name || "-"}
</td>
<td className="p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -399,26 +494,49 @@ export default function TransactionsPage() {
color: category.color,
}}
>
<CategoryIcon icon={category.icon} color={category.color} size={12} />
<CategoryIcon
icon={category.icon}
color={category.color}
size={12}
/>
{category.name}
</Badge>
) : (
<Badge variant="outline" className="text-muted-foreground">
<Badge
variant="outline"
className="text-muted-foreground"
>
Non catégorisé
</Badge>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setCategory(transaction.id, null)}>
<DropdownMenuItem
onClick={() =>
setCategory(transaction.id, null)
}
>
Aucune catégorie
</DropdownMenuItem>
<DropdownMenuSeparator />
{data.categories.map((cat) => (
<DropdownMenuItem key={cat.id} onClick={() => setCategory(transaction.id, cat.id)}>
<CategoryIcon icon={cat.icon} color={cat.color} size={14} className="mr-2" />
<DropdownMenuItem
key={cat.id}
onClick={() =>
setCategory(transaction.id, cat.id)
}
>
<CategoryIcon
icon={cat.icon}
color={cat.color}
size={14}
className="mr-2"
/>
{cat.name}
{transaction.categoryId === cat.id && <Check className="w-4 h-4 ml-auto" />}
{transaction.categoryId === cat.id && (
<Check className="w-4 h-4 ml-auto" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -427,7 +545,9 @@ export default function TransactionsPage() {
<td
className={cn(
"p-3 text-right font-semibold tabular-nums",
transaction.amount >= 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() {
<td className="p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => toggleReconciled(transaction.id)}>
{transaction.isReconciled ? "Dépointer" : "Pointer"}
<DropdownMenuItem
onClick={() =>
toggleReconciled(transaction.id)
}
>
{transaction.isReconciled
? "Dépointer"
: "Pointer"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
)
);
})}
</tbody>
</table>
@@ -471,5 +601,5 @@ export default function TransactionsPage() {
</div>
</main>
</div>
)
);
}