import { useState, useEffect } from 'react'; import { useParams, Link, useNavigate, useLocation } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi, gdprApi } from '../../services/api'; import { ContractEmailsSection } from '../../components/email'; import { ContractDetailModal, ContractHistorySection } from '../../components/contracts'; import InvoicesSection from '../../components/contracts/InvoicesSection'; import { useAuth } from '../../context/AuthContext'; import Card from '../../components/ui/Card'; import Button from '../../components/ui/Button'; import Badge from '../../components/ui/Badge'; import Input from '../../components/ui/Input'; import Modal from '../../components/ui/Modal'; import FileUpload from '../../components/ui/FileUpload'; import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield } from 'lucide-react'; import { calculateConsumption, calculateCosts } from '../../utils/energyCalculations'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask } from '../../types'; const typeLabels: Record = { ELECTRICITY: 'Strom', GAS: 'Gas', DSL: 'DSL', CABLE: 'Kabelinternet', FIBER: 'Glasfaser', MOBILE: 'Mobilfunk', TV: 'TV', CAR_INSURANCE: 'KFZ-Versicherung', }; const statusLabels: Record = { DRAFT: 'Entwurf', PENDING: 'Ausstehend', ACTIVE: 'Aktiv', CANCELLED: 'Gekündigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', }; const statusVariants: Record = { ACTIVE: 'success', PENDING: 'warning', CANCELLED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', }; // 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 (

Vertragsstatus-Übersicht

{statusDescriptions.map(({ status, label, description, color }) => (
{label} {description}
))}
); } // Prüft ob die Laufzeit als "unbefristet" gilt (≤ 4 Wochen / 1 Monat / 30 Tage) function isUnlimitedDuration(durationCode: string): boolean { const match = durationCode.match(/^(\d+)([TMWJ])$/); if (!match) return false; const value = parseInt(match[1]); const unit = match[2]; // Alles in Tage umrechnen (1 Monat ≈ 30 Tage, 1 Woche = 7 Tage) let days = 0; if (unit === 'T') days = value; else if (unit === 'W') days = value * 7; else if (unit === 'M') days = value * 30; else if (unit === 'J') days = value * 365; // ≤ 30 Tage (entspricht 4 Wochen = 28 Tage, 1 Monat = 30 Tage) return days <= 30; } // SimCard Display Component with PIN/PUK reveal function SimCardDisplay({ simCard }: { simCard: SimCard }) { const [showCredentials, setShowCredentials] = useState(false); const [credentials, setCredentials] = useState<{ pin: string | null; puk: string | null } | null>(null); const [isLoading, setIsLoading] = useState(false); const handleShowCredentials = async () => { if (showCredentials) { setShowCredentials(false); setCredentials(null); } else { setIsLoading(true); try { const res = await contractApi.getSimCardCredentials(simCard.id); if (res.data) { setCredentials(res.data); setShowCredentials(true); } } catch (err) { alert('PIN/PUK konnte nicht geladen werden'); } finally { setIsLoading(false); } } }; return (
{simCard.isMain && Hauptkarte} {simCard.isMultisim && Multisim}
{simCard.phoneNumber && (
Rufnummer
{simCard.phoneNumber}
)} {simCard.simCardNumber && (
SIM-Nr.
{simCard.simCardNumber}
)}
PIN
{showCredentials && credentials?.pin ? ( <> {credentials.pin} ) : '••••'}
PUK
{showCredentials && credentials?.puk ? ( <> {credentials.puk} ) : '••••••••'}
); } // Meter Readings Section Component function MeterReadingsSection({ meterId, meterType, readings, contractId, canEdit, }: { meterId: number; meterType: 'ELECTRICITY' | 'GAS'; readings: MeterReading[]; contractId: number; canEdit: boolean; }) { const [isExpanded, setIsExpanded] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [editingReading, setEditingReading] = useState(null); const queryClient = useQueryClient(); const deleteReadingMutation = useMutation({ mutationFn: (readingId: number) => meterApi.deleteReading(meterId, readingId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); }, }); // Sort readings by date (newest first) const sortedReadings = [...readings].sort( (a, b) => new Date(b.readingDate).getTime() - new Date(a.readingDate).getTime() ); const defaultUnit = meterType === 'ELECTRICITY' ? 'kWh' : 'm³'; return (

