import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Plus, Edit, Trash2, ChevronDown, ChevronUp, History, Clock, Bot, User } from 'lucide-react'; import Modal from '../ui/Modal'; import Button from '../ui/Button'; import Input from '../ui/Input'; import Badge from '../ui/Badge'; import { contractHistoryApi } from '../../services/api'; import type { ContractHistoryEntry } from '../../types'; interface ContractHistorySectionProps { contractId: number; canEdit: boolean; // Map: contractNumber → contractId. Wird genutzt um in title/description // erwähnte Vertragsnummern als Link auf den jeweiligen Vertrag zu rendern. // Aufgebaut aus previousContract + followUpContract des aktuellen Vertrags. knownContracts?: Record; } // Vertragsnummer-Pattern: 3 Großbuchstaben + Bindestrich + Alphanumerisch // (siehe backend/src/utils/helpers.ts generateContractNumber). const CONTRACT_NUMBER_REGEX = /\b([A-Z]{3}-[A-Z0-9]{6,})\b/g; // Rendert einen Text und ersetzt enthaltene Vertragsnummern durch Links, // falls sie in der knownContracts-Map auflösbar sind. Nicht aufgelöste Nummern // bleiben als normaler Text. function renderTextWithContractLinks( text: string, knownContracts?: Record, ): React.ReactNode { if (!knownContracts || Object.keys(knownContracts).length === 0) return text; const parts: React.ReactNode[] = []; let lastIndex = 0; let match: RegExpExecArray | null; CONTRACT_NUMBER_REGEX.lastIndex = 0; while ((match = CONTRACT_NUMBER_REGEX.exec(text)) !== null) { const num = match[1]; const id = knownContracts[num]; if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index)); if (id) { parts.push( {num} , ); } else { parts.push(num); } lastIndex = match.index + num.length; } if (lastIndex < text.length) parts.push(text.slice(lastIndex)); return parts.length > 0 ? <>{parts} : text; } export default function ContractHistorySection({ contractId, canEdit, knownContracts, }: ContractHistorySectionProps) { const [isExpanded, setIsExpanded] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [editingEntry, setEditingEntry] = useState(null); const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['contract-history', contractId], queryFn: () => contractHistoryApi.getByContract(contractId), }); const deleteEntryMutation = useMutation({ mutationFn: (entryId: number) => contractHistoryApi.delete(contractId, entryId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract-history', contractId] }); }, }); const entries = data?.data || []; // Sort entries by date (newest first) const sortedEntries = [...entries].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); return (

Vertragshistorie

{entries.length}
{canEdit && ( )} {entries.length > 0 && ( )}
{/* Loading state */} {isLoading && (

Laden...

)} {/* Collapsed view - show latest entry */} {!isExpanded && !isLoading && sortedEntries.length > 0 && (
{new Date(sortedEntries[0].createdAt).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', })} {' - '} {renderTextWithContractLinks(sortedEntries[0].title, knownContracts)}
)} {/* Expanded view */} {isExpanded && !isLoading && sortedEntries.length > 0 && (
{sortedEntries.map((entry) => (
{renderTextWithContractLinks(entry.title, knownContracts)} {entry.isAutomatic ? ( Auto ) : ( Manuell )}
{entry.description && (

{renderTextWithContractLinks(entry.description, knownContracts)}

)}
{new Date(entry.createdAt).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', })} von {entry.createdBy}
{canEdit && !entry.isAutomatic && (
)}
))}
)} {isExpanded && !isLoading && sortedEntries.length === 0 && (

Keine Historie vorhanden.

)} {/* Add/Edit Modal */} {(showAddModal || editingEntry) && ( { setShowAddModal(false); setEditingEntry(null); }} contractId={contractId} entry={editingEntry} /> )}
); } // History Entry Modal Component function HistoryEntryModal({ isOpen, onClose, contractId, entry, }: { isOpen: boolean; onClose: () => void; contractId: number; entry?: ContractHistoryEntry | null; }) { const queryClient = useQueryClient(); const isEditing = !!entry; const [formData, setFormData] = useState({ title: entry?.title || '', description: entry?.description || '', }); const [error, setError] = useState(null); const createMutation = useMutation({ mutationFn: () => contractHistoryApi.create(contractId, { title: formData.title, description: formData.description || undefined, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract-history', contractId] }); onClose(); }, onError: (err: Error) => { setError(err.message); }, }); const updateMutation = useMutation({ mutationFn: () => contractHistoryApi.update(contractId, entry!.id, { title: formData.title, description: formData.description || undefined, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contract-history', contractId] }); onClose(); }, onError: (err: Error) => { setError(err.message); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!formData.title.trim()) { setError('Titel ist erforderlich'); return; } if (isEditing) { updateMutation.mutate(); } else { createMutation.mutate(); } }; const isPending = createMutation.isPending || updateMutation.isPending; return (
{error && (
{error}
)} setFormData({ ...formData, title: e.target.value })} placeholder="z.B. kWh auf 18000 erhöht" required />