From d059912c1e93533ac8105862260cff5dc5a49cb1 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 17 Jul 2026 21:45:58 +0200 Subject: [PATCH] =?UTF-8?q?Kundenansicht:=20Toggle=20=E2=80=9EDeaktivierte?= =?UTF-8?q?=20Vertr=C3=A4ge=20anzeigen"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Vertragsbaum beim Kunden blendete DEACTIVATED-Verträge komplett aus. Da der aktuellste Vertrag die Baumwurzel ist und Vorgänger als Children hängen, verschwand eine ganze Kette aus der Ansicht, sobald die Wurzel deaktiviert wurde – so „verschwand" ein Vertrag scheinbar, als ein aktiver Folgevertrag gelöscht und der Vorgänger vorher deaktiviert worden war. getContractTreeForCustomer bekommt ein optionales includeDeactivated-Flag (Default false = bisheriges Verhalten), durchgereicht per Query-Param includeDeactivated=true. Toggle- Button (Eye/EyeOff) im Kunden-Vertragstab; showDeactivated ist Teil des Query-Keys → frischer Fetch beim Umschalten. Deaktivierte behalten ihr graues DEACTIVATED-Badge. Co-Authored-By: Claude Opus 4.7 --- .../src/controllers/contract.controller.ts | 5 ++-- backend/src/services/contract.service.ts | 14 ++++++++--- docs/todo.md | 16 ++++++++++++ .../src/pages/customers/CustomerDetail.tsx | 25 +++++++++++++------ frontend/src/services/api.ts | 6 +++-- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index 8c3d2ef0..93a9a742 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -74,12 +74,13 @@ function sanitizeContractBody(body: unknown, parentKey?: string): unknown { export async function getContracts(req: AuthRequest, res: Response): Promise { try { - const { customerId, type, status, search, page, limit, tree } = req.query; + const { customerId, type, status, search, page, limit, tree, includeDeactivated } = req.query; // Baumstruktur für Kundenansicht if (tree === 'true' && customerId) { const treeData = await contractService.getContractTreeForCustomer( - parseInt(customerId as string) + parseInt(customerId as string), + includeDeactivated === 'true', ); res.json({ success: true, data: treeData } as ApiResponse); return; diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts index c4689dbb..5c7291a9 100644 --- a/backend/src/services/contract.service.ts +++ b/backend/src/services/contract.service.ts @@ -1222,12 +1222,20 @@ export interface ContractTreeNode { * Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge) * Vorgänger werden rekursiv eingebettet. */ -export async function getContractTreeForCustomer(customerId: number): Promise { - // Alle Verträge des Kunden laden (außer DEACTIVATED) +export async function getContractTreeForCustomer( + customerId: number, + includeDeactivated = false, +): Promise { + // Alle Verträge des Kunden laden. DEACTIVATED standardmäßig ausgeblendet, + // per Toggle (includeDeactivated) aber sichtbar machen – sonst + // verschwinden ganze Ketten aus der Ansicht, sobald der aktuellste + // (Wurzel-)Vertrag deaktiviert ist. Genau dieser Fall trat auf, als ein + // aktiver Folgevertrag gelöscht wurde und der deaktivierte Vorgänger zur + // neuen (unsichtbaren) Wurzel wurde. const allContracts = await prisma.contract.findMany({ where: { customerId, - status: { not: ContractStatus.DEACTIVATED }, + ...(includeDeactivated ? {} : { status: { not: ContractStatus.DEACTIVATED } }), }, select: { id: true, diff --git a/docs/todo.md b/docs/todo.md index 466ba858..2d09afb0 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **👁 Kundenansicht: Toggle „Deaktivierte Verträge anzeigen"** + - Der Vertragsbaum beim Kunden (`CustomerDetail` → Tab Verträge) + blendete `DEACTIVATED`-Verträge komplett aus. Da der jeweils + aktuellste Vertrag die Baumwurzel ist und Vorgänger als Children + hängen, verschwand eine ganze Kette aus der Ansicht, sobald die + Wurzel deaktiviert wurde – genau das passierte, als ein aktiver + Folgevertrag gelöscht und der Vorgänger vorher auf „deaktiviert" + gesetzt worden war. Der Vertrag „verschwand" scheinbar, war aber + nur unsichtbar gefiltert. + - Fix: `getContractTreeForCustomer(customerId, includeDeactivated)` + mit optionalem Flag (Default false = bisheriges Verhalten). Query- + Param `includeDeactivated=true`, Toggle-Button im Kunden-Vertragstab + (`Eye`/`EyeOff`). `showDeactivated` ist Teil des Query-Keys → + frischer Fetch beim Umschalten. Deaktivierte Verträge tragen ihr + graues `DEACTIVATED`-Badge, bleiben also klar erkennbar. + - [x] **🐞 Vertrag-Löschen: Kette unterbrach, Liste aktualisierte nicht** - Zwei zusammenspielende Bugs: (1) im Service scheiterte das Umbiegen des Folgevertrags an der `@unique`-Regel auf `Contract.previousContractId` diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index fceda6f3..0f369daa 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -1691,11 +1691,13 @@ function ContractsTab({ const queryClient = useQueryClient(); const [expandedContracts, setExpandedContracts] = useState>(new Set()); const [showStatusInfo, setShowStatusInfo] = useState(false); + const [showDeactivated, setShowDeactivated] = useState(false); - // Lade Vertragsbaum statt flacher Liste + // Lade Vertragsbaum statt flacher Liste. showDeactivated ist Teil des + // Query-Keys, damit der Toggle einen frischen Fetch auslöst. const { data: treeData, isLoading } = useQuery({ - queryKey: ['contract-tree', customerId], - queryFn: () => contractApi.getTreeForCustomer(customerId), + queryKey: ['contract-tree', customerId, showDeactivated], + queryFn: () => contractApi.getTreeForCustomer(customerId, showDeactivated), }); const contractTree = treeData?.data || []; @@ -1898,16 +1900,25 @@ function ContractsTab({ return (
- {hasPermission('contracts:create') && ( -
+
+ {hasPermission('contracts:create') ? ( -
- )} + ) : } + +
{contractTree.length > 0 ? (
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 6aa98268..721fb3da 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -815,8 +815,10 @@ export const contractApi = { const res = await api.get>('/contracts', { params }); return res.data; }, - getTreeForCustomer: async (customerId: number) => { - const res = await api.get>('/contracts', { params: { customerId, tree: 'true' } }); + getTreeForCustomer: async (customerId: number, includeDeactivated = false) => { + const res = await api.get>('/contracts', { + params: { customerId, tree: 'true', ...(includeDeactivated ? { includeDeactivated: 'true' } : {}) }, + }); return res.data; }, getById: async (id: number) => {