diff --git a/app/accounts/page.tsx b/app/accounts/page.tsx index 6447020..16462de 100644 --- a/app/accounts/page.tsx +++ b/app/accounts/page.tsx @@ -99,6 +99,7 @@ export default function AccountsPage() { folderId: "folder-root", externalUrl: "", initialBalance: 0, + totalBalance: 0, }); // Folder management state @@ -131,7 +132,7 @@ export default function AccountsPage() { // Convert accountsWithStats to regular accounts for compatibility const accounts = accountsWithStats.map( - ({ transactionCount: _transactionCount, ...account }) => account, + ({ transactionCount: _transactionCount, calculatedBalance: _calculatedBalance, ...account }) => account, ); const formatCurrency = (amount: number) => { @@ -143,12 +144,14 @@ export default function AccountsPage() { const handleEdit = (account: Account) => { setEditingAccount(account); + const totalBalance = getAccountBalance(account); setFormData({ name: account.name, type: account.type, folderId: account.folderId || "folder-root", externalUrl: account.externalUrl || "", initialBalance: account.initialBalance || 0, + totalBalance: totalBalance, }); setIsDialogOpen(true); }; @@ -157,13 +160,21 @@ export default function AccountsPage() { if (!editingAccount) return; try { + // Calculer le balance à partir du solde total et du solde initial + // balance = totalBalance - initialBalance + const balance = formData.totalBalance - formData.initialBalance; + + // Convertir "folder-root" en null + const folderId = formData.folderId === "folder-root" ? null : formData.folderId; + const updatedAccount = { ...editingAccount, name: formData.name, type: formData.type, - folderId: formData.folderId, + folderId: folderId, externalUrl: formData.externalUrl || null, initialBalance: formData.initialBalance, + balance: balance, }; await updateAccount(updatedAccount); invalidateAllAccountQueries(queryClient); @@ -334,7 +345,7 @@ export default function AccountsPage() { // Update cache directly queryClient.setQueryData( ["accounts-with-stats"], - (old: Array | undefined) => { + (old: Array | undefined) => { if (!old) return old; return old.map((a) => (a.id === accountId ? updatedAccount : a)); }, @@ -508,21 +519,25 @@ export default function AccountsPage() { (f: FolderType) => f.id === account.folderId, ); - return ( - - ); + const accountWithStats = accountsWithStats.find( + (a) => a.id === account.id, + ); + return ( + + ); })} @@ -625,12 +640,16 @@ export default function AccountsPage() { (f: FolderType) => f.id === account.folderId, ); + const accountWithStats = accountsWithStats.find( + (a) => a.id === account.id, + ); return ( void; onDelete: (accountId: string) => void; formatCurrency: (amount: number) => string; @@ -42,6 +43,7 @@ export function AccountCard({ account, folder, transactionCount, + calculatedBalance, onEdit, onDelete, formatCurrency, @@ -53,6 +55,8 @@ export function AccountCard({ const isMobile = useIsMobile(); const Icon = accountTypeIcons[account.type]; const realBalance = getAccountBalance(account); + const hasBalanceDifference = calculatedBalance !== undefined && + Math.abs(account.balance - calculatedBalance) > 0.01; const { attributes, @@ -172,21 +176,35 @@ export function AccountCard({ className={cn(isMobile ? "px-2 pb-2 pt-1" : "pt-1", compact && "pt-0")} >
-
= 0 ? "text-emerald-600" : "text-red-600", +
+
= 0 ? "text-emerald-600" : "text-red-600", + )} + > + {formatCurrency(realBalance)} +
+ {!compact && calculatedBalance !== undefined && ( +
+ + Calculé: {formatCurrency(calculatedBalance)} + + {hasBalanceDifference && ( + + (diff: {formatCurrency(account.balance - calculatedBalance)}) + + )} +
)} - > - {formatCurrency(realBalance)}
{compact && (
- + onFormDataChange({ ...formData, - initialBalance: parseFloat(e.target.value) || 0, + totalBalance: parseFloat(e.target.value) || 0, }) } placeholder="0.00" />

- Solde de départ pour équilibrer le compte + Solde total du compte (balance + solde initial) +

+
+
+ + { + const newInitialBalance = parseFloat(e.target.value) || 0; + onFormDataChange({ + ...formData, + initialBalance: newInitialBalance, + // Ajuster le solde total pour maintenir la cohérence + totalBalance: formData.totalBalance, + }); + }} + placeholder="0.00" + /> +

+ Solde de départ pour équilibrer le compte. Le balance sera calculé automatiquement (solde total - solde initial).

diff --git a/dev.db b/dev.db deleted file mode 100644 index e69de29..0000000 diff --git a/lib/hooks.ts b/lib/hooks.ts index 4d7dfc4..d088506 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -199,7 +199,7 @@ export function useAccountsWithStats() { throw new Error("Failed to fetch accounts with stats"); } return response.json() as Promise< - Array + Array >; }, staleTime: 60 * 1000, // 1 minute diff --git a/scripts/fix-account-balances.ts b/scripts/fix-account-balances.ts new file mode 100644 index 0000000..e4be429 --- /dev/null +++ b/scripts/fix-account-balances.ts @@ -0,0 +1,76 @@ +import { prisma } from "../lib/prisma"; + +async function main() { + const accountNumbers = process.argv.slice(2); + + if (accountNumbers.length === 0) { + console.error("Usage: tsx scripts/fix-account-balances.ts [accountNumber2] ..."); + console.error("Exemple: tsx scripts/fix-account-balances.ts 0748461N022 7555880857A"); + process.exit(1); + } + + for (const accountNumber of accountNumbers) { + console.log(`\n=== Correction du compte: ${accountNumber} ===\n`); + + const account = await prisma.account.findFirst({ + where: { + accountNumber: accountNumber, + }, + include: { + transactions: { + select: { + amount: true, + }, + }, + }, + }); + + if (!account) { + console.log(`❌ Compte non trouvé: ${accountNumber}`); + continue; + } + + console.log(`Compte trouvé:`); + console.log(` ID: ${account.id}`); + console.log(` Nom: ${account.name}`); + console.log(` Bank ID: ${account.bankId}`); + console.log(` Balance actuelle: ${account.balance}`); + console.log(` Initial Balance: ${account.initialBalance}`); + console.log(` Transactions: ${account.transactions.length}`); + + // Recalculer le solde à partir des transactions + const calculatedBalance = account.transactions.reduce( + (sum, t) => sum + t.amount, + 0, + ); + + console.log(` Balance calculée: ${calculatedBalance}`); + + if (Math.abs(account.balance - calculatedBalance) > 0.01) { + console.log(`\n⚠️ Différence détectée: ${Math.abs(account.balance - calculatedBalance).toFixed(2)}`); + console.log(`Mise à jour du solde...`); + + await prisma.account.update({ + where: { + id: account.id, + }, + data: { + balance: calculatedBalance, + }, + }); + + console.log(`✅ Solde corrigé: ${calculatedBalance}`); + } else { + console.log(`\n✅ Le solde est déjà correct.`); + } + } + + await prisma.$disconnect(); +} + +main() + .catch((e) => { + console.error("Erreur:", e); + process.exit(1); + }); + diff --git a/scripts/merge-duplicate-accounts.ts b/scripts/merge-duplicate-accounts.ts new file mode 100644 index 0000000..7bf9207 --- /dev/null +++ b/scripts/merge-duplicate-accounts.ts @@ -0,0 +1,168 @@ +import { prisma } from "../lib/prisma"; + +async function main() { + const accountNumber = process.argv[2]; + + if (!accountNumber) { + console.error("Usage: tsx scripts/merge-duplicate-accounts.ts "); + process.exit(1); + } + + console.log(`Fusion des comptes dupliqués pour: ${accountNumber}\n`); + + // Trouver tous les comptes avec ce numéro + const accounts = await prisma.account.findMany({ + where: { + accountNumber: accountNumber, + }, + include: { + transactions: { + select: { + id: true, + date: true, + amount: true, + }, + orderBy: { + date: "desc", + }, + }, + folder: true, + }, + orderBy: { + createdAt: "desc", // Le plus récent en premier + }, + }); + + if (accounts.length < 2) { + console.log("Aucun doublon trouvé."); + return; + } + + console.log(`Trouvé ${accounts.length} comptes à fusionner:\n`); + accounts.forEach((acc, idx) => { + console.log(`Compte ${idx + 1}:`); + console.log(` ID: ${acc.id}`); + console.log(` Bank ID: ${acc.bankId}`); + console.log(` Balance: ${acc.balance}`); + console.log(` Transactions: ${acc.transactions.length}`); + console.log(` Folder: ${acc.folder?.name || "Aucun"}`); + console.log(` Created: ${acc.createdAt}`); + console.log(""); + }); + + // Le compte avec bankId numérique (pas "FR") est le bon - le garder comme principal + // Si pas de bankId numérique, garder le plus récent + const primaryAccount = accounts.find(acc => acc.bankId !== "FR" && acc.bankId !== "") || accounts[0]; + const accountsToMerge = accounts.filter(acc => acc.id !== primaryAccount.id); + + console.log(`\nCompte principal (conservé): ${primaryAccount.id} (bankId: ${primaryAccount.bankId})`); + console.log(`Comptes à fusionner: ${accountsToMerge.map((a) => `${a.id} (bankId: ${a.bankId})`).join(", ")}\n`); + + // Calculer l'initialBalance total (somme des initialBalance) + const totalInitialBalance = accounts.reduce( + (sum, acc) => sum + acc.initialBalance, + 0, + ); + + console.log(`Initial balance totale: ${totalInitialBalance}\n`); + + // Fusionner les transactions + let totalTransactionsMoved = 0; + for (const accountToMerge of accountsToMerge) { + console.log( + `Déplacement des transactions du compte ${accountToMerge.id}...`, + ); + + // Déplacer toutes les transactions vers le compte principal + const updateResult = await prisma.transaction.updateMany({ + where: { + accountId: accountToMerge.id, + }, + data: { + accountId: primaryAccount.id, + }, + }); + + totalTransactionsMoved += updateResult.count; + console.log(` → ${updateResult.count} transactions déplacées`); + + // Supprimer le compte fusionné + await prisma.account.delete({ + where: { + id: accountToMerge.id, + }, + }); + + console.log(` → Compte ${accountToMerge.id} supprimé\n`); + } + + // Recalculer le solde à partir de toutes les transactions du compte fusionné + const allTransactions = await prisma.transaction.findMany({ + where: { + accountId: primaryAccount.id, + }, + select: { + amount: true, + }, + }); + + const calculatedBalance = allTransactions.reduce( + (sum, t) => sum + t.amount, + 0, + ); + + console.log(`Balance calculée à partir des transactions: ${calculatedBalance}`); + + // Mettre à jour la balance du compte principal + // Garder le bankId du compte principal (celui qui est correct) + await prisma.account.update({ + where: { + id: primaryAccount.id, + }, + data: { + balance: calculatedBalance, + initialBalance: totalInitialBalance, + // Garder le bankId du compte principal (le bon) + bankId: primaryAccount.bankId, + // Garder le dernier import le plus récent parmi tous les comptes + lastImport: + accounts.reduce((latest, acc) => { + if (!acc.lastImport) return latest; + if (!latest) return acc.lastImport; + return acc.lastImport > latest ? acc.lastImport : latest; + }, null as string | null), + }, + }); + + console.log(`\n✅ Fusion terminée:`); + console.log(` - Compte principal: ${primaryAccount.id}`); + console.log(` - Transactions totales déplacées: ${totalTransactionsMoved}`); + console.log(` - Nouvelle balance (calculée): ${calculatedBalance}`); + console.log(` - Comptes supprimés: ${accountsToMerge.length}`); + + // Vérification finale + const finalAccount = await prisma.account.findUnique({ + where: { id: primaryAccount.id }, + include: { + transactions: { + select: { id: true }, + }, + }, + }); + + if (finalAccount) { + console.log(`\nVérification finale:`); + console.log(` - Transactions dans le compte: ${finalAccount.transactions.length}`); + console.log(` - Balance: ${finalAccount.balance}`); + console.log(` - Bank ID: ${finalAccount.bankId}`); + } + + await prisma.$disconnect(); +} + +main() + .catch((e) => { + console.error("Erreur:", e); + process.exit(1); + }); + diff --git a/services/banking.service.ts b/services/banking.service.ts index 9431f30..6292202 100644 --- a/services/banking.service.ts +++ b/services/banking.service.ts @@ -369,7 +369,7 @@ export const bankingService = { }, async getAccountsWithStats(): Promise< - Array + Array > { const accounts = await prisma.account.findMany({ include: { @@ -377,32 +377,40 @@ export const bankingService = { }, }); - // Get transaction counts for all accounts in one query - const transactionCounts = await prisma.transaction.groupBy({ + // Get transaction counts and sums for all accounts in one query + const transactionStats = await prisma.transaction.groupBy({ by: ["accountId"], _count: { id: true, }, + _sum: { + amount: true, + }, }); const countMap = new Map(); - transactionCounts.forEach((tc) => { - countMap.set(tc.accountId, tc._count.id); + const balanceMap = new Map(); + transactionStats.forEach((ts) => { + countMap.set(ts.accountId, ts._count.id); + balanceMap.set(ts.accountId, ts._sum.amount || 0); }); - return accounts.map((a): Account & { transactionCount: number } => ({ - id: a.id, - name: a.name, - bankId: a.bankId, - accountNumber: a.accountNumber, - type: a.type as Account["type"], - folderId: a.folderId, - balance: a.balance, - initialBalance: a.initialBalance, - currency: a.currency, - lastImport: a.lastImport, - externalUrl: a.externalUrl, - transactionCount: countMap.get(a.id) || 0, - })); + return accounts.map( + (a): Account & { transactionCount: number; calculatedBalance: number } => ({ + id: a.id, + name: a.name, + bankId: a.bankId, + accountNumber: a.accountNumber, + type: a.type as Account["type"], + folderId: a.folderId, + balance: a.balance, + initialBalance: a.initialBalance, + currency: a.currency, + lastImport: a.lastImport, + externalUrl: a.externalUrl, + transactionCount: countMap.get(a.id) || 0, + calculatedBalance: balanceMap.get(a.id) || 0, + }), + ); }, };