Kundenportal: Toggle "Deaktivierte Verträge anzeigen"

Der Admin-/Mitarbeiter-Blick (CustomerDetail) hatte den Toggle schon;
jetzt auch in der Kundenportal-Vertragsübersicht (ContractList,
Portal-Zweig). Button nur bei isCustomerPortal, reicht
includeDeactivated an die getTreeForCustomer-Queries durch
(showDeactivated im Query-Key → frischer Fetch beim Umschalten).

Security: derselbe Endpoint hinter canAccessCustomer (R120) – der
Portal-Kunde bekommt nur eigene/vertretene Bäume, das Flag weitet
nur den Status-Filter innerhalb der erlaubten Daten. Keine neue
Exposition.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:48:24 +02:00
co-authored by Claude Opus 4.7
parent e8d996d07e
commit fd8be1d5c4
2 changed files with 37 additions and 10 deletions
+11
View File
@@ -97,6 +97,17 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **👁 Kundenportal: Toggle „Deaktivierte Verträge anzeigen"**
- Der Admin-/Mitarbeiter-Blick (CustomerDetail) hatte den Toggle schon;
jetzt auch in der Kundenportal-Vertragsübersicht (`ContractList`,
Portal-Zweig). Button nur bei `isCustomerPortal`, reicht
`includeDeactivated` an die `getTreeForCustomer`-Queries durch
(`showDeactivated` im Query-Key → frischer Fetch beim Umschalten).
- Security: nutzt denselben Endpoint hinter `canAccessCustomer`
(R120) Portal-Kunde sieht nur eigene/vertretene Bäume, das Flag
weitet nur den Status-Filter innerhalb der erlaubten Daten. Keine
neue Exposition.
- [x] **🔒 Mobilfunknetz: server-seitige Whitelist (Pentest-INFO)**
- `mobileNetwork` akzeptierte serverseitig jeden (stripHtml-bereinigten)
String das Frontend-Dropdown beschränkte nur clientseitig. Jetzt
+26 -10
View File
@@ -10,7 +10,7 @@ import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import Badge from '../../components/ui/Badge';
import CopyButton from '../../components/ui/CopyButton';
import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X, ShieldAlert } from 'lucide-react';
import { Plus, Search, Eye, EyeOff, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X, ShieldAlert } from 'lucide-react';
import { gdprApi } from '../../services/api';
import { formatDate } from '../../utils/dateFormat';
import { getContractTypeInfo } from '../../utils/contractInfo';
@@ -100,6 +100,9 @@ export default function ContractList() {
// Status-Info Modal
const [showStatusInfo, setShowStatusInfo] = useState(false);
// Toggle: deaktivierte Verträge im Kundenportal-Baum mit anzeigen
const [showDeactivated, setShowDeactivated] = useState(false);
const { hasPermission, isCustomer, isCustomerPortal, user } = useAuth();
const queryClient = useQueryClient();
@@ -167,8 +170,8 @@ export default function ContractList() {
// Baumstruktur für alle Kunden laden (Kundenportal)
const treeQueries = useQueries({
queries: allCustomerIds.map(customerId => ({
queryKey: ['contract-tree', customerId],
queryFn: () => contractApi.getTreeForCustomer(customerId),
queryKey: ['contract-tree', customerId, showDeactivated],
queryFn: () => contractApi.getTreeForCustomer(customerId, showDeactivated),
enabled: isCustomerPortal,
})),
});
@@ -341,14 +344,27 @@ export default function ContractList() {
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">Verträge</h1>
{hasPermission('contracts:create') && !isCustomer && (
<Link to="/contracts/new">
<Button>
<Plus className="w-4 h-4 mr-2" />
Neuer Vertrag
<div className="flex items-center gap-3">
{isCustomerPortal && (
<Button
size="sm"
variant={showDeactivated ? 'secondary' : 'ghost'}
onClick={() => setShowDeactivated((v) => !v)}
title="Deaktivierte Verträge 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>
</Link>
)}
)}
{hasPermission('contracts:create') && !isCustomer && (
<Link to="/contracts/new">
<Button>
<Plus className="w-4 h-4 mr-2" />
Neuer Vertrag
</Button>
</Link>
)}
</div>
</div>
<Card className="mb-6">