Zählerstände

{readings.length}
{canEdit && ( )} {readings.length > 0 && ( )}
{isExpanded && readings.length > 0 && (
{sortedReadings.map((reading) => (
{new Date(reading.readingDate).toLocaleDateString('de-DE')}
{reading.value.toLocaleString('de-DE')} {reading.unit} {canEdit && (
)}
))}
)} {!isExpanded && readings.length > 0 && (

Letzter Stand: {sortedReadings[0].value.toLocaleString('de-DE')} {sortedReadings[0].unit} ({new Date(sortedReadings[0].readingDate).toLocaleDateString('de-DE')})

)} {readings.length === 0 && (

Keine Zählerstände vorhanden.

)} {/* Add/Edit Reading Modal */} {(showAddModal || editingReading) && ( { setShowAddModal(false); setEditingReading(null); }} meterId={meterId} contractId={contractId} reading={editingReading} defaultUnit={defaultUnit} /> )}
); } // Meter Reading Modal Component function MeterReadingModal({ isOpen, onClose, meterId, contractId, reading, defaultUnit, }: { isOpen: boolean; onClose: () => void; meterId: number; contractId: number; reading?: MeterReading | null; defaultUnit: string; }) { const queryClient = useQueryClient(); const isEditing = !!reading; const [formData, setFormData] = useState({ readingDate: reading?.readingDate ? new Date(reading.readingDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], value: reading?.value?.toString() || '', notes: reading?.notes || '', }); const createMutation = useMutation({ mutationFn: (data: Partial) => meterApi.addReading(meterId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); onClose(); }, }); const updateMutation = useMutation({ mutationFn: (data: Partial) => meterApi.updateReading(meterId, reading!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data = { readingDate: new Date(formData.readingDate), value: parseFloat(formData.value), unit: defaultUnit, notes: formData.notes || undefined, }; if (isEditing) { updateMutation.mutate(data as unknown as Partial); } else { createMutation.mutate(data as unknown as Partial); } }; const isPending = createMutation.isPending || updateMutation.isPending; return (
setFormData({ ...formData, readingDate: e.target.value })} required />
setFormData({ ...formData, value: e.target.value })} required />
{defaultUnit}
setFormData({ ...formData, notes: e.target.value })} />
); } // Energy Consumption and Cost Calculation Component function EnergyConsumptionCalculation({ contractType, readings, startDate, endDate, basePrice, unitPrice, bonus, }: { contractType: 'ELECTRICITY' | 'GAS'; readings: MeterReading[]; startDate: string; endDate: string; basePrice?: number; unitPrice?: number; bonus?: number; }) { // Berechnung durchführen const consumption = calculateConsumption(readings, startDate, endDate, contractType); const costs = consumption.consumptionKwh > 0 ? calculateCosts(consumption.consumptionKwh, basePrice, unitPrice, bonus) : null; // Nichts anzeigen wenn keine Daten if (consumption.type === 'none') return null; const formatNumber = (num: number, decimals: number = 2) => num.toLocaleString('de-DE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); const formatDate = (dateStr: string) => new Date(dateStr).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); return (
{/* Header */}

Verbrauch & Kosten

