Files
breakpilot-compliance/admin-compliance/app/sdk/document-generator/page.tsx
Sharang Parnerkar eeb9931d87 refactor(admin): split document-generator page.tsx into colocated components
Split 1130-LOC document-generator page into _components and _constants
modules. page.tsx now 243 LOC (wire-up only). Behavior preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:01:56 +02:00

244 lines
10 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { useSDK } from '@/lib/sdk'
import { useEinwilligungen, EinwilligungenProvider } from '@/lib/sdk/einwilligungen/context'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { LegalTemplateResult } from '@/lib/sdk/types'
import { DataPointsPreview } from './components/DataPointsPreview'
import { DocumentValidation } from './components/DocumentValidation'
import { generateAllPlaceholders } from '@/lib/sdk/document-generator/datapoint-helpers'
import { loadAllTemplates } from './searchTemplates'
import { TemplateContext, EMPTY_CONTEXT } from './contextBridge'
import { CATEGORIES } from './_constants'
import TemplateLibrary from './_components/TemplateLibrary'
import GeneratorSection from './_components/GeneratorSection'
function DocumentGeneratorPageInner() {
const { state } = useSDK()
const { selectedDataPointsData } = useEinwilligungen()
// Library state
const [allTemplates, setAllTemplates] = useState<LegalTemplateResult[]>([])
const [isLoadingLibrary, setIsLoadingLibrary] = useState(true)
const [activeCategory, setActiveCategory] = useState<string>('all')
const [activeLanguage, setActiveLanguage] = useState<'all' | 'de' | 'en'>('all')
const [librarySearch, setLibrarySearch] = useState('')
const [expandedPreviewId, setExpandedPreviewId] = useState<string | null>(null)
// Generator state
const [activeTemplate, setActiveTemplate] = useState<LegalTemplateResult | null>(null)
const [context, setContext] = useState<TemplateContext>(EMPTY_CONTEXT)
const [extraPlaceholders, setExtraPlaceholders] = useState<Record<string, string>>({})
const [enabledModules, setEnabledModules] = useState<string[]>([])
const generatorRef = useRef<HTMLDivElement>(null)
const [totalCount, setTotalCount] = useState<number>(0)
// Load all templates on mount
useEffect(() => {
setIsLoadingLibrary(true)
loadAllTemplates(200).then((templates) => {
setAllTemplates(templates)
setTotalCount(templates.length)
setIsLoadingLibrary(false)
})
}, [])
// Pre-fill context from company profile + SDK state
useEffect(() => {
if (state?.companyProfile) {
const profile = state.companyProfile
const p = profile as unknown as Record<string, string>
setContext((prev) => ({
...prev,
PROVIDER: {
...prev.PROVIDER,
LEGAL_NAME: profile.companyName || prev.PROVIDER.LEGAL_NAME,
LEGAL_FORM: p.legalForm || prev.PROVIDER.LEGAL_FORM,
ADDRESS_LINE: p.addressLine || prev.PROVIDER.ADDRESS_LINE,
POSTAL_CODE: p.postalCode || prev.PROVIDER.POSTAL_CODE,
CITY: p.city || prev.PROVIDER.CITY,
COUNTRY: p.country || prev.PROVIDER.COUNTRY,
EMAIL: p.email || prev.PROVIDER.EMAIL,
PHONE: p.phone || prev.PROVIDER.PHONE,
WEBSITE_URL: p.websiteUrl || prev.PROVIDER.WEBSITE_URL,
CEO_NAME: p.representedByName || p.ceoName || prev.PROVIDER.CEO_NAME,
REGISTER_COURT: p.registerCourt || prev.PROVIDER.REGISTER_COURT,
REGISTER_NUMBER: p.registerNumber || prev.PROVIDER.REGISTER_NUMBER,
VAT_ID: p.vatId || prev.PROVIDER.VAT_ID,
},
PRIVACY: {
...prev.PRIVACY,
DPO_NAME: profile.dpoName || prev.PRIVACY.DPO_NAME,
DPO_EMAIL: profile.dpoEmail || prev.PRIVACY.DPO_EMAIL,
CONTACT_EMAIL: profile.dpoEmail || prev.PRIVACY.CONTACT_EMAIL,
SUPERVISORY_AUTHORITY_NAME: p.supervisoryAuthorityName || prev.PRIVACY.SUPERVISORY_AUTHORITY_NAME,
SUPERVISORY_AUTHORITY_ADDRESS: p.supervisoryAuthorityAddress || prev.PRIVACY.SUPERVISORY_AUTHORITY_ADDRESS,
},
FEATURES: {
...prev.FEATURES,
DATA_SUBJECT_REQUEST_CHANNEL: p.email
? `per E-Mail an ${p.email}`
: prev.FEATURES.DATA_SUBJECT_REQUEST_CHANNEL,
},
}))
}
}, [state?.companyProfile])
// Pre-fill extra placeholders from Einwilligungen data points
useEffect(() => {
if (selectedDataPointsData && selectedDataPointsData.length > 0) {
const generated = generateAllPlaceholders(selectedDataPointsData, 'de')
setExtraPlaceholders((prev) => ({ ...prev, ...generated }))
}
}, [selectedDataPointsData])
// Filtered templates (computed)
const filteredTemplates = useMemo(() => {
const category = CATEGORIES.find((c) => c.key === activeCategory)
return allTemplates.filter((t) => {
if (category && category.types !== null) {
if (!category.types.includes(t.templateType || '')) return false
}
if (activeLanguage !== 'all' && t.language !== activeLanguage) return false
if (librarySearch.trim()) {
const q = librarySearch.toLowerCase()
const title = (t.documentTitle || '').toLowerCase()
const type = (t.templateType || '').toLowerCase()
if (!title.includes(q) && !type.includes(q)) return false
}
return true
})
}, [allTemplates, activeCategory, activeLanguage, librarySearch])
const handleUseTemplate = useCallback((t: LegalTemplateResult) => {
setActiveTemplate(t)
setExpandedPreviewId(null)
setExtraPlaceholders({})
setEnabledModules([])
setTimeout(() => {
generatorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
}, [])
const handleContextChange = useCallback(
(section: keyof TemplateContext, key: string, value: unknown) => {
setContext((prev) => ({
...prev,
[section]: { ...(prev[section] as unknown as Record<string, unknown>), [key]: value },
}))
},
[]
)
const handleInsertPlaceholder = useCallback((placeholder: string) => {
if (!extraPlaceholders[placeholder]) {
const generated = generateAllPlaceholders(selectedDataPointsData || [], 'de')
const val = generated[placeholder as keyof typeof generated]
if (val) {
setExtraPlaceholders((prev) => ({ ...prev, [placeholder]: val }))
}
}
}, [extraPlaceholders, selectedDataPointsData])
const stepInfo = STEP_EXPLANATIONS['document-generator'] || {
title: 'Dokumentengenerator',
description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen',
explanation: 'Der Dokumentengenerator nutzt frei lizenzierte Textbausteine um Datenschutzerklaerungen, AGB und andere rechtliche Dokumente zu erstellen.',
tips: ['Wählen Sie eine Vorlage aus der Bibliothek aus', 'Füllen Sie die Platzhalter mit Ihren Unternehmensdaten'],
}
return (
<div className="space-y-8">
<StepHeader
stepId="document-generator"
title="Dokumentengenerator"
description="Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen"
explanation={stepInfo.explanation}
tips={stepInfo.tips}
/>
{/* Status bar */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-5">
<div className="text-sm text-gray-500">Vorlagen gesamt</div>
<div className="text-3xl font-bold text-gray-900">{totalCount}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-5">
<div className="text-sm text-gray-500">Angezeigt</div>
<div className="text-3xl font-bold text-purple-600">{filteredTemplates.length}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-5">
<div className="text-sm text-gray-500">Aktive Vorlage</div>
<div className="text-sm font-medium text-gray-900 mt-1 truncate">
{activeTemplate ? activeTemplate.documentTitle : '—'}
</div>
</div>
</div>
<TemplateLibrary
allTemplates={allTemplates}
filteredTemplates={filteredTemplates}
isLoadingLibrary={isLoadingLibrary}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
activeLanguage={activeLanguage}
onLanguageChange={setActiveLanguage}
librarySearch={librarySearch}
onSearchChange={setLibrarySearch}
expandedPreviewId={expandedPreviewId}
onTogglePreview={(id) => setExpandedPreviewId((prev) => (prev === id ? null : id))}
onUseTemplate={handleUseTemplate}
/>
{activeTemplate && (
<div ref={generatorRef} className="space-y-6">
<GeneratorSection
template={activeTemplate}
context={context}
onContextChange={handleContextChange}
extraPlaceholders={extraPlaceholders}
onExtraChange={(key, value) => setExtraPlaceholders((prev) => ({ ...prev, [key]: value }))}
onClose={() => setActiveTemplate(null)}
enabledModules={enabledModules}
onModuleToggle={(mod, checked) =>
setEnabledModules((prev) =>
checked ? [...prev, mod] : prev.filter((m) => m !== mod)
)
}
/>
{selectedDataPointsData && selectedDataPointsData.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<DocumentValidation
dataPoints={selectedDataPointsData}
documentContent={activeTemplate.text}
language="de"
onInsertPlaceholder={handleInsertPlaceholder}
/>
</div>
<div className="lg:col-span-1">
<DataPointsPreview
dataPoints={selectedDataPointsData}
onInsertPlaceholder={handleInsertPlaceholder}
language="de"
/>
</div>
</div>
)}
</div>
)}
</div>
)
}
export default function DocumentGeneratorPage() {
return (
<EinwilligungenProvider>
<DocumentGeneratorPageInner />
</EinwilligungenProvider>
)
}