import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { providerApi, tariffApi } from '../../services/api'; import { useAuth } from '../../context/AuthContext'; import Card from '../../components/ui/Card'; import Button from '../../components/ui/Button'; import Input from '../../components/ui/Input'; import Modal from '../../components/ui/Modal'; import Badge from '../../components/ui/Badge'; import { Plus, Edit, Trash2, ArrowLeft, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react'; import { Link } from 'react-router-dom'; import type { Provider, Tariff } from '../../types'; export default function ProviderList() { const [showModal, setShowModal] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [showInactive, setShowInactive] = useState(false); const [expandedProviders, setExpandedProviders] = useState>(new Set()); const { hasPermission } = useAuth(); const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['providers', showInactive], queryFn: () => providerApi.getAll(showInactive), }); const deleteMutation = useMutation({ mutationFn: providerApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['providers'] }); }, onError: (error: Error) => { alert(error.message); }, }); const toggleExpanded = (providerId: number) => { setExpandedProviders(prev => { const next = new Set(prev); if (next.has(providerId)) { next.delete(providerId); } else { next.add(providerId); } return next; }); }; const handleEdit = (provider: Provider) => { setEditingProvider(provider); setShowModal(true); }; const handleClose = () => { setShowModal(false); setEditingProvider(null); }; return (

Anbieter & Tarife

{hasPermission('providers:create') && ( )}
{isLoading ? (
Laden...
) : data?.data && data.data.length > 0 ? (
{data.data.map((provider) => ( toggleExpanded(provider.id)} onEdit={() => handleEdit(provider)} onDelete={() => { if (confirm('Anbieter wirklich löschen?')) { deleteMutation.mutate(provider.id); } }} hasPermission={hasPermission} showInactive={showInactive} /> ))}
) : (
Keine Anbieter vorhanden.
)}
); } function ProviderRow({ provider, isExpanded, onToggle, onEdit, onDelete, hasPermission, showInactive, }: { provider: Provider; isExpanded: boolean; onToggle: () => void; onEdit: () => void; onDelete: () => void; hasPermission: (permission: string) => boolean; showInactive: boolean; }) { const [showTariffModal, setShowTariffModal] = useState(false); const [editingTariff, setEditingTariff] = useState(null); const queryClient = useQueryClient(); const deleteTariffMutation = useMutation({ mutationFn: tariffApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['providers'] }); }, onError: (error: Error) => { alert(error.message); }, }); const tariffs = provider.tariffs?.filter(t => showInactive || t.isActive) || []; return (
{provider.name} {provider.isActive ? 'Aktiv' : 'Inaktiv'} ({tariffs.length} Tarife, {provider._count?.contracts || 0} Verträge)
{provider.portalUrl && ( {provider.portalUrl} )}
{hasPermission('providers:update') && ( )} {hasPermission('providers:delete') && ( )}
{isExpanded && (

Tarife

{hasPermission('providers:create') && ( )}
{tariffs.length > 0 ? (
{tariffs.map((tariff) => (
{tariff.name} {tariff.isActive ? 'Aktiv' : 'Inaktiv'} {tariff._count?.contracts !== undefined && ( ({tariff._count.contracts} Verträge) )}
{hasPermission('providers:update') && ( )} {hasPermission('providers:delete') && ( )}
))}
) : (

Keine Tarife vorhanden.

)}
)} { setShowTariffModal(false); setEditingTariff(null); }} providerId={provider.id} tariff={editingTariff} />
); } function ProviderModal({ isOpen, onClose, provider, }: { isOpen: boolean; onClose: () => void; provider: Provider | null; }) { const queryClient = useQueryClient(); const [formData, setFormData] = useState({ name: '', portalUrl: '', usernameFieldName: '', passwordFieldName: '', contactEmail: '', contactPhone: '', contactFax: '', contactAddress: '', cancellationEmail: '', cancellationFax: '', cancellationAddress: '', isActive: true, // Pentest 47.1: bei Portal-URL-Domain-Wechsel muss der aufrufende // Admin sein eigenes Passwort mitsenden – Schutz gegen kompromittierten // JWT, der sonst Phishing-URLs auf existierende Anbieter setzen könnte. currentPassword: '', }); const originalPortalUrl = provider?.portalUrl ?? ''; // Pentest 49.1: Jede URL-Änderung (inkl. Pfad/Query) braucht Re-Auth – // nicht nur Host-Wechsel. Normalisierung (Trailing-Slash, Whitespace, // Case) passend zum Backend, damit der Banner mit der Backend-Prüfung // übereinstimmt. const normalizeUrl = (u: string) => u.trim().replace(/\/+$/, '').toLowerCase(); const portalUrlChanged = normalizeUrl(formData.portalUrl) !== normalizeUrl(originalPortalUrl); const portalUrlSetOnCreate = !provider && !!formData.portalUrl.trim(); const needsReAuth = portalUrlChanged || portalUrlSetOnCreate; useEffect(() => { if (isOpen) { if (provider) { setFormData({ name: provider.name, portalUrl: provider.portalUrl || '', usernameFieldName: provider.usernameFieldName || '', passwordFieldName: provider.passwordFieldName || '', contactEmail: provider.contactEmail || '', contactPhone: provider.contactPhone || '', contactFax: provider.contactFax || '', contactAddress: provider.contactAddress || '', cancellationEmail: provider.cancellationEmail || '', cancellationFax: provider.cancellationFax || '', cancellationAddress: provider.cancellationAddress || '', isActive: provider.isActive, currentPassword: '', }); } else { setFormData({ name: '', portalUrl: '', usernameFieldName: '', passwordFieldName: '', contactEmail: '', contactPhone: '', contactFax: '', contactAddress: '', cancellationEmail: '', cancellationFax: '', cancellationAddress: '', isActive: true, currentPassword: '', }); } } }, [isOpen, provider]); const createMutation = useMutation({ mutationFn: providerApi.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['providers'] }); onClose(); }, onError: (error: Error) => { alert(error.message); }, }); const updateMutation = useMutation({ mutationFn: (data: Partial) => providerApi.update(provider!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['providers'] }); onClose(); }, onError: (error: Error) => { alert(error.message); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (needsReAuth && !formData.currentPassword) { alert('Bitte das eigene Passwort zur Bestätigung der Portal-URL eingeben.'); return; } // currentPassword wird nur mitgesendet wenn überhaupt nötig const payload: any = { ...formData }; if (!needsReAuth) delete payload.currentPassword; if (provider) { updateMutation.mutate(payload); } else { createMutation.mutate(payload); } }; const isLoading = createMutation.isPending || updateMutation.isPending; return (
setFormData({ ...formData, name: e.target.value })} required placeholder="z.B. Vodafone, E.ON, Allianz" /> setFormData({ ...formData, portalUrl: e.target.value })} placeholder="https://kundenportal.anbieter.de/login" /> {needsReAuth && (

Bestätigung erforderlich:{' '} {provider ? 'Die Portal-URL wurde geändert. Diese URL ist anschließend für alle Portal-Kunden dieses Anbieters klickbar.' : 'Mit dem Speichern wird die Portal-URL für alle Portal-Kunden dieses Anbieters klickbar.'} {' '}Zur Sicherheit ist eine Bestätigung mit dem eigenen Passwort nötig.

setFormData({ ...formData, currentPassword: e.target.value })} required autoComplete="current-password" />
)}

Auto-Login Felder (optional)
Feldnamen für URL-Parameter beim Auto-Login:

setFormData({ ...formData, usernameFieldName: e.target.value })} placeholder="z.B. username, email, login" /> setFormData({ ...formData, passwordFieldName: e.target.value })} placeholder="z.B. password, pwd, kennwort" />

Kontakt & Kündigung (optional)
Erreichbarkeit des Anbieters – wird im CRM zum Nachschlagen angezeigt, nicht an Portal-Kunden ausgespielt.

Kontakt
setFormData({ ...formData, contactEmail: e.target.value })} placeholder="z.B. service@anbieter.de" /> setFormData({ ...formData, contactPhone: e.target.value })} placeholder="z.B. +49 30 1234567" /> setFormData({ ...formData, contactFax: e.target.value })} placeholder="z.B. +49 30 7654321" />