'use client' /** * Document-Library — zentraler Tab für alle für den Mandanten erzeugten * Dokumente. Listet compliance_legal_documents + jeweils latest/published * Version, gruppiert nach Empfehlungs-Klassifikation (required/recommended/ * optional/uncategorized). * * Recommend-Engine (compliance_template_rules) wird gegen das aktuelle * CompanyProfile + ComplianceScope ausgewertet, um document_type → Klassifi- * kation zu mappen. * * Click auf eine Zeile → /sdk/workflow?doc= (Workflow-Editor öffnet * den Doc automatisch). */ import { useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/navigation' import { useSDK } from '@/lib/sdk' import { StepHeader } from '@/components/sdk/StepHeader' import type { CompanyProfile } from '@/lib/sdk/types/company-profile' import type { ComplianceScopeState } from '@/lib/sdk/compliance-scope-types/state' const DOCS_ENDPOINT = '/api/sdk/v1/compliance/legal-documents/documents-with-versions' const RECOMMEND_ENDPOINT = '/api/sdk/v1/compliance/recommend' type Classification = 'required' | 'recommended' | 'optional' | 'uncategorized' type VersionStatus = | 'draft' | 'review' | 'review_internal' | 'review_client' | 'approved' | 'published' | 'archived' | 'rejected' interface DocVersion { id: string document_id: string version: string status: VersionStatus title: string created_at: string updated_at: string | null approved_internal_at: string | null approved_client_at: string | null } interface DocWithVersions { id: string type: string name: string description: string | null created_at: string updated_at: string | null latest_version: DocVersion | null published_version: DocVersion | null } interface Rec { document_type: string title: string classification: 'required' | 'recommended' | 'optional' source_citation: string override_applied: boolean } export default function DocumentLibraryPage() { const { state } = useSDK() const router = useRouter() const [docs, setDocs] = useState([]) const [recommendations, setRecommendations] = useState>(new Map()) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('all') useEffect(() => { let cancelled = false async function load() { setLoading(true) setError(null) try { const profile = buildRecommendProfile(state.companyProfile ?? null, state.complianceScope ?? null) const [docsRes, recRes] = await Promise.all([ fetch(DOCS_ENDPOINT), fetch(RECOMMEND_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile, compliance_depth_level: profile.compliance_depth_level ?? 'L2', }), }), ]) if (!docsRes.ok) throw new Error(`Docs-API: ${docsRes.status}`) if (!recRes.ok) throw new Error(`Recommend-API: ${recRes.status}`) const docsData = await docsRes.json() as { documents: DocWithVersions[] } const recData = await recRes.json() const recMap = new Map() for (const cls of ['required', 'recommended', 'optional'] as const) { for (const item of (recData[cls] ?? []) as Rec[]) { recMap.set(item.document_type, { ...item, classification: cls }) } } if (!cancelled) { setDocs(docsData.documents ?? []) setRecommendations(recMap) } } catch (e) { if (!cancelled) setError((e as Error).message) } finally { if (!cancelled) setLoading(false) } } load() return () => { cancelled = true } }, [state.companyProfile, state.complianceScope]) const grouped = useMemo(() => { const groups: Record = { required: [], recommended: [], optional: [], uncategorized: [], } const q = search.toLowerCase().trim() for (const doc of docs) { // Filter if (q) { const hit = doc.name.toLowerCase().includes(q) || doc.type.toLowerCase().includes(q) || (doc.description?.toLowerCase() ?? '').includes(q) if (!hit) continue } if (statusFilter !== 'all') { const s = doc.latest_version?.status if (s !== statusFilter) continue } const rec = recommendations.get(doc.type) const klass: Classification = rec?.classification ?? 'uncategorized' groups[klass].push(doc) } return groups }, [docs, recommendations, search, statusFilter]) const totalShown = grouped.required.length + grouped.recommended.length + grouped.optional.length + grouped.uncategorized.length return (
setSearch(e.target.value)} className="text-sm px-3 py-1.5 border border-gray-300 rounded w-72" />
{loading ? 'lädt…' : `${totalShown} sichtbar · ${docs.length} insgesamt`}
{error && (
{error}
)}
{!loading && docs.length === 0 && (
Noch keine Dokumente vorhanden. Generiere welche über den{' '} Document Generator{' '} (Bulk-Modus „Empfohlene generieren →").
)} router.push(`/sdk/workflow?doc=${id}`)} /> router.push(`/sdk/workflow?doc=${id}`)} /> router.push(`/sdk/workflow?doc=${id}`)} /> router.push(`/sdk/workflow?doc=${id}`)} />
) } function Group({ title, chipCls, docs, recommendations, onOpen, }: { title: string chipCls: string docs: DocWithVersions[] recommendations: Map onOpen: (id: string) => void }) { if (docs.length === 0) return null return (

{title} {docs.length}

{docs.map((doc) => ( ))}
Titel Type Status Version Geändert Override
) } function DocRow({ doc, rec, onOpen, }: { doc: DocWithVersions rec: Rec | undefined onOpen: (id: string) => void }) { const latest = doc.latest_version const updated = doc.updated_at ?? doc.created_at return ( onOpen(doc.id)} > {doc.name} {doc.type} {latest ? : } {latest?.version ?? '—'} {doc.published_version && doc.published_version.id !== latest?.id && ( (live: {doc.published_version.version}) )} {new Date(updated).toLocaleString('de-DE')} {rec?.override_applied && ( Override )} ) } function StatusBadge({ status }: { status: VersionStatus }) { const map: Record = { draft: { label: 'Entwurf', cls: 'bg-slate-100 text-slate-700 border-slate-300' }, review: { label: 'Prüfung', cls: 'bg-amber-50 text-amber-800 border-amber-300' }, review_internal: { label: 'DSB-Prüfung', cls: 'bg-amber-50 text-amber-800 border-amber-300' }, review_client: { label: 'Mandant-Prüfung', cls: 'bg-blue-50 text-blue-800 border-blue-300' }, approved: { label: 'Freigegeben', cls: 'bg-emerald-50 text-emerald-800 border-emerald-300' }, published: { label: 'Live', cls: 'bg-emerald-100 text-emerald-900 border-emerald-400 font-medium' }, archived: { label: 'Archiviert', cls: 'bg-gray-100 text-gray-600 border-gray-300' }, rejected: { label: 'Abgelehnt', cls: 'bg-rose-50 text-rose-800 border-rose-300' }, } const { label, cls } = map[status] ?? { label: status, cls: 'bg-gray-100 text-gray-700 border-gray-300' } return {label} } // ----- Profile-Builder (gleich wie in BulkGenerateModal — könnten wir später extrahieren) ----- function buildRecommendProfile( companyProfile: CompanyProfile | null, complianceScope: ComplianceScopeState | null, ): Record { const profile: Record = {} if (companyProfile) { if (companyProfile.employeeCount) { profile.org_employee_count = String(companyProfile.employeeCount).replace(/-/g, '_') } if (companyProfile.businessModel) { profile.org_business_model = String(companyProfile.businessModel).toLowerCase().replace(/\s+/g, '_') } if (companyProfile.isDataProcessor) { profile.comp_has_processors = 'yes' } } if (complianceScope?.answers) { for (const a of complianceScope.answers) { if (!a.questionId) continue if (a.value === null || a.value === undefined || a.value === '') continue profile[a.questionId] = a.value } } if (complianceScope?.decision?.determinedLevel) { profile.compliance_depth_level = complianceScope.decision.determinedLevel } return profile }