Kundenansicht: Toggle „Deaktivierte Verträge anzeigen"
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 <noreply@anthropic.com>
This commit is contained in:
@@ -74,12 +74,13 @@ function sanitizeContractBody(body: unknown, parentKey?: string): unknown {
|
|||||||
|
|
||||||
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
|
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
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
|
// Baumstruktur für Kundenansicht
|
||||||
if (tree === 'true' && customerId) {
|
if (tree === 'true' && customerId) {
|
||||||
const treeData = await contractService.getContractTreeForCustomer(
|
const treeData = await contractService.getContractTreeForCustomer(
|
||||||
parseInt(customerId as string)
|
parseInt(customerId as string),
|
||||||
|
includeDeactivated === 'true',
|
||||||
);
|
);
|
||||||
res.json({ success: true, data: treeData } as ApiResponse);
|
res.json({ success: true, data: treeData } as ApiResponse);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1222,12 +1222,20 @@ export interface ContractTreeNode {
|
|||||||
* Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge)
|
* Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge)
|
||||||
* Vorgänger werden rekursiv eingebettet.
|
* Vorgänger werden rekursiv eingebettet.
|
||||||
*/
|
*/
|
||||||
export async function getContractTreeForCustomer(customerId: number): Promise<ContractTreeNode[]> {
|
export async function getContractTreeForCustomer(
|
||||||
// Alle Verträge des Kunden laden (außer DEACTIVATED)
|
customerId: number,
|
||||||
|
includeDeactivated = false,
|
||||||
|
): Promise<ContractTreeNode[]> {
|
||||||
|
// 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({
|
const allContracts = await prisma.contract.findMany({
|
||||||
where: {
|
where: {
|
||||||
customerId,
|
customerId,
|
||||||
status: { not: ContractStatus.DEACTIVATED },
|
...(includeDeactivated ? {} : { status: { not: ContractStatus.DEACTIVATED } }),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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**
|
- [x] **🐞 Vertrag-Löschen: Kette unterbrach, Liste aktualisierte nicht**
|
||||||
- Zwei zusammenspielende Bugs: (1) im Service scheiterte das Umbiegen
|
- Zwei zusammenspielende Bugs: (1) im Service scheiterte das Umbiegen
|
||||||
des Folgevertrags an der `@unique`-Regel auf `Contract.previousContractId`
|
des Folgevertrags an der `@unique`-Regel auf `Contract.previousContractId`
|
||||||
|
|||||||
@@ -1691,11 +1691,13 @@ function ContractsTab({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [expandedContracts, setExpandedContracts] = useState<Set<number>>(new Set());
|
const [expandedContracts, setExpandedContracts] = useState<Set<number>>(new Set());
|
||||||
const [showStatusInfo, setShowStatusInfo] = useState(false);
|
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({
|
const { data: treeData, isLoading } = useQuery({
|
||||||
queryKey: ['contract-tree', customerId],
|
queryKey: ['contract-tree', customerId, showDeactivated],
|
||||||
queryFn: () => contractApi.getTreeForCustomer(customerId),
|
queryFn: () => contractApi.getTreeForCustomer(customerId, showDeactivated),
|
||||||
});
|
});
|
||||||
|
|
||||||
const contractTree = treeData?.data || [];
|
const contractTree = treeData?.data || [];
|
||||||
@@ -1898,16 +1900,25 @@ function ContractsTab({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{hasPermission('contracts:create') && (
|
<div className="mb-4 flex items-center justify-between gap-3 flex-wrap">
|
||||||
<div className="mb-4">
|
{hasPermission('contracts:create') ? (
|
||||||
<Link to={`/contracts/new?customerId=${customerId}`}>
|
<Link to={`/contracts/new?customerId=${customerId}`}>
|
||||||
<Button size="sm">
|
<Button size="sm">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Vertrag anlegen
|
Vertrag anlegen
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
) : <span />}
|
||||||
)}
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={showDeactivated ? 'secondary' : 'ghost'}
|
||||||
|
onClick={() => setShowDeactivated((v) => !v)}
|
||||||
|
title="Deaktivierte Verträge in der Liste ein-/ausblenden"
|
||||||
|
>
|
||||||
|
{showDeactivated ? <EyeOff className="w-4 h-4 mr-2" /> : <Eye className="w-4 h-4 mr-2" />}
|
||||||
|
{showDeactivated ? 'Deaktivierte ausblenden' : 'Deaktivierte anzeigen'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{contractTree.length > 0 ? (
|
{contractTree.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -815,8 +815,10 @@ export const contractApi = {
|
|||||||
const res = await api.get<ApiResponse<Contract[]>>('/contracts', { params });
|
const res = await api.get<ApiResponse<Contract[]>>('/contracts', { params });
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
getTreeForCustomer: async (customerId: number) => {
|
getTreeForCustomer: async (customerId: number, includeDeactivated = false) => {
|
||||||
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', { params: { customerId, tree: 'true' } });
|
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', {
|
||||||
|
params: { customerId, tree: 'true', ...(includeDeactivated ? { includeDeactivated: 'true' } : {}) },
|
||||||
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
getById: async (id: number) => {
|
getById: async (id: number) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user