Vertragshistorie: Vertragsnummern als Link in neuem Tab
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, number>;
|
||||
}
|
||||
|
||||
// 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<string, number>,
|
||||
): 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(
|
||||
<Link
|
||||
key={`${match.index}-${num}`}
|
||||
to={`/contracts/${id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 hover:underline font-mono"
|
||||
>
|
||||
{num}
|
||||
</Link>,
|
||||
);
|
||||
} 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({
|
||||
})}
|
||||
</span>
|
||||
{' - '}
|
||||
{sortedEntries[0].title}
|
||||
{renderTextWithContractLinks(sortedEntries[0].title, knownContracts)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -99,7 +146,7 @@ export default function ContractHistorySection({
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{entry.title}
|
||||
{renderTextWithContractLinks(entry.title, knownContracts)}
|
||||
</span>
|
||||
{entry.isAutomatic ? (
|
||||
<span className="flex items-center gap-1 px-1.5 py-0.5 text-xs rounded bg-blue-100 text-blue-700" title="Automatisch erstellt">
|
||||
@@ -115,7 +162,7 @@ export default function ContractHistorySection({
|
||||
</div>
|
||||
{entry.description && (
|
||||
<p className="text-sm text-gray-600 whitespace-pre-wrap mb-1">
|
||||
{entry.description}
|
||||
{renderTextWithContractLinks(entry.description, knownContracts)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400">
|
||||
|
||||
@@ -3224,6 +3224,14 @@ export default function ContractDetail() {
|
||||
<ContractHistorySection
|
||||
contractId={contractId}
|
||||
canEdit={hasPermission('contracts:update')}
|
||||
knownContracts={{
|
||||
...(c.previousContract?.contractNumber
|
||||
? { [c.previousContract.contractNumber]: c.previousContract.id }
|
||||
: {}),
|
||||
...(c.followUpContract?.contractNumber
|
||||
? { [c.followUpContract.contractNumber]: c.followUpContract.id }
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user