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,
|
CATEGORY_METADATA,
|
||||||
RETENTION_PERIOD_INFO,
|
RETENTION_PERIOD_INFO,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
import { generateHTMLFromContent } from './pdf-helpers'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@@ -277,216 +278,6 @@ export async function generatePDFBlob(
|
|||||||
return new Blob([html], { type: 'text/html' })
|
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
|
// 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)
|
* Generiert die ersten 4 Abschnitte der Datenschutzerklaerung (DSI).
|
||||||
* aus dem Datenpunktkatalog.
|
* Sections 5-7 live in privacy-policy-sections-part2.ts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +19,14 @@ import {
|
|||||||
RETENTION_PERIOD_INFO,
|
RETENTION_PERIOD_INFO,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
|
// Re-export sections 5-7 for backward compatibility
|
||||||
|
export {
|
||||||
|
generateRecipientsSection,
|
||||||
|
generateRetentionSection,
|
||||||
|
generateSpecialCategoriesSection,
|
||||||
|
generateRightsSection,
|
||||||
|
} from './privacy-policy-sections-part2'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// KONSTANTEN
|
// 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,
|
DataProfile,
|
||||||
ArchitectureProfile,
|
ArchitectureProfile,
|
||||||
SecurityProfile,
|
SecurityProfile,
|
||||||
RiskProfile,
|
|
||||||
EvidenceDocument,
|
|
||||||
DerivedTOM,
|
|
||||||
GapAnalysisResult,
|
|
||||||
TOM_GENERATOR_STEPS,
|
TOM_GENERATOR_STEPS,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { getTOMRulesEngine } from '../rules-engine'
|
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
|
// 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
|
// 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,
|
CONTROL_CATEGORIES,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { getControlById, getCategoryMetadata } from '../controls/loader'
|
import { getControlById, getCategoryMetadata } from '../controls/loader'
|
||||||
|
import {
|
||||||
|
groupTOMsByCategory,
|
||||||
|
formatRole,
|
||||||
|
formatProtectionLevel,
|
||||||
|
formatType,
|
||||||
|
formatImplementationStatus,
|
||||||
|
formatApplicability,
|
||||||
|
generateHTMLFromContent,
|
||||||
|
} from './docx-helpers'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@@ -320,78 +329,6 @@ export function generateDOCXContent(
|
|||||||
return elements
|
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
|
// DOCX BLOB GENERATION
|
||||||
// Uses simple XML structure compatible with docx libraries
|
// Uses simple XML structure compatible with docx libraries
|
||||||
@@ -421,90 +358,6 @@ export async function generateDOCXBlob(
|
|||||||
return blob
|
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
|
// 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,
|
CONTROL_CATEGORIES,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { getControlById } from '../controls/loader'
|
import { getControlById } from '../controls/loader'
|
||||||
|
import {
|
||||||
|
generateCategorySummary,
|
||||||
|
formatProtectionLevel,
|
||||||
|
formatType,
|
||||||
|
formatImplementationStatus,
|
||||||
|
formatApplicability,
|
||||||
|
getCIAMeaning,
|
||||||
|
} from './pdf-helpers'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@@ -369,85 +377,6 @@ export function generatePDFContent(
|
|||||||
return sections
|
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
|
// PDF BLOB GENERATION
|
||||||
// Note: For production, use jspdf or pdfmake library
|
// 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 { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
|
||||||
import { generateDOCXContent, DOCXExportOptions } from './docx'
|
import { generateDOCXContent, DOCXExportOptions } from './docx'
|
||||||
import { generatePDFContent, PDFExportOptions } from './pdf'
|
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
|
// TYPES
|
||||||
@@ -267,217 +276,6 @@ export function generateZIPFiles(
|
|||||||
return files
|
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
|
// ZIP BLOB GENERATION
|
||||||
// Note: For production, use jszip library
|
// 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,
|
useMemo,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
|
useContext,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VendorComplianceContextValue,
|
VendorComplianceContextValue,
|
||||||
ProcessingActivity,
|
|
||||||
VendorStatistics,
|
VendorStatistics,
|
||||||
ComplianceStatistics,
|
ComplianceStatistics,
|
||||||
RiskOverview,
|
RiskOverview,
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { initialState, vendorComplianceReducer } from './reducer'
|
import { initialState, vendorComplianceReducer } from './reducer'
|
||||||
import { VendorComplianceContext } from './hooks'
|
import { VendorComplianceContext } from './hooks'
|
||||||
import { useVendorComplianceActions } from './use-actions'
|
import { useVendorComplianceActions } from './use-actions'
|
||||||
|
import { useContextApiActions } from './context-actions'
|
||||||
|
|
||||||
// Re-export hooks and selectors for barrel
|
// Re-export hooks and selectors for barrel
|
||||||
export {
|
export {
|
||||||
@@ -201,243 +202,19 @@ export function VendorComplianceProvider({
|
|||||||
}, [state.vendors, state.findings])
|
}, [state.vendors, state.findings])
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// API CALLS
|
// API CALLS (extracted to context-actions.tsx)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
const apiBase = '/api/sdk/v1/vendor-compliance'
|
const {
|
||||||
|
loadData,
|
||||||
const loadData = useCallback(async () => {
|
refresh,
|
||||||
dispatch({ type: 'SET_LOADING', payload: true })
|
createProcessingActivity,
|
||||||
dispatch({ type: 'SET_ERROR', payload: null })
|
deleteProcessingActivity,
|
||||||
|
duplicateProcessingActivity,
|
||||||
try {
|
deleteVendor,
|
||||||
const [
|
deleteContract,
|
||||||
activitiesRes,
|
startContractReview,
|
||||||
vendorsRes,
|
} = useContextApiActions(state, dispatch)
|
||||||
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]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
|
|||||||
Reference in New Issue
Block a user