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> {
|
||||
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;
|
||||
|
||||
@@ -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<ContractTreeNode[]> {
|
||||
// Alle Verträge des Kunden laden (außer DEACTIVATED)
|
||||
export async function getContractTreeForCustomer(
|
||||
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({
|
||||
where: {
|
||||
customerId,
|
||||
status: { not: ContractStatus.DEACTIVATED },
|
||||
...(includeDeactivated ? {} : { status: { not: ContractStatus.DEACTIVATED } }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -1691,11 +1691,13 @@ function ContractsTab({
|
||||
const queryClient = useQueryClient();
|
||||
const [expandedContracts, setExpandedContracts] = useState<Set<number>>(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 (
|
||||
<div>
|
||||
{hasPermission('contracts:create') && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-4 flex items-center justify-between gap-3 flex-wrap">
|
||||
{hasPermission('contracts:create') ? (
|
||||
<Link to={`/contracts/new?customerId=${customerId}`}>
|
||||
<Button size="sm">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Vertrag anlegen
|
||||
</Button>
|
||||
</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 ? (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -815,8 +815,10 @@ export const contractApi = {
|
||||
const res = await api.get<ApiResponse<Contract[]>>('/contracts', { params });
|
||||
return res.data;
|
||||
},
|
||||
getTreeForCustomer: async (customerId: number) => {
|
||||
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', { params: { customerId, tree: 'true' } });
|
||||
getTreeForCustomer: async (customerId: number, includeDeactivated = false) => {
|
||||
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', {
|
||||
params: { customerId, tree: 'true', ...(includeDeactivated ? { includeDeactivated: 'true' } : {}) },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
getById: async (id: number) => {
|
||||
|
||||
Reference in New Issue
Block a user