{consumption.type === 'exact' && ( Exakt )} {consumption.type === 'projected' && ( Hochrechnung )}
{/* Fall C: Unzureichende Daten */} {consumption.type === 'insufficient' ? (

{consumption.message}

) : (
{/* Verbrauchsanzeige */}
Berechneter Verbrauch {consumption.type === 'projected' && ' (hochgerechnet)'}
{contractType === 'GAS' ? ( <> {formatNumber(consumption.consumptionM3 || 0)} m³ = {formatNumber(consumption.consumptionKwh)} kWh ) : ( {formatNumber(consumption.consumptionKwh)} kWh )}
{consumption.startReading && consumption.endReading && (

Basierend auf Zählerständen vom {formatDate(consumption.startReading.readingDate)} bis {formatDate(consumption.endReading.readingDate)}

)}
{/* Kostenrechnung */} {costs && (
Kostenvorschau
{/* Grundpreis */} {basePrice != null && basePrice > 0 && (
Grundpreis: {formatNumber(basePrice)} €/Mon × 12 {formatNumber(costs.annualBaseCost)} €
)} {/* Arbeitspreis */} {unitPrice != null && unitPrice > 0 && (
Arbeitspreis: {formatNumber(consumption.consumptionKwh)} kWh × {formatNumber(unitPrice, 4)} € {formatNumber(costs.annualConsumptionCost)} €
)} {/* Trennlinie */}
Jahreskosten {formatNumber(costs.annualTotalCost)} €
{/* Bonus */} {costs.bonus != null && costs.bonus > 0 && ( <>
Bonus - {formatNumber(costs.bonus)} €
Effektive Jahreskosten {formatNumber(costs.effectiveAnnualCost)} €
)} {/* Monatlicher Abschlag */}
Monatlicher Abschlag {formatNumber(costs.monthlyPayment)} €
)}
)}
); } // Contract Task Item Component (handles subtasks) function ContractTaskItem({ task, contractId, canEdit, isCustomerPortal, isCompleted, onEdit, }: { task: ContractTask; contractId: number; canEdit: boolean; isCustomerPortal: boolean; isCompleted: boolean; onEdit: () => void; }) { const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); const [showSubtaskInput, setShowSubtaskInput] = useState(false); const [editingSubtaskId, setEditingSubtaskId] = useState(null); const [editSubtaskTitle, setEditSubtaskTitle] = useState(''); const queryClient = useQueryClient(); const completeMutation = useMutation({ mutationFn: (taskId: number) => contractTaskApi.complete(taskId), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); }, }); const reopenMutation = useMutation({ mutationFn: (taskId: number) => contractTaskApi.reopen(taskId), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); }, }); const deleteMutation = useMutation({ mutationFn: (taskId: number) => contractTaskApi.delete(taskId), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); }, }); const createSubtaskMutation = useMutation({ mutationFn: (title: string) => contractTaskApi.createSubtask(task.id, title), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); setNewSubtaskTitle(''); setShowSubtaskInput(false); }, onError: (error) => { console.error('Fehler beim Erstellen der Unteraufgabe:', error); alert('Fehler beim Erstellen der Unteraufgabe. Bitte versuchen Sie es erneut.'); }, }); // Kundenportal: Antwort auf eigenes Ticket const createReplyMutation = useMutation({ mutationFn: (title: string) => contractTaskApi.createReply(task.id, title), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); setNewSubtaskTitle(''); setShowSubtaskInput(false); }, onError: (error) => { console.error('Fehler beim Erstellen der Antwort:', error); alert('Fehler beim Erstellen der Antwort. Bitte versuchen Sie es erneut.'); }, }); const updateSubtaskMutation = useMutation({ mutationFn: ({ id, title }: { id: number; title: string }) => contractTaskApi.updateSubtask(id, title), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); setEditingSubtaskId(null); setEditSubtaskTitle(''); }, }); const completeSubtaskMutation = useMutation({ mutationFn: (subtaskId: number) => contractTaskApi.completeSubtask(subtaskId), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); }, }); const reopenSubtaskMutation = useMutation({ mutationFn: (subtaskId: number) => contractTaskApi.reopenSubtask(subtaskId), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); }, }); const deleteSubtaskMutation = useMutation({ mutationFn: (subtaskId: number) => contractTaskApi.deleteSubtask(subtaskId), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); }, }); const handleAddSubtask = (e: React.FormEvent) => { e.preventDefault(); if (newSubtaskTitle.trim()) { if (isCustomerPortal) { createReplyMutation.mutate(newSubtaskTitle.trim()); } else { createSubtaskMutation.mutate(newSubtaskTitle.trim()); } } }; const handleEditSubtask = (e: React.FormEvent) => { e.preventDefault(); if (editSubtaskTitle.trim() && editingSubtaskId) { updateSubtaskMutation.mutate({ id: editingSubtaskId, title: editSubtaskTitle.trim() }); } }; const startEditingSubtask = (subtaskId: number, currentTitle: string) => { setEditingSubtaskId(subtaskId); setEditSubtaskTitle(currentTitle); }; const cancelEditingSubtask = () => { setEditingSubtaskId(null); setEditSubtaskTitle(''); }; const subtasks = task.subtasks || []; const openSubtasks = subtasks.filter(s => s.status === 'OPEN'); const completedSubtasks = subtasks.filter(s => s.status === 'COMPLETED'); // Labels für Subtasks (Antwort im Portal, Unteraufgabe für Mitarbeiter) const subtaskLabels = isCustomerPortal ? { singular: 'Antwort', placeholder: 'Antwort...', deleteConfirm: 'Antwort löschen?' } : { singular: 'Unteraufgabe', placeholder: 'Unteraufgabe...', deleteConfirm: 'Unteraufgabe löschen?' }; // Render a single subtask row const renderSubtask = (subtask: ContractTaskSubtask, isSubtaskCompleted: boolean) => { const isEditing = editingSubtaskId === subtask.id; if (isEditing) { return (
setEditSubtaskTitle(e.target.value)} className="flex-1 text-sm px-2 py-1 border rounded focus:outline-none focus:ring-1 focus:ring-blue-500" autoFocus />
); } return (
{subtask.title} {canEdit && !isCustomerPortal && !isSubtaskCompleted && (
)} {canEdit && !isCustomerPortal && isSubtaskCompleted && ( )}

{subtask.createdBy && `${subtask.createdBy} • `} {isSubtaskCompleted ? `Erledigt am ${subtask.completedAt ? new Date(subtask.completedAt).toLocaleDateString('de-DE') : new Date(subtask.updatedAt).toLocaleDateString('de-DE')}` : new Date(subtask.createdAt).toLocaleDateString('de-DE')}

); }; return (
{task.title} {task.visibleInPortal && ( Portal )} {subtasks.length > 0 && ( ({completedSubtasks.length}/{subtasks.length}) )}
{task.description && (

{task.description}

)}

{task.createdBy && `${task.createdBy} • `} {isCompleted ? `Erledigt am ${task.completedAt ? new Date(task.completedAt).toLocaleDateString('de-DE') : '-'}` : new Date(task.createdAt).toLocaleDateString('de-DE')}

{/* Subtasks */} {subtasks.length > 0 && (
{openSubtasks.map((subtask) => renderSubtask(subtask, false))} {completedSubtasks.map((subtask) => renderSubtask(subtask, true))}
)} {/* Add Subtask */} {/* Subtask/Antwort hinzufügen: Mitarbeiter mit Berechtigung ODER Kundenportal */} {!isCompleted && ((canEdit && !isCustomerPortal) || isCustomerPortal) && (
{showSubtaskInput ? (
setNewSubtaskTitle(e.target.value)} placeholder={subtaskLabels.placeholder} className="flex-1 text-sm px-2 py-1 border rounded focus:outline-none focus:ring-1 focus:ring-blue-500" autoFocus />
) : ( )}
)}
{canEdit && !isCustomerPortal && (
{!isCompleted && ( )}
)}
); } // Contract Tasks Section Component function ContractTasksSection({ contractId, canEdit, isCustomerPortal, }: { contractId: number; canEdit: boolean; isCustomerPortal: boolean; }) { const [showAddModal, setShowAddModal] = useState(false); const [editingTask, setEditingTask] = useState(null); const { data: tasksData, isLoading } = useQuery({ queryKey: ['contract-tasks', contractId], queryFn: () => contractTaskApi.getByContract(contractId), staleTime: 0, // Immer als "stale" behandeln gcTime: 0, // Kein Caching refetchOnMount: 'always', // Immer neu laden beim Mounten }); // Lade öffentliche Einstellungen (für Kundenportal) const { data: settingsData, isLoading: isSettingsLoading } = useQuery({ queryKey: ['app-settings-public'], queryFn: () => appSettingsApi.getPublic(), enabled: isCustomerPortal, staleTime: 0, // Immer neu laden, damit Einstellungsänderungen sofort wirken }); // Wichtig: Nur true wenn explizit aktiviert UND geladen const supportTicketsEnabled = !isSettingsLoading && settingsData?.data?.customerSupportTicketsEnabled === 'true'; const tasks = tasksData?.data || []; const openTasks = tasks.filter(t => t.status === 'OPEN'); const completedTasks = tasks.filter(t => t.status === 'COMPLETED'); // Labels je nach Portal/Mitarbeiter const labels = isCustomerPortal ? { title: 'Support-Anfragen', singular: 'Support-Anfrage', button: 'Anfrage erstellen', empty: 'Keine Support-Anfragen vorhanden.' } : { title: 'Aufgaben', singular: 'Aufgabe', button: 'Aufgabe', empty: 'Keine Aufgaben vorhanden.' }; const IconComponent = isCustomerPortal ? MessageSquare : ClipboardList; // Warte auf beide Queries bevor gerändert wird const stillLoading = isLoading || (isCustomerPortal && isSettingsLoading); if (stillLoading) { return (
Laden...
); } // Zeige Button wenn: Mitarbeiter mit Berechtigung ODER Kundenportal mit aktivierten Support-Tickets const canCreateTask = (canEdit && !isCustomerPortal) || (isCustomerPortal && supportTicketsEnabled); return (
{openTasks.length} offen, {completedTasks.length} erledigt
{canCreateTask && ( )}
{tasks.length === 0 ? (

{labels.empty}

) : (
{/* Open Tasks first */} {openTasks.map((task) => ( setEditingTask(task)} /> ))} {/* Completed Tasks */} {completedTasks.length > 0 && openTasks.length > 0 && (
)} {completedTasks.map((task) => ( {}} /> ))}
)} {/* Add/Edit Task Modal */} {(showAddModal || editingTask) && ( { setShowAddModal(false); setEditingTask(null); }} contractId={contractId} task={editingTask} isCustomerPortal={isCustomerPortal} /> )} ); } // Contract Task Modal Component function ContractTaskModal({ isOpen, onClose, contractId, task, isCustomerPortal = false, }: { isOpen: boolean; onClose: () => void; contractId: number; task?: ContractTask | null; isCustomerPortal?: boolean; }) { const queryClient = useQueryClient(); const isEditing = !!task; const [formData, setFormData] = useState({ title: task?.title || '', description: task?.description || '', visibleInPortal: task?.visibleInPortal || false, }); // Reset formData when modal opens or task changes useEffect(() => { if (isOpen) { setFormData({ title: task?.title || '', description: task?.description || '', visibleInPortal: task?.visibleInPortal || false, }); } }, [isOpen, task]); // Für Mitarbeiter: normale Task-Erstellung const createMutation = useMutation({ mutationFn: (data: { title: string; description?: string; visibleInPortal?: boolean }) => contractTaskApi.create(contractId, data), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); onClose(); }, }); // Für Kundenportal: Support-Ticket-Erstellung const createSupportTicketMutation = useMutation({ mutationFn: (data: { title: string; description?: string }) => contractTaskApi.createSupportTicket(contractId, data), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); onClose(); }, }); const updateMutation = useMutation({ mutationFn: (data: { title?: string; description?: string; visibleInPortal?: boolean }) => contractTaskApi.update(task!.id, data), onSuccess: async () => { await queryClient.refetchQueries({ queryKey: ['contract-tasks', contractId] }); onClose(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (isEditing) { updateMutation.mutate({ title: formData.title, description: formData.description || undefined, visibleInPortal: formData.visibleInPortal, }); } else if (isCustomerPortal) { // Kundenportal: Support-Ticket erstellen createSupportTicketMutation.mutate({ title: formData.title, description: formData.description || undefined, }); } else { // Mitarbeiter: normale Aufgabe erstellen createMutation.mutate({ title: formData.title, description: formData.description || undefined, visibleInPortal: formData.visibleInPortal, }); } }; const isPending = createMutation.isPending || createSupportTicketMutation.isPending || updateMutation.isPending; // Labels für Kundenportal vs. Mitarbeiter const labels = isCustomerPortal ? { modalTitle: isEditing ? 'Anfrage bearbeiten' : 'Neue Support-Anfrage', titleLabel: 'Betreff', titlePlaceholder: 'Kurze Beschreibung Ihrer Anfrage', descLabel: 'Ihre Nachricht', descPlaceholder: 'Beschreiben Sie Ihr Anliegen...', submitBtn: isEditing ? 'Speichern' : 'Anfrage senden', } : { modalTitle: isEditing ? 'Aufgabe bearbeiten' : 'Neue Aufgabe', titleLabel: 'Titel', titlePlaceholder: 'Kurze Beschreibung der Aufgabe', descLabel: 'Beschreibung (optional)', descPlaceholder: 'Details zur Aufgabe...', submitBtn: isEditing ? 'Speichern' : 'Erstellen', }; return (
setFormData({ ...formData, title: e.target.value })} required placeholder={labels.titlePlaceholder} />