Files
breakpilot-compliance/admin-compliance/app/sdk/vendor-compliance/contracts/page.tsx
Benjamin Admin 215b95adfa
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard).
SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest.
Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:43:00 +01:00

383 lines
16 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import Link from 'next/link'
import {
useVendorCompliance,
ContractDocument,
DocumentType,
ContractStatus,
ContractReviewStatus,
DOCUMENT_TYPE_META,
formatDate,
} from '@/lib/sdk/vendor-compliance'
export default function ContractsPage() {
const { contracts, vendors, deleteContract, startContractReview, isLoading } = useVendorCompliance()
const [searchTerm, setSearchTerm] = useState('')
const [typeFilter, setTypeFilter] = useState<DocumentType | 'ALL'>('ALL')
const [statusFilter, setStatusFilter] = useState<ContractStatus | 'ALL'>('ALL')
const [reviewFilter, setReviewFilter] = useState<ContractReviewStatus | 'ALL'>('ALL')
const filteredContracts = useMemo(() => {
let result = [...contracts]
// Search filter
if (searchTerm) {
const term = searchTerm.toLowerCase()
result = result.filter((c) => {
const vendor = vendors.find((v) => v.id === c.vendorId)
return (
c.originalName.toLowerCase().includes(term) ||
vendor?.name.toLowerCase().includes(term)
)
})
}
// Type filter
if (typeFilter !== 'ALL') {
result = result.filter((c) => c.documentType === typeFilter)
}
// Status filter
if (statusFilter !== 'ALL') {
result = result.filter((c) => c.status === statusFilter)
}
// Review filter
if (reviewFilter !== 'ALL') {
result = result.filter((c) => c.reviewStatus === reviewFilter)
}
// Sort by date (newest first)
result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return result
}, [contracts, vendors, searchTerm, typeFilter, statusFilter, reviewFilter])
const handleDelete = async (id: string) => {
if (confirm('Möchten Sie diesen Vertrag wirklich löschen?')) {
await deleteContract(id)
}
}
const handleStartReview = async (id: string) => {
await startContractReview(id)
}
const getVendorName = (vendorId: string) => {
return vendors.find((v) => v.id === vendorId)?.name || 'Unbekannt'
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Verträge
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
AVV, SCC und andere Verträge mit LLM-gestützter Prüfung
</p>
</div>
<Link
href="/sdk/vendor-compliance/contracts/upload"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
Vertrag hochladen
</Link>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="md:col-span-2">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Dateiname oder Vendor suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div>
<select
className="block w-full pl-3 pr-10 py-2 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as DocumentType | 'ALL')}
>
<option value="ALL">Alle Typen</option>
{Object.entries(DOCUMENT_TYPE_META).map(([key, value]) => (
<option key={key} value={key}>{value.de}</option>
))}
</select>
</div>
<div>
<select
className="block w-full pl-3 pr-10 py-2 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as ContractStatus | 'ALL')}
>
<option value="ALL">Alle Status</option>
<option value="DRAFT">Entwurf</option>
<option value="SIGNED">Unterschrieben</option>
<option value="ACTIVE">Aktiv</option>
<option value="EXPIRED">Abgelaufen</option>
<option value="TERMINATED">Beendet</option>
</select>
</div>
<div>
<select
className="block w-full pl-3 pr-10 py-2 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
value={reviewFilter}
onChange={(e) => setReviewFilter(e.target.value as ContractReviewStatus | 'ALL')}
>
<option value="ALL">Alle Reviews</option>
<option value="PENDING">Ausstehend</option>
<option value="IN_PROGRESS">In Bearbeitung</option>
<option value="COMPLETED">Abgeschlossen</option>
<option value="FAILED">Fehlgeschlagen</option>
</select>
</div>
</div>
</div>
{/* Contracts Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Dokument
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Vendor
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Typ
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Status
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Compliance
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Laufzeit
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Aktionen</span>
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredContracts.map((contract) => (
<tr key={contract.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded">
<svg className="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{contract.originalName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
v{contract.version} {(contract.fileSize / 1024).toFixed(1)} KB
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<Link
href={`/sdk/vendor-compliance/vendors/${contract.vendorId}`}
className="text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
{getVendorName(contract.vendorId)}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
{DOCUMENT_TYPE_META[contract.documentType]?.de || contract.documentType}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<ContractStatusBadge status={contract.status} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<ReviewStatusBadge
reviewStatus={contract.reviewStatus}
complianceScore={contract.complianceScore}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{contract.effectiveDate ? (
<>
{formatDate(contract.effectiveDate)}
{contract.expirationDate && (
<> - {formatDate(contract.expirationDate)}</>
)}
</>
) : (
'-'
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end gap-2">
<Link
href={`/sdk/vendor-compliance/contracts/${contract.id}`}
className="text-blue-600 hover:text-blue-900 dark:text-blue-400"
>
Anzeigen
</Link>
{contract.reviewStatus === 'PENDING' && (
<button
onClick={() => handleStartReview(contract.id)}
className="text-green-600 hover:text-green-900 dark:text-green-400"
>
Prüfen
</button>
)}
{contract.reviewStatus === 'COMPLETED' && (
<Link
href={`/sdk/vendor-compliance/contracts/${contract.id}/review`}
className="text-purple-600 hover:text-purple-900 dark:text-purple-400"
>
Ergebnis
</Link>
)}
<button
onClick={() => handleDelete(contract.id)}
className="text-red-600 hover:text-red-900 dark:text-red-400"
>
Löschen
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{filteredContracts.length === 0 && (
<div className="text-center py-12">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
Keine Verträge gefunden
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Laden Sie einen Vertrag hoch, um zu beginnen.
</p>
<div className="mt-6">
<Link
href="/sdk/vendor-compliance/contracts/upload"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
Vertrag hochladen
</Link>
</div>
</div>
)}
</div>
{/* Summary */}
<div className="text-sm text-gray-500 dark:text-gray-400">
{filteredContracts.length} von {contracts.length} Verträgen
</div>
</div>
)
}
function ContractStatusBadge({ status }: { status: ContractStatus }) {
const config = {
DRAFT: { label: 'Entwurf', color: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' },
SIGNED: { label: 'Unterschrieben', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
ACTIVE: { label: 'Aktiv', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
EXPIRED: { label: 'Abgelaufen', color: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' },
TERMINATED: { label: 'Beendet', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config[status].color}`}>
{config[status].label}
</span>
)
}
function ReviewStatusBadge({
reviewStatus,
complianceScore,
}: {
reviewStatus: ContractReviewStatus
complianceScore?: number
}) {
if (reviewStatus === 'COMPLETED' && complianceScore !== undefined) {
const scoreColor =
complianceScore >= 80
? 'text-green-600 dark:text-green-400'
: complianceScore >= 60
? 'text-yellow-600 dark:text-yellow-400'
: 'text-red-600 dark:text-red-400'
return (
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className={`h-2 rounded-full ${
complianceScore >= 80
? 'bg-green-500'
: complianceScore >= 60
? 'bg-yellow-500'
: 'bg-red-500'
}`}
style={{ width: `${complianceScore}%` }}
/>
</div>
<span className={`text-sm font-medium ${scoreColor}`}>{complianceScore}%</span>
</div>
)
}
const config = {
PENDING: { label: 'Ausstehend', color: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' },
IN_PROGRESS: { label: 'In Prüfung', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
COMPLETED: { label: 'Geprüft', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
FAILED: { label: 'Fehlgeschlagen', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config[reviewStatus].color}`}>
{config[reviewStatus].label}
</span>
)
}