-
- {/* Confidence Distribution Bar */}
- {evidenceDistribution && evidenceDistribution.total > 0 && (
-
- )}
-
- {/* Multi-Score Dimensions */}
- {dashboard.multi_score && (
-
- )}
-
- {/* Next Actions + Findings */}
-
- {/* Next Actions */}
-
-
Naechste Aktionen
- {nextActions.length === 0 ? (
-
Keine offenen Aktionen.
- ) : (
-
- {nextActions.map(action => (
-
-
0 ? 'bg-red-500' : 'bg-yellow-500'
- }`} />
-
-
{action.title}
-
- {action.control_id} Β· {DOMAIN_LABELS[action.domain] || action.domain}
- {action.days_overdue > 0 && {action.days_overdue}d ueberfaellig}
-
-
-
- {action.status}
-
-
- ))}
-
- )}
-
-
- {/* Audit Findings */}
-
-
-
Audit Findings
-
- Audit Checkliste β
-
-
-
-
-
-
{findings?.open_majors || 0}
-
offen (blockiert Zertifizierung)
-
-
-
-
{findings?.open_minors || 0}
-
offen (erfordert CAPA)
-
-
-
-
- Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI)
-
- {(findings?.open_majors || 0) === 0 ? (
-
- Zertifizierung moeglich
-
- ) : (
-
- Zertifizierung blockiert
-
- )}
-
-
-
-
- {/* Control-Mappings & Domain Chart */}
-
-
-
-
Control-Mappings
-
- Alle anzeigen β
-
-
-
-
-
{mappings?.total || 0}
-
Mappings gesamt
-
-
-
Nach Verordnung
-
- {mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => (
-
- {reg}: {count}
-
- ))}
- {!mappings?.by_regulation && (
- Keine Mappings vorhanden
- )}
-
-
-
-
-
-
-
Controls nach Domain
-
- {Object.entries(dashboard?.controls_by_domain || {}).slice(0, 6).map(([domain, stats]) => {
- const total = stats.total || 0
- const pass = stats.pass || 0
- const partial = stats.partial || 0
- const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0
-
- return (
-
-
- {DOMAIN_LABELS[domain] || domain}
-
-
-
{passPercent.toFixed(0)}%
-
- )
- })}
-
-
-
-
- {/* Regulations Table */}
-
-
-
Verordnungen & Standards ({regulations.length})
-
-
-
-
-
-
- | Code |
- Name |
- Typ |
- Anforderungen |
-
-
-
- {regulations.slice(0, 15).map((reg) => (
-
- |
- {reg.code}
- |
-
- {reg.name}
- |
-
-
- {reg.regulation_type === 'eu_regulation' ? 'EU-VO' :
- reg.regulation_type === 'eu_directive' ? 'EU-RL' :
- reg.regulation_type === 'bsi_standard' ? 'BSI' :
- reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type}
-
- |
-
- {reg.requirement_count}
- |
-
- ))}
-
-
-
-
- >
+ {hub.activeTab === 'overview' && (
+
)}
- {/* ============================================================ */}
- {/* TAB: Roadmap */}
- {/* ============================================================ */}
- {activeTab === 'roadmap' && (
-
- {!roadmap ? (
-
- ) : (
-
- {(['quick_wins', 'must_have', 'should_have', 'nice_to_have'] as const).map(bucketKey => {
- const meta = BUCKET_LABELS[bucketKey]
- const items = roadmap.buckets[bucketKey] || []
-
- return (
-
-
-
{meta.label}
-
- {items.length}
-
-
-
- {items.length === 0 ? (
-
Keine Eintraege
- ) : (
- items.map(item => (
-
-
{item.title}
-
- {item.control_id}
- Β·
- {DOMAIN_LABELS[item.domain] || item.domain}
-
- {item.days_overdue > 0 && (
-
{item.days_overdue}d ueberfaellig
- )}
- {item.owner && (
-
{item.owner}
- )}
-
- ))
- )}
-
-
- )
- })}
-
- )}
-
+ {hub.activeTab === 'roadmap' && (
+
)}
- {/* ============================================================ */}
- {/* TAB: Module */}
- {/* ============================================================ */}
- {activeTab === 'modules' && (
-
- {!moduleStatus ? (
-
- ) : (
- <>
- {/* Summary */}
-
-
-
Gesamt-Fortschritt
-
{moduleStatus.overall_progress.toFixed(0)}%
-
-
-
Module gestartet
-
{moduleStatus.started}/{moduleStatus.total}
-
-
-
Module abgeschlossen
-
{moduleStatus.complete}/{moduleStatus.total}
-
-
-
- {/* Module Grid */}
-
- {moduleStatus.modules.map(mod => (
-
-
-
{MODULE_ICONS[mod.key] || 'π¦'}
-
-
{mod.label}
-
{mod.count} Eintraege
-
-
- {mod.status === 'complete' ? 'Fertig' :
- mod.status === 'in_progress' ? 'In Arbeit' : 'Offen'}
-
-
-
-
- ))}
-
- >
- )}
-
+ {hub.activeTab === 'modules' && (
+
)}
- {/* ============================================================ */}
- {/* TAB: Trend */}
- {/* ============================================================ */}
- {activeTab === 'trend' && (
-
-
-
Score-Verlauf
-
-
-
- {scoreHistory.length === 0 ? (
-
-
Noch keine Score-Snapshots vorhanden.
-
Klicken Sie auf "Aktuellen Score speichern", um den ersten Datenpunkt zu erstellen.
-
- ) : (
- <>
- {/* Simple SVG Line Chart */}
-
-
- {/* Y-axis labels */}
-
- 100%
- 75%
- 50%
- 25%
- 0%
-
-
-
- {/* Snapshot Table */}
-
-
-
-
- | Datum |
- Score |
- Controls |
- Bestanden |
-
-
-
- {scoreHistory.slice().reverse().map(snap => (
-
- | {new Date(snap.snapshot_date).toLocaleDateString('de-DE')} |
-
- = 80 ? 'text-green-600' : snap.score >= 60 ? 'text-yellow-600' : 'text-red-600'
- }`}>
- {typeof snap.score === 'number' ? snap.score.toFixed(1) : snap.score}%
-
- |
- {snap.controls_total} |
- {snap.controls_pass} |
-
- ))}
-
-
-
- >
- )}
-
+ {hub.activeTab === 'trend' && (
+
)}
- {/* Traceability Tab */}
- {activeTab === 'traceability' && (
-
- {traceabilityLoading ? (
-
-
-
Traceability Matrix wird geladen...
-
- ) : !traceabilityMatrix ? (
-
- Keine Daten verfuegbar. Stellen Sie sicher, dass Controls und Evidence vorhanden sind.
-
- ) : (() => {
- const summary = traceabilityMatrix.summary
- const totalControls = summary.total_controls || 0
- const covered = summary.covered || 0
- const fullyVerified = summary.fully_verified || 0
- const uncovered = summary.uncovered || 0
-
- const filteredControls = (traceabilityMatrix.controls || []).filter(ctrl => {
- if (traceabilityFilter === 'covered' && !ctrl.coverage.has_evidence) return false
- if (traceabilityFilter === 'uncovered' && ctrl.coverage.has_evidence) return false
- if (traceabilityFilter === 'fully_verified' && !ctrl.coverage.all_assertions_verified) return false
- if (traceabilityDomainFilter !== 'all' && ctrl.domain !== traceabilityDomainFilter) return false
- return true
- })
-
- const domains = [...new Set(traceabilityMatrix.controls.map(c => c.domain))].sort()
-
- return (
- <>
- {/* Summary Cards */}
-
-
-
{totalControls}
-
Total Controls
-
-
-
{covered}
-
Abgedeckt
-
-
-
{fullyVerified}
-
Vollst. verifiziert
-
-
-
{uncovered}
-
Unabgedeckt
-
-
-
- {/* Filter Bar */}
-
-
- {([
- { key: 'all', label: 'Alle' },
- { key: 'covered', label: 'Abgedeckt' },
- { key: 'uncovered', label: 'Nicht abgedeckt' },
- { key: 'fully_verified', label: 'Vollst. verifiziert' },
- ] as const).map(f => (
-
- ))}
-
-
-
-
- {domains.map(d => (
-
- ))}
-
-
-
- {/* Controls List */}
-
- {filteredControls.length === 0 ? (
-
- Keine Controls fuer diesen Filter gefunden.
-
- ) : filteredControls.map(ctrl => {
- const isExpanded = expandedControls.has(ctrl.id)
- const coverageIcon = ctrl.coverage.all_assertions_verified
- ? { symbol: '\u2713', color: 'text-green-600 bg-green-50' }
- : ctrl.coverage.has_evidence
- ? { symbol: '\u25D0', color: 'text-yellow-600 bg-yellow-50' }
- : { symbol: '\u2717', color: 'text-red-600 bg-red-50' }
-
- return (
-
- {/* Control Row */}
-
-
- {/* Expanded: Evidence list */}
- {isExpanded && (
-
- {ctrl.evidence.length === 0 ? (
-
- Kein Evidence verknuepft.
-
- ) : ctrl.evidence.map(ev => {
- const evExpanded = expandedEvidence.has(ev.id)
- return (
-
-
-
- {/* Expanded: Assertions list */}
- {evExpanded && ev.assertions.length > 0 && (
-
-
-
-
- | Aussage |
- Typ |
- Konfidenz |
- Status |
-
-
-
- {ev.assertions.map(a => (
-
- | {a.sentence_text} |
- {a.assertion_type} |
-
- = 0.8 ? 'text-green-600'
- : a.confidence >= 0.5 ? 'text-yellow-600'
- : 'text-red-600'
- }`}>
- {(a.confidence * 100).toFixed(0)}%
-
- |
-
- {a.verified
- ? {'\u2713'}
- : {'\u2717'}
- }
- |
-
- ))}
-
-
-
- )}
-
- )
- })}
-
- )}
-
- )
- })}
-
- >
- )
- })()}
-
+ {hub.activeTab === 'traceability' && (
+
)}
>
)}
diff --git a/admin-compliance/app/sdk/document-generator/page.tsx b/admin-compliance/app/sdk/document-generator/page.tsx
index db306d0..4de3814 100644
--- a/admin-compliance/app/sdk/document-generator/page.tsx
+++ b/admin-compliance/app/sdk/document-generator/page.tsx
@@ -9,807 +9,10 @@ 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,
- contextToPlaceholders, getRelevantSections,
- getUncoveredPlaceholders, getMissingRequired,
-} from './contextBridge'
-import {
- runRuleset, getDocType, applyBlockRemoval,
- buildBoolContext, applyConditionalBlocks,
- type RuleInput, type RuleEngineResult,
-} from './ruleEngine'
-
-// =============================================================================
-// CATEGORY CONFIG
-// =============================================================================
-
-const CATEGORIES: { key: string; label: string; types: string[] | null }[] = [
- { key: 'all', label: 'Alle', types: null },
- // Legal / Vertragsvorlagen
- { key: 'privacy_policy', label: 'Datenschutz', types: ['privacy_policy'] },
- { key: 'terms', label: 'AGB', types: ['terms_of_service', 'agb', 'clause'] },
- { key: 'impressum', label: 'Impressum', types: ['impressum'] },
- { key: 'dpa', label: 'AVV/DPA', types: ['dpa'] },
- { key: 'nda', label: 'NDA', types: ['nda'] },
- { key: 'sla', label: 'SLA', types: ['sla'] },
- { key: 'widerruf', label: 'Widerruf', types: ['widerruf'] },
- { key: 'cookie', label: 'Cookie', types: ['cookie_policy', 'cookie_banner'] },
- { key: 'cloud', label: 'Cloud', types: ['cloud_service_agreement'] },
- { key: 'misc', label: 'Weitere', types: ['community_guidelines', 'copyright_policy', 'data_usage_clause'] },
- { key: 'dsfa', label: 'DSFA', types: ['dsfa'] },
- // Sicherheitskonzepte (Migration 051)
- { key: 'security', label: 'Sicherheitskonzepte', types: ['it_security_concept', 'data_protection_concept', 'backup_recovery_concept', 'logging_concept', 'incident_response_plan', 'access_control_concept', 'risk_management_concept', 'cybersecurity_policy'] },
- // Policy-Bibliothek (Migration 071/072)
- { key: 'it_security_policies', label: 'IT-Sicherheit Policies', types: ['information_security_policy', 'access_control_policy', 'password_policy', 'encryption_policy', 'logging_policy', 'backup_policy', 'incident_response_policy', 'change_management_policy', 'patch_management_policy', 'asset_management_policy', 'cloud_security_policy', 'devsecops_policy', 'secrets_management_policy', 'vulnerability_management_policy'] },
- { key: 'data_policies', label: 'Daten-Policies', types: ['data_protection_policy', 'data_classification_policy', 'data_retention_policy', 'data_transfer_policy', 'privacy_incident_policy'] },
- { key: 'hr_policies', label: 'Personal-Policies', types: ['employee_security_policy', 'security_awareness_policy', 'acceptable_use', 'remote_work_policy', 'offboarding_policy'] },
- { key: 'vendor_policies', label: 'Lieferanten-Policies', types: ['vendor_risk_management_policy', 'third_party_security_policy', 'supplier_security_policy'] },
- { key: 'bcm_policies', label: 'BCM/Notfall', types: ['business_continuity_policy', 'disaster_recovery_policy', 'crisis_management_policy'] },
- // Modul-Dokumente (Migration 073)
- { key: 'module_docs', label: 'DSGVO-Dokumente', types: ['vvt_register', 'tom_documentation', 'loeschkonzept', 'pflichtenregister'] },
-]
-
-// =============================================================================
-// CONTEXT FORM CONFIG
-// =============================================================================
-
-const SECTION_LABELS: Record
= {
- PROVIDER: 'Anbieter',
- CUSTOMER: 'Kunde / Gegenpartei',
- SERVICE: 'Dienst / Produkt',
- LEGAL: 'Rechtliches',
- PRIVACY: 'Datenschutz',
- SLA: 'Service Level (SLA)',
- PAYMENTS: 'Zahlungskonditionen',
- SECURITY: 'Sicherheit & Logs',
- NDA: 'Geheimhaltung (NDA)',
- CONSENT: 'Cookie / Einwilligung',
- HOSTING: 'Hosting-Provider',
- FEATURES: 'Dokument-Features & Textbausteine',
-}
-
-type FieldType = 'text' | 'email' | 'number' | 'select' | 'textarea' | 'boolean'
-interface FieldDef {
- key: string
- label: string
- type?: FieldType
- opts?: string[]
- span?: boolean
- nullable?: boolean
-}
-
-const SECTION_FIELDS: Record = {
- PROVIDER: [
- { key: 'LEGAL_NAME', label: 'Firmenname' },
- { key: 'EMAIL', label: 'Kontakt-E-Mail', type: 'email' },
- { key: 'LEGAL_FORM', label: 'Rechtsform' },
- { key: 'ADDRESS_LINE', label: 'Adresse' },
- { key: 'POSTAL_CODE', label: 'PLZ' },
- { key: 'CITY', label: 'Stadt' },
- { key: 'WEBSITE_URL', label: 'Website-URL' },
- { key: 'CEO_NAME', label: 'GeschΓ€ftsfΓΌhrer' },
- { key: 'REGISTER_COURT', label: 'Registergericht' },
- { key: 'REGISTER_NUMBER', label: 'HRB-Nummer' },
- { key: 'VAT_ID', label: 'USt-ID' },
- { key: 'PHONE', label: 'Telefon' },
- ],
- CUSTOMER: [
- { key: 'LEGAL_NAME', label: 'Name / Firma' },
- { key: 'EMAIL', label: 'E-Mail', type: 'email' },
- { key: 'CONTACT_NAME', label: 'Ansprechpartner' },
- { key: 'ADDRESS_LINE', label: 'Adresse' },
- { key: 'POSTAL_CODE', label: 'PLZ' },
- { key: 'CITY', label: 'Stadt' },
- { key: 'COUNTRY', label: 'Land' },
- { key: 'IS_CONSUMER', label: 'Verbraucher (B2C)', type: 'boolean' },
- { key: 'IS_BUSINESS', label: 'Unternehmer (B2B)', type: 'boolean' },
- ],
- SERVICE: [
- { key: 'NAME', label: 'Dienstname' },
- { key: 'DESCRIPTION', label: 'Beschreibung', type: 'textarea', span: true },
- { key: 'MODEL', label: 'Modell', type: 'select', opts: ['SaaS', 'PaaS', 'IaaS', 'OnPrem', 'Hybrid'] },
- { key: 'TIER', label: 'Plan / Tier' },
- { key: 'DATA_LOCATION', label: 'Datenspeicherort' },
- { key: 'EXPORT_WINDOW_DAYS', label: 'Export-Frist (Tage)', type: 'number' },
- { key: 'MIN_TERM_MONTHS', label: 'Mindestlaufzeit (Monate)', type: 'number' },
- { key: 'TERMINATION_NOTICE_DAYS', label: 'KΓΌndigungsfrist (Tage)', type: 'number' },
- ],
- LEGAL: [
- { key: 'GOVERNING_LAW', label: 'Anwendbares Recht' },
- { key: 'JURISDICTION_CITY', label: 'Gerichtsstand (Stadt)' },
- { key: 'VERSION_DATE', label: 'Versionsstand (JJJJ-MM-TT)' },
- { key: 'EFFECTIVE_DATE', label: 'GΓΌltig ab (JJJJ-MM-TT)' },
- ],
- PRIVACY: [
- { key: 'DPO_NAME', label: 'DSB-Name' },
- { key: 'DPO_EMAIL', label: 'DSB-E-Mail', type: 'email' },
- { key: 'CONTACT_EMAIL', label: 'Datenschutz-Kontakt', type: 'email' },
- { key: 'PRIVACY_POLICY_URL', label: 'Datenschutz-URL' },
- { key: 'COOKIE_POLICY_URL', label: 'Cookie-Policy-URL' },
- { key: 'ANALYTICS_RETENTION_MONTHS', label: 'Analytics-Aufbewahrung (Monate)', type: 'number' },
- { key: 'SUPERVISORY_AUTHORITY_NAME', label: 'AufsichtsbehΓΆrde' },
- ],
- SLA: [
- { key: 'AVAILABILITY_PERCENT', label: 'VerfΓΌgbarkeit (%)', type: 'number' },
- { key: 'MAINTENANCE_NOTICE_HOURS', label: 'WartungsankΓΌndigung (h)', type: 'number' },
- { key: 'SUPPORT_EMAIL', label: 'Support-E-Mail', type: 'email' },
- { key: 'SUPPORT_HOURS', label: 'Support-Zeiten' },
- { key: 'RESPONSE_CRITICAL_H', label: 'Reaktion Kritisch (h)', type: 'number' },
- { key: 'RESOLUTION_CRITICAL_H', label: 'LΓΆsung Kritisch (h)', type: 'number' },
- { key: 'RESPONSE_HIGH_H', label: 'Reaktion Hoch (h)', type: 'number' },
- { key: 'RESOLUTION_HIGH_H', label: 'LΓΆsung Hoch (h)', type: 'number' },
- { key: 'RESPONSE_MEDIUM_H', label: 'Reaktion Mittel (h)', type: 'number' },
- { key: 'RESOLUTION_MEDIUM_H', label: 'LΓΆsung Mittel (h)', type: 'number' },
- { key: 'RESPONSE_LOW_H', label: 'Reaktion Niedrig (h)', type: 'number' },
- ],
- PAYMENTS: [
- { key: 'MONTHLY_FEE_EUR', label: 'Monatl. GebΓΌhr (EUR)', type: 'number' },
- { key: 'PAYMENT_DUE_DAY', label: 'FΓ€lligkeitstag', type: 'number' },
- { key: 'PAYMENT_METHOD', label: 'Zahlungsmethode' },
- { key: 'PAYMENT_DAYS', label: 'Zahlungsziel (Tage)', type: 'number' },
- ],
- SECURITY: [
- { key: 'INCIDENT_NOTICE_HOURS', label: 'Meldepflicht VorfΓ€lle (h)', type: 'number' },
- { key: 'LOG_RETENTION_DAYS', label: 'Log-Aufbewahrung (Tage)', type: 'number' },
- { key: 'SECURITY_LOG_RETENTION_DAYS', label: 'Sicherheits-Log (Tage)', type: 'number' },
- ],
- NDA: [
- { key: 'PURPOSE', label: 'Zweck', type: 'textarea', span: true },
- { key: 'DURATION_YEARS', label: 'Laufzeit (Jahre)', type: 'number' },
- { key: 'PENALTY_AMOUNT_EUR', label: 'Vertragsstrafe EUR (leer = keine)', type: 'number', nullable: true },
- ],
- CONSENT: [
- { key: 'WEBSITE_NAME', label: 'Website-Name' },
- { key: 'ANALYTICS_TOOLS', label: 'Analytics-Tools (leer = kein Block)', nullable: true },
- { key: 'MARKETING_PARTNERS', label: 'Marketing-Partner (leer = kein Block)', nullable: true },
- ],
- HOSTING: [
- { key: 'PROVIDER_NAME', label: 'Hosting-Anbieter' },
- { key: 'COUNTRY', label: 'Hosting-Land' },
- { key: 'CONTRACT_TYPE', label: 'Vertragstyp (z. B. AVV nach Art. 28 DSGVO)' },
- ],
- FEATURES: [
- // ββ DSI / Cookie βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- { key: 'CONSENT_WITHDRAWAL_PATH', label: 'Einwilligungs-Widerrufspfad' },
- { key: 'SECURITY_MEASURES_SUMMARY', label: 'SicherheitsmaΓnahmen (kurz)' },
- { key: 'DATA_SUBJECT_REQUEST_CHANNEL', label: 'Kanal fΓΌr Betroffenenanfragen' },
- { key: 'HAS_THIRD_COUNTRY', label: 'DrittlandΓΌbermittlung mΓΆglich', type: 'boolean' },
- { key: 'TRANSFER_GUARDS', label: 'Garantien (z. B. SCC)' },
- // ββ Cookie/Consent βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- { key: 'HAS_FUNCTIONAL_COOKIES', label: 'Funktionale Cookies aktiviert', type: 'boolean' },
- { key: 'CMP_NAME', label: 'Consent-Manager-Name (optional)' },
- { key: 'CMP_LOGS_CONSENTS', label: 'Consent-Protokollierung aktiv', type: 'boolean' },
- { key: 'ANALYTICS_TOOLS_DETAIL', label: 'Analyse-Tools (Detailtext)', type: 'textarea', span: true },
- { key: 'MARKETING_TOOLS_DETAIL', label: 'Marketing-Tools (Detailtext)', type: 'textarea', span: true },
- // ββ Service-Features βββββββββββββββββββββββββββββββββββββββββββββββββββββ
- { key: 'HAS_ACCOUNT', label: 'Nutzerkonten vorhanden', type: 'boolean' },
- { key: 'HAS_PAYMENTS', label: 'Zahlungsabwicklung vorhanden', type: 'boolean' },
- { key: 'PAYMENT_PROVIDER_DETAIL', label: 'Zahlungsanbieter (Detailtext)', type: 'textarea', span: true },
- { key: 'HAS_SUPPORT', label: 'Support-Funktion vorhanden', type: 'boolean' },
- { key: 'SUPPORT_CHANNELS_TEXT', label: 'Support-KanΓ€le / Zeiten' },
- { key: 'HAS_NEWSLETTER', label: 'Newsletter vorhanden', type: 'boolean' },
- { key: 'NEWSLETTER_PROVIDER_DETAIL', label: 'Newsletter-Anbieter (Detailtext)', type: 'textarea', span: true },
- { key: 'HAS_SOCIAL_MEDIA', label: 'Social-Media-PrΓ€senz', type: 'boolean' },
- { key: 'SOCIAL_MEDIA_DETAIL', label: 'Social-Media-Details', type: 'textarea', span: true },
- // ββ AGB ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- { key: 'HAS_PAID_PLANS', label: 'Kostenpflichtige PlΓ€ne', type: 'boolean' },
- { key: 'PRICES_TEXT', label: 'Preise (Text/Link)', type: 'textarea', span: true },
- { key: 'PAYMENT_TERMS_TEXT', label: 'Zahlungsbedingungen', type: 'textarea', span: true },
- { key: 'CONTRACT_TERM_TEXT', label: 'Laufzeit & KΓΌndigung', type: 'textarea', span: true },
- { key: 'HAS_SLA', label: 'SLA vorhanden', type: 'boolean' },
- { key: 'SLA_URL', label: 'SLA-URL' },
- { key: 'HAS_EXPORT_POLICY', label: 'Datenexport/LΓΆschung geregelt', type: 'boolean' },
- { key: 'EXPORT_POLICY_TEXT', label: 'Datenexport-Regelung (Text)', type: 'textarea', span: true },
- { key: 'HAS_WITHDRAWAL', label: 'Widerrufsrecht (B2C digital)', type: 'boolean' },
- { key: 'CONSUMER_WITHDRAWAL_TEXT', label: 'Widerrufsbelehrung (Text)', type: 'textarea', span: true },
- { key: 'LIMITATION_CAP_TEXT', label: 'Haftungsdeckel B2B (Text)' },
- // ββ Impressum ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- { key: 'HAS_REGULATED_PROFESSION', label: 'Reglementierter Beruf', type: 'boolean' },
- { key: 'REGULATED_PROFESSION_TEXT', label: 'Berufsrecht-Text', type: 'textarea', span: true },
- { key: 'HAS_EDITORIAL_RESPONSIBLE', label: 'V.i.S.d.P. (redaktionell)', type: 'boolean' },
- { key: 'EDITORIAL_RESPONSIBLE_NAME', label: 'V.i.S.d.P. Name' },
- { key: 'EDITORIAL_RESPONSIBLE_ADDRESS', label: 'V.i.S.d.P. Adresse' },
- { key: 'HAS_DISPUTE_RESOLUTION', label: 'Streitbeilegungshinweis', type: 'boolean' },
- { key: 'DISPUTE_RESOLUTION_TEXT', label: 'Streitbeilegungstext', type: 'textarea', span: true },
- ],
-}
-
-// =============================================================================
-// SMALL COMPONENTS
-// =============================================================================
-
-function LicenseBadge({ licenseId, small = false }: { licenseId: LicenseType | null; small?: boolean }) {
- if (!licenseId) return null
- const colors: Partial> = {
- public_domain: 'bg-green-100 text-green-700 border-green-200',
- cc0: 'bg-green-100 text-green-700 border-green-200',
- unlicense: 'bg-green-100 text-green-700 border-green-200',
- mit: 'bg-blue-100 text-blue-700 border-blue-200',
- cc_by_4: 'bg-purple-100 text-purple-700 border-purple-200',
- reuse_notice: 'bg-orange-100 text-orange-700 border-orange-200',
- }
- return (
-
- {LICENSE_TYPE_LABELS[licenseId] || licenseId}
-
- )
-}
-
-// =============================================================================
-// LIBRARY CARD
-// =============================================================================
-
-function LibraryCard({
- template,
- expanded,
- onTogglePreview,
- onUse,
-}: {
- template: LegalTemplateResult
- expanded: boolean
- onTogglePreview: () => void
- onUse: () => void
-}) {
- const typeLabel = template.templateType
- ? (TEMPLATE_TYPE_LABELS[template.templateType as TemplateType] || template.templateType)
- : null
- const placeholderCount = template.placeholders?.length ?? 0
-
- return (
-
-
-
-
- {template.documentTitle || 'Vorlage'}
-
- {template.language}
-
-
- {typeLabel && (
-
- {typeLabel}
-
- )}
-
- {placeholderCount > 0 && (
- {placeholderCount} Platzh.
- )}
-
-
-
-
-
-
-
- {expanded && (
-
- )}
-
- )
-}
-
-// =============================================================================
-// CONTEXT SECTION FORM
-// =============================================================================
-
-function ContextSectionForm({
- section,
- context,
- onChange,
-}: {
- section: keyof TemplateContext
- context: TemplateContext
- onChange: (section: keyof TemplateContext, key: string, value: unknown) => void
-}) {
- const fields = SECTION_FIELDS[section]
- const sectionData = context[section] as unknown as Record
-
- return (
-
- {fields.map((field) => {
- const rawValue = sectionData[field.key]
- const inputCls = 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-400'
-
- if (field.type === 'boolean') {
- return (
-
- onChange(section, field.key, e.target.checked)}
- className="w-4 h-4 accent-purple-600"
- />
-
-
- )
- }
-
- if (field.type === 'select' && field.opts) {
- return (
-
-
-
-
- )
- }
-
- if (field.type === 'textarea') {
- return (
-
-
-
- )
- }
-
- if (field.type === 'number') {
- return (
-
-
- {
- const v = e.target.value
- onChange(section, field.key, field.nullable && v === '' ? null : v === '' ? '' : Number(v))
- }}
- className={inputCls}
- />
-
- )
- }
-
- // default: text / email
- return (
-
-
- onChange(section, field.key, field.nullable && e.target.value === '' ? null : e.target.value)}
- className={inputCls}
- />
-
- )
- })}
-
- )
-}
-
-// =============================================================================
-// GENERATOR SECTION
-// =============================================================================
-
-// Available module definitions (id β display label)
-const MODULE_LABELS: Record = {
- CLOUD_EXPORT_DELETE_DE: 'Datenexport & LΓΆschrecht',
- B2C_WITHDRAWAL_DE: 'Widerrufsrecht (B2C)',
-}
-
-function GeneratorSection({
- template,
- context,
- onContextChange,
- extraPlaceholders,
- onExtraChange,
- onClose,
- enabledModules,
- onModuleToggle,
-}: {
- template: LegalTemplateResult
- context: TemplateContext
- onContextChange: (section: keyof TemplateContext, key: string, value: unknown) => void
- extraPlaceholders: Record
- onExtraChange: (key: string, value: string) => void
- onClose: () => void
- enabledModules: string[]
- onModuleToggle: (mod: string, checked: boolean) => void
-}) {
- const [activeTab, setActiveTab] = useState<'placeholders' | 'preview'>('placeholders')
- const [expandedSections, setExpandedSections] = useState>(new Set(['PROVIDER', 'LEGAL']))
-
- const placeholders = template.placeholders || []
- const relevantSections = useMemo(() => getRelevantSections(placeholders), [placeholders])
- const uncovered = useMemo(() => getUncoveredPlaceholders(placeholders, context), [placeholders, context])
- const missing = useMemo(() => getMissingRequired(placeholders, context), [placeholders, context])
-
- // Rule engine evaluation
- const ruleResult = useMemo((): RuleEngineResult | null => {
- if (!template) return null
- return runRuleset({
- doc_type: getDocType(template.templateType ?? '', template.language ?? 'de'),
- render: { lang: template.language ?? 'de', variant: 'standard' },
- context,
- modules: { enabled: enabledModules },
- } satisfies RuleInput)
- }, [template, context, enabledModules])
-
- const allPlaceholderValues = useMemo(() => ({
- ...contextToPlaceholders(ruleResult?.contextAfterDefaults ?? context),
- ...extraPlaceholders,
- }), [context, extraPlaceholders, ruleResult])
-
- // Boolean context for {{#IF}} rendering
- const boolCtx = useMemo(
- () => ruleResult ? buildBoolContext(ruleResult.contextAfterDefaults, ruleResult.computedFlags) : {},
- [ruleResult]
- )
-
- const renderedContent = useMemo(() => {
- // 1. Remove ruleset-driven blocks ([BLOCK:ID])
- let content = applyBlockRemoval(template.text, ruleResult?.removedBlocks ?? [])
- // 2. Evaluate {{#IF}} / {{#IF_NOT}} / {{#IF_ANY}} directives
- content = applyConditionalBlocks(content, boolCtx)
- // 3. Substitute placeholders
- for (const [key, value] of Object.entries(allPlaceholderValues)) {
- if (value) {
- content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
- }
- }
- return content
- }, [template.text, allPlaceholderValues, ruleResult, boolCtx])
-
- // Compute which modules are relevant (mentioned in violations/warnings)
- const relevantModules = useMemo(() => {
- if (!ruleResult) return []
- const mentioned = new Set()
- const allIssues = [...ruleResult.violations, ...ruleResult.warnings]
- for (const issue of allIssues) {
- if (issue.phase === 'module_requirements') {
- // Extract module ID from message
- for (const modId of Object.keys(MODULE_LABELS)) {
- if (issue.message.includes(modId)) mentioned.add(modId)
- }
- }
- }
- // Also show modules that are enabled but not mentioned
- for (const mod of enabledModules) {
- if (mod in MODULE_LABELS) mentioned.add(mod)
- }
- return [...mentioned]
- }, [ruleResult, enabledModules])
-
- const handleCopy = () => navigator.clipboard.writeText(renderedContent)
-
- const handleExportMarkdown = () => {
- const blob = new Blob([renderedContent], { type: 'text/markdown' })
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = `${(template.documentTitle || 'dokument').replace(/\s+/g, '-').toLowerCase()}.md`
- a.click()
- URL.revokeObjectURL(url)
- }
-
- const toggleSection = (sec: string) => {
- setExpandedSections((prev) => {
- const next = new Set(prev)
- if (next.has(sec)) next.delete(sec)
- else next.add(sec)
- return next
- })
- }
-
- // Auto-expand all relevant sections on first render
- useEffect(() => {
- if (relevantSections.length > 0) {
- setExpandedSections(new Set(relevantSections))
- }
- }, [template.id]) // eslint-disable-line react-hooks/exhaustive-deps
-
- // Computed flags pills config
- const flagPills: { key: string; label: string; color: string }[] = ruleResult ? [
- { key: 'IS_B2C', label: 'B2C', color: 'bg-blue-100 text-blue-700' },
- { key: 'SERVICE_IS_SAAS', label: 'SaaS', color: 'bg-green-100 text-green-700' },
- { key: 'HAS_PENALTY', label: 'Vertragsstrafe', color: 'bg-orange-100 text-orange-700' },
- { key: 'HAS_ANALYTICS', label: 'Analytics', color: 'bg-gray-100 text-gray-600' },
- ] : []
-
- return (
-
- {/* Header */}
-
-
-
-
-
Generator
-
{template.documentTitle}
-
- {/* Computed flags pills */}
- {ruleResult && (
-
- {flagPills.map(({ key, label, color }) =>
- ruleResult.computedFlags[key] ? (
-
- {label}
-
- ) : null
- )}
-
- )}
-
-
-
-
- {/* Tab bar */}
-
- {(['placeholders', 'preview'] as const).map((tab) => (
-
- ))}
-
-
- {/* Tab content */}
-
- {activeTab === 'placeholders' && (
-
- {placeholders.length === 0 ? (
-
- Keine Platzhalter β Vorlage kann direkt verwendet werden.
-
- ) : (
- <>
- {/* Relevant sections */}
- {relevantSections.length === 0 ? (
-
Alle Platzhalter mΓΌssen manuell befΓΌllt werden.
- ) : (
-
- {relevantSections.map((section) => (
-
-
- {expandedSections.has(section) && (
-
-
-
- )}
-
- ))}
-
- )}
-
- {/* Uncovered / manual placeholders */}
- {uncovered.length > 0 && (
-
-
- Weitere Platzhalter (manuell ausfΓΌllen)
-
-
- {uncovered.map((ph) => (
-
-
- onExtraChange(ph, e.target.value)}
- placeholder={`Wert fΓΌr ${ph}`}
- className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-400"
- />
-
- ))}
-
-
- )}
-
- {/* Module toggles */}
- {relevantModules.length > 0 && (
-
-
Module
-
- {relevantModules.map((modId) => (
-
- ))}
-
-
- )}
-
- {/* Validation summary + CTA */}
-
-
- {missing.length > 0 ? (
-
- β {missing.length} Pflichtfeld{missing.length > 1 ? 'er' : ''} fehlt{missing.length === 1 ? '' : 'en'}
-
- ({missing.map((m) => m.replace(/\{\{|\}\}/g, '')).slice(0, 3).join(', ')}{missing.length > 3 ? ` +${missing.length - 3}` : ''})
-
-
- ) : (
- Alle Pflichtfelder ausgefΓΌllt
- )}
-
-
-
- >
- )}
-
- )}
-
- {activeTab === 'preview' && (
-
- {/* Rule engine banners */}
- {ruleResult && ruleResult.violations.length > 0 && (
-
-
- π΄ {ruleResult.violations.length} Fehler
-
-
- {ruleResult.violations.map((v) => (
- -
- [{v.id}] {v.message}
-
- ))}
-
-
- )}
- {ruleResult && ruleResult.warnings.filter((w) => w.id !== 'WARN_LEGAL_REVIEW').length > 0 && (
-
-
- {ruleResult.warnings
- .filter((w) => w.id !== 'WARN_LEGAL_REVIEW')
- .map((w) => (
- -
- π‘ [{w.id}] {w.message}
-
- ))}
-
-
- )}
- {ruleResult && (
-
-
- βΉοΈ Rechtlicher Hinweis: Diese Vorlage ist MIT-lizenziert. Vor Produktionseinsatz
- wird eine rechtliche ΓberprΓΌfung dringend empfohlen.
-
-
- )}
- {ruleResult && ruleResult.appliedDefaults.length > 0 && (
-
- Defaults angewendet: {ruleResult.appliedDefaults.join(', ')}
-
- )}
-
-
-
- {missing.length > 0 && (
-
- β {missing.length} Platzhalter noch nicht ausgefΓΌllt
-
- )}
-
-
-
-
-
-
-
-
-
- {renderedContent}
-
-
- {template.attributionRequired && template.attributionText && (
-
- Attribution erforderlich: {template.attributionText}
-
- )}
-
- )}
-
-
- )
-}
-
-// =============================================================================
-// MAIN PAGE
-// =============================================================================
+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()
@@ -893,7 +96,7 @@ function DocumentGeneratorPageInner() {
// Filtered templates (computed)
const filteredTemplates = useMemo(() => {
- const category = CATEGORIES.find((c) => c.key === activeCategory)
+ const category = CATEGORIES.find((c: { key: string }) => c.key === activeCategory)
return allTemplates.filter((t) => {
if (category && category.types !== null) {
if (!category.types.includes(t.templateType || '')) return false
diff --git a/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx b/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx
index 10d392a..8a401c7 100644
--- a/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx
+++ b/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx
@@ -83,6 +83,19 @@ export default function ObligationDetail({ obligation, onClose, onStatusChange,
)}
+ {obligation.linked_vendor_ids && obligation.linked_vendor_ids.length > 0 && (
+