opencrm/frontend/src/pages/contracts/ContractForm.tsx

1382 lines
61 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import type { ContractType } from '../../types';
import { Plus, Trash2, Eye, EyeOff, Info, X } from 'lucide-react';
// Contract types are now loaded dynamically from the database
const statusOptions = [
{ value: 'DRAFT', label: 'Entwurf' },
{ value: 'PENDING', label: 'Ausstehend' },
{ value: 'ACTIVE', label: 'Aktiv' },
{ value: 'CANCELLED', label: 'Gekündigt' },
{ value: 'EXPIRED', label: 'Abgelaufen' },
{ value: 'DEACTIVATED', label: 'Deaktiviert' },
];
// Status-Erklärungen für Info-Modal
const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
];
function StatusInfoModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/20" onClick={onClose} />
<div className="relative bg-white rounded-lg shadow-xl p-4 max-w-sm w-full mx-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900">Vertragsstatus-Übersicht</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
{statusDescriptions.map(({ status, label, description, color }) => (
<div key={status} className="flex items-start gap-2">
<span className={`font-medium text-sm min-w-[90px] ${color}`}>{label}</span>
<span className="text-sm text-gray-600">{description}</span>
</div>
))}
</div>
</div>
</div>
);
}
export default function ContractForm() {
const { id } = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const isEdit = !!id;
const preselectedCustomerId = searchParams.get('customerId');
const { register, handleSubmit, reset, watch, setValue, formState: { errors } } = useForm<any>({
defaultValues: {
customerId: preselectedCustomerId || '',
type: 'ELECTRICITY',
status: 'DRAFT',
previousContractId: '',
},
});
const contractType = watch('type') as ContractType;
const customerId = watch('customerId');
// Fetch existing contract for edit
const { data: contract } = useQuery({
queryKey: ['contract', id],
queryFn: () => contractApi.getById(parseInt(id!)),
enabled: isEdit,
});
// Fetch customers for dropdown
const { data: customersData } = useQuery({
queryKey: ['customers-all'],
queryFn: () => customerApi.getAll({ limit: 1000 }),
});
// Fetch customer details when selected
const { data: customerDetail } = useQuery({
queryKey: ['customer', customerId],
queryFn: () => customerApi.getById(parseInt(customerId)),
enabled: !!customerId,
});
// Fetch contracts for same customer (for predecessor selection)
const { data: customerContractsData } = useQuery({
queryKey: ['customer-contracts-for-predecessor', customerId],
queryFn: () => contractApi.getAll({ customerId: parseInt(customerId), limit: 1000 }),
enabled: !!customerId,
});
// Fetch platforms
const { data: platformsData } = useQuery({
queryKey: ['platforms'],
queryFn: () => platformApi.getAll(),
});
// Fetch cancellation periods
const { data: cancellationPeriodsData } = useQuery({
queryKey: ['cancellation-periods'],
queryFn: () => cancellationPeriodApi.getAll(),
});
// Fetch contract durations
const { data: contractDurationsData } = useQuery({
queryKey: ['contract-durations'],
queryFn: () => contractDurationApi.getAll(),
});
// Fetch providers
const { data: providersData } = useQuery({
queryKey: ['providers'],
queryFn: () => providerApi.getAll(),
});
// Fetch contract categories (types)
const { data: contractCategoriesData } = useQuery({
queryKey: ['contract-categories'],
queryFn: () => contractCategoryApi.getAll(),
});
// Watch providerId for tariff filtering
const selectedProviderId = watch('providerId');
// Reset tariffId when providerId changes (but not on initial load)
const [previousProviderId, setPreviousProviderId] = useState<string | null>(null);
// SIM-Karten State für Mobilfunk
interface SimCardInput {
id?: number;
phoneNumber: string;
simCardNumber: string;
pin: string;
puk: string;
hasExistingPin?: boolean; // Zeigt an ob PIN bereits in DB vorhanden
hasExistingPuk?: boolean; // Zeigt an ob PUK bereits in DB vorhanden
isMultisim: boolean;
isMain: boolean;
}
const [simCards, setSimCards] = useState<SimCardInput[]>([]);
// Rufnummern State für Internet-Verträge
interface PhoneNumberInput {
id?: number;
phoneNumber: string;
sipUsername: string;
sipPassword: string;
hasExistingSipPassword?: boolean;
sipServer: string;
isMain: boolean;
}
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumberInput[]>([]);
const [isContractLoaded, setIsContractLoaded] = useState(false);
// Portal-Benutzername: Manuell oder Stressfrei-Adresse
const [usernameType, setUsernameType] = useState<'manual' | 'stressfrei'>('manual');
const [selectedStressfreiEmailId, setSelectedStressfreiEmailId] = useState<string>('');
// Passwort-Sichtbarkeit
const [showPortalPassword, setShowPortalPassword] = useState(false);
const [showInternetPassword, setShowInternetPassword] = useState(false);
const [showSipPasswords, setShowSipPasswords] = useState<Record<number, boolean>>({});
const [showSimPins, setShowSimPins] = useState<Record<number, boolean>>({});
const [showSimPuks, setShowSimPuks] = useState<Record<number, boolean>>({});
// Status-Info Modal
const [showStatusInfo, setShowStatusInfo] = useState(false);
// For new contracts, mark as "loaded" immediately so provider change detection works
useEffect(() => {
if (!isEdit) {
setIsContractLoaded(true);
}
}, [isEdit]);
// Set preselected customer from URL params (for new contracts)
useEffect(() => {
if (!isEdit && preselectedCustomerId && customersData?.data) {
// Only set if the customer exists in the list
const customerExists = customersData.data.some(c => c.id.toString() === preselectedCustomerId);
if (customerExists) {
setValue('customerId', preselectedCustomerId);
}
}
}, [isEdit, preselectedCustomerId, customersData, setValue]);
// Reset tariffId when providerId changes (but only after initial contract load)
useEffect(() => {
// Only reset tariff if:
// 1. Contract has been loaded (or we're creating new)
// 2. Provider actually changed (not empty/null -> value which happens on load)
// Note: previousProviderId can be '' (empty string) initially, so we need to check for truthy
if (isContractLoaded && previousProviderId && selectedProviderId !== previousProviderId) {
setValue('tariffId', '');
}
setPreviousProviderId(selectedProviderId);
}, [selectedProviderId, previousProviderId, setValue, isContractLoaded]);
// Set default type when categories are loaded (only for new contracts)
useEffect(() => {
if (!isEdit && contractCategoriesData?.data && contractCategoriesData.data.length > 0) {
const currentType = watch('type');
// Only set if current type is empty or doesn't exist in categories
const categories = contractCategoriesData.data.filter(c => c.isActive);
const typeExists = categories.some(c => c.code === currentType);
if (!currentType || !typeExists) {
const firstCategory = categories.sort((a, b) => a.sortOrder - b.sortOrder)[0];
if (firstCategory) {
setValue('type', firstCategory.code);
}
}
}
}, [contractCategoriesData, isEdit, setValue, watch]);
// Bootstrap: Set customerId first so customerDetail can load (needed for dropdowns)
useEffect(() => {
if (isEdit && contract?.data && !customerId) {
setValue('customerId', contract.data.customerId.toString());
}
}, [isEdit, contract, customerId, setValue]);
// Reset form when contract data AND ALL dropdown data are loaded
// This ensures dropdowns can properly display the selected values
// We need customerDetail for addresses/bankCards/documents and providersData for tariffs
useEffect(() => {
if (contract?.data && platformsData?.data && contractCategoriesData?.data && providersData?.data && customerDetail?.data) {
const c = contract.data;
reset({
customerId: c.customerId.toString(),
type: c.type,
status: c.status,
addressId: c.addressId?.toString() || '',
billingAddressId: c.billingAddressId?.toString() || '',
bankCardId: c.bankCardId?.toString() || '',
identityDocumentId: c.identityDocumentId?.toString() || '',
salesPlatformId: c.salesPlatformId?.toString() || '',
providerId: c.providerId?.toString() || '',
tariffId: c.tariffId?.toString() || '',
providerName: c.providerName || '',
tariffName: c.tariffName || '',
customerNumberAtProvider: c.customerNumberAtProvider || '',
priceFirst12Months: c.priceFirst12Months || '',
priceFrom13Months: c.priceFrom13Months || '',
priceAfter24Months: c.priceAfter24Months || '',
startDate: c.startDate ? c.startDate.split('T')[0] : '',
endDate: c.endDate ? c.endDate.split('T')[0] : '',
cancellationPeriodId: c.cancellationPeriodId?.toString() || '',
contractDurationId: c.contractDurationId?.toString() || '',
commission: c.commission || '',
portalUsername: c.portalUsername || '',
notes: c.notes || '',
// Energy details
meterId: c.energyDetails?.meterId?.toString() || '',
maloId: c.energyDetails?.maloId || '',
annualConsumption: c.energyDetails?.annualConsumption || '',
annualConsumptionKwh: c.energyDetails?.annualConsumptionKwh || '',
basePrice: c.energyDetails?.basePrice || '',
unitPrice: c.energyDetails?.unitPrice || '',
bonus: c.energyDetails?.bonus || '',
previousProviderName: c.energyDetails?.previousProviderName || '',
previousCustomerNumber: c.energyDetails?.previousCustomerNumber || '',
// Internet details
downloadSpeed: c.internetDetails?.downloadSpeed || '',
uploadSpeed: c.internetDetails?.uploadSpeed || '',
routerModel: c.internetDetails?.routerModel || '',
routerSerialNumber: c.internetDetails?.routerSerialNumber || '',
installationDate: c.internetDetails?.installationDate ? c.internetDetails.installationDate.split('T')[0] : '',
internetUsername: c.internetDetails?.internetUsername || '',
homeId: c.internetDetails?.homeId || '',
activationCode: c.internetDetails?.activationCode || '',
// Mobile details
requiresMultisim: c.mobileDetails?.requiresMultisim || false,
dataVolume: c.mobileDetails?.dataVolume || '',
includedMinutes: c.mobileDetails?.includedMinutes || '',
includedSMS: c.mobileDetails?.includedSMS || '',
deviceModel: c.mobileDetails?.deviceModel || '',
deviceImei: c.mobileDetails?.deviceImei || '',
// Legacy
phoneNumber: c.mobileDetails?.phoneNumber || '',
simCardNumber: c.mobileDetails?.simCardNumber || '',
// TV details
receiverModel: c.tvDetails?.receiverModel || '',
smartcardNumber: c.tvDetails?.smartcardNumber || '',
tvPackage: c.tvDetails?.package || '',
// Car insurance details
licensePlate: c.carInsuranceDetails?.licensePlate || '',
hsn: c.carInsuranceDetails?.hsn || '',
tsn: c.carInsuranceDetails?.tsn || '',
vin: c.carInsuranceDetails?.vin || '',
vehicleType: c.carInsuranceDetails?.vehicleType || '',
firstRegistration: c.carInsuranceDetails?.firstRegistration ? c.carInsuranceDetails.firstRegistration.split('T')[0] : '',
noClaimsClass: c.carInsuranceDetails?.noClaimsClass || '',
insuranceType: c.carInsuranceDetails?.insuranceType || 'LIABILITY',
deductiblePartial: c.carInsuranceDetails?.deductiblePartial || '',
deductibleFull: c.carInsuranceDetails?.deductibleFull || '',
policyNumber: c.carInsuranceDetails?.policyNumber || '',
previousInsurer: c.carInsuranceDetails?.previousInsurer || '',
// Kündigungsdaten
cancellationConfirmationDate: c.cancellationConfirmationDate ? c.cancellationConfirmationDate.split('T')[0] : '',
cancellationConfirmationOptionsDate: c.cancellationConfirmationOptionsDate ? c.cancellationConfirmationOptionsDate.split('T')[0] : '',
wasSpecialCancellation: c.wasSpecialCancellation || false,
// Vorgänger-Vertrag
previousContractId: c.previousContractId?.toString() || '',
});
// Load simCards if available
if (c.mobileDetails?.simCards && c.mobileDetails.simCards.length > 0) {
setSimCards(c.mobileDetails.simCards.map(sc => ({
id: sc.id,
phoneNumber: sc.phoneNumber || '',
simCardNumber: sc.simCardNumber || '',
pin: '', // PIN/PUK werden nicht im Klartext geladen
puk: '',
hasExistingPin: !!sc.pin, // true wenn verschlüsselter Wert vorhanden
hasExistingPuk: !!sc.puk, // true wenn verschlüsselter Wert vorhanden
isMultisim: sc.isMultisim,
isMain: sc.isMain,
})));
} else {
setSimCards([]);
}
// Load phoneNumbers if available (Internet contracts)
if (c.internetDetails?.phoneNumbers && c.internetDetails.phoneNumbers.length > 0) {
setPhoneNumbers(c.internetDetails.phoneNumbers.map(pn => ({
id: pn.id,
phoneNumber: pn.phoneNumber || '',
sipUsername: pn.sipUsername || '',
sipPassword: '', // SIP-Passwort wird nicht im Klartext geladen
hasExistingSipPassword: !!pn.sipPasswordEncrypted,
sipServer: pn.sipServer || '',
isMain: pn.isMain,
})));
} else {
setPhoneNumbers([]);
}
// Portal-Benutzername-Typ initialisieren
if (c.stressfreiEmailId) {
setUsernameType('stressfrei');
setSelectedStressfreiEmailId(c.stressfreiEmailId.toString());
} else {
setUsernameType('manual');
setSelectedStressfreiEmailId('');
}
// Mark contract as loaded so provider change detection works correctly
setIsContractLoaded(true);
}
}, [contract, reset, platformsData, contractCategoriesData, providersData, customerDetail]);
// Watch für Enddatum-Berechnung
const startDate = watch('startDate');
const contractDurationId = watch('contractDurationId');
// Berechne Vertragsende aus Beginn + Laufzeit
useEffect(() => {
if (startDate && contractDurationId && contractDurationsData?.data) {
const duration = contractDurationsData.data.find(d => d.id === parseInt(contractDurationId));
if (duration) {
const start = new Date(startDate);
const code = duration.code;
// Parse duration code (z.B. "12M", "24M", "1J", "2J")
const match = code.match(/^(\d+)([MTJ])$/);
if (match) {
const value = parseInt(match[1]);
const unit = match[2];
let endDate = new Date(start);
if (unit === 'T') {
endDate.setDate(endDate.getDate() + value);
} else if (unit === 'M') {
endDate.setMonth(endDate.getMonth() + value);
} else if (unit === 'J') {
endDate.setFullYear(endDate.getFullYear() + value);
}
// Setze das berechnete Enddatum
setValue('endDate', endDate.toISOString().split('T')[0]);
}
}
}
}, [startDate, contractDurationId, contractDurationsData, setValue]);
const createMutation = useMutation({
mutationFn: contractApi.create,
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['contracts'] });
if (variables.customerId) {
queryClient.invalidateQueries({ queryKey: ['customer', variables.customerId.toString()] });
}
queryClient.invalidateQueries({ queryKey: ['customers'] });
// Navigate back to customer's contracts tab if coming from customer
if (preselectedCustomerId) {
navigate(`/customers/${preselectedCustomerId}?tab=contracts`);
} else {
navigate('/contracts');
}
},
});
const updateMutation = useMutation({
mutationFn: (data: any) => contractApi.update(parseInt(id!), data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['contracts'] });
queryClient.invalidateQueries({ queryKey: ['contract', id] });
if (variables.customerId) {
queryClient.invalidateQueries({ queryKey: ['customer', variables.customerId.toString()] });
}
queryClient.invalidateQueries({ queryKey: ['customers'] });
navigate(`/contracts/${id}`);
},
});
const onSubmit = (data: any) => {
// Helper to safely parse integers - returns undefined for empty/invalid values
const safeParseInt = (val: string | number | undefined | null): number | undefined => {
if (val === undefined || val === null || val === '') return undefined;
const parsed = parseInt(String(val));
return isNaN(parsed) ? undefined : parsed;
};
// Find the selected category to get its ID
const selectedCategory = contractCategories.find(c => c.code === data.type);
// Validate required fields
const customerIdParsed = safeParseInt(data.customerId);
if (!customerIdParsed) {
alert('Bitte wählen Sie einen Kunden aus');
return;
}
if (!data.type || !selectedCategory) {
alert('Bitte wählen Sie einen Vertragstyp aus');
return;
}
// Helper: Leere Strings werden zu null, damit Prisma das Feld auf NULL setzt
const emptyToNull = (val: string | undefined | null): string | null => {
if (val === undefined || val === null || val === '') return null;
return val;
};
const contractData: any = {
customerId: customerIdParsed,
type: data.type,
contractCategoryId: selectedCategory.id,
status: data.status,
addressId: safeParseInt(data.addressId) ?? null,
billingAddressId: safeParseInt(data.billingAddressId) ?? null,
bankCardId: safeParseInt(data.bankCardId) ?? null,
identityDocumentId: safeParseInt(data.identityDocumentId) ?? null,
salesPlatformId: safeParseInt(data.salesPlatformId) ?? null,
providerId: safeParseInt(data.providerId) ?? null,
tariffId: safeParseInt(data.tariffId) ?? null,
providerName: emptyToNull(data.providerName),
tariffName: emptyToNull(data.tariffName),
customerNumberAtProvider: emptyToNull(data.customerNumberAtProvider),
priceFirst12Months: emptyToNull(data.priceFirst12Months),
priceFrom13Months: emptyToNull(data.priceFrom13Months),
priceAfter24Months: emptyToNull(data.priceAfter24Months),
startDate: data.startDate ? new Date(data.startDate) : null,
endDate: data.endDate ? new Date(data.endDate) : null,
cancellationPeriodId: safeParseInt(data.cancellationPeriodId) ?? null,
contractDurationId: safeParseInt(data.contractDurationId) ?? null,
commission: data.commission ? parseFloat(data.commission) : null,
// Portal-Benutzername: entweder manuell oder Stressfrei-Adresse
portalUsername: usernameType === 'manual' ? emptyToNull(data.portalUsername) : null,
stressfreiEmailId: usernameType === 'stressfrei' && selectedStressfreiEmailId ? parseInt(selectedStressfreiEmailId) : null,
portalPassword: data.portalPassword || undefined, // Passwort: undefined = nicht ändern
notes: emptyToNull(data.notes),
cancellationConfirmationDate: data.cancellationConfirmationDate ? new Date(data.cancellationConfirmationDate) : null,
cancellationConfirmationOptionsDate: data.cancellationConfirmationOptionsDate ? new Date(data.cancellationConfirmationOptionsDate) : null,
wasSpecialCancellation: data.wasSpecialCancellation || false,
previousContractId: safeParseInt(data.previousContractId) ?? null,
};
// Add type-specific details
if (['ELECTRICITY', 'GAS'].includes(data.type)) {
contractData.energyDetails = {
meterId: safeParseInt(data.meterId) ?? null,
maloId: emptyToNull(data.maloId),
annualConsumption: data.annualConsumption ? parseFloat(data.annualConsumption) : null,
annualConsumptionKwh: data.annualConsumptionKwh ? parseFloat(data.annualConsumptionKwh) : null,
basePrice: data.basePrice ? parseFloat(data.basePrice) : null,
unitPrice: data.unitPrice ? parseFloat(data.unitPrice) : null,
bonus: data.bonus ? parseFloat(data.bonus) : null,
previousProviderName: emptyToNull(data.previousProviderName),
previousCustomerNumber: emptyToNull(data.previousCustomerNumber),
};
}
if (['DSL', 'CABLE', 'FIBER'].includes(data.type)) {
contractData.internetDetails = {
downloadSpeed: safeParseInt(data.downloadSpeed) ?? null,
uploadSpeed: safeParseInt(data.uploadSpeed) ?? null,
routerModel: emptyToNull(data.routerModel),
routerSerialNumber: emptyToNull(data.routerSerialNumber),
installationDate: data.installationDate ? new Date(data.installationDate) : null,
// Internet-Zugangsdaten
internetUsername: emptyToNull(data.internetUsername),
internetPassword: data.internetPassword || undefined, // Passwort: undefined = nicht ändern
// Glasfaser-spezifisch
homeId: emptyToNull(data.homeId),
// Vodafone DSL/Kabel spezifisch
activationCode: emptyToNull(data.activationCode),
// Rufnummern mit SIP-Daten
phoneNumbers: phoneNumbers.length > 0 ? phoneNumbers.map(pn => ({
id: pn.id,
phoneNumber: pn.phoneNumber || '',
isMain: pn.isMain ?? false,
sipUsername: emptyToNull(pn.sipUsername),
sipPassword: pn.sipPassword || undefined, // Passwort: undefined = nicht ändern
sipServer: emptyToNull(pn.sipServer),
})) : undefined,
};
}
if (data.type === 'MOBILE') {
contractData.mobileDetails = {
requiresMultisim: data.requiresMultisim || false,
dataVolume: data.dataVolume ? parseFloat(data.dataVolume) : null,
includedMinutes: safeParseInt(data.includedMinutes) ?? null,
includedSMS: safeParseInt(data.includedSMS) ?? null,
deviceModel: emptyToNull(data.deviceModel),
deviceImei: emptyToNull(data.deviceImei),
// Legacy
phoneNumber: emptyToNull(data.phoneNumber),
simCardNumber: emptyToNull(data.simCardNumber),
// SIM-Karten
simCards: simCards.length > 0 ? simCards.map(sc => ({
id: sc.id,
phoneNumber: emptyToNull(sc.phoneNumber),
simCardNumber: emptyToNull(sc.simCardNumber),
pin: sc.pin || undefined, // Passwort: undefined = nicht ändern
puk: sc.puk || undefined, // Passwort: undefined = nicht ändern
isMultisim: sc.isMultisim,
isMain: sc.isMain,
})) : undefined,
};
}
if (data.type === 'TV') {
contractData.tvDetails = {
receiverModel: emptyToNull(data.receiverModel),
smartcardNumber: emptyToNull(data.smartcardNumber),
package: emptyToNull(data.tvPackage),
};
}
if (data.type === 'CAR_INSURANCE') {
contractData.carInsuranceDetails = {
licensePlate: emptyToNull(data.licensePlate),
hsn: emptyToNull(data.hsn),
tsn: emptyToNull(data.tsn),
vin: emptyToNull(data.vin),
vehicleType: emptyToNull(data.vehicleType),
firstRegistration: data.firstRegistration ? new Date(data.firstRegistration) : null,
noClaimsClass: emptyToNull(data.noClaimsClass),
insuranceType: data.insuranceType,
deductiblePartial: data.deductiblePartial ? parseFloat(data.deductiblePartial) : null,
deductibleFull: data.deductibleFull ? parseFloat(data.deductibleFull) : null,
policyNumber: emptyToNull(data.policyNumber),
previousInsurer: emptyToNull(data.previousInsurer),
};
}
if (isEdit) {
updateMutation.mutate(contractData);
} else {
createMutation.mutate(contractData);
}
};
const isLoading = createMutation.isPending || updateMutation.isPending;
const error = createMutation.error || updateMutation.error;
const customer = customerDetail?.data;
const addresses = customer?.addresses || [];
const bankCards = customer?.bankCards?.filter((c) => c.isActive) || [];
const documents = customer?.identityDocuments?.filter((d) => d.isActive) || [];
const meters = customer?.meters?.filter((m) => m.isActive) || [];
const stressfreiEmails = customer?.stressfreiEmails?.filter((e: { isActive: boolean }) => e.isActive) || [];
const platforms = platformsData?.data || [];
const cancellationPeriods = cancellationPeriodsData?.data || [];
const contractDurations = contractDurationsData?.data || [];
const providers = providersData?.data?.filter(p => p.isActive) || [];
const contractCategories = contractCategoriesData?.data?.filter(c => c.isActive).sort((a, b) => a.sortOrder - b.sortOrder) || [];
const typeOptions = contractCategories.map(c => ({ value: c.code, label: c.name }));
// Available predecessor contracts (same customer, excluding current contract if editing)
const predecessorContracts = (customerContractsData?.data || [])
.filter(c => !isEdit || c.id !== parseInt(id!))
.sort((a, b) => new Date(b.startDate || 0).getTime() - new Date(a.startDate || 0).getTime());
// Get tariffs for selected provider
const selectedProvider = providers.find(p => p.id === parseInt(selectedProviderId || '0'));
const availableTariffs = selectedProvider?.tariffs?.filter(t => t.isActive) || [];
// Helper to format customer label with optional birth date
const formatCustomerLabel = (c: { customerNumber: string; companyName?: string; firstName: string; lastName: string; birthDate?: string }) => {
const name = c.companyName || `${c.firstName} ${c.lastName}`;
const birthDateStr = c.birthDate
? ` (geb. ${new Date(c.birthDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })})`
: '';
return `${c.customerNumber} - ${name}${birthDateStr}`;
};
// Build customer options, ensuring current customer from contract is always included
const customerOptions = (() => {
const customers = customersData?.data || [];
const options = customers.map((c) => ({
value: c.id.toString(),
label: formatCustomerLabel(c),
}));
// If editing and contract's customer isn't in the list yet, add it
if (isEdit && contract?.data?.customer) {
const contractCustomer = contract.data.customer;
const exists = options.some((o) => o.value === contractCustomer.id.toString());
if (!exists) {
options.unshift({
value: contractCustomer.id.toString(),
label: formatCustomerLabel(contractCustomer),
});
}
}
return options;
})();
return (
<div>
<h1 className="text-2xl font-bold mb-6">
{isEdit ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}
</h1>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
{error instanceof Error ? error.message : 'Ein Fehler ist aufgetreten'}
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<Card className="mb-6" title="Vertragsdaten">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Kunde *"
{...register('customerId', { required: 'Kunde erforderlich' })}
options={customerOptions}
error={errors.customerId?.message as string}
/>
<Select
label="Vertragstyp *"
{...register('type', { required: 'Typ erforderlich' })}
options={typeOptions}
/>
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-sm font-medium text-gray-700">Status</label>
<button
type="button"
onClick={() => setShowStatusInfo(true)}
className="text-gray-400 hover:text-blue-600 transition-colors"
title="Status-Erklärung"
>
<Info className="w-4 h-4" />
</button>
</div>
<Select
{...register('status')}
options={statusOptions}
/>
</div>
<Select
label="Vertriebsplattform"
{...register('salesPlatformId')}
options={platforms.map((p) => ({ value: p.id, label: p.name }))}
/>
{/* Vorgänger-Vertrag auswählen (nur wenn Kunde gewählt) */}
{customerId && (
<Select
label="Vorgänger-Vertrag"
{...register('previousContractId')}
options={predecessorContracts.map((c) => ({
value: c.id,
label: `${c.contractNumber} (${c.type}${c.startDate ? ` - ${new Date(c.startDate).toLocaleDateString('de-DE')}` : ''})`,
}))}
placeholder="Keinen Vorgänger auswählen"
/>
)}
</div>
</Card>
{customerId && (
<Card className="mb-6" title="Kundendaten verknüpfen">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<Select
label="Lieferadresse"
{...register('addressId')}
options={addresses
.filter((a) => a.type === 'DELIVERY_RESIDENCE')
.map((a) => ({
value: a.id,
label: `${a.street} ${a.houseNumber}, ${a.postalCode} ${a.city}`,
}))}
/>
<Select
label="Rechnungsadresse"
{...register('billingAddressId')}
options={addresses
.filter((a) => a.type === 'BILLING')
.map((a) => ({
value: a.id,
label: `${a.street} ${a.houseNumber}, ${a.postalCode} ${a.city}`,
}))}
placeholder="Wie Lieferadresse"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Bankkarte"
{...register('bankCardId')}
options={bankCards.map((c) => ({
value: c.id,
label: `${c.iban} (${c.accountHolder})`,
}))}
/>
<Select
label="Ausweis"
{...register('identityDocumentId')}
options={documents.map((d) => ({
value: d.id,
label: `${d.documentNumber} (${d.type})`,
}))}
/>
</div>
</Card>
)}
<Card className="mb-6" title="Anbieter & Tarif">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Anbieter"
{...register('providerId')}
options={providers.map((p) => ({ value: p.id, label: p.name }))}
/>
<Select
label="Tarif"
{...register('tariffId')}
options={availableTariffs.map((t) => ({ value: t.id, label: t.name }))}
disabled={!selectedProviderId}
/>
<Input label="Kundennummer beim Anbieter" {...register('customerNumberAtProvider')} />
<Input label="Provision (€)" type="number" step="0.01" {...register('commission')} />
<Input label="Preis erste 12 Monate" {...register('priceFirst12Months')} placeholder="z.B. 29,99 €/Monat" />
<Input label="Preis ab 13. Monat" {...register('priceFrom13Months')} placeholder="z.B. 39,99 €/Monat" />
<Input label="Preis nach 24 Monaten" {...register('priceAfter24Months')} placeholder="z.B. 49,99 €/Monat" />
</div>
</Card>
<Card className="mb-6" title="Laufzeit und Kündigung">
<p className="text-sm text-gray-500 mb-4 bg-blue-50 border border-blue-200 rounded-lg p-3">
<strong>Hinweis:</strong> Ist die Laufzeit 4 Wochen, 1 Monat oder 30 Tage, gilt der Vertrag als unbefristet mit der jeweiligen Kündigungsfrist.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Vertragsbeginn"
type="date"
{...register('startDate')}
value={watch('startDate') || ''}
onClear={() => setValue('startDate', '')}
/>
<Input label="Vertragsende (berechnet)" type="date" {...register('endDate')} disabled className="bg-gray-50" />
<Select
label="Vertragslaufzeit"
{...register('contractDurationId')}
options={contractDurations.map((cd) => ({ value: cd.id, label: cd.description }))}
/>
<Select
label="Kündigungsfrist"
{...register('cancellationPeriodId')}
options={cancellationPeriods.map((cp) => ({ value: cp.id, label: cp.description }))}
/>
<Input
label="Kündigungsbestätigungsdatum"
type="date"
{...register('cancellationConfirmationDate')}
value={watch('cancellationConfirmationDate') || ''}
onClear={() => setValue('cancellationConfirmationDate', '')}
/>
<Input
label="Kündigungsbestätigungsoptionendatum"
type="date"
{...register('cancellationConfirmationOptionsDate')}
value={watch('cancellationConfirmationOptionsDate') || ''}
onClear={() => setValue('cancellationConfirmationOptionsDate', '')}
/>
<div className="col-span-2">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" {...register('wasSpecialCancellation')} className="rounded border-gray-300" />
<span>Wurde sondergekündigt?</span>
</label>
</div>
</div>
</Card>
<Card className="mb-6" title="Zugangsdaten (verschlüsselt gespeichert)">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Portal Benutzername</label>
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="usernameType"
checked={usernameType === 'manual'}
onChange={() => {
setUsernameType('manual');
setSelectedStressfreiEmailId('');
}}
className="text-blue-600"
/>
<span className="text-sm">Manuell eingeben</span>
</label>
{usernameType === 'manual' && (
<Input
{...register('portalUsername')}
placeholder="Benutzername eingeben..."
/>
)}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="usernameType"
checked={usernameType === 'stressfrei'}
onChange={() => {
setUsernameType('stressfrei');
setValue('portalUsername', '');
}}
className="text-blue-600"
/>
<span className="text-sm">Stressfrei-Wechseln Adresse</span>
</label>
{usernameType === 'stressfrei' && (
<Select
value={selectedStressfreiEmailId}
onChange={(e) => setSelectedStressfreiEmailId(e.target.value)}
options={stressfreiEmails.map((e: { id: number; email: string; notes?: string }) => ({
value: e.id,
label: e.email + (e.notes ? ` (${e.notes})` : ''),
}))}
placeholder={stressfreiEmails.length === 0 ? 'Keine Stressfrei-Adressen vorhanden' : 'Adresse auswählen...'}
/>
)}
{usernameType === 'stressfrei' && stressfreiEmails.length === 0 && (
<p className="text-xs text-amber-600">
Keine Stressfrei-Wechseln Adressen für diesen Kunden vorhanden. Bitte zuerst beim Kunden anlegen.
</p>
)}
</div>
</div>
<div className="mt-8">
<label className="block text-sm font-medium text-gray-700 mb-1">
{isEdit ? 'Neues Passwort (leer lassen = unverändert)' : 'Portal Passwort'}
</label>
<div className="relative">
<input
type={showPortalPassword ? 'text' : 'password'}
{...register('portalPassword')}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={() => setShowPortalPassword(!showPortalPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
>
{showPortalPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
</div>
</Card>
{/* Energy-specific fields */}
{['ELECTRICITY', 'GAS'].includes(contractType) && (
<Card className="mb-6" title={contractType === 'ELECTRICITY' ? 'Strom-Details' : 'Gas-Details'}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Zähler"
{...register('meterId')}
options={meters
.filter((m) => m.type === contractType)
.map((m) => ({
value: m.id,
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}`,
}))}
/>
<Input
label="MaLo-ID (Marktlokations-ID)"
{...register('maloId')}
/>
<Input
label={`Jahresverbrauch (${contractType === 'ELECTRICITY' ? 'kWh' : 'm³'})`}
type="number"
{...register('annualConsumption')}
/>
{contractType === 'GAS' && (
<Input
label="Jahresverbrauch (kWh)"
type="number"
{...register('annualConsumptionKwh')}
/>
)}
<Input label="Grundpreis (€/Monat)" type="number" step="any" {...register('basePrice')} />
<Input
label="Arbeitspreis (€/kWh)"
type="number"
step="any"
{...register('unitPrice')}
/>
<Input label="Bonus (€)" type="number" step="0.01" {...register('bonus')} />
<Input label="Vorversorger" {...register('previousProviderName')} />
<Input label="Kundennr. beim Vorversorger" {...register('previousCustomerNumber')} />
</div>
</Card>
)}
{/* Internet-specific fields */}
{['DSL', 'CABLE', 'FIBER'].includes(contractType) && (
<>
<Card className="mb-6" title={
contractType === 'DSL' ? 'DSL-Details' :
contractType === 'CABLE' ? 'Kabelinternet-Details' :
'Glasfaser-Details'
}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input label="Download (Mbit/s)" type="number" {...register('downloadSpeed')} />
<Input label="Upload (Mbit/s)" type="number" {...register('uploadSpeed')} />
<Input label="Router Modell" {...register('routerModel')} />
<Input label="Router Seriennummer" {...register('routerSerialNumber')} />
<Input
label="Installationsdatum"
type="date"
{...register('installationDate')}
value={watch('installationDate') || ''}
onClear={() => setValue('installationDate', '')}
/>
{/* HomeID nur bei Glasfaser */}
{contractType === 'FIBER' && (
<Input label="Home-ID" {...register('homeId')} />
)}
{/* Aktivierungscode bei Vodafone + DSL oder Kabel */}
{selectedProvider?.name?.toLowerCase().includes('vodafone') && ['DSL', 'CABLE'].includes(contractType) && (
<Input label="Aktivierungscode" {...register('activationCode')} />
)}
</div>
</Card>
<Card className="mb-6" title="Internet-Zugangsdaten (verschlüsselt)">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input label="Benutzername" {...register('internetUsername')} />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{isEdit ? 'Neues Passwort (leer = beibehalten)' : 'Passwort'}
</label>
<div className="relative">
<input
type={showInternetPassword ? 'text' : 'password'}
{...register('internetPassword')}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={() => setShowInternetPassword(!showInternetPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
>
{showInternetPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
</div>
</Card>
<Card className="mb-6" title="Rufnummern & SIP-Zugangsdaten">
<p className="text-sm text-gray-500 mb-4">
Hier können Sie Festnetz-Rufnummern mit SIP-Zugangsdaten erfassen.
</p>
{phoneNumbers.length > 0 && (
<div className="space-y-4 mb-4">
{phoneNumbers.map((pn, index) => (
<div key={index} className="p-4 border rounded-lg bg-gray-50">
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-3">
<span className="font-medium">Rufnummer {index + 1}</span>
<label className="flex items-center gap-1 text-sm">
<input
type="checkbox"
checked={pn.isMain}
onChange={(e) => {
const updated = [...phoneNumbers];
if (e.target.checked) {
updated.forEach((p, i) => p.isMain = i === index);
} else {
updated[index].isMain = false;
}
setPhoneNumbers(updated);
}}
className="rounded border-gray-300"
/>
Hauptnummer
</label>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setPhoneNumbers(phoneNumbers.filter((_, i) => i !== index));
}}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<Input
label="Rufnummer"
value={pn.phoneNumber}
onChange={(e) => {
const updated = [...phoneNumbers];
updated[index].phoneNumber = e.target.value;
setPhoneNumbers(updated);
}}
placeholder="z.B. 030 123456"
/>
<Input
label="SIP-Benutzername"
value={pn.sipUsername}
onChange={(e) => {
const updated = [...phoneNumbers];
updated[index].sipUsername = e.target.value;
setPhoneNumbers(updated);
}}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{pn.hasExistingSipPassword ? "SIP-Passwort (bereits hinterlegt)" : "SIP-Passwort"}
</label>
<div className="relative">
<input
type={showSipPasswords[index] ? 'text' : 'password'}
value={pn.sipPassword}
onChange={(e) => {
const updated = [...phoneNumbers];
updated[index].sipPassword = e.target.value;
setPhoneNumbers(updated);
}}
placeholder={pn.hasExistingSipPassword ? "Leer = beibehalten" : ""}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={() => setShowSipPasswords(prev => ({ ...prev, [index]: !prev[index] }))}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
>
{showSipPasswords[index] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Input
label="SIP-Server"
value={pn.sipServer}
onChange={(e) => {
const updated = [...phoneNumbers];
updated[index].sipServer = e.target.value;
setPhoneNumbers(updated);
}}
placeholder="z.B. sip.provider.de"
/>
</div>
</div>
))}
</div>
)}
<Button
type="button"
variant="secondary"
onClick={() => {
setPhoneNumbers([...phoneNumbers, {
phoneNumber: '',
sipUsername: '',
sipPassword: '',
sipServer: '',
isMain: phoneNumbers.length === 0,
}]);
}}
>
<Plus className="w-4 h-4 mr-2" />
Rufnummer hinzufügen
</Button>
</Card>
</>
)}
{/* Mobile-specific fields */}
{contractType === 'MOBILE' && (
<>
<Card className="mb-6" title="Mobilfunk-Details">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input label="Datenvolumen (GB)" type="number" {...register('dataVolume')} />
<Input label="Inklusiv-Minuten" type="number" {...register('includedMinutes')} />
<Input label="Inklusiv-SMS" type="number" {...register('includedSMS')} />
<Input label="Gerät (Modell)" {...register('deviceModel')} />
<Input label="Gerät (IMEI)" {...register('deviceImei')} />
</div>
{/* Multisim erforderlich */}
<div className="mt-4 pt-4 border-t">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
{...register('requiresMultisim')}
className="mt-1 rounded border-gray-300"
/>
<div>
<span className="font-medium">Multisim erforderlich</span>
<p className="text-sm text-amber-600 mt-1">
Hinweis: Multisim ist bei Klarmobil, Congstar und Otelo nicht buchbar. Muss Freenet oder vergleichbar sein.
</p>
</div>
</label>
</div>
</Card>
<Card className="mb-6" title="SIM-Karten">
<p className="text-sm text-gray-500 mb-4">
Hier können Sie alle SIM-Karten zum Vertrag erfassen (Hauptkarte und Multisim-Karten).
</p>
{simCards.length > 0 && (
<div className="space-y-4 mb-4">
{simCards.map((card, index) => (
<div key={index} className="p-4 border rounded-lg bg-gray-50">
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-3">
<span className="font-medium">SIM-Karte {index + 1}</span>
<label className="flex items-center gap-1 text-sm">
<input
type="checkbox"
checked={card.isMain}
onChange={(e) => {
const updated = [...simCards];
// Nur eine Hauptkarte erlauben
if (e.target.checked) {
updated.forEach((c, i) => c.isMain = i === index);
} else {
updated[index].isMain = false;
}
setSimCards(updated);
}}
className="rounded border-gray-300"
/>
Hauptkarte
</label>
<label className="flex items-center gap-1 text-sm">
<input
type="checkbox"
checked={card.isMultisim}
onChange={(e) => {
const updated = [...simCards];
updated[index].isMultisim = e.target.checked;
setSimCards(updated);
}}
className="rounded border-gray-300"
/>
Multisim
</label>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setSimCards(simCards.filter((_, i) => i !== index));
}}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<Input
label="Rufnummer"
value={card.phoneNumber}
onChange={(e) => {
const updated = [...simCards];
updated[index].phoneNumber = e.target.value;
setSimCards(updated);
}}
placeholder="z.B. 0171 1234567"
/>
<Input
label="SIM-Kartennummer"
value={card.simCardNumber}
onChange={(e) => {
const updated = [...simCards];
updated[index].simCardNumber = e.target.value;
setSimCards(updated);
}}
placeholder="ICCID"
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{card.hasExistingPin ? "PIN (bereits hinterlegt)" : "PIN"}
</label>
<div className="relative">
<input
type={showSimPins[index] ? 'text' : 'password'}
value={card.pin}
onChange={(e) => {
const updated = [...simCards];
updated[index].pin = e.target.value;
setSimCards(updated);
}}
placeholder={card.hasExistingPin ? "Leer = beibehalten" : "4-stellig"}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={() => setShowSimPins(prev => ({ ...prev, [index]: !prev[index] }))}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
>
{showSimPins[index] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{card.hasExistingPuk ? "PUK (bereits hinterlegt)" : "PUK"}
</label>
<div className="relative">
<input
type={showSimPuks[index] ? 'text' : 'password'}
value={card.puk}
onChange={(e) => {
const updated = [...simCards];
updated[index].puk = e.target.value;
setSimCards(updated);
}}
placeholder={card.hasExistingPuk ? "Leer = beibehalten" : "8-stellig"}
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={() => setShowSimPuks(prev => ({ ...prev, [index]: !prev[index] }))}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
>
{showSimPuks[index] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
<Button
type="button"
variant="secondary"
onClick={() => {
setSimCards([...simCards, {
phoneNumber: '',
simCardNumber: '',
pin: '',
puk: '',
isMultisim: false,
isMain: simCards.length === 0, // Erste Karte ist Hauptkarte
}]);
}}
>
<Plus className="w-4 h-4 mr-2" />
SIM-Karte hinzufügen
</Button>
</Card>
</>
)}
{/* TV-specific fields */}
{contractType === 'TV' && (
<Card className="mb-6" title="TV-Details">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input label="Receiver Modell" {...register('receiverModel')} />
<Input label="Smartcard-Nummer" {...register('smartcardNumber')} />
<Input label="Paket" {...register('tvPackage')} placeholder="z.B. Basis, Premium, Sport" />
</div>
</Card>
)}
{/* Car Insurance-specific fields */}
{contractType === 'CAR_INSURANCE' && (
<Card className="mb-6" title="KFZ-Versicherung Details">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input label="Kennzeichen" {...register('licensePlate')} />
<Input label="HSN" {...register('hsn')} />
<Input label="TSN" {...register('tsn')} />
<Input label="FIN (VIN)" {...register('vin')} />
<Input label="Fahrzeugtyp" {...register('vehicleType')} />
<Input
label="Erstzulassung"
type="date"
{...register('firstRegistration')}
value={watch('firstRegistration') || ''}
onClear={() => setValue('firstRegistration', '')}
/>
<Input label="SF-Klasse" {...register('noClaimsClass')} />
<Select
label="Versicherungsart"
{...register('insuranceType')}
options={[
{ value: 'LIABILITY', label: 'Haftpflicht' },
{ value: 'PARTIAL', label: 'Teilkasko' },
{ value: 'FULL', label: 'Vollkasko' },
]}
/>
<Input label="SB Teilkasko (€)" type="number" {...register('deductiblePartial')} />
<Input label="SB Vollkasko (€)" type="number" {...register('deductibleFull')} />
<Input label="Versicherungsscheinnummer" {...register('policyNumber')} />
<Input label="Vorversicherer" {...register('previousInsurer')} />
</div>
</Card>
)}
<Card className="mb-6" title="Notizen">
<textarea
{...register('notes')}
rows={4}
className="block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Interne Notizen..."
/>
</Card>
<div className="flex justify-end gap-4">
<Button type="button" variant="secondary" onClick={() => navigate(-1)}>
Abbrechen
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Speichern...' : 'Speichern'}
</Button>
</div>
</form>
{/* Status-Info Modal */}
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
</div>
);
}