From 3a9cece9299190bc43a71d283803b19e807f95be Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 14:33:11 +0200 Subject: [PATCH] Vertragshistorie: Vertragsnummern als Link in neuem Tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erwähnte Vertragsnummern (Pattern PREFIX-RANDOM) in Title und Description werden gegen previousContract + followUpContract des aktuellen Vertrags aufgelöst und als Link mit target="_blank" gerendert. Nicht aufgelöste Nummern bleiben als Text. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../contracts/ContractHistorySection.tsx | 53 +++++++++++++++++-- .../src/pages/contracts/ContractDetail.tsx | 8 +++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/contracts/ContractHistorySection.tsx b/frontend/src/components/contracts/ContractHistorySection.tsx index a944b7af..6df7ff87 100644 --- a/frontend/src/components/contracts/ContractHistorySection.tsx +++ b/frontend/src/components/contracts/ContractHistorySection.tsx @@ -1,4 +1,5 @@ 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'; @@ -11,11 +12,57 @@ 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); @@ -84,7 +131,7 @@ export default function ContractHistorySection({ })} {' - '} - {sortedEntries[0].title} + {renderTextWithContractLinks(sortedEntries[0].title, knownContracts)} )} @@ -99,7 +146,7 @@ export default function ContractHistorySection({
- {entry.title} + {renderTextWithContractLinks(entry.title, knownContracts)} {entry.isAutomatic ? ( @@ -115,7 +162,7 @@ export default function ContractHistorySection({
{entry.description && (

- {entry.description} + {renderTextWithContractLinks(entry.description, knownContracts)}

)}
diff --git a/frontend/src/pages/contracts/ContractDetail.tsx b/frontend/src/pages/contracts/ContractDetail.tsx index 71524935..10fa9995 100644 --- a/frontend/src/pages/contracts/ContractDetail.tsx +++ b/frontend/src/pages/contracts/ContractDetail.tsx @@ -3224,6 +3224,14 @@ export default function ContractDetail() { )}