refactor(admin-compliance): split 7 oversized files under 500 LOC hard cap (batch 3)
- tom-generator/export/zip.ts: extract private helpers to zip-helpers.ts (544→342 LOC) - tom-generator/export/docx.ts: extract private helpers to docx-helpers.ts (525→378 LOC) - tom-generator/export/pdf.ts: extract private helpers to pdf-helpers.ts (517→446 LOC) - tom-generator/demo-data/index.ts: extract DEMO_RISK_PROFILES + DEMO_EVIDENCE_DOCUMENTS to demo-data-part2.ts (518→360 LOC) - einwilligungen/generator/privacy-policy-sections.ts: extract sections 5-7 to part2 (559→313 LOC) - einwilligungen/export/pdf.ts: extract HTML/CSS helpers to pdf-helpers.ts (505→296 LOC) - vendor-compliance/context.tsx: extract API action hooks to context-actions.tsx (509→286 LOC) All originals re-export from sibling files — zero consumer import changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
218
admin-compliance/lib/sdk/einwilligungen/export/pdf-helpers.ts
Normal file
218
admin-compliance/lib/sdk/einwilligungen/export/pdf-helpers.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
// =============================================================================
|
||||
// Privacy Policy PDF Export - Helper Functions
|
||||
// Private helpers extracted from pdf.ts to stay under 500 LOC hard cap
|
||||
// =============================================================================
|
||||
|
||||
import type { PDFExportOptions, PDFSection } from './pdf'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function generateHTMLFromContent(
|
||||
content: PDFSection[],
|
||||
options: PDFExportOptions
|
||||
): string {
|
||||
const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in'
|
||||
const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in'
|
||||
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${options.language}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
|
||||
<style>
|
||||
@page {
|
||||
size: ${pageWidth} ${pageHeight};
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Calibri, Arial, sans-serif;
|
||||
font-size: ${options.fontSize}pt;
|
||||
line-height: 1.6;
|
||||
color: #1e293b;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24pt;
|
||||
color: ${options.primaryColor};
|
||||
border-bottom: 2px solid ${options.primaryColor};
|
||||
padding-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16pt;
|
||||
color: ${options.primaryColor};
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 13pt;
|
||||
color: #334155;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28pt;
|
||||
text-align: center;
|
||||
color: ${options.primaryColor};
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: ${options.primaryColor};
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.pagebreak {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 9pt;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
.pagebreak {
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const section of content) {
|
||||
switch (section.type) {
|
||||
case 'title':
|
||||
html += `<div class="title" style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</div>\n`
|
||||
break
|
||||
|
||||
case 'heading':
|
||||
html += `<h1 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h1>\n`
|
||||
break
|
||||
|
||||
case 'subheading':
|
||||
html += `<h3 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h3>\n`
|
||||
break
|
||||
|
||||
case 'paragraph': {
|
||||
const alignClass = section.style?.align === 'center' ? ' class="center"' : ''
|
||||
html += `<p${alignClass} style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</p>\n`
|
||||
break
|
||||
}
|
||||
|
||||
case 'list':
|
||||
html += '<ul>\n'
|
||||
for (const item of section.items || []) {
|
||||
html += ` <li>${escapeHtml(item)}</li>\n`
|
||||
}
|
||||
html += '</ul>\n'
|
||||
break
|
||||
|
||||
case 'table':
|
||||
if (section.table) {
|
||||
html += '<table>\n<thead><tr>\n'
|
||||
for (const header of section.table.headers) {
|
||||
html += ` <th>${escapeHtml(header)}</th>\n`
|
||||
}
|
||||
html += '</tr></thead>\n<tbody>\n'
|
||||
for (const row of section.table.rows) {
|
||||
html += '<tr>\n'
|
||||
for (const cell of row) {
|
||||
html += ` <td>${escapeHtml(cell)}</td>\n`
|
||||
}
|
||||
html += '</tr>\n'
|
||||
}
|
||||
html += '</tbody></table>\n'
|
||||
}
|
||||
break
|
||||
|
||||
case 'pagebreak':
|
||||
html += '<div class="pagebreak"></div>\n'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
export function getStyleString(style?: PDFSection['style']): string {
|
||||
if (!style) return ''
|
||||
|
||||
const parts: string[] = []
|
||||
if (style.color) parts.push(`color: ${style.color}`)
|
||||
if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`)
|
||||
if (style.bold) parts.push('font-weight: bold')
|
||||
if (style.italic) parts.push('font-style: italic')
|
||||
if (style.align) parts.push(`text-align: ${style.align}`)
|
||||
|
||||
return parts.join('; ')
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CATEGORY_METADATA,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
import { generateHTMLFromContent } from './pdf-helpers'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -277,216 +278,6 @@ export async function generatePDFBlob(
|
||||
return new Blob([html], { type: 'text/html' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate printable HTML from PDF content
|
||||
*/
|
||||
function generateHTMLFromContent(
|
||||
content: PDFSection[],
|
||||
options: PDFExportOptions
|
||||
): string {
|
||||
const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in'
|
||||
const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in'
|
||||
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${options.language}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
|
||||
<style>
|
||||
@page {
|
||||
size: ${pageWidth} ${pageHeight};
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Calibri, Arial, sans-serif;
|
||||
font-size: ${options.fontSize}pt;
|
||||
line-height: 1.6;
|
||||
color: #1e293b;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24pt;
|
||||
color: ${options.primaryColor};
|
||||
border-bottom: 2px solid ${options.primaryColor};
|
||||
padding-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16pt;
|
||||
color: ${options.primaryColor};
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 13pt;
|
||||
color: #334155;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28pt;
|
||||
text-align: center;
|
||||
color: ${options.primaryColor};
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: ${options.primaryColor};
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.pagebreak {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 9pt;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
.pagebreak {
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const section of content) {
|
||||
switch (section.type) {
|
||||
case 'title':
|
||||
html += `<div class="title" style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</div>\n`
|
||||
break
|
||||
|
||||
case 'heading':
|
||||
html += `<h1 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h1>\n`
|
||||
break
|
||||
|
||||
case 'subheading':
|
||||
html += `<h3 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h3>\n`
|
||||
break
|
||||
|
||||
case 'paragraph':
|
||||
const alignClass = section.style?.align === 'center' ? ' class="center"' : ''
|
||||
html += `<p${alignClass} style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</p>\n`
|
||||
break
|
||||
|
||||
case 'list':
|
||||
html += '<ul>\n'
|
||||
for (const item of section.items || []) {
|
||||
html += ` <li>${escapeHtml(item)}</li>\n`
|
||||
}
|
||||
html += '</ul>\n'
|
||||
break
|
||||
|
||||
case 'table':
|
||||
if (section.table) {
|
||||
html += '<table>\n<thead><tr>\n'
|
||||
for (const header of section.table.headers) {
|
||||
html += ` <th>${escapeHtml(header)}</th>\n`
|
||||
}
|
||||
html += '</tr></thead>\n<tbody>\n'
|
||||
for (const row of section.table.rows) {
|
||||
html += '<tr>\n'
|
||||
for (const cell of row) {
|
||||
html += ` <td>${escapeHtml(cell)}</td>\n`
|
||||
}
|
||||
html += '</tr>\n'
|
||||
}
|
||||
html += '</tbody></table>\n'
|
||||
}
|
||||
break
|
||||
|
||||
case 'pagebreak':
|
||||
html += '<div class="pagebreak"></div>\n'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
function getStyleString(style?: PDFSection['style']): string {
|
||||
if (!style) return ''
|
||||
|
||||
const parts: string[] = []
|
||||
if (style.color) parts.push(`color: ${style.color}`)
|
||||
if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`)
|
||||
if (style.bold) parts.push('font-weight: bold')
|
||||
if (style.italic) parts.push('font-style: italic')
|
||||
if (style.align) parts.push(`text-align: ${style.align}`)
|
||||
|
||||
return parts.join('; ')
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Privacy Policy Section Generators (Part 2)
|
||||
*
|
||||
* Sections 5-7: recipients, retention, special-categories, rights
|
||||
* Extracted from privacy-policy-sections.ts to stay under 500 LOC hard cap.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
SupportedLanguage,
|
||||
LocalizedText,
|
||||
RetentionMatrixEntry,
|
||||
LegalBasis,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// Private helpers (duplicated locally to avoid cross-file private import)
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
||||
const thirdParties = new Set<string>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
thirdParties.add(recipient)
|
||||
}
|
||||
}
|
||||
return Array.from(thirdParties).sort()
|
||||
}
|
||||
|
||||
export function generateRecipientsSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '5. Empfaenger und Datenweitergabe',
|
||||
en: '5. Recipients and Data Sharing',
|
||||
}
|
||||
|
||||
const thirdParties = extractThirdParties(dataPoints)
|
||||
|
||||
if (thirdParties.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
||||
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
||||
}
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRetentionSection(
|
||||
dataPoints: DataPoint[],
|
||||
retentionMatrix: RetentionMatrixEntry[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '6. Speicherdauer',
|
||||
en: '6. Data Retention',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const entry of retentionMatrix) {
|
||||
const categoryData = grouped.get(entry.category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryName = t(entry.categoryName, language)
|
||||
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
||||
|
||||
const dataRetention = categoryData
|
||||
.map((dp) => {
|
||||
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
||||
return `- ${t(dp.name, language)}: ${period}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
||||
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'retention',
|
||||
order: 6,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
if (specialCategoryDataPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title: LocalizedText = {
|
||||
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
||||
}
|
||||
|
||||
const dataList = specialCategoryDataPoints
|
||||
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Ihre ausdrueckliche Einwilligung
|
||||
|
||||
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
||||
|
||||
### Ihre Rechte bei Art. 9 Daten
|
||||
|
||||
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
||||
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
||||
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
||||
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
||||
|
||||
### Besondere Schutzmassnahmen
|
||||
|
||||
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
||||
- Ende-zu-Ende-Verschluesselung
|
||||
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
||||
- Audit-Logging aller Zugriffe
|
||||
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
||||
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Your Explicit Consent
|
||||
|
||||
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
||||
|
||||
### Your Rights Regarding Art. 9 Data
|
||||
|
||||
- You can **withdraw your consent at any time**
|
||||
- Withdrawal does not affect the lawfulness of previous processing
|
||||
- Upon withdrawal, your data will be deleted immediately
|
||||
- You have the right to **access, rectification, and erasure**
|
||||
|
||||
### Special Protection Measures
|
||||
|
||||
For this sensitive data, we have implemented special technical and organizational measures:
|
||||
- End-to-end encryption
|
||||
- Strict access control (need-to-know principle)
|
||||
- Audit logging of all access
|
||||
- Regular data protection impact assessments`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'special-categories',
|
||||
order: 6.5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '7. Ihre Rechte als betroffene Person',
|
||||
en: '7. Your Rights as a Data Subject',
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
||||
|
||||
### Auskunftsrecht (Art. 15 DSGVO)
|
||||
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
||||
|
||||
### Recht auf Berichtigung (Art. 16 DSGVO)
|
||||
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
||||
|
||||
### Recht auf Loeschung (Art. 17 DSGVO)
|
||||
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||
|
||||
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
||||
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
||||
|
||||
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
||||
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
||||
|
||||
### Widerspruchsrecht (Art. 21 DSGVO)
|
||||
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
||||
|
||||
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
||||
|
||||
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
||||
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
||||
|
||||
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
||||
en: `You have the following rights regarding your personal data:
|
||||
|
||||
### Right of Access (Art. 15 GDPR)
|
||||
You have the right to request information about the personal data we process about you.
|
||||
|
||||
### Right to Rectification (Art. 16 GDPR)
|
||||
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
||||
|
||||
### Right to Erasure (Art. 17 GDPR)
|
||||
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
||||
|
||||
### Right to Restriction of Processing (Art. 18 GDPR)
|
||||
You have the right to request the restriction of processing of your data.
|
||||
|
||||
### Right to Data Portability (Art. 20 GDPR)
|
||||
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
||||
|
||||
### Right to Object (Art. 21 GDPR)
|
||||
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
||||
|
||||
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
||||
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
||||
|
||||
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
||||
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
||||
|
||||
**To exercise your rights, please contact us using the contact details provided above.**`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'rights',
|
||||
order: 7,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Privacy Policy Section Generators
|
||||
* Privacy Policy Section Generators (Sections 1-4)
|
||||
*
|
||||
* Generiert die 9 Abschnitte der Datenschutzerklaerung (DSI)
|
||||
* aus dem Datenpunktkatalog.
|
||||
* Generiert die ersten 4 Abschnitte der Datenschutzerklaerung (DSI).
|
||||
* Sections 5-7 live in privacy-policy-sections-part2.ts.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -19,6 +19,14 @@ import {
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// Re-export sections 5-7 for backward compatibility
|
||||
export {
|
||||
generateRecipientsSection,
|
||||
generateRetentionSection,
|
||||
generateSpecialCategoriesSection,
|
||||
generateRightsSection,
|
||||
} from './privacy-policy-sections-part2'
|
||||
|
||||
// =============================================================================
|
||||
// KONSTANTEN
|
||||
// =============================================================================
|
||||
@@ -303,257 +311,3 @@ export function generateLegalBasisSection(
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRecipientsSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '5. Empfaenger und Datenweitergabe',
|
||||
en: '5. Recipients and Data Sharing',
|
||||
}
|
||||
|
||||
const thirdParties = extractThirdParties(dataPoints)
|
||||
|
||||
if (thirdParties.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
||||
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
||||
}
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRetentionSection(
|
||||
dataPoints: DataPoint[],
|
||||
retentionMatrix: RetentionMatrixEntry[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '6. Speicherdauer',
|
||||
en: '6. Data Retention',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const entry of retentionMatrix) {
|
||||
const categoryData = grouped.get(entry.category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryName = t(entry.categoryName, language)
|
||||
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
||||
|
||||
const dataRetention = categoryData
|
||||
.map((dp) => {
|
||||
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
||||
return `- ${t(dp.name, language)}: ${period}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
||||
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'retention',
|
||||
order: 6,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
if (specialCategoryDataPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title: LocalizedText = {
|
||||
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
||||
}
|
||||
|
||||
const dataList = specialCategoryDataPoints
|
||||
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Ihre ausdrueckliche Einwilligung
|
||||
|
||||
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
||||
|
||||
### Ihre Rechte bei Art. 9 Daten
|
||||
|
||||
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
||||
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
||||
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
||||
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
||||
|
||||
### Besondere Schutzmassnahmen
|
||||
|
||||
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
||||
- Ende-zu-Ende-Verschluesselung
|
||||
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
||||
- Audit-Logging aller Zugriffe
|
||||
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
||||
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Your Explicit Consent
|
||||
|
||||
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
||||
|
||||
### Your Rights Regarding Art. 9 Data
|
||||
|
||||
- You can **withdraw your consent at any time**
|
||||
- Withdrawal does not affect the lawfulness of previous processing
|
||||
- Upon withdrawal, your data will be deleted immediately
|
||||
- You have the right to **access, rectification, and erasure**
|
||||
|
||||
### Special Protection Measures
|
||||
|
||||
For this sensitive data, we have implemented special technical and organizational measures:
|
||||
- End-to-end encryption
|
||||
- Strict access control (need-to-know principle)
|
||||
- Audit logging of all access
|
||||
- Regular data protection impact assessments`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'special-categories',
|
||||
order: 6.5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '7. Ihre Rechte als betroffene Person',
|
||||
en: '7. Your Rights as a Data Subject',
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
||||
|
||||
### Auskunftsrecht (Art. 15 DSGVO)
|
||||
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
||||
|
||||
### Recht auf Berichtigung (Art. 16 DSGVO)
|
||||
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
||||
|
||||
### Recht auf Loeschung (Art. 17 DSGVO)
|
||||
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||
|
||||
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
||||
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
||||
|
||||
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
||||
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
||||
|
||||
### Widerspruchsrecht (Art. 21 DSGVO)
|
||||
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
||||
|
||||
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
||||
|
||||
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
||||
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
||||
|
||||
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
||||
en: `You have the following rights regarding your personal data:
|
||||
|
||||
### Right of Access (Art. 15 GDPR)
|
||||
You have the right to request information about the personal data we process about you.
|
||||
|
||||
### Right to Rectification (Art. 16 GDPR)
|
||||
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
||||
|
||||
### Right to Erasure (Art. 17 GDPR)
|
||||
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
||||
|
||||
### Right to Restriction of Processing (Art. 18 GDPR)
|
||||
You have the right to request the restriction of processing of your data.
|
||||
|
||||
### Right to Data Portability (Art. 20 GDPR)
|
||||
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
||||
|
||||
### Right to Object (Art. 21 GDPR)
|
||||
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
||||
|
||||
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
||||
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
||||
|
||||
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
||||
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
||||
|
||||
**To exercise your rights, please contact us using the contact details provided above.**`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'rights',
|
||||
order: 7,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Demo Data (Part 2)
|
||||
// DEMO_RISK_PROFILES and DEMO_EVIDENCE_DOCUMENTS
|
||||
// Extracted from index.ts to stay under 500 LOC hard cap
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
RiskProfile,
|
||||
EvidenceDocument,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// DEMO RISK PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
|
||||
saas: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 3,
|
||||
integrity: 3,
|
||||
availability: 4,
|
||||
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
|
||||
},
|
||||
protectionLevel: 'HIGH',
|
||||
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
|
||||
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
|
||||
hasHighRiskProcessing: false,
|
||||
dsfaRequired: false,
|
||||
},
|
||||
healthcare: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 5,
|
||||
integrity: 5,
|
||||
availability: 4,
|
||||
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
|
||||
},
|
||||
protectionLevel: 'VERY_HIGH',
|
||||
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
|
||||
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
|
||||
hasHighRiskProcessing: true,
|
||||
dsfaRequired: true,
|
||||
},
|
||||
enterprise: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 4,
|
||||
integrity: 5,
|
||||
availability: 5,
|
||||
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
|
||||
},
|
||||
protectionLevel: 'VERY_HIGH',
|
||||
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
|
||||
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
|
||||
hasHighRiskProcessing: true,
|
||||
dsfaRequired: true,
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO EVIDENCE DOCUMENTS
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
|
||||
{
|
||||
id: 'demo-evidence-1',
|
||||
filename: 'iso27001-certificate.pdf',
|
||||
originalName: 'ISO 27001 Zertifikat.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 245678,
|
||||
uploadedAt: new Date('2025-01-15'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'CERTIFICATE',
|
||||
detectedType: 'CERTIFICATE',
|
||||
hash: 'sha256:abc123def456',
|
||||
validFrom: new Date('2024-06-01'),
|
||||
validUntil: new Date('2027-05-31'),
|
||||
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
|
||||
aiAnalysis: {
|
||||
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'Zertifiziert nach ISO/IEC 27001:2022',
|
||||
type: 'certification',
|
||||
relatedControlId: 'TOM-RV-04',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
|
||||
gaps: [],
|
||||
confidence: 0.95,
|
||||
analyzedAt: new Date('2025-01-15'),
|
||||
},
|
||||
status: 'VERIFIED',
|
||||
},
|
||||
{
|
||||
id: 'demo-evidence-2',
|
||||
filename: 'passwort-richtlinie.pdf',
|
||||
originalName: 'Passwortrichtlinie v2.1.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 128456,
|
||||
uploadedAt: new Date('2025-01-10'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'POLICY',
|
||||
detectedType: 'POLICY',
|
||||
hash: 'sha256:xyz789abc012',
|
||||
validFrom: new Date('2024-09-01'),
|
||||
validUntil: null,
|
||||
linkedControlIds: ['TOM-ADM-02'],
|
||||
aiAnalysis: {
|
||||
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
|
||||
type: 'password-policy',
|
||||
relatedControlId: 'TOM-ADM-02',
|
||||
},
|
||||
{
|
||||
id: 'clause-2',
|
||||
text: 'Passwörter müssen alle 90 Tage geändert werden',
|
||||
type: 'password-policy',
|
||||
relatedControlId: 'TOM-ADM-02',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-ADM-02'],
|
||||
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
|
||||
confidence: 0.85,
|
||||
analyzedAt: new Date('2025-01-10'),
|
||||
},
|
||||
status: 'ANALYZED',
|
||||
},
|
||||
{
|
||||
id: 'demo-evidence-3',
|
||||
filename: 'aws-avv.pdf',
|
||||
originalName: 'AWS Data Processing Addendum.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 456789,
|
||||
uploadedAt: new Date('2025-01-05'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'AVV',
|
||||
detectedType: 'DPA',
|
||||
hash: 'sha256:qwe123rty456',
|
||||
validFrom: new Date('2024-01-01'),
|
||||
validUntil: null,
|
||||
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
|
||||
aiAnalysis: {
|
||||
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
|
||||
type: 'data-processing',
|
||||
relatedControlId: 'TOM-OR-01',
|
||||
},
|
||||
{
|
||||
id: 'clause-2',
|
||||
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
|
||||
type: 'audit',
|
||||
relatedControlId: 'TOM-OR-02',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
|
||||
gaps: [],
|
||||
confidence: 0.9,
|
||||
analyzedAt: new Date('2025-01-05'),
|
||||
},
|
||||
status: 'VERIFIED',
|
||||
},
|
||||
]
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
DataProfile,
|
||||
ArchitectureProfile,
|
||||
SecurityProfile,
|
||||
RiskProfile,
|
||||
EvidenceDocument,
|
||||
DerivedTOM,
|
||||
GapAnalysisResult,
|
||||
TOM_GENERATOR_STEPS,
|
||||
} from '../types'
|
||||
import { getTOMRulesEngine } from '../rules-engine'
|
||||
import { DEMO_RISK_PROFILES, DEMO_EVIDENCE_DOCUMENTS } from './demo-data-part2'
|
||||
|
||||
// Re-export risk profiles and evidence from part2 for backward compatibility
|
||||
export { DEMO_RISK_PROFILES, DEMO_EVIDENCE_DOCUMENTS } from './demo-data-part2'
|
||||
|
||||
// =============================================================================
|
||||
// DEMO COMPANY PROFILES
|
||||
@@ -216,164 +216,6 @@ export const DEMO_SECURITY_PROFILES: Record<string, SecurityProfile> = {
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO RISK PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
|
||||
saas: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 3,
|
||||
integrity: 3,
|
||||
availability: 4,
|
||||
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
|
||||
},
|
||||
protectionLevel: 'HIGH',
|
||||
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
|
||||
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
|
||||
hasHighRiskProcessing: false,
|
||||
dsfaRequired: false,
|
||||
},
|
||||
healthcare: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 5,
|
||||
integrity: 5,
|
||||
availability: 4,
|
||||
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
|
||||
},
|
||||
protectionLevel: 'VERY_HIGH',
|
||||
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
|
||||
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
|
||||
hasHighRiskProcessing: true,
|
||||
dsfaRequired: true,
|
||||
},
|
||||
enterprise: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 4,
|
||||
integrity: 5,
|
||||
availability: 5,
|
||||
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
|
||||
},
|
||||
protectionLevel: 'VERY_HIGH',
|
||||
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
|
||||
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
|
||||
hasHighRiskProcessing: true,
|
||||
dsfaRequired: true,
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO EVIDENCE DOCUMENTS
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
|
||||
{
|
||||
id: 'demo-evidence-1',
|
||||
filename: 'iso27001-certificate.pdf',
|
||||
originalName: 'ISO 27001 Zertifikat.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 245678,
|
||||
uploadedAt: new Date('2025-01-15'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'CERTIFICATE',
|
||||
detectedType: 'CERTIFICATE',
|
||||
hash: 'sha256:abc123def456',
|
||||
validFrom: new Date('2024-06-01'),
|
||||
validUntil: new Date('2027-05-31'),
|
||||
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
|
||||
aiAnalysis: {
|
||||
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'Zertifiziert nach ISO/IEC 27001:2022',
|
||||
type: 'certification',
|
||||
relatedControlId: 'TOM-RV-04',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
|
||||
gaps: [],
|
||||
confidence: 0.95,
|
||||
analyzedAt: new Date('2025-01-15'),
|
||||
},
|
||||
status: 'VERIFIED',
|
||||
},
|
||||
{
|
||||
id: 'demo-evidence-2',
|
||||
filename: 'passwort-richtlinie.pdf',
|
||||
originalName: 'Passwortrichtlinie v2.1.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 128456,
|
||||
uploadedAt: new Date('2025-01-10'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'POLICY',
|
||||
detectedType: 'POLICY',
|
||||
hash: 'sha256:xyz789abc012',
|
||||
validFrom: new Date('2024-09-01'),
|
||||
validUntil: null,
|
||||
linkedControlIds: ['TOM-ADM-02'],
|
||||
aiAnalysis: {
|
||||
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
|
||||
type: 'password-policy',
|
||||
relatedControlId: 'TOM-ADM-02',
|
||||
},
|
||||
{
|
||||
id: 'clause-2',
|
||||
text: 'Passwörter müssen alle 90 Tage geändert werden',
|
||||
type: 'password-policy',
|
||||
relatedControlId: 'TOM-ADM-02',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-ADM-02'],
|
||||
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
|
||||
confidence: 0.85,
|
||||
analyzedAt: new Date('2025-01-10'),
|
||||
},
|
||||
status: 'ANALYZED',
|
||||
},
|
||||
{
|
||||
id: 'demo-evidence-3',
|
||||
filename: 'aws-avv.pdf',
|
||||
originalName: 'AWS Data Processing Addendum.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 456789,
|
||||
uploadedAt: new Date('2025-01-05'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'AVV',
|
||||
detectedType: 'DPA',
|
||||
hash: 'sha256:qwe123rty456',
|
||||
validFrom: new Date('2024-01-01'),
|
||||
validUntil: null,
|
||||
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
|
||||
aiAnalysis: {
|
||||
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
|
||||
type: 'data-processing',
|
||||
relatedControlId: 'TOM-OR-01',
|
||||
},
|
||||
{
|
||||
id: 'clause-2',
|
||||
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
|
||||
type: 'audit',
|
||||
relatedControlId: 'TOM-OR-02',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
|
||||
gaps: [],
|
||||
confidence: 0.9,
|
||||
analyzedAt: new Date('2025-01-05'),
|
||||
},
|
||||
status: 'VERIFIED',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// DEMO STATE GENERATOR
|
||||
// =============================================================================
|
||||
|
||||
165
admin-compliance/lib/sdk/tom-generator/export/docx-helpers.ts
Normal file
165
admin-compliance/lib/sdk/tom-generator/export/docx-helpers.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
// =============================================================================
|
||||
// TOM Generator DOCX Export - Helper Functions
|
||||
// Private helpers extracted from docx.ts to stay under 500 LOC hard cap
|
||||
// =============================================================================
|
||||
|
||||
import { DerivedTOM, ControlCategory } from '../types'
|
||||
import { getControlById } from '../controls/loader'
|
||||
import type { DOCXExportOptions, DocxTableRow } from './docx'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function groupTOMsByCategory(
|
||||
toms: DerivedTOM[],
|
||||
includeNotApplicable: boolean
|
||||
): Map<ControlCategory, DerivedTOM[]> {
|
||||
const grouped = new Map<ControlCategory, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
if (!includeNotApplicable && tom.applicability === 'NOT_APPLICABLE') {
|
||||
continue
|
||||
}
|
||||
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
const existing = grouped.get(category) || []
|
||||
existing.push(tom)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
export function formatRole(role: string, language: 'de' | 'en'): string {
|
||||
const roles: Record<string, Record<'de' | 'en', string>> = {
|
||||
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
|
||||
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
|
||||
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
|
||||
}
|
||||
return roles[role]?.[language] || role
|
||||
}
|
||||
|
||||
export function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
|
||||
const levels: Record<string, Record<'de' | 'en', string>> = {
|
||||
NORMAL: { de: 'Normal', en: 'Normal' },
|
||||
HIGH: { de: 'Hoch', en: 'High' },
|
||||
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return levels[level]?.[language] || level
|
||||
}
|
||||
|
||||
export function formatType(type: string, language: 'de' | 'en'): string {
|
||||
const types: Record<string, Record<'de' | 'en', string>> = {
|
||||
TECHNICAL: { de: 'Technisch', en: 'Technical' },
|
||||
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
|
||||
}
|
||||
return types[type]?.[language] || type
|
||||
}
|
||||
|
||||
export function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise umgesetzt', en: 'Partially Implemented' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[language] || status
|
||||
}
|
||||
|
||||
export function formatApplicability(applicability: string, language: 'de' | 'en'): string {
|
||||
const apps: Record<string, Record<'de' | 'en', string>> = {
|
||||
REQUIRED: { de: 'Erforderlich', en: 'Required' },
|
||||
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
|
||||
OPTIONAL: { de: 'Optional', en: 'Optional' },
|
||||
NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' },
|
||||
}
|
||||
return apps[applicability]?.[language] || applicability
|
||||
}
|
||||
|
||||
export function generateHTMLFromContent(
|
||||
content: Array<{ type: string; content?: string; headers?: string[]; rows?: DocxTableRow[] }>,
|
||||
options: Partial<DOCXExportOptions>
|
||||
): string {
|
||||
const DEFAULT_COLOR = '#1a56db'
|
||||
const primaryColor = options.primaryColor || DEFAULT_COLOR
|
||||
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.5; }
|
||||
h1 { font-size: 24pt; color: ${primaryColor}; border-bottom: 2px solid ${primaryColor}; }
|
||||
h2 { font-size: 18pt; color: ${primaryColor}; margin-top: 24pt; }
|
||||
h3 { font-size: 14pt; color: #333; margin-top: 18pt; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8pt; text-align: left; }
|
||||
th { background-color: ${primaryColor}; color: white; }
|
||||
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||
ul { margin: 6pt 0; }
|
||||
li { margin: 3pt 0; }
|
||||
.warning { color: #dc2626; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const element of content) {
|
||||
if (element.type === 'table' && element.headers && element.rows) {
|
||||
html += '<table>'
|
||||
html += '<tr>'
|
||||
for (const header of element.headers) {
|
||||
html += `<th>${escapeHtml(header)}</th>`
|
||||
}
|
||||
html += '</tr>'
|
||||
for (const row of element.rows) {
|
||||
html += '<tr>'
|
||||
for (const cell of row.cells) {
|
||||
html += `<td>${escapeHtml(cell)}</td>`
|
||||
}
|
||||
html += '</tr>'
|
||||
}
|
||||
html += '</table>'
|
||||
} else {
|
||||
const tag = getHtmlTag(element.type)
|
||||
const processedContent = processContent(element.content || '')
|
||||
html += `<${tag}>${processedContent}</${tag}>\n`
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
export function getHtmlTag(type: string): string {
|
||||
switch (type) {
|
||||
case 'heading1':
|
||||
return 'h1'
|
||||
case 'heading2':
|
||||
return 'h2'
|
||||
case 'heading3':
|
||||
return 'h3'
|
||||
case 'bullet':
|
||||
return 'li'
|
||||
default:
|
||||
return 'p'
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export function processContent(content: string): string {
|
||||
// Convert markdown-style bold to HTML
|
||||
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
}
|
||||
@@ -10,6 +10,15 @@ import {
|
||||
CONTROL_CATEGORIES,
|
||||
} from '../types'
|
||||
import { getControlById, getCategoryMetadata } from '../controls/loader'
|
||||
import {
|
||||
groupTOMsByCategory,
|
||||
formatRole,
|
||||
formatProtectionLevel,
|
||||
formatType,
|
||||
formatImplementationStatus,
|
||||
formatApplicability,
|
||||
generateHTMLFromContent,
|
||||
} from './docx-helpers'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -320,78 +329,6 @@ export function generateDOCXContent(
|
||||
return elements
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function groupTOMsByCategory(
|
||||
toms: DerivedTOM[],
|
||||
includeNotApplicable: boolean
|
||||
): Map<ControlCategory, DerivedTOM[]> {
|
||||
const grouped = new Map<ControlCategory, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
if (!includeNotApplicable && tom.applicability === 'NOT_APPLICABLE') {
|
||||
continue
|
||||
}
|
||||
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
const existing = grouped.get(category) || []
|
||||
existing.push(tom)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
function formatRole(role: string, language: 'de' | 'en'): string {
|
||||
const roles: Record<string, Record<'de' | 'en', string>> = {
|
||||
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
|
||||
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
|
||||
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
|
||||
}
|
||||
return roles[role]?.[language] || role
|
||||
}
|
||||
|
||||
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
|
||||
const levels: Record<string, Record<'de' | 'en', string>> = {
|
||||
NORMAL: { de: 'Normal', en: 'Normal' },
|
||||
HIGH: { de: 'Hoch', en: 'High' },
|
||||
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return levels[level]?.[language] || level
|
||||
}
|
||||
|
||||
function formatType(type: string, language: 'de' | 'en'): string {
|
||||
const types: Record<string, Record<'de' | 'en', string>> = {
|
||||
TECHNICAL: { de: 'Technisch', en: 'Technical' },
|
||||
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
|
||||
}
|
||||
return types[type]?.[language] || type
|
||||
}
|
||||
|
||||
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise umgesetzt', en: 'Partially Implemented' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[language] || status
|
||||
}
|
||||
|
||||
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
|
||||
const apps: Record<string, Record<'de' | 'en', string>> = {
|
||||
REQUIRED: { de: 'Erforderlich', en: 'Required' },
|
||||
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
|
||||
OPTIONAL: { de: 'Optional', en: 'Optional' },
|
||||
NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' },
|
||||
}
|
||||
return apps[applicability]?.[language] || applicability
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX BLOB GENERATION
|
||||
// Uses simple XML structure compatible with docx libraries
|
||||
@@ -421,90 +358,6 @@ export async function generateDOCXBlob(
|
||||
return blob
|
||||
}
|
||||
|
||||
function generateHTMLFromContent(
|
||||
content: DocxElement[],
|
||||
options: Partial<DOCXExportOptions>
|
||||
): string {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.5; }
|
||||
h1 { font-size: 24pt; color: ${opts.primaryColor}; border-bottom: 2px solid ${opts.primaryColor}; }
|
||||
h2 { font-size: 18pt; color: ${opts.primaryColor}; margin-top: 24pt; }
|
||||
h3 { font-size: 14pt; color: #333; margin-top: 18pt; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8pt; text-align: left; }
|
||||
th { background-color: ${opts.primaryColor}; color: white; }
|
||||
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||
ul { margin: 6pt 0; }
|
||||
li { margin: 3pt 0; }
|
||||
.warning { color: #dc2626; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const element of content) {
|
||||
if (element.type === 'table') {
|
||||
html += '<table>'
|
||||
html += '<tr>'
|
||||
for (const header of element.headers) {
|
||||
html += `<th>${escapeHtml(header)}</th>`
|
||||
}
|
||||
html += '</tr>'
|
||||
for (const row of element.rows) {
|
||||
html += '<tr>'
|
||||
for (const cell of row.cells) {
|
||||
html += `<td>${escapeHtml(cell)}</td>`
|
||||
}
|
||||
html += '</tr>'
|
||||
}
|
||||
html += '</table>'
|
||||
} else {
|
||||
const tag = getHtmlTag(element.type)
|
||||
const processedContent = processContent(element.content)
|
||||
html += `<${tag}>${processedContent}</${tag}>\n`
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
function getHtmlTag(type: string): string {
|
||||
switch (type) {
|
||||
case 'heading1':
|
||||
return 'h1'
|
||||
case 'heading2':
|
||||
return 'h2'
|
||||
case 'heading3':
|
||||
return 'h3'
|
||||
case 'bullet':
|
||||
return 'li'
|
||||
default:
|
||||
return 'p'
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function processContent(content: string): string {
|
||||
// Convert markdown-style bold to HTML
|
||||
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
87
admin-compliance/lib/sdk/tom-generator/export/pdf-helpers.ts
Normal file
87
admin-compliance/lib/sdk/tom-generator/export/pdf-helpers.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// =============================================================================
|
||||
// TOM Generator PDF Export - Helper Functions
|
||||
// Private helpers extracted from pdf.ts to stay under 500 LOC hard cap
|
||||
// =============================================================================
|
||||
|
||||
import { DerivedTOM, CONTROL_CATEGORIES } from '../types'
|
||||
import { getControlById } from '../controls/loader'
|
||||
import type { PDFExportOptions } from './pdf'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function generateCategorySummary(
|
||||
toms: DerivedTOM[],
|
||||
opts: PDFExportOptions
|
||||
): string[][] {
|
||||
const summary: string[][] = []
|
||||
|
||||
for (const category of CONTROL_CATEGORIES) {
|
||||
const categoryTOMs = toms.filter((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === category.id
|
||||
})
|
||||
|
||||
if (categoryTOMs.length === 0) continue
|
||||
|
||||
const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length
|
||||
const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
|
||||
|
||||
summary.push([
|
||||
category.name[opts.language],
|
||||
String(categoryTOMs.length),
|
||||
String(required),
|
||||
String(implemented),
|
||||
])
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
export function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
|
||||
const levels: Record<string, Record<'de' | 'en', string>> = {
|
||||
NORMAL: { de: 'Normal', en: 'Normal' },
|
||||
HIGH: { de: 'Hoch', en: 'High' },
|
||||
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return levels[level]?.[language] || level
|
||||
}
|
||||
|
||||
export function formatType(type: string, language: 'de' | 'en'): string {
|
||||
const types: Record<string, Record<'de' | 'en', string>> = {
|
||||
TECHNICAL: { de: 'Technisch', en: 'Technical' },
|
||||
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
|
||||
}
|
||||
return types[type]?.[language] || type
|
||||
}
|
||||
|
||||
export function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise', en: 'Partial' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[language] || status
|
||||
}
|
||||
|
||||
export function formatApplicability(applicability: string, language: 'de' | 'en'): string {
|
||||
const apps: Record<string, Record<'de' | 'en', string>> = {
|
||||
REQUIRED: { de: 'Erforderlich', en: 'Required' },
|
||||
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
|
||||
OPTIONAL: { de: 'Optional', en: 'Optional' },
|
||||
NOT_APPLICABLE: { de: 'N/A', en: 'N/A' },
|
||||
}
|
||||
return apps[applicability]?.[language] || applicability
|
||||
}
|
||||
|
||||
export function getCIAMeaning(rating: number, language: 'de' | 'en'): string {
|
||||
const meanings: Record<number, Record<'de' | 'en', string>> = {
|
||||
1: { de: 'Sehr gering', en: 'Very Low' },
|
||||
2: { de: 'Gering', en: 'Low' },
|
||||
3: { de: 'Mittel', en: 'Medium' },
|
||||
4: { de: 'Hoch', en: 'High' },
|
||||
5: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return meanings[rating]?.[language] || String(rating)
|
||||
}
|
||||
@@ -9,6 +9,14 @@ import {
|
||||
CONTROL_CATEGORIES,
|
||||
} from '../types'
|
||||
import { getControlById } from '../controls/loader'
|
||||
import {
|
||||
generateCategorySummary,
|
||||
formatProtectionLevel,
|
||||
formatType,
|
||||
formatImplementationStatus,
|
||||
formatApplicability,
|
||||
getCIAMeaning,
|
||||
} from './pdf-helpers'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -369,85 +377,6 @@ export function generatePDFContent(
|
||||
return sections
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function generateCategorySummary(
|
||||
toms: DerivedTOM[],
|
||||
opts: PDFExportOptions
|
||||
): string[][] {
|
||||
const summary: string[][] = []
|
||||
|
||||
for (const category of CONTROL_CATEGORIES) {
|
||||
const categoryTOMs = toms.filter((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === category.id
|
||||
})
|
||||
|
||||
if (categoryTOMs.length === 0) continue
|
||||
|
||||
const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length
|
||||
const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
|
||||
|
||||
summary.push([
|
||||
category.name[opts.language],
|
||||
String(categoryTOMs.length),
|
||||
String(required),
|
||||
String(implemented),
|
||||
])
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
|
||||
const levels: Record<string, Record<'de' | 'en', string>> = {
|
||||
NORMAL: { de: 'Normal', en: 'Normal' },
|
||||
HIGH: { de: 'Hoch', en: 'High' },
|
||||
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return levels[level]?.[language] || level
|
||||
}
|
||||
|
||||
function formatType(type: string, language: 'de' | 'en'): string {
|
||||
const types: Record<string, Record<'de' | 'en', string>> = {
|
||||
TECHNICAL: { de: 'Technisch', en: 'Technical' },
|
||||
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
|
||||
}
|
||||
return types[type]?.[language] || type
|
||||
}
|
||||
|
||||
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise', en: 'Partial' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[language] || status
|
||||
}
|
||||
|
||||
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
|
||||
const apps: Record<string, Record<'de' | 'en', string>> = {
|
||||
REQUIRED: { de: 'Erforderlich', en: 'Required' },
|
||||
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
|
||||
OPTIONAL: { de: 'Optional', en: 'Optional' },
|
||||
NOT_APPLICABLE: { de: 'N/A', en: 'N/A' },
|
||||
}
|
||||
return apps[applicability]?.[language] || applicability
|
||||
}
|
||||
|
||||
function getCIAMeaning(rating: number, language: 'de' | 'en'): string {
|
||||
const meanings: Record<number, Record<'de' | 'en', string>> = {
|
||||
1: { de: 'Sehr gering', en: 'Very Low' },
|
||||
2: { de: 'Gering', en: 'Low' },
|
||||
3: { de: 'Mittel', en: 'Medium' },
|
||||
4: { de: 'Hoch', en: 'High' },
|
||||
5: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return meanings[rating]?.[language] || String(rating)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF BLOB GENERATION
|
||||
// Note: For production, use jspdf or pdfmake library
|
||||
|
||||
219
admin-compliance/lib/sdk/tom-generator/export/zip-helpers.ts
Normal file
219
admin-compliance/lib/sdk/tom-generator/export/zip-helpers.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
// =============================================================================
|
||||
// TOM Generator ZIP Export - Helper Functions
|
||||
// Private helpers extracted from zip.ts to stay under 500 LOC hard cap
|
||||
// =============================================================================
|
||||
|
||||
import { TOMGeneratorState, DerivedTOM } from '../types'
|
||||
import { getControlById } from '../controls/loader'
|
||||
import type { ZIPExportOptions } from './zip'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function generateReadme(
|
||||
state: TOMGeneratorState,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const lang = opts.language
|
||||
|
||||
return `# TOM Export Package
|
||||
|
||||
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
|
||||
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
|
||||
|
||||
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
|
||||
|
||||
### /data
|
||||
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
|
||||
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
|
||||
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
|
||||
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
|
||||
|
||||
### /reference
|
||||
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
|
||||
|
||||
### /documents
|
||||
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
|
||||
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
|
||||
|
||||
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
|
||||
|
||||
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
|
||||
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
|
||||
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
|
||||
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
|
||||
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
|
||||
|
||||
---
|
||||
|
||||
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
|
||||
`
|
||||
}
|
||||
|
||||
export function groupTOMsByCategory(
|
||||
toms: DerivedTOM[]
|
||||
): Map<string, DerivedTOM[]> {
|
||||
const grouped = new Map<string, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
const existing: DerivedTOM[] = grouped.get(category) || []
|
||||
existing.push(tom)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
export function generateImplementationSummary(
|
||||
toms: Array<{ implementationStatus: string; applicability: string }>
|
||||
): Record<string, number> {
|
||||
return {
|
||||
total: toms.length,
|
||||
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
|
||||
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
|
||||
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
|
||||
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
|
||||
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
|
||||
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
|
||||
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
|
||||
}
|
||||
}
|
||||
|
||||
export function groupEvidenceByControl(
|
||||
documents: Array<{ id: string; linkedControlIds: string[] }>
|
||||
): Map<string, string[]> {
|
||||
const grouped = new Map<string, string[]>()
|
||||
|
||||
for (const doc of documents) {
|
||||
for (const controlId of doc.linkedControlIds) {
|
||||
const existing = grouped.get(controlId) || []
|
||||
existing.push(doc.id)
|
||||
grouped.set(controlId, existing)
|
||||
}
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
export function generateRecommendationsMarkdown(
|
||||
recommendations: string[],
|
||||
language: 'de' | 'en'
|
||||
): string {
|
||||
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
|
||||
|
||||
return `# ${title}
|
||||
|
||||
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
|
||||
|
||||
---
|
||||
|
||||
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
|
||||
`
|
||||
}
|
||||
|
||||
export function generateMarkdownSummary(
|
||||
state: TOMGeneratorState,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const lang = opts.language
|
||||
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
|
||||
|
||||
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
|
||||
|
||||
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
|
||||
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
|
||||
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
|
||||
|
||||
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
|
||||
|
||||
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|
||||
|--------|-------|
|
||||
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
|
||||
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
|
||||
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
|
||||
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
|
||||
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
|
||||
|
||||
`
|
||||
|
||||
if (state.gapAnalysis) {
|
||||
md += `
|
||||
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
|
||||
|
||||
**${state.gapAnalysis.overallScore}%**
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
// Add required TOMs table
|
||||
const requiredTOMs = state.derivedTOMs.filter(
|
||||
(t) => t.applicability === 'REQUIRED'
|
||||
)
|
||||
|
||||
if (requiredTOMs.length > 0) {
|
||||
md += `
|
||||
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
|
||||
|
||||
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|
||||
|----|----------|--------|
|
||||
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
export function generateCSV(
|
||||
toms: Array<{
|
||||
controlId: string
|
||||
name: string
|
||||
description: string
|
||||
applicability: string
|
||||
implementationStatus: string
|
||||
responsiblePerson: string | null
|
||||
}>,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const lang = opts.language
|
||||
|
||||
const headers = lang === 'de'
|
||||
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
|
||||
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
|
||||
|
||||
const rows = toms.map((tom) => [
|
||||
tom.controlId,
|
||||
escapeCSV(tom.name),
|
||||
escapeCSV(tom.description),
|
||||
tom.applicability,
|
||||
tom.implementationStatus,
|
||||
tom.responsiblePerson || '',
|
||||
])
|
||||
|
||||
return [
|
||||
headers.join(','),
|
||||
...rows.map((row) => row.join(',')),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function escapeCSV(value: string): string {
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function formatStatus(status: string, lang: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise', en: 'Partial' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[lang] || status
|
||||
}
|
||||
@@ -6,7 +6,16 @@
|
||||
import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
|
||||
import { generateDOCXContent, DOCXExportOptions } from './docx'
|
||||
import { generatePDFContent, PDFExportOptions } from './pdf'
|
||||
import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader'
|
||||
import { getAllControls, getLibraryMetadata } from '../controls/loader'
|
||||
import {
|
||||
generateReadme,
|
||||
groupTOMsByCategory,
|
||||
generateImplementationSummary,
|
||||
groupEvidenceByControl,
|
||||
generateRecommendationsMarkdown,
|
||||
generateMarkdownSummary,
|
||||
generateCSV,
|
||||
} from './zip-helpers'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -267,217 +276,6 @@ export function generateZIPFiles(
|
||||
return files
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function generateReadme(
|
||||
state: TOMGeneratorState,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const lang = opts.language
|
||||
|
||||
return `# TOM Export Package
|
||||
|
||||
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
|
||||
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
|
||||
|
||||
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
|
||||
|
||||
### /data
|
||||
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
|
||||
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
|
||||
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
|
||||
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
|
||||
|
||||
### /reference
|
||||
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
|
||||
|
||||
### /documents
|
||||
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
|
||||
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
|
||||
|
||||
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
|
||||
|
||||
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
|
||||
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
|
||||
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
|
||||
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
|
||||
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
|
||||
|
||||
---
|
||||
|
||||
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
|
||||
`
|
||||
}
|
||||
|
||||
function groupTOMsByCategory(
|
||||
toms: DerivedTOM[]
|
||||
): Map<string, DerivedTOM[]> {
|
||||
const grouped = new Map<string, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
const existing: DerivedTOM[] = grouped.get(category) || []
|
||||
existing.push(tom)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
function generateImplementationSummary(
|
||||
toms: Array<{ implementationStatus: string; applicability: string }>
|
||||
): Record<string, number> {
|
||||
return {
|
||||
total: toms.length,
|
||||
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
|
||||
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
|
||||
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
|
||||
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
|
||||
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
|
||||
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
|
||||
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
|
||||
}
|
||||
}
|
||||
|
||||
function groupEvidenceByControl(
|
||||
documents: Array<{ id: string; linkedControlIds: string[] }>
|
||||
): Map<string, string[]> {
|
||||
const grouped = new Map<string, string[]>()
|
||||
|
||||
for (const doc of documents) {
|
||||
for (const controlId of doc.linkedControlIds) {
|
||||
const existing = grouped.get(controlId) || []
|
||||
existing.push(doc.id)
|
||||
grouped.set(controlId, existing)
|
||||
}
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
function generateRecommendationsMarkdown(
|
||||
recommendations: string[],
|
||||
language: 'de' | 'en'
|
||||
): string {
|
||||
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
|
||||
|
||||
return `# ${title}
|
||||
|
||||
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
|
||||
|
||||
---
|
||||
|
||||
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
|
||||
`
|
||||
}
|
||||
|
||||
function generateMarkdownSummary(
|
||||
state: TOMGeneratorState,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const lang = opts.language
|
||||
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
|
||||
|
||||
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
|
||||
|
||||
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
|
||||
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
|
||||
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
|
||||
|
||||
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
|
||||
|
||||
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|
||||
|--------|-------|
|
||||
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
|
||||
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
|
||||
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
|
||||
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
|
||||
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
|
||||
|
||||
`
|
||||
|
||||
if (state.gapAnalysis) {
|
||||
md += `
|
||||
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
|
||||
|
||||
**${state.gapAnalysis.overallScore}%**
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
// Add required TOMs table
|
||||
const requiredTOMs = state.derivedTOMs.filter(
|
||||
(t) => t.applicability === 'REQUIRED'
|
||||
)
|
||||
|
||||
if (requiredTOMs.length > 0) {
|
||||
md += `
|
||||
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
|
||||
|
||||
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|
||||
|----|----------|--------|
|
||||
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
function generateCSV(
|
||||
toms: Array<{
|
||||
controlId: string
|
||||
name: string
|
||||
description: string
|
||||
applicability: string
|
||||
implementationStatus: string
|
||||
responsiblePerson: string | null
|
||||
}>,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const lang = opts.language
|
||||
|
||||
const headers = lang === 'de'
|
||||
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
|
||||
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
|
||||
|
||||
const rows = toms.map((tom) => [
|
||||
tom.controlId,
|
||||
escapeCSV(tom.name),
|
||||
escapeCSV(tom.description),
|
||||
tom.applicability,
|
||||
tom.implementationStatus,
|
||||
tom.responsiblePerson || '',
|
||||
])
|
||||
|
||||
return [
|
||||
headers.join(','),
|
||||
...rows.map((row) => row.join(',')),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function escapeCSV(value: string): string {
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function formatStatus(status: string, lang: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise', en: 'Partial' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[lang] || status
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ZIP BLOB GENERATION
|
||||
// Note: For production, use jszip library
|
||||
|
||||
248
admin-compliance/lib/sdk/vendor-compliance/context-actions.tsx
Normal file
248
admin-compliance/lib/sdk/vendor-compliance/context-actions.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Vendor Compliance Context - API Actions
|
||||
*
|
||||
* Extracted from context.tsx to stay under 500 LOC hard cap.
|
||||
* Contains loadData, refresh, and all CRUD API action hooks.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import type { Dispatch } from 'react'
|
||||
import type { ProcessingActivity, VendorComplianceAction } from './types'
|
||||
|
||||
const API_BASE = '/api/sdk/v1/vendor-compliance'
|
||||
|
||||
export function useContextApiActions(
|
||||
state: { processingActivities: ProcessingActivity[]; contracts: Array<{ id: string; vendorId: string; expirationDate?: Date | null; status: string }>; vendors: Array<{ id: string; contracts: string[] }> },
|
||||
dispatch: Dispatch<VendorComplianceAction>
|
||||
) {
|
||||
const loadData = useCallback(async () => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const [
|
||||
activitiesRes,
|
||||
vendorsRes,
|
||||
contractsRes,
|
||||
findingsRes,
|
||||
controlsRes,
|
||||
controlInstancesRes,
|
||||
] = await Promise.all([
|
||||
fetch(`${API_BASE}/processing-activities`),
|
||||
fetch(`${API_BASE}/vendors`),
|
||||
fetch(`${API_BASE}/contracts`),
|
||||
fetch(`${API_BASE}/findings`),
|
||||
fetch(`${API_BASE}/controls`),
|
||||
fetch(`${API_BASE}/control-instances`),
|
||||
])
|
||||
|
||||
if (activitiesRes.ok) {
|
||||
const data = await activitiesRes.json()
|
||||
dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (vendorsRes.ok) {
|
||||
const data = await vendorsRes.json()
|
||||
dispatch({ type: 'SET_VENDORS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (contractsRes.ok) {
|
||||
const data = await contractsRes.json()
|
||||
dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (findingsRes.ok) {
|
||||
const data = await findingsRes.json()
|
||||
dispatch({ type: 'SET_FINDINGS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (controlsRes.ok) {
|
||||
const data = await controlsRes.json()
|
||||
dispatch({ type: 'SET_CONTROLS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (controlInstancesRes.ok) {
|
||||
const data = await controlInstancesRes.json()
|
||||
dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vendor compliance data:', error)
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: 'Fehler beim Laden der Daten',
|
||||
})
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await loadData()
|
||||
}, [loadData])
|
||||
|
||||
const createProcessingActivity = useCallback(
|
||||
async (
|
||||
data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
||||
): Promise<ProcessingActivity> => {
|
||||
const response = await fetch(`${API_BASE}/processing-activities`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const activity = result.data
|
||||
|
||||
dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: activity })
|
||||
|
||||
return activity
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteProcessingActivity = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const response = await fetch(`${API_BASE}/processing-activities/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const duplicateProcessingActivity = useCallback(
|
||||
async (id: string): Promise<ProcessingActivity> => {
|
||||
const original = state.processingActivities.find((a) => a.id === id)
|
||||
if (!original) {
|
||||
throw new Error('Verarbeitungstätigkeit nicht gefunden')
|
||||
}
|
||||
|
||||
const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original
|
||||
|
||||
const newActivity = await createProcessingActivity({
|
||||
...rest,
|
||||
vvtId: '',
|
||||
name: {
|
||||
de: `${original.name.de} (Kopie)`,
|
||||
en: `${original.name.en} (Copy)`,
|
||||
},
|
||||
status: 'DRAFT',
|
||||
})
|
||||
|
||||
return newActivity
|
||||
},
|
||||
[state.processingActivities, createProcessingActivity]
|
||||
)
|
||||
|
||||
const deleteVendor = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const response = await fetch(`${API_BASE}/vendors/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen des Vendors')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_VENDOR', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteContract = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const contract = state.contracts.find((c) => c.id === id)
|
||||
|
||||
const response = await fetch(`${API_BASE}/contracts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen des Vertrags')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_CONTRACT', payload: id })
|
||||
|
||||
if (contract) {
|
||||
const vendor = state.vendors.find((v) => v.id === contract.vendorId)
|
||||
if (vendor) {
|
||||
dispatch({
|
||||
type: 'UPDATE_VENDOR',
|
||||
payload: {
|
||||
id: vendor.id,
|
||||
data: { contracts: vendor.contracts.filter((cId) => cId !== id) },
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch, state.contracts, state.vendors]
|
||||
)
|
||||
|
||||
const startContractReview = useCallback(
|
||||
async (contractId: string): Promise<void> => {
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } },
|
||||
})
|
||||
|
||||
const response = await fetch(`${API_BASE}/contracts/${contractId}/review`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: { id: contractId, data: { reviewStatus: 'FAILED' } },
|
||||
})
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: {
|
||||
id: contractId,
|
||||
data: {
|
||||
reviewStatus: 'COMPLETED',
|
||||
reviewCompletedAt: new Date(),
|
||||
complianceScore: result.data.complianceScore,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (result.data.findings && result.data.findings.length > 0) {
|
||||
dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings })
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return {
|
||||
loadData,
|
||||
refresh,
|
||||
createProcessingActivity,
|
||||
deleteProcessingActivity,
|
||||
duplicateProcessingActivity,
|
||||
deleteVendor,
|
||||
deleteContract,
|
||||
startContractReview,
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import React, {
|
||||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
useContext,
|
||||
} from 'react'
|
||||
|
||||
import {
|
||||
VendorComplianceContextValue,
|
||||
ProcessingActivity,
|
||||
VendorStatistics,
|
||||
ComplianceStatistics,
|
||||
RiskOverview,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { initialState, vendorComplianceReducer } from './reducer'
|
||||
import { VendorComplianceContext } from './hooks'
|
||||
import { useVendorComplianceActions } from './use-actions'
|
||||
import { useContextApiActions } from './context-actions'
|
||||
|
||||
// Re-export hooks and selectors for barrel
|
||||
export {
|
||||
@@ -201,243 +202,19 @@ export function VendorComplianceProvider({
|
||||
}, [state.vendors, state.findings])
|
||||
|
||||
// ==========================================
|
||||
// API CALLS
|
||||
// API CALLS (extracted to context-actions.tsx)
|
||||
// ==========================================
|
||||
|
||||
const apiBase = '/api/sdk/v1/vendor-compliance'
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const [
|
||||
activitiesRes,
|
||||
vendorsRes,
|
||||
contractsRes,
|
||||
findingsRes,
|
||||
controlsRes,
|
||||
controlInstancesRes,
|
||||
] = await Promise.all([
|
||||
fetch(`${apiBase}/processing-activities`),
|
||||
fetch(`${apiBase}/vendors`),
|
||||
fetch(`${apiBase}/contracts`),
|
||||
fetch(`${apiBase}/findings`),
|
||||
fetch(`${apiBase}/controls`),
|
||||
fetch(`${apiBase}/control-instances`),
|
||||
])
|
||||
|
||||
if (activitiesRes.ok) {
|
||||
const data = await activitiesRes.json()
|
||||
dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (vendorsRes.ok) {
|
||||
const data = await vendorsRes.json()
|
||||
dispatch({ type: 'SET_VENDORS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (contractsRes.ok) {
|
||||
const data = await contractsRes.json()
|
||||
dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (findingsRes.ok) {
|
||||
const data = await findingsRes.json()
|
||||
dispatch({ type: 'SET_FINDINGS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (controlsRes.ok) {
|
||||
const data = await controlsRes.json()
|
||||
dispatch({ type: 'SET_CONTROLS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (controlInstancesRes.ok) {
|
||||
const data = await controlInstancesRes.json()
|
||||
dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vendor compliance data:', error)
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: 'Fehler beim Laden der Daten',
|
||||
})
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
}, [apiBase])
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await loadData()
|
||||
}, [loadData])
|
||||
|
||||
// ==========================================
|
||||
// PROCESSING ACTIVITIES ACTIONS
|
||||
// ==========================================
|
||||
|
||||
const createProcessingActivity = useCallback(
|
||||
async (
|
||||
data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
||||
): Promise<ProcessingActivity> => {
|
||||
const response = await fetch(`${apiBase}/processing-activities`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const activity = result.data
|
||||
|
||||
dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: activity })
|
||||
|
||||
return activity
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
const deleteProcessingActivity = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const response = await fetch(`${apiBase}/processing-activities/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id })
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
const duplicateProcessingActivity = useCallback(
|
||||
async (id: string): Promise<ProcessingActivity> => {
|
||||
const original = state.processingActivities.find((a) => a.id === id)
|
||||
if (!original) {
|
||||
throw new Error('Verarbeitungstätigkeit nicht gefunden')
|
||||
}
|
||||
|
||||
const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original
|
||||
|
||||
const newActivity = await createProcessingActivity({
|
||||
...rest,
|
||||
vvtId: '', // Will be generated by backend
|
||||
name: {
|
||||
de: `${original.name.de} (Kopie)`,
|
||||
en: `${original.name.en} (Copy)`,
|
||||
},
|
||||
status: 'DRAFT',
|
||||
})
|
||||
|
||||
return newActivity
|
||||
},
|
||||
[state.processingActivities, createProcessingActivity]
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// VENDOR ACTIONS
|
||||
// ==========================================
|
||||
|
||||
const deleteVendor = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const response = await fetch(`${apiBase}/vendors/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen des Vendors')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_VENDOR', payload: id })
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// CONTRACT ACTIONS
|
||||
// ==========================================
|
||||
|
||||
const deleteContract = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const contract = state.contracts.find((c) => c.id === id)
|
||||
|
||||
const response = await fetch(`${apiBase}/contracts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen des Vertrags')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_CONTRACT', payload: id })
|
||||
|
||||
// Update vendor's contracts list
|
||||
if (contract) {
|
||||
const vendor = state.vendors.find((v) => v.id === contract.vendorId)
|
||||
if (vendor) {
|
||||
dispatch({
|
||||
type: 'UPDATE_VENDOR',
|
||||
payload: {
|
||||
id: vendor.id,
|
||||
data: { contracts: vendor.contracts.filter((cId) => cId !== id) },
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[apiBase, state.contracts, state.vendors]
|
||||
)
|
||||
|
||||
const startContractReview = useCallback(
|
||||
async (contractId: string): Promise<void> => {
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } },
|
||||
})
|
||||
|
||||
const response = await fetch(`${apiBase}/contracts/${contractId}/review`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: { id: contractId, data: { reviewStatus: 'FAILED' } },
|
||||
})
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Update contract with review results
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: {
|
||||
id: contractId,
|
||||
data: {
|
||||
reviewStatus: 'COMPLETED',
|
||||
reviewCompletedAt: new Date(),
|
||||
complianceScore: result.data.complianceScore,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Add findings
|
||||
if (result.data.findings && result.data.findings.length > 0) {
|
||||
dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings })
|
||||
}
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
const {
|
||||
loadData,
|
||||
refresh,
|
||||
createProcessingActivity,
|
||||
deleteProcessingActivity,
|
||||
duplicateProcessingActivity,
|
||||
deleteVendor,
|
||||
deleteContract,
|
||||
startContractReview,
|
||||
} = useContextApiActions(state, dispatch)
|
||||
|
||||
// ==========================================
|
||||
// INITIALIZATION
|
||||
|
||||
Reference in New Issue
Block a user