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'
+ 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 += ` | ${escapeHtml(header)} | \n`
+ }
+ html += '
\n\n'
+ for (const row of section.table.rows) {
+ html += '\n'
+ for (const cell of row) {
+ html += ` | ${escapeHtml(cell)} | \n`
+ }
+ html += '
\n'
+ }
+ html += '
\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 += ` | ${escapeHtml(header)} | \n`
- }
- html += '
\n\n'
- for (const row of section.table.rows) {
- html += '\n'
- for (const cell of row) {
- html += ` | ${escapeHtml(cell)} | \n`
- }
- html += '
\n'
- }
- html += '
\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 += `| ${escapeHtml(header)} | `
+ }
+ html += '
'
+ for (const row of element.rows) {
+ html += ''
+ for (const cell of row.cells) {
+ html += `| ${escapeHtml(cell)} | `
+ }
+ html += '
'
+ }
+ html += '
'
+ } else {
+ const tag = getHtmlTag(element.type)
+ const processedContent = processContent(element.content || '')
+ html += `<${tag}>${processedContent}${tag}>\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 += `| ${escapeHtml(header)} | `
- }
- html += '
'
- for (const row of element.rows) {
- html += ''
- for (const cell of row.cells) {
- html += `| ${escapeHtml(cell)} | `
- }
- html += '
'
- }
- html += '
'
- } else {
- const tag = getHtmlTag(element.type)
- const processedContent = processContent(element.content)
- html += `<${tag}>${processedContent}${tag}>\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