diff --git a/app/accounts/page.tsx b/app/accounts/page.tsx index 16462de..08c7170 100644 --- a/app/accounts/page.tsx +++ b/app/accounts/page.tsx @@ -16,6 +16,7 @@ import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; import { AccountCard, AccountEditDialog, + AccountMergeSelectDialog, AccountBulkActions, } from "@/components/accounts"; import { FolderEditDialog } from "@/components/folders"; @@ -112,6 +113,7 @@ export default function AccountsPage() { }); const [activeId, setActiveId] = useState(null); const [isCompactView, setIsCompactView] = useState(false); + const [isMergeDialogOpen, setIsMergeDialogOpen] = useState(false); const sensors = useSensors( useSensor(PointerSensor, { @@ -300,6 +302,43 @@ export default function AccountsPage() { } }; + const handleMergeAccounts = async ( + sourceAccountId: string, + targetAccountId: string, + ) => { + try { + const response = await fetch("/api/banking/accounts/merge", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sourceAccountId, + targetAccountId, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to merge accounts"); + } + + const result = await response.json(); + invalidateAllAccountQueries(queryClient); + + // Réinitialiser la sélection + setSelectedAccounts(new Set()); + + // Afficher un message de succès + alert( + `Fusion réussie ! ${result.transactionCount} transactions déplacées vers le compte de destination.`, + ); + } catch (error) { + console.error("Error merging accounts:", error); + throw error; + } + }; + // Drag and drop handlers const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string); @@ -469,7 +508,13 @@ export default function AccountsPage() { <> setIsMergeDialogOpen(true) + : undefined + } /> + + { + setIsMergeDialogOpen(open); + if (!open) { + // Réinitialiser la sélection après fusion + setSelectedAccounts(new Set()); + } + }} + accounts={accounts} + selectedAccountIds={Array.from(selectedAccounts)} + onMerge={handleMergeAccounts} + formatCurrency={formatCurrency} + /> ); } diff --git a/app/api/banking/accounts/merge/route.ts b/app/api/banking/accounts/merge/route.ts new file mode 100644 index 0000000..67d6b3f --- /dev/null +++ b/app/api/banking/accounts/merge/route.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth-utils"; + +export async function POST(request: NextRequest) { + const authError = await requireAuth(); + if (authError) return authError; + + try { + const { sourceAccountId, targetAccountId } = await request.json(); + + if (!sourceAccountId || !targetAccountId) { + return NextResponse.json( + { error: "Source and target account IDs are required" }, + { status: 400 }, + ); + } + + if (sourceAccountId === targetAccountId) { + return NextResponse.json( + { error: "Source and target accounts must be different" }, + { status: 400 }, + ); + } + + // Vérifier que les comptes existent + const [sourceAccount, targetAccount] = await Promise.all([ + prisma.account.findUnique({ + where: { id: sourceAccountId }, + include: { + transactions: { + select: { + id: true, + }, + }, + }, + }), + prisma.account.findUnique({ + where: { id: targetAccountId }, + }), + ]); + + if (!sourceAccount || !targetAccount) { + return NextResponse.json( + { error: "One or both accounts not found" }, + { status: 404 }, + ); + } + + const transactionCount = sourceAccount.transactions.length; + + // Déplacer toutes les transactions vers le compte cible + await prisma.transaction.updateMany({ + where: { + accountId: sourceAccountId, + }, + data: { + accountId: targetAccountId, + }, + }); + + // Recalculer le solde du compte cible à partir de toutes ses transactions + const allTransactions = await prisma.transaction.findMany({ + where: { + accountId: targetAccountId, + }, + select: { + amount: true, + }, + }); + + const calculatedBalance = allTransactions.reduce( + (sum, t) => sum + t.amount, + 0, + ); + + // Calculer l'initialBalance total (somme des deux comptes) + const totalInitialBalance = + sourceAccount.initialBalance + targetAccount.initialBalance; + + // Mettre à jour le compte cible + await prisma.account.update({ + where: { + id: targetAccountId, + }, + data: { + balance: calculatedBalance, + initialBalance: totalInitialBalance, + // Garder le bankId du compte cible (celui qui est conservé) + // Garder le dernier import le plus récent + lastImport: + sourceAccount.lastImport && targetAccount.lastImport + ? sourceAccount.lastImport > targetAccount.lastImport + ? sourceAccount.lastImport + : targetAccount.lastImport + : sourceAccount.lastImport || targetAccount.lastImport, + }, + }); + + // Supprimer le compte source + await prisma.account.delete({ + where: { + id: sourceAccountId, + }, + }); + + // Revalider les caches + revalidatePath("/accounts", "page"); + revalidatePath("/transactions", "page"); + revalidatePath("/statistics", "page"); + revalidatePath("/dashboard", "page"); + + return NextResponse.json({ + success: true, + message: `Fusion réussie : ${transactionCount} transactions déplacées`, + targetAccountId, + transactionCount, + calculatedBalance, + }); + } catch (error) { + console.error("Error merging accounts:", error); + return NextResponse.json( + { error: "Failed to merge accounts" }, + { status: 500 }, + ); + } +} + diff --git a/components/accounts/account-bulk-actions.tsx b/components/accounts/account-bulk-actions.tsx index 2333825..2c5d027 100644 --- a/components/accounts/account-bulk-actions.tsx +++ b/components/accounts/account-bulk-actions.tsx @@ -2,29 +2,35 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Trash2 } from "lucide-react"; +import { Trash2, GitMerge } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; interface AccountBulkActionsProps { selectedCount: number; + selectedAccountIds: string[]; onDelete: () => void; + onMerge?: () => void; } export function AccountBulkActions({ selectedCount, + selectedAccountIds: _selectedAccountIds, onDelete, + onMerge, }: AccountBulkActionsProps) { const isMobile = useIsMobile(); if (selectedCount === 0) return null; + const canMerge = selectedCount === 2 && onMerge; + return (
@@ -32,6 +38,19 @@ export function AccountBulkActions({ {selectedCount} compte{selectedCount > 1 ? "s" : ""} sélectionné {selectedCount > 1 ? "s" : ""} + {canMerge && ( + + )} + +
+ + + + ); +} + diff --git a/components/accounts/index.ts b/components/accounts/index.ts index 17bac58..f1a2789 100644 --- a/components/accounts/index.ts +++ b/components/accounts/index.ts @@ -1,4 +1,5 @@ export { AccountCard } from "./account-card"; export { AccountEditDialog } from "./account-edit-dialog"; +export { AccountMergeSelectDialog } from "./account-merge-select-dialog"; export { AccountBulkActions } from "./account-bulk-actions"; export { accountTypeIcons, accountTypeLabels } from "./constants";