Files
breakpilot-compliance/admin-compliance/app/sdk/obligations/page.tsx
Sharang Parnerkar 2ade65431a refactor(admin): split compliance-hub, obligations, document-generator pages
Each page.tsx was >1000 LOC; extract components to _components/ and hooks
to _hooks/ so page files stay under 500 LOC (164 / 255 / 243 respectively).
Zero behavior changes — logic relocated verbatim.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:10:14 +02:00

256 lines
11 KiB
TypeScript

'use client'
import React, { useMemo } from 'react'
import GapAnalysisView from '@/components/sdk/obligations/GapAnalysisView'
import { ObligationDocumentTab } from '@/components/sdk/obligations/ObligationDocumentTab'
import { useObligations } from './_hooks/useObligations'
import ObligationModal from './_components/ObligationModal'
import ObligationDetail from './_components/ObligationDetail'
import ObligationCard from './_components/ObligationCard'
import StatsGrid from './_components/StatsGrid'
import FilterBar from './_components/FilterBar'
import { ApplicableRegsBanner, NoProfileWarning, OverdueAlert, EmptyList } from './_components/InfoBanners'
import ObligationsHeader from './_components/ObligationsHeader'
// Tab definitions
type Tab = 'uebersicht' | 'editor' | 'profiling' | 'gap-analyse' | 'pflichtenregister'
const TABS: { key: Tab; label: string }[] = [
{ key: 'uebersicht', label: 'Uebersicht' },
{ key: 'editor', label: 'Detail-Editor' },
{ key: 'profiling', label: 'Profiling' },
{ key: 'gap-analyse', label: 'Gap-Analyse' },
{ key: 'pflichtenregister', label: 'Pflichtenregister' },
]
export default function ObligationsPage() {
const o = useObligations()
const complianceScore = o.complianceResult ? o.complianceResult.score : null
const renderUebersichtTab = () => (
<>
{o.error && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">{o.error}</div>
)}
<ApplicableRegsBanner regs={o.applicableRegs} />
{!o.sdkState.companyProfile && <NoProfileWarning />}
<StatsGrid stats={o.stats} complianceScore={complianceScore} />
<OverdueAlert stats={o.stats} />
{o.complianceResult && o.complianceResult.issues.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Compliance-Befunde ({o.complianceResult.issues.length})</h3>
<div className="space-y-2">
{o.complianceResult.issues.map((issue, i) => (
<div key={i} className="flex items-start gap-3 text-sm">
<span className={`px-2 py-0.5 text-xs rounded-full flex-shrink-0 ${
issue.severity === 'CRITICAL' ? 'bg-red-100 text-red-700' :
issue.severity === 'HIGH' ? 'bg-orange-100 text-orange-700' :
issue.severity === 'MEDIUM' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{issue.severity === 'CRITICAL' ? 'Kritisch' : issue.severity === 'HIGH' ? 'Hoch' : issue.severity === 'MEDIUM' ? 'Mittel' : 'Niedrig'}
</span>
<span className="text-gray-700">{issue.message}</span>
</div>
))}
</div>
</div>
)}
<FilterBar
filter={o.filter}
regulationFilter={o.regulationFilter}
searchQuery={o.searchQuery}
onFilter={o.setFilter}
onRegulationFilter={o.setRegulationFilter}
onSearch={o.setSearchQuery}
/>
{o.loading && <div className="text-center py-8 text-gray-500 text-sm">Lade Pflichten...</div>}
{!o.loading && (
<div className="space-y-3">
{o.filteredObligations.map(obl => (
<ObligationCard
key={obl.id}
obligation={obl}
onStatusChange={o.handleStatusChange}
onDetails={() => o.setDetailObligation(obl)}
/>
))}
{o.filteredObligations.length === 0 && <EmptyList onCreate={() => o.setShowModal(true)} />}
</div>
)}
</>
)
const renderEditorTab = () => (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-900">Pflichten bearbeiten ({o.obligations.length})</h3>
<button onClick={() => o.setShowModal(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">
Pflicht hinzufuegen
</button>
</div>
{o.loading && <p className="text-gray-500 text-sm">Lade...</p>}
{!o.loading && o.obligations.length === 0 && (
<p className="text-gray-500 text-sm">Noch keine Pflichten vorhanden. Erstellen Sie eine neue Pflicht oder nutzen Sie Auto-Profiling.</p>
)}
{!o.loading && o.obligations.length > 0 && (
<div className="space-y-2 max-h-[60vh] overflow-y-auto">
{o.obligations.map(obl => (
<div
key={obl.id}
className="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:bg-gray-50 cursor-pointer"
onClick={() => o.setEditObligation(obl)}
>
<div className="flex items-center gap-3 min-w-0">
<span className="text-sm text-gray-900 truncate">{obl.title}</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs text-gray-400">{obl.source}</span>
<button onClick={(e) => { e.stopPropagation(); o.setEditObligation(obl) }} className="text-xs text-purple-600 hover:text-purple-700 font-medium">
Bearbeiten
</button>
</div>
</div>
))}
</div>
)}
</div>
)
const renderProfilingTab = () => (
<>
{o.error && <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">{o.error}</div>}
{!o.sdkState.companyProfile && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-700">
Kein Unternehmensprofil vorhanden. Auto-Profiling verwendet Beispieldaten.{' '}
<a href="/sdk/company-profile" className="underline font-medium">Profil anlegen </a>
</div>
)}
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="w-12 h-12 mx-auto bg-purple-50 rounded-full flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 className="text-sm font-semibold text-gray-900">Auto-Profiling</h3>
<p className="text-xs text-gray-500 mt-1 mb-4">
Ermittelt automatisch anwendbare Regulierungen und Pflichten aus dem Unternehmensprofil und Compliance-Scope.
</p>
<button
onClick={o.handleAutoProfiling}
disabled={o.profiling}
className="px-5 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{o.profiling ? 'Profiling laeuft...' : 'Auto-Profiling starten'}
</button>
</div>
{o.applicableRegs.length > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<h3 className="text-sm font-semibold text-blue-900 mb-2">Anwendbare Regulierungen</h3>
<div className="flex flex-wrap gap-2">
{o.applicableRegs.map(reg => (
<span key={reg.id} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-white border border-blue-300 text-blue-800">
<svg className="w-3.5 h-3.5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{reg.name}
{reg.classification && <span className="text-blue-500">({reg.classification})</span>}
<span className="text-blue-400">{reg.obligation_count} Pflichten</span>
</span>
))}
</div>
</div>
)}
</>
)
const renderTabContent = () => {
switch (o.activeTab) {
case 'uebersicht': return renderUebersichtTab()
case 'editor': return renderEditorTab()
case 'profiling': return renderProfilingTab()
case 'gap-analyse': return <GapAnalysisView />
case 'pflichtenregister': return <ObligationDocumentTab obligations={o.obligations} complianceResult={o.complianceResult} />
}
}
return (
<div className="space-y-6">
{/* Modals */}
{(o.showModal || o.editObligation) && !o.detailObligation && (
<ObligationModal
initial={o.editObligation ? {
title: o.editObligation.title,
description: o.editObligation.description,
source: o.editObligation.source,
source_article: o.editObligation.source_article,
deadline: o.editObligation.deadline ? o.editObligation.deadline.slice(0, 10) : '',
status: o.editObligation.status,
priority: o.editObligation.priority,
responsible: o.editObligation.responsible,
linked_systems: o.editObligation.linked_systems?.join(', ') || '',
linked_vendor_ids: o.editObligation.linked_vendor_ids?.join(', ') || '',
notes: o.editObligation.notes || '',
} : undefined}
onClose={() => { o.setShowModal(false); o.setEditObligation(null) }}
onSave={async (form) => {
if (o.editObligation) {
await o.handleUpdate(o.editObligation.id, form)
o.setEditObligation(null)
} else {
await o.handleCreate(form)
o.setShowModal(false)
}
}}
/>
)}
{o.detailObligation && (
<ObligationDetail
obligation={o.detailObligation}
onClose={() => o.setDetailObligation(null)}
onStatusChange={o.handleStatusChange}
onDelete={o.handleDelete}
onEdit={() => {
o.setEditObligation(o.detailObligation)
o.setDetailObligation(null)
}}
/>
)}
{/* Header */}
<ObligationsHeader
profiling={o.profiling}
showGapAnalysis={o.activeTab === 'gap-analyse'}
onAutoProfiling={o.handleAutoProfiling}
onToggleGap={() => o.setActiveTab(o.activeTab === 'gap-analyse' ? 'uebersicht' : 'gap-analyse')}
onAdd={() => o.setShowModal(true)}
/>
{/* Tab Navigation */}
<div className="flex gap-1 border-b border-gray-200">
{TABS.map(tab => (
<button
key={tab.key}
onClick={() => o.setActiveTab(tab.key)}
className={`px-4 py-2.5 text-sm font-medium transition-colors ${
o.activeTab === tab.key
? 'border-b-2 border-purple-500 text-purple-700'
: 'text-gray-500 hover:text-gray-700 hover:border-b-2 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{renderTabContent()}
</div>
)
}