From 7d8e5667c9339bfad27b41644d01c9f9c22e4af1 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:43:41 +0200 Subject: [PATCH] refactor(admin-compliance): split 7 oversized files under 500 LOC hard cap (batch 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../sdk/einwilligungen/export/pdf-helpers.ts | 218 +++++++++++++ .../lib/sdk/einwilligungen/export/pdf.ts | 211 +----------- .../privacy-policy-sections-part2.ts | 299 ++++++++++++++++++ .../generator/privacy-policy-sections.ts | 268 +--------------- .../demo-data/demo-data-part2.ts | 168 ++++++++++ .../lib/sdk/tom-generator/demo-data/index.ts | 166 +--------- .../sdk/tom-generator/export/docx-helpers.ts | 165 ++++++++++ .../lib/sdk/tom-generator/export/docx.ts | 165 +--------- .../sdk/tom-generator/export/pdf-helpers.ts | 87 +++++ .../lib/sdk/tom-generator/export/pdf.ts | 87 +---- .../sdk/tom-generator/export/zip-helpers.ts | 219 +++++++++++++ .../lib/sdk/tom-generator/export/zip.ts | 222 +------------ .../sdk/vendor-compliance/context-actions.tsx | 248 +++++++++++++++ .../lib/sdk/vendor-compliance/context.tsx | 249 +-------------- 14 files changed, 1460 insertions(+), 1312 deletions(-) create mode 100644 admin-compliance/lib/sdk/einwilligungen/export/pdf-helpers.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections-part2.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/demo-data/demo-data-part2.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/export/docx-helpers.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/export/pdf-helpers.ts create mode 100644 admin-compliance/lib/sdk/tom-generator/export/zip-helpers.ts create mode 100644 admin-compliance/lib/sdk/vendor-compliance/context-actions.tsx diff --git a/admin-compliance/lib/sdk/einwilligungen/export/pdf-helpers.ts b/admin-compliance/lib/sdk/einwilligungen/export/pdf-helpers.ts new file mode 100644 index 0000000..6c26ab9 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/export/pdf-helpers.ts @@ -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 = ` + + + + + ${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'} + + + +` + + for (const section of content) { + switch (section.type) { + case 'title': + html += `
${escapeHtml(section.content || '')}
\n` + break + + case 'heading': + html += `

${escapeHtml(section.content || '')}

\n` + break + + case 'subheading': + html += `

${escapeHtml(section.content || '')}

\n` + break + + case 'paragraph': { + const alignClass = section.style?.align === 'center' ? ' class="center"' : '' + html += `${escapeHtml(section.content || '')}

\n` + break + } + + case 'list': + html += '\n' + break + + case 'table': + if (section.table) { + html += '\n\n' + for (const header of section.table.headers) { + html += ` \n` + } + html += '\n\n' + for (const row of section.table.rows) { + html += '\n' + for (const cell of row) { + html += ` \n` + } + html += '\n' + } + html += '
${escapeHtml(header)}
${escapeHtml(cell)}
\n' + } + break + + case 'pagebreak': + html += '
\n' + break + } + } + + 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, ''') +} diff --git a/admin-compliance/lib/sdk/einwilligungen/export/pdf.ts b/admin-compliance/lib/sdk/einwilligungen/export/pdf.ts index ff04c4e..845658a 100644 --- a/admin-compliance/lib/sdk/einwilligungen/export/pdf.ts +++ b/admin-compliance/lib/sdk/einwilligungen/export/pdf.ts @@ -12,6 +12,7 @@ import { CATEGORY_METADATA, RETENTION_PERIOD_INFO, } from '../types' +import { generateHTMLFromContent } from './pdf-helpers' // ============================================================================= // TYPES @@ -277,216 +278,6 @@ export async function generatePDFBlob( return new Blob([html], { type: 'text/html' }) } -/** - * Generate printable HTML from PDF content - */ -function generateHTMLFromContent( - content: PDFSection[], - options: PDFExportOptions -): string { - const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in' - const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in' - - let html = ` - - - - - ${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'} - - - -` - - for (const section of content) { - switch (section.type) { - case 'title': - html += `
${escapeHtml(section.content || '')}
\n` - break - - case 'heading': - html += `

${escapeHtml(section.content || '')}

\n` - break - - case 'subheading': - html += `

${escapeHtml(section.content || '')}

\n` - break - - case 'paragraph': - const alignClass = section.style?.align === 'center' ? ' class="center"' : '' - html += `${escapeHtml(section.content || '')}

\n` - break - - case 'list': - html += '
    \n' - for (const item of section.items || []) { - html += `
  • ${escapeHtml(item)}
  • \n` - } - html += '
\n' - break - - case 'table': - if (section.table) { - html += '\n\n' - for (const header of section.table.headers) { - html += ` \n` - } - html += '\n\n' - for (const row of section.table.rows) { - html += '\n' - for (const cell of row) { - html += ` \n` - } - html += '\n' - } - html += '
${escapeHtml(header)}
${escapeHtml(cell)}
\n' - } - break - - case 'pagebreak': - html += '
\n' - break - } - } - - 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, ''') -} - // ============================================================================= // FILENAME GENERATION // ============================================================================= diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections-part2.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections-part2.ts new file mode 100644 index 0000000..cfcbd01 --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections-part2.ts @@ -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 { + const grouped = new Map() + 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() + 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() + 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, + } +} diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts index 689dda2..e183913 100644 --- a/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts +++ b/admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts @@ -1,8 +1,8 @@ /** - * Privacy Policy Section Generators + * Privacy Policy Section Generators (Sections 1-4) * - * Generiert die 9 Abschnitte der Datenschutzerklaerung (DSI) - * aus dem Datenpunktkatalog. + * Generiert die ersten 4 Abschnitte der Datenschutzerklaerung (DSI). + * Sections 5-7 live in privacy-policy-sections-part2.ts. */ import { @@ -19,6 +19,14 @@ import { RETENTION_PERIOD_INFO, } from '../types' +// Re-export sections 5-7 for backward compatibility +export { + generateRecipientsSection, + generateRetentionSection, + generateSpecialCategoriesSection, + generateRightsSection, +} from './privacy-policy-sections-part2' + // ============================================================================= // KONSTANTEN // ============================================================================= @@ -303,257 +311,3 @@ export function generateLegalBasisSection( } } -export function generateRecipientsSection( - dataPoints: DataPoint[], - language: SupportedLanguage -): PrivacyPolicySection { - const title: LocalizedText = { - de: '5. Empfaenger und Datenweitergabe', - en: '5. Recipients and Data Sharing', - } - - const thirdParties = extractThirdParties(dataPoints) - - if (thirdParties.length === 0) { - const content: LocalizedText = { - de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.', - en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.', - } - return { - id: 'recipients', - order: 5, - title, - content, - dataPointIds: [], - isRequired: true, - isGenerated: false, - } - } - - const recipientDetails = new Map() - 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, - } -} diff --git a/admin-compliance/lib/sdk/tom-generator/demo-data/demo-data-part2.ts b/admin-compliance/lib/sdk/tom-generator/demo-data/demo-data-part2.ts new file mode 100644 index 0000000..14f33f9 --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/demo-data/demo-data-part2.ts @@ -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 = { + 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', + }, +] diff --git a/admin-compliance/lib/sdk/tom-generator/demo-data/index.ts b/admin-compliance/lib/sdk/tom-generator/demo-data/index.ts index c047a4b..5de5a41 100644 --- a/admin-compliance/lib/sdk/tom-generator/demo-data/index.ts +++ b/admin-compliance/lib/sdk/tom-generator/demo-data/index.ts @@ -9,13 +9,13 @@ import { DataProfile, ArchitectureProfile, SecurityProfile, - RiskProfile, - EvidenceDocument, - DerivedTOM, - GapAnalysisResult, TOM_GENERATOR_STEPS, } from '../types' import { getTOMRulesEngine } from '../rules-engine' +import { DEMO_RISK_PROFILES, DEMO_EVIDENCE_DOCUMENTS } from './demo-data-part2' + +// Re-export risk profiles and evidence from part2 for backward compatibility +export { DEMO_RISK_PROFILES, DEMO_EVIDENCE_DOCUMENTS } from './demo-data-part2' // ============================================================================= // DEMO COMPANY PROFILES @@ -216,164 +216,6 @@ export const DEMO_SECURITY_PROFILES: Record = { }, } -// ============================================================================= -// DEMO RISK PROFILES -// ============================================================================= - -export const DEMO_RISK_PROFILES: Record = { - 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 // ============================================================================= diff --git a/admin-compliance/lib/sdk/tom-generator/export/docx-helpers.ts b/admin-compliance/lib/sdk/tom-generator/export/docx-helpers.ts new file mode 100644 index 0000000..498016b --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/export/docx-helpers.ts @@ -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 { + const grouped = new Map() + + 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> = { + 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> = { + 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> = { + 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> = { + 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> = { + 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 +): string { + const DEFAULT_COLOR = '#1a56db' + const primaryColor = options.primaryColor || DEFAULT_COLOR + + let html = ` + + + + + + + +` + + for (const element of content) { + if (element.type === 'table' && element.headers && element.rows) { + html += '' + html += '' + for (const header of element.headers) { + html += `` + } + html += '' + for (const row of element.rows) { + html += '' + for (const cell of row.cells) { + html += `` + } + html += '' + } + html += '
${escapeHtml(header)}
${escapeHtml(cell)}
' + } else { + const tag = getHtmlTag(element.type) + const processedContent = processContent(element.content || '') + html += `<${tag}>${processedContent}\n` + } + } + + 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, ''') +} + +export function processContent(content: string): string { + // Convert markdown-style bold to HTML + return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '$1') +} diff --git a/admin-compliance/lib/sdk/tom-generator/export/docx.ts b/admin-compliance/lib/sdk/tom-generator/export/docx.ts index 0daf34a..f4f89c3 100644 --- a/admin-compliance/lib/sdk/tom-generator/export/docx.ts +++ b/admin-compliance/lib/sdk/tom-generator/export/docx.ts @@ -10,6 +10,15 @@ import { CONTROL_CATEGORIES, } from '../types' import { getControlById, getCategoryMetadata } from '../controls/loader' +import { + groupTOMsByCategory, + formatRole, + formatProtectionLevel, + formatType, + formatImplementationStatus, + formatApplicability, + generateHTMLFromContent, +} from './docx-helpers' // ============================================================================= // TYPES @@ -320,78 +329,6 @@ export function generateDOCXContent( return elements } -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -function groupTOMsByCategory( - toms: DerivedTOM[], - includeNotApplicable: boolean -): Map { - const grouped = new Map() - - 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> = { - 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> = { - 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> = { - 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> = { - 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> = { - REQUIRED: { de: 'Erforderlich', en: 'Required' }, - RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' }, - OPTIONAL: { de: 'Optional', en: 'Optional' }, - NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' }, - } - return apps[applicability]?.[language] || applicability -} - // ============================================================================= // DOCX BLOB GENERATION // Uses simple XML structure compatible with docx libraries @@ -421,90 +358,6 @@ export async function generateDOCXBlob( return blob } -function generateHTMLFromContent( - content: DocxElement[], - options: Partial -): string { - const opts = { ...DEFAULT_OPTIONS, ...options } - - let html = ` - - - - - - - -` - - for (const element of content) { - if (element.type === 'table') { - html += '' - html += '' - for (const header of element.headers) { - html += `` - } - html += '' - for (const row of element.rows) { - html += '' - for (const cell of row.cells) { - html += `` - } - html += '' - } - html += '
${escapeHtml(header)}
${escapeHtml(cell)}
' - } else { - const tag = getHtmlTag(element.type) - const processedContent = processContent(element.content) - html += `<${tag}>${processedContent}\n` - } - } - - 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, ''') -} - -function processContent(content: string): string { - // Convert markdown-style bold to HTML - return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '$1') -} - // ============================================================================= // FILENAME GENERATION // ============================================================================= diff --git a/admin-compliance/lib/sdk/tom-generator/export/pdf-helpers.ts b/admin-compliance/lib/sdk/tom-generator/export/pdf-helpers.ts new file mode 100644 index 0000000..f93ee05 --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/export/pdf-helpers.ts @@ -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> = { + 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> = { + 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> = { + 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> = { + 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> = { + 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) +} diff --git a/admin-compliance/lib/sdk/tom-generator/export/pdf.ts b/admin-compliance/lib/sdk/tom-generator/export/pdf.ts index 1facd83..c4cfb80 100644 --- a/admin-compliance/lib/sdk/tom-generator/export/pdf.ts +++ b/admin-compliance/lib/sdk/tom-generator/export/pdf.ts @@ -9,6 +9,14 @@ import { CONTROL_CATEGORIES, } from '../types' import { getControlById } from '../controls/loader' +import { + generateCategorySummary, + formatProtectionLevel, + formatType, + formatImplementationStatus, + formatApplicability, + getCIAMeaning, +} from './pdf-helpers' // ============================================================================= // TYPES @@ -369,85 +377,6 @@ export function generatePDFContent( return sections } -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -function generateCategorySummary( - toms: DerivedTOM[], - opts: PDFExportOptions -): string[][] { - const summary: string[][] = [] - - for (const category of CONTROL_CATEGORIES) { - const categoryTOMs = toms.filter((tom) => { - const control = getControlById(tom.controlId) - return control?.category === category.id - }) - - if (categoryTOMs.length === 0) continue - - const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length - const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length - - summary.push([ - category.name[opts.language], - String(categoryTOMs.length), - String(required), - String(implemented), - ]) - } - - return summary -} - -function formatProtectionLevel(level: string, language: 'de' | 'en'): string { - const levels: Record> = { - 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> = { - 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> = { - 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> = { - 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> = { - 1: { de: 'Sehr gering', en: 'Very Low' }, - 2: { de: 'Gering', en: 'Low' }, - 3: { de: 'Mittel', en: 'Medium' }, - 4: { de: 'Hoch', en: 'High' }, - 5: { de: 'Sehr hoch', en: 'Very High' }, - } - return meanings[rating]?.[language] || String(rating) -} - // ============================================================================= // PDF BLOB GENERATION // Note: For production, use jspdf or pdfmake library diff --git a/admin-compliance/lib/sdk/tom-generator/export/zip-helpers.ts b/admin-compliance/lib/sdk/tom-generator/export/zip-helpers.ts new file mode 100644 index 0000000..322472e --- /dev/null +++ b/admin-compliance/lib/sdk/tom-generator/export/zip-helpers.ts @@ -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 { + const grouped = new Map() + + 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 { + 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 { + const grouped = new Map() + + 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> = { + NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' }, + PARTIAL: { de: 'Teilweise', en: 'Partial' }, + IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' }, + } + return statuses[status]?.[lang] || status +} diff --git a/admin-compliance/lib/sdk/tom-generator/export/zip.ts b/admin-compliance/lib/sdk/tom-generator/export/zip.ts index 876ecf2..21cc4bc 100644 --- a/admin-compliance/lib/sdk/tom-generator/export/zip.ts +++ b/admin-compliance/lib/sdk/tom-generator/export/zip.ts @@ -6,7 +6,16 @@ import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types' import { generateDOCXContent, DOCXExportOptions } from './docx' import { generatePDFContent, PDFExportOptions } from './pdf' -import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader' +import { getAllControls, getLibraryMetadata } from '../controls/loader' +import { + generateReadme, + groupTOMsByCategory, + generateImplementationSummary, + groupEvidenceByControl, + generateRecommendationsMarkdown, + generateMarkdownSummary, + generateCSV, +} from './zip-helpers' // ============================================================================= // TYPES @@ -267,217 +276,6 @@ export function generateZIPFiles( return files } -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -function generateReadme( - state: TOMGeneratorState, - opts: ZIPExportOptions -): string { - const date = new Date().toISOString().split('T')[0] - const lang = opts.language - - return `# TOM Export Package - -${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date} -${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'} - -## ${lang === 'de' ? 'Inhalt' : 'Contents'} - -### /data -- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'} -- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'} -- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'} -- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'} - -### /reference -- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'} - -### /documents -- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'} -- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'} - -## ${lang === 'de' ? 'Statistiken' : 'Statistics'} - -- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length} -- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} -- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} -- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'} -${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''} - ---- - -${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'} -` -} - -function groupTOMsByCategory( - toms: DerivedTOM[] -): Map { - const grouped = new Map() - - 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 { - 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 { - const grouped = new Map() - - 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> = { - NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' }, - PARTIAL: { de: 'Teilweise', en: 'Partial' }, - IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' }, - } - return statuses[status]?.[lang] || status -} - // ============================================================================= // ZIP BLOB GENERATION // Note: For production, use jszip library diff --git a/admin-compliance/lib/sdk/vendor-compliance/context-actions.tsx b/admin-compliance/lib/sdk/vendor-compliance/context-actions.tsx new file mode 100644 index 0000000..17d8cc1 --- /dev/null +++ b/admin-compliance/lib/sdk/vendor-compliance/context-actions.tsx @@ -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 +) { + 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 + ): Promise => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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, + } +} diff --git a/admin-compliance/lib/sdk/vendor-compliance/context.tsx b/admin-compliance/lib/sdk/vendor-compliance/context.tsx index ea415b7..6ef2790 100644 --- a/admin-compliance/lib/sdk/vendor-compliance/context.tsx +++ b/admin-compliance/lib/sdk/vendor-compliance/context.tsx @@ -5,11 +5,11 @@ import React, { useMemo, useEffect, useState, + useContext, } from 'react' import { VendorComplianceContextValue, - ProcessingActivity, VendorStatistics, ComplianceStatistics, RiskOverview, @@ -24,6 +24,7 @@ import { import { initialState, vendorComplianceReducer } from './reducer' import { VendorComplianceContext } from './hooks' import { useVendorComplianceActions } from './use-actions' +import { useContextApiActions } from './context-actions' // Re-export hooks and selectors for barrel export { @@ -201,243 +202,19 @@ export function VendorComplianceProvider({ }, [state.vendors, state.findings]) // ========================================== - // API CALLS + // API CALLS (extracted to context-actions.tsx) // ========================================== - const apiBase = '/api/sdk/v1/vendor-compliance' - - const loadData = useCallback(async () => { - dispatch({ type: 'SET_LOADING', payload: true }) - dispatch({ type: 'SET_ERROR', payload: null }) - - try { - const [ - activitiesRes, - vendorsRes, - contractsRes, - findingsRes, - controlsRes, - controlInstancesRes, - ] = await Promise.all([ - fetch(`${apiBase}/processing-activities`), - fetch(`${apiBase}/vendors`), - fetch(`${apiBase}/contracts`), - fetch(`${apiBase}/findings`), - fetch(`${apiBase}/controls`), - fetch(`${apiBase}/control-instances`), - ]) - - if (activitiesRes.ok) { - const data = await activitiesRes.json() - dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] }) - } - - if (vendorsRes.ok) { - const data = await vendorsRes.json() - dispatch({ type: 'SET_VENDORS', payload: data.data || [] }) - } - - if (contractsRes.ok) { - const data = await contractsRes.json() - dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] }) - } - - if (findingsRes.ok) { - const data = await findingsRes.json() - dispatch({ type: 'SET_FINDINGS', payload: data.data || [] }) - } - - if (controlsRes.ok) { - const data = await controlsRes.json() - dispatch({ type: 'SET_CONTROLS', payload: data.data || [] }) - } - - if (controlInstancesRes.ok) { - const data = await controlInstancesRes.json() - dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] }) - } - } catch (error) { - console.error('Failed to load vendor compliance data:', error) - dispatch({ - type: 'SET_ERROR', - payload: 'Fehler beim Laden der Daten', - }) - } finally { - dispatch({ type: 'SET_LOADING', payload: false }) - } - }, [apiBase]) - - const refresh = useCallback(async () => { - await loadData() - }, [loadData]) - - // ========================================== - // PROCESSING ACTIVITIES ACTIONS - // ========================================== - - const createProcessingActivity = useCallback( - async ( - data: Omit - ): Promise => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - dispatch({ - type: 'UPDATE_CONTRACT', - payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } }, - }) - - const response = await fetch(`${apiBase}/contracts/${contractId}/review`, { - method: 'POST', - }) - - if (!response.ok) { - dispatch({ - type: 'UPDATE_CONTRACT', - payload: { id: contractId, data: { reviewStatus: 'FAILED' } }, - }) - const error = await response.json() - throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung') - } - - const result = await response.json() - - // Update contract with review results - dispatch({ - type: 'UPDATE_CONTRACT', - payload: { - id: contractId, - data: { - reviewStatus: 'COMPLETED', - reviewCompletedAt: new Date(), - complianceScore: result.data.complianceScore, - }, - }, - }) - - // Add findings - if (result.data.findings && result.data.findings.length > 0) { - dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings }) - } - }, - [apiBase] - ) + const { + loadData, + refresh, + createProcessingActivity, + deleteProcessingActivity, + duplicateProcessingActivity, + deleteVendor, + deleteContract, + startContractReview, + } = useContextApiActions(state, dispatch) // ========================================== // INITIALIZATION