Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
sed replacement left orphaned hostname references in story page and empty lines in getApiBase functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
374 lines
15 KiB
TypeScript
374 lines
15 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
import {
|
||
REGULATIONS,
|
||
DOC_TYPES,
|
||
INDUSTRIES_LIST,
|
||
INDUSTRIES,
|
||
INDUSTRY_REGULATION_MAP,
|
||
TYPE_COLORS,
|
||
THEMATIC_GROUPS,
|
||
KEY_INTERSECTIONS,
|
||
RAG_DOCUMENTS,
|
||
isInRag,
|
||
} from '../rag-data'
|
||
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
|
||
import {
|
||
FutureOutlookSection,
|
||
RagCoverageSection,
|
||
FutureRegulationsSection,
|
||
LegalBasisSection,
|
||
} from './MapTabSections'
|
||
|
||
interface MapTabProps {
|
||
hook: UseRAGPageReturn
|
||
}
|
||
|
||
export function MapTab({ hook }: MapTabProps) {
|
||
const {
|
||
expandedRegulation,
|
||
setExpandedRegulation,
|
||
expandedDocTypes,
|
||
setExpandedDocTypes,
|
||
expandedMatrixDoc,
|
||
setExpandedMatrixDoc,
|
||
setActiveTab,
|
||
} = hook
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Industry Filter */}
|
||
<IndustryFilter
|
||
expandedRegulation={expandedRegulation}
|
||
setExpandedRegulation={setExpandedRegulation}
|
||
/>
|
||
|
||
{/* Thematic Groups */}
|
||
<ThematicGroupsSection setActiveTab={setActiveTab} setExpandedRegulation={setExpandedRegulation} />
|
||
|
||
{/* Key Intersections */}
|
||
<KeyIntersectionsSection />
|
||
|
||
{/* Regulation Matrix */}
|
||
<RegulationMatrix
|
||
expandedDocTypes={expandedDocTypes}
|
||
setExpandedDocTypes={setExpandedDocTypes}
|
||
expandedMatrixDoc={expandedMatrixDoc}
|
||
setExpandedMatrixDoc={setExpandedMatrixDoc}
|
||
/>
|
||
|
||
{/* Future Outlook Section */}
|
||
<FutureOutlookSection />
|
||
|
||
{/* RAG Coverage Overview */}
|
||
<RagCoverageSection />
|
||
|
||
{/* Potential Future Regulations */}
|
||
<FutureRegulationsSection />
|
||
|
||
{/* Legal Basis Info */}
|
||
<LegalBasisSection />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// --- Sub-components ---
|
||
|
||
function IndustryFilter({
|
||
expandedRegulation,
|
||
setExpandedRegulation,
|
||
}: {
|
||
expandedRegulation: string | null
|
||
setExpandedRegulation: (v: string | null) => void
|
||
}) {
|
||
return (
|
||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">Regulierungen nach Branche</h3>
|
||
<p className="text-sm text-slate-500 mb-4">
|
||
Waehlen Sie Ihre Branche, um relevante Regulierungen zu sehen.
|
||
</p>
|
||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||
{INDUSTRIES.map((industry) => {
|
||
const regs = INDUSTRY_REGULATION_MAP[industry.id] || []
|
||
return (
|
||
<button
|
||
key={industry.id}
|
||
onClick={() => setExpandedRegulation(industry.id === expandedRegulation ? null : industry.id)}
|
||
className={`p-4 rounded-lg border text-left transition-all ${
|
||
expandedRegulation === industry.id
|
||
? 'border-teal-500 bg-teal-50 ring-2 ring-teal-200'
|
||
: 'border-slate-200 hover:border-slate-300 hover:bg-slate-50'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-2">{industry.icon}</div>
|
||
<div className="font-medium text-slate-900 text-sm">{industry.name}</div>
|
||
<div className="text-xs text-slate-500 mt-1">{regs.length} Regulierungen</div>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* Selected Industry Details */}
|
||
{expandedRegulation && INDUSTRIES.find(i => i.id === expandedRegulation) && (
|
||
<div className="mt-6 p-4 bg-slate-50 rounded-lg">
|
||
{(() => {
|
||
const industry = INDUSTRIES.find(i => i.id === expandedRegulation)!
|
||
const regCodes = INDUSTRY_REGULATION_MAP[industry.id] || []
|
||
const regs = REGULATIONS.filter(r => regCodes.includes(r.code))
|
||
return (
|
||
<>
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<span className="text-3xl">{industry.icon}</span>
|
||
<div>
|
||
<h4 className="font-semibold text-slate-900">{industry.name}</h4>
|
||
<p className="text-sm text-slate-500">{industry.description}</p>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||
{regs.map((reg) => {
|
||
const regInRag = isInRag(reg.code)
|
||
return (
|
||
<div
|
||
key={reg.code}
|
||
className={`bg-white p-3 rounded-lg border ${regInRag ? 'border-green-200' : 'border-slate-200'}`}
|
||
>
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className={`px-2 py-0.5 text-xs rounded ${TYPE_COLORS[reg.type]}`}>
|
||
{reg.code}
|
||
</span>
|
||
{regInRag ? (
|
||
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-green-100 text-green-600 rounded">RAG</span>
|
||
) : (
|
||
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-red-50 text-red-400 rounded">✗</span>
|
||
)}
|
||
</div>
|
||
<div className="font-medium text-sm text-slate-900">{reg.name}</div>
|
||
<div className="text-xs text-slate-500 mt-1 line-clamp-2">{reg.description}</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</>
|
||
)
|
||
})()}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function ThematicGroupsSection({
|
||
setActiveTab,
|
||
setExpandedRegulation,
|
||
}: {
|
||
setActiveTab: (v: any) => void
|
||
setExpandedRegulation: (v: string | null) => void
|
||
}) {
|
||
return (
|
||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">Thematische Cluster</h3>
|
||
<p className="text-sm text-slate-500 mb-4">
|
||
Regulierungen gruppiert nach Themenbereichen - zeigt Ueberschneidungen.
|
||
</p>
|
||
<div className="space-y-4">
|
||
{THEMATIC_GROUPS.map((group) => (
|
||
<div key={group.id} className="border border-slate-200 rounded-lg overflow-hidden">
|
||
<div className={`${group.color} px-4 py-2 text-white font-medium flex items-center justify-between`}>
|
||
<span>{group.name}</span>
|
||
<span className="text-sm opacity-80">{group.regulations.length} Regulierungen</span>
|
||
</div>
|
||
<div className="p-4">
|
||
<p className="text-sm text-slate-600 mb-3">{group.description}</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{group.regulations.map((code) => {
|
||
const reg = REGULATIONS.find(r => r.code === code)
|
||
const codeInRag = isInRag(code)
|
||
return (
|
||
<span
|
||
key={code}
|
||
className={`px-3 py-1.5 rounded-full text-sm font-medium cursor-pointer ${
|
||
codeInRag
|
||
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||
}`}
|
||
onClick={() => {
|
||
setActiveTab('regulations')
|
||
setExpandedRegulation(code)
|
||
}}
|
||
title={`${reg?.fullName || code}${codeInRag ? ' (im RAG)' : ' (nicht im RAG)'}`}
|
||
>
|
||
{codeInRag ? '✓ ' : '✗ '}{code}
|
||
</span>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function KeyIntersectionsSection() {
|
||
return (
|
||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">Wichtige Schnittstellen</h3>
|
||
<p className="text-sm text-slate-500 mb-4">
|
||
Bereiche, in denen sich mehrere Regulierungen ueberschneiden und zusammenwirken.
|
||
</p>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{KEY_INTERSECTIONS.map((intersection, idx) => (
|
||
<div key={idx} className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-lg p-4 border border-slate-200">
|
||
<div className="flex flex-wrap gap-1 mb-2">
|
||
{intersection.regulations.map((code) => (
|
||
<span
|
||
key={code}
|
||
className={`px-2 py-0.5 text-xs font-medium rounded ${
|
||
isInRag(code)
|
||
? 'bg-green-100 text-green-700'
|
||
: 'bg-red-50 text-red-500'
|
||
}`}
|
||
>
|
||
{isInRag(code) ? '✓ ' : '✗ '}{code}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="font-medium text-slate-900 text-sm mb-1">{intersection.topic}</div>
|
||
<div className="text-xs text-slate-500">{intersection.description}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function RegulationMatrix({
|
||
expandedDocTypes,
|
||
setExpandedDocTypes,
|
||
expandedMatrixDoc,
|
||
setExpandedMatrixDoc,
|
||
}: {
|
||
expandedDocTypes: string[]
|
||
setExpandedDocTypes: (fn: (prev: string[]) => string[]) => void
|
||
expandedMatrixDoc: string | null
|
||
setExpandedMatrixDoc: (v: string | null) => void
|
||
}) {
|
||
return (
|
||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||
<div className="px-4 py-3 border-b bg-slate-50">
|
||
<h3 className="font-semibold text-slate-900">Branchen-Regulierungs-Matrix</h3>
|
||
<p className="text-sm text-slate-500">{RAG_DOCUMENTS.length} Dokumente in {DOC_TYPES.length} Kategorien</p>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-xs">
|
||
<thead className="bg-slate-50 border-b sticky top-0 z-10">
|
||
<tr>
|
||
<th className="px-2 py-2 text-left font-medium text-slate-500 sticky left-0 bg-slate-50 min-w-[200px]">Regulierung</th>
|
||
{INDUSTRIES_LIST.filter((i: any) => i.id !== 'all').map((industry: any) => (
|
||
<th key={industry.id} className="px-2 py-2 text-center font-medium text-slate-500 min-w-[60px]">
|
||
<div className="flex flex-col items-center">
|
||
<span className="text-lg">{industry.icon}</span>
|
||
<span className="text-[10px] leading-tight">{industry.name.split('/')[0]}</span>
|
||
</div>
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{DOC_TYPES.map((docType: any) => {
|
||
const docsInType = RAG_DOCUMENTS.filter((d: any) => d.doc_type === docType.id)
|
||
if (docsInType.length === 0) return null
|
||
|
||
const isExpanded = expandedDocTypes.includes(docType.id)
|
||
|
||
return (
|
||
<React.Fragment key={docType.id}>
|
||
<tr
|
||
className="bg-slate-100 border-t-2 border-slate-300 cursor-pointer hover:bg-slate-200"
|
||
onClick={() => {
|
||
setExpandedDocTypes(prev =>
|
||
prev.includes(docType.id)
|
||
? prev.filter((id: string) => id !== docType.id)
|
||
: [...prev, docType.id]
|
||
)
|
||
}}
|
||
>
|
||
<td colSpan={INDUSTRIES_LIST.length} className="px-3 py-2 font-bold text-slate-700">
|
||
<span className="mr-2">{isExpanded ? '\u25BC' : '\u25B6'}</span>
|
||
{docType.icon} {docType.label} ({docsInType.length})
|
||
</td>
|
||
</tr>
|
||
|
||
{isExpanded && docsInType.map((doc: any) => (
|
||
<React.Fragment key={doc.code}>
|
||
<tr
|
||
className={`hover:bg-slate-50 border-b border-slate-100 cursor-pointer ${expandedMatrixDoc === doc.code ? 'bg-teal-50' : ''}`}
|
||
onClick={() => setExpandedMatrixDoc(expandedMatrixDoc === doc.code ? null : doc.code)}
|
||
>
|
||
<td className="px-2 py-1.5 font-medium sticky left-0 bg-white">
|
||
<span className="flex items-center gap-1">
|
||
{isInRag(doc.code) ? (
|
||
<span className="text-green-500 text-[10px]">●</span>
|
||
) : (
|
||
<span className="text-red-300 text-[10px]">○</span>
|
||
)}
|
||
<span className="text-teal-600 truncate max-w-[180px]" title={doc.full_name || doc.name}>
|
||
{doc.name}
|
||
</span>
|
||
{(doc.applicability_note || doc.description) && (
|
||
<span className="text-slate-400 text-[10px] ml-1">{expandedMatrixDoc === doc.code ? '▼' : 'ⓘ'}</span>
|
||
)}
|
||
</span>
|
||
</td>
|
||
{INDUSTRIES_LIST.filter((i: any) => i.id !== 'all').map((industry: any) => {
|
||
const applies = doc.industries.includes(industry.id) || doc.industries.includes('all')
|
||
return (
|
||
<td key={industry.id} className="px-2 py-1.5 text-center">
|
||
{applies ? (
|
||
<span className="inline-flex items-center justify-center w-5 h-5 bg-teal-100 text-teal-600 rounded-full">✓</span>
|
||
) : (
|
||
<span className="inline-flex items-center justify-center w-5 h-5 text-slate-300">–</span>
|
||
)}
|
||
</td>
|
||
)
|
||
})}
|
||
</tr>
|
||
{expandedMatrixDoc === doc.code && (doc.applicability_note || doc.description) && (
|
||
<tr className="bg-teal-50 border-b border-teal-200">
|
||
<td colSpan={INDUSTRIES_LIST.length} className="px-4 py-3">
|
||
<div className="text-xs space-y-1.5">
|
||
{doc.full_name && (
|
||
<p className="font-semibold text-slate-700">{doc.full_name}</p>
|
||
)}
|
||
{doc.applicability_note && (
|
||
<p className="text-teal-700 bg-teal-100 px-2 py-1 rounded inline-block">
|
||
<span className="font-medium">Branchenrelevanz:</span> {doc.applicability_note}
|
||
</p>
|
||
)}
|
||
{doc.description && (
|
||
<p className="text-slate-600">{doc.description}</p>
|
||
)}
|
||
{doc.effective_date && (
|
||
<p className="text-slate-400">In Kraft: {doc.effective_date}</p>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</React.Fragment>
|
||
))}
|
||
</React.Fragment>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// FutureOutlookSection, RagCoverageSection, FutureRegulationsSection,
|
||
// LegalBasisSection are imported from ./MapTabSections.tsx
|