Vertrag-Löschen: Kette bleibt intakt, Liste refresht

Zwei zusammenspielende Bugs:

1) Backend: deleteContract-Reihenfolge scheiterte am UNIQUE-
   Constraint auf Contract.previousContractId. Beim Middle-Delete
   (A → B → C, B löschen) hielt B im UPDATE-Moment noch selbst
   previousContractId=A – der Versuch, C ebenfalls auf A
   umzubiegen, warf MariaDB Duplicate-Entry, das UPDATE brach ab.

   Fix: Transaction, umgedrehte Reihenfolge – erst B löschen
   (DB-Cascade ON DELETE SET NULL räumt C.previousContractId ab,
   gibt A-Slot frei), dann C sauber auf A umbiegen.

2) Frontend: deleteMutation invalidierte weder Contract-Listen
   noch Kunden-Vertragsbaum und hatte keinen onError-Handler.
   Nach dem Delete wurde nach /contracts navigiert, dort zeigte
   der stale Cache noch den gelöschten Vertrag – Eindruck: „ganze
   Historie weg". Bei Bug 1 fehlgeschlagen brach das schweigend
   ab.

   Fix: queryClient.invalidateQueries für ['contracts'],
   ['contract-tree', customerId], ['customer', customerId] nach
   Erfolg. onError-Toast bei Fehler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 20:52:02 +02:00
co-authored by Claude Opus 4.7
parent 66fb5092fe
commit d0611b740a
3 changed files with 71 additions and 14 deletions
+39 -14
View File
@@ -707,22 +707,47 @@ export async function updateContract(
export async function deleteContract(id: number) {
// Vertragskette erhalten beim Löschen:
// Wenn A → B → C und B gelöscht wird, soll C direkt auf A zeigen (A → C)
// Wenn A → B → C und B gelöscht wird, soll C direkt auf A zeigen (A → C).
//
// Hintergrund: `Contract.previousContractId` hat ein UNIQUE-Constraint
// (jeder Vertrag darf nur EINEN Vorgänger sein, umgekehrt gilt auch:
// ein Vorgänger kann nur EINEN Nachfolger haben). Naive Reihenfolge
// „C erst auf A umbiegen, dann B löschen" scheitert an genau diesem
// Constraint, weil B in dem Moment noch selbst auf A zeigt →
// Duplicate-Entry-Fehler (Prisma P2002) und das UPDATE bricht ab.
// Vorher landete der User dadurch bei einem 400-Fehler, das Cache-
// Invalidate war ebenfalls nicht sauber (siehe Frontend-Fix), so dass
// es wie „Vertrag verschwindet aber ist noch da" aussah.
//
// Fix: erst B löschen (DB-Cascade `ON DELETE SET NULL` räumt
// C.previousContractId auf null ab und gibt damit den A-Slot wieder
// frei), dann C sauber auf A umbiegen. Alles in einer Transaktion,
// damit kein Zwischenzustand sichtbar wird und ein Fehler die
// gesamte Operation zurückrollt.
return prisma.$transaction(async (tx) => {
const contractToDelete = await tx.contract.findUnique({
where: { id },
select: { previousContractId: true },
});
const followUp = await tx.contract.findFirst({
where: { previousContractId: id },
select: { id: true },
});
// 1. Zu löschenden Vertrag holen um dessen Vorgänger zu kennen
const contractToDelete = await prisma.contract.findUnique({
where: { id },
select: { previousContractId: true },
const deleted = await tx.contract.delete({ where: { id } });
// Kette schließen, falls sowohl Vorgänger (A) als auch Folgevertrag
// (C) existieren. Ohne einen der beiden ist nichts zu tun der
// verbleibende Endpunkt wird zum neuen Kettenanfang bzw. -ende.
if (followUp && contractToDelete?.previousContractId) {
await tx.contract.update({
where: { id: followUp.id },
data: { previousContractId: contractToDelete.previousContractId },
});
}
return deleted;
});
// 2. Folgevertrag(e) mit dem Vorgänger des gelöschten Vertrags verbinden
// So bleibt die Kette erhalten: A → B → C wird zu A → C
await prisma.contract.updateMany({
where: { previousContractId: id },
data: { previousContractId: contractToDelete?.previousContractId ?? null },
});
return prisma.contract.delete({ where: { id } });
}
export async function createFollowUpContract(previousContractId: number) {