feat: add account merging functionality with dialog support; update bulk actions to include merge option
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m39s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m39s
This commit is contained in:
@@ -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<string | null>(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() {
|
||||
<>
|
||||
<AccountBulkActions
|
||||
selectedCount={selectedAccounts.size}
|
||||
selectedAccountIds={Array.from(selectedAccounts)}
|
||||
onDelete={handleBulkDelete}
|
||||
onMerge={
|
||||
selectedAccounts.size === 2
|
||||
? () => setIsMergeDialogOpen(true)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@@ -715,6 +760,21 @@ export default function AccountsPage() {
|
||||
folders={metadata.folders}
|
||||
onSave={handleSaveFolder}
|
||||
/>
|
||||
|
||||
<AccountMergeSelectDialog
|
||||
open={isMergeDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
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}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
129
app/api/banking/accounts/merge/route.ts
Normal file
129
app/api/banking/accounts/merge/route.ts
Normal file
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user