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 { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Plus, Edit, Trash2, ChevronDown, ChevronUp, History, Clock, Bot, User } from 'lucide-react';
|
import { Plus, Edit, Trash2, ChevronDown, ChevronUp, History, Clock, Bot, User } from 'lucide-react';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
@@ -11,11 +12,57 @@ import type { ContractHistoryEntry } from '../../types';
|
|||||||
interface ContractHistorySectionProps {
|
interface ContractHistorySectionProps {
|
||||||
contractId: number;
|
contractId: number;
|
||||||
canEdit: boolean;
|
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({
|
export default function ContractHistorySection({
|
||||||
contractId,
|
contractId,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
knownContracts,
|
||||||
}: ContractHistorySectionProps) {
|
}: ContractHistorySectionProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
@@ -84,7 +131,7 @@ export default function ContractHistorySection({
|
|||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
{' - '}
|
{' - '}
|
||||||
{sortedEntries[0].title}
|
{renderTextWithContractLinks(sortedEntries[0].title, knownContracts)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -99,7 +146,7 @@ export default function ContractHistorySection({
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="text-sm font-medium text-gray-800">
|
<span className="text-sm font-medium text-gray-800">
|
||||||
{entry.title}
|
{renderTextWithContractLinks(entry.title, knownContracts)}
|
||||||
</span>
|
</span>
|
||||||
{entry.isAutomatic ? (
|
{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">
|
<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>
|
</div>
|
||||||
{entry.description && (
|
{entry.description && (
|
||||||
<p className="text-sm text-gray-600 whitespace-pre-wrap mb-1">
|
<p className="text-sm text-gray-600 whitespace-pre-wrap mb-1">
|
||||||
{entry.description}
|
{renderTextWithContractLinks(entry.description, knownContracts)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-400">
|
<div className="flex items-center gap-3 text-xs text-gray-400">
|
||||||
|
|||||||
@@ -3224,6 +3224,14 @@ export default function ContractDetail() {
|
|||||||
<ContractHistorySection
|
<ContractHistorySection
|
||||||
contractId={contractId}
|
contractId={contractId}
|
||||||
canEdit={hasPermission('contracts:update')}
|
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