Sieben neue optionale Felder am Provider (contactEmail, contactPhone, contactFax, contactAddress, cancellationEmail, cancellationFax, cancellationAddress). Postadressen TEXT, Rest VARCHAR(191). Migration mit IF NOT EXISTS. Modal "Anbieter bearbeiten" bekommt neue Sektion "Kontakt & Kündigung" mit zwei Untergruppen. Backend validiert Emails gegen isValidEmail (Header-Injection-Schutz), Telefon/Fax gegen sanitizePhoneField (kein CRLF), Postadressen via sanitizeNotes mit 500-Cap. Factory-Defaults Export/Import mitgezogen. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
654 lines
22 KiB
TypeScript
654 lines
22 KiB
TypeScript
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<Provider | null>(null);
|
||
const [showInactive, setShowInactive] = useState(false);
|
||
const [expandedProviders, setExpandedProviders] = useState<Set<number>>(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 (
|
||
<div>
|
||
<div className="flex items-center gap-4 mb-6">
|
||
<Link to="/settings">
|
||
<Button variant="ghost" size="sm">
|
||
<ArrowLeft className="w-4 h-4" />
|
||
</Button>
|
||
</Link>
|
||
<h1 className="text-2xl font-bold flex-1">Anbieter & Tarife</h1>
|
||
{hasPermission('providers:create') && (
|
||
<Button onClick={() => setShowModal(true)}>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Neuer Anbieter
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
<Card>
|
||
<div className="mb-4">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={showInactive}
|
||
onChange={(e) => setShowInactive(e.target.checked)}
|
||
className="rounded"
|
||
/>
|
||
Inaktive anzeigen
|
||
</label>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div className="text-center py-8 text-gray-500">Laden...</div>
|
||
) : data?.data && data.data.length > 0 ? (
|
||
<div className="space-y-2">
|
||
{data.data.map((provider) => (
|
||
<ProviderRow
|
||
key={provider.id}
|
||
provider={provider}
|
||
isExpanded={expandedProviders.has(provider.id)}
|
||
onToggle={() => toggleExpanded(provider.id)}
|
||
onEdit={() => handleEdit(provider)}
|
||
onDelete={() => {
|
||
if (confirm('Anbieter wirklich löschen?')) {
|
||
deleteMutation.mutate(provider.id);
|
||
}
|
||
}}
|
||
hasPermission={hasPermission}
|
||
showInactive={showInactive}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-8 text-gray-500">Keine Anbieter vorhanden.</div>
|
||
)}
|
||
</Card>
|
||
|
||
<ProviderModal
|
||
isOpen={showModal}
|
||
onClose={handleClose}
|
||
provider={editingProvider}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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<Tariff | null>(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 (
|
||
<div className="border rounded-lg">
|
||
<div className="flex items-center p-4 hover:bg-gray-50">
|
||
<button onClick={onToggle} className="mr-3 p-1 hover:bg-gray-200 rounded">
|
||
{isExpanded ? (
|
||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||
) : (
|
||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||
)}
|
||
</button>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="font-medium">{provider.name}</span>
|
||
<Badge variant={provider.isActive ? 'success' : 'danger'}>
|
||
{provider.isActive ? 'Aktiv' : 'Inaktiv'}
|
||
</Badge>
|
||
<span className="text-sm text-gray-500">
|
||
({tariffs.length} Tarife, {provider._count?.contracts || 0} Verträge)
|
||
</span>
|
||
</div>
|
||
{provider.portalUrl && (
|
||
<a
|
||
href={provider.portalUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="text-sm text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
||
>
|
||
<ExternalLink className="w-3 h-3" />
|
||
{provider.portalUrl}
|
||
</a>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-2 ml-4">
|
||
{hasPermission('providers:update') && (
|
||
<Button variant="ghost" size="sm" onClick={onEdit} title="Bearbeiten">
|
||
<Edit className="w-4 h-4" />
|
||
</Button>
|
||
)}
|
||
{hasPermission('providers:delete') && (
|
||
<Button variant="ghost" size="sm" onClick={onDelete} title="Löschen">
|
||
<Trash2 className="w-4 h-4 text-red-500" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{isExpanded && (
|
||
<div className="border-t bg-gray-50 p-4">
|
||
<div className="flex justify-between items-center mb-3">
|
||
<h4 className="font-medium text-gray-700">Tarife</h4>
|
||
{hasPermission('providers:create') && (
|
||
<Button size="sm" onClick={() => setShowTariffModal(true)}>
|
||
<Plus className="w-4 h-4 mr-1" />
|
||
Tarif hinzufügen
|
||
</Button>
|
||
)}
|
||
</div>
|
||
{tariffs.length > 0 ? (
|
||
<div className="space-y-2">
|
||
{tariffs.map((tariff) => (
|
||
<div key={tariff.id} className="flex items-center justify-between bg-white p-3 rounded border">
|
||
<div className="flex items-center gap-2">
|
||
<span>{tariff.name}</span>
|
||
<Badge variant={tariff.isActive ? 'success' : 'danger'} className="text-xs">
|
||
{tariff.isActive ? 'Aktiv' : 'Inaktiv'}
|
||
</Badge>
|
||
{tariff._count?.contracts !== undefined && (
|
||
<span className="text-xs text-gray-500">
|
||
({tariff._count.contracts} Verträge)
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-1">
|
||
{hasPermission('providers:update') && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
setEditingTariff(tariff);
|
||
setShowTariffModal(true);
|
||
}}
|
||
title="Bearbeiten"
|
||
>
|
||
<Edit className="w-3 h-3" />
|
||
</Button>
|
||
)}
|
||
{hasPermission('providers:delete') && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
if (confirm('Tarif wirklich löschen?')) {
|
||
deleteTariffMutation.mutate(tariff.id);
|
||
}
|
||
}}
|
||
title="Löschen"
|
||
>
|
||
<Trash2 className="w-3 h-3 text-red-500" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-gray-500">Keine Tarife vorhanden.</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<TariffModal
|
||
isOpen={showTariffModal}
|
||
onClose={() => {
|
||
setShowTariffModal(false);
|
||
setEditingTariff(null);
|
||
}}
|
||
providerId={provider.id}
|
||
tariff={editingTariff}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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<Provider>) =>
|
||
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 (
|
||
<Modal
|
||
isOpen={isOpen}
|
||
onClose={onClose}
|
||
title={provider ? 'Anbieter bearbeiten' : 'Neuer Anbieter'}
|
||
>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<Input
|
||
label="Anbietername *"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||
required
|
||
placeholder="z.B. Vodafone, E.ON, Allianz"
|
||
/>
|
||
|
||
<Input
|
||
label="Portal-URL (Login-Seite)"
|
||
value={formData.portalUrl}
|
||
onChange={(e) => setFormData({ ...formData, portalUrl: e.target.value })}
|
||
placeholder="https://kundenportal.anbieter.de/login"
|
||
/>
|
||
|
||
{needsReAuth && (
|
||
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg space-y-2">
|
||
<p className="text-sm text-amber-800">
|
||
<strong>Bestätigung erforderlich:</strong>{' '}
|
||
{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.
|
||
</p>
|
||
<Input
|
||
label="Eigenes Passwort zur Bestätigung *"
|
||
type="password"
|
||
value={formData.currentPassword}
|
||
onChange={(e) => setFormData({ ...formData, currentPassword: e.target.value })}
|
||
required
|
||
autoComplete="current-password"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className="p-3 bg-gray-50 rounded-lg space-y-3">
|
||
<p className="text-sm text-gray-600">
|
||
<strong>Auto-Login Felder</strong> (optional)<br />
|
||
Feldnamen für URL-Parameter beim Auto-Login:
|
||
</p>
|
||
<Input
|
||
label="Benutzername-Feldname"
|
||
value={formData.usernameFieldName}
|
||
onChange={(e) => setFormData({ ...formData, usernameFieldName: e.target.value })}
|
||
placeholder="z.B. username, email, login"
|
||
/>
|
||
<Input
|
||
label="Passwort-Feldname"
|
||
value={formData.passwordFieldName}
|
||
onChange={(e) => setFormData({ ...formData, passwordFieldName: e.target.value })}
|
||
placeholder="z.B. password, pwd, kennwort"
|
||
/>
|
||
</div>
|
||
|
||
<div className="p-3 bg-gray-50 rounded-lg space-y-3">
|
||
<p className="text-sm text-gray-600">
|
||
<strong>Kontakt & Kündigung</strong> (optional)<br />
|
||
Erreichbarkeit des Anbieters – wird im CRM zum Nachschlagen
|
||
angezeigt, nicht an Portal-Kunden ausgespielt.
|
||
</p>
|
||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Kontakt</div>
|
||
<Input
|
||
label="Kontakt-Emailadresse"
|
||
type="email"
|
||
value={formData.contactEmail}
|
||
onChange={(e) => setFormData({ ...formData, contactEmail: e.target.value })}
|
||
placeholder="z.B. service@anbieter.de"
|
||
/>
|
||
<Input
|
||
label="Kontakt-Telefonnummer"
|
||
value={formData.contactPhone}
|
||
onChange={(e) => setFormData({ ...formData, contactPhone: e.target.value })}
|
||
placeholder="z.B. +49 30 1234567"
|
||
/>
|
||
<Input
|
||
label="Kontakt-Faxnummer"
|
||
value={formData.contactFax}
|
||
onChange={(e) => setFormData({ ...formData, contactFax: e.target.value })}
|
||
placeholder="z.B. +49 30 7654321"
|
||
/>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Kontakt-Postadresse
|
||
</label>
|
||
<textarea
|
||
value={formData.contactAddress}
|
||
onChange={(e) => setFormData({ ...formData, contactAddress: e.target.value })}
|
||
rows={3}
|
||
maxLength={500}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="z.B. Musteranbieter GmbH Musterstraße 1 12345 Berlin"
|
||
/>
|
||
</div>
|
||
|
||
<div className="pt-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Kündigung</div>
|
||
<Input
|
||
label="Kündigungs-Emailadresse"
|
||
type="email"
|
||
value={formData.cancellationEmail}
|
||
onChange={(e) => setFormData({ ...formData, cancellationEmail: e.target.value })}
|
||
placeholder="z.B. kuendigung@anbieter.de"
|
||
/>
|
||
<Input
|
||
label="Kündigungs-Faxnummer"
|
||
value={formData.cancellationFax}
|
||
onChange={(e) => setFormData({ ...formData, cancellationFax: e.target.value })}
|
||
placeholder="z.B. +49 30 9876543"
|
||
/>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Kündigungs-Postadresse
|
||
</label>
|
||
<textarea
|
||
value={formData.cancellationAddress}
|
||
onChange={(e) => setFormData({ ...formData, cancellationAddress: e.target.value })}
|
||
rows={3}
|
||
maxLength={500}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="z.B. Musteranbieter GmbH Abteilung Kündigung Musterstraße 1 12345 Berlin"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{provider && (
|
||
<label className="flex items-center gap-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.isActive}
|
||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||
className="rounded"
|
||
/>
|
||
Aktiv
|
||
</label>
|
||
)}
|
||
|
||
<div className="flex justify-end gap-2">
|
||
<Button type="button" variant="secondary" onClick={onClose}>
|
||
Abbrechen
|
||
</Button>
|
||
<Button type="submit" disabled={isLoading}>
|
||
{isLoading ? 'Speichern...' : 'Speichern'}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
function TariffModal({
|
||
isOpen,
|
||
onClose,
|
||
providerId,
|
||
tariff,
|
||
}: {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
providerId: number;
|
||
tariff: Tariff | null;
|
||
}) {
|
||
const queryClient = useQueryClient();
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
isActive: true,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
if (tariff) {
|
||
setFormData({
|
||
name: tariff.name,
|
||
isActive: tariff.isActive,
|
||
});
|
||
} else {
|
||
setFormData({ name: '', isActive: true });
|
||
}
|
||
}
|
||
}, [isOpen, tariff]);
|
||
|
||
const createMutation = useMutation({
|
||
mutationFn: (data: { name: string }) => providerApi.createTariff(providerId, data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['providers'] });
|
||
onClose();
|
||
},
|
||
onError: (error: Error) => {
|
||
alert(error.message);
|
||
},
|
||
});
|
||
|
||
const updateMutation = useMutation({
|
||
mutationFn: (data: Partial<Tariff>) => tariffApi.update(tariff!.id, data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['providers'] });
|
||
onClose();
|
||
},
|
||
onError: (error: Error) => {
|
||
alert(error.message);
|
||
},
|
||
});
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (tariff) {
|
||
updateMutation.mutate(formData);
|
||
} else {
|
||
createMutation.mutate(formData);
|
||
}
|
||
};
|
||
|
||
const isLoading = createMutation.isPending || updateMutation.isPending;
|
||
|
||
return (
|
||
<Modal
|
||
isOpen={isOpen}
|
||
onClose={onClose}
|
||
title={tariff ? 'Tarif bearbeiten' : 'Neuer Tarif'}
|
||
>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<Input
|
||
label="Tarifname *"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||
required
|
||
placeholder="z.B. Comfort Plus, Basic 100"
|
||
/>
|
||
|
||
{tariff && (
|
||
<label className="flex items-center gap-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.isActive}
|
||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||
className="rounded"
|
||
/>
|
||
Aktiv
|
||
</label>
|
||
)}
|
||
|
||
<div className="flex justify-end gap-2">
|
||
<Button type="button" variant="secondary" onClick={onClose}>
|
||
Abbrechen
|
||
</Button>
|
||
<Button type="submit" disabled={isLoading}>
|
||
{isLoading ? 'Speichern...' : 'Speichern'}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
);
|
||
}
|