refactor(admin): split 8 oversized lib/ files into focused modules under 500 LOC
Split these files that exceeded the 500-line hard cap: - privacy-policy.ts (965 LOC) -> sections + renderers - academy/api.ts (787 LOC) -> courses + mock-data - whistleblower/api.ts (755 LOC) -> operations + mock-data - vvt-profiling.ts (659 LOC) -> data + logic - cookie-banner.ts (595 LOC) -> config + embed - dsr/types.ts (581 LOC) -> core + api types - tom-generator/rules-engine.ts (560 LOC) -> evaluator + gap-analysis - datapoint-helpers.ts (548 LOC) -> generators + validators Each original file becomes a barrel re-export for backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Cookie Banner — Configuration & Category Generation
|
||||
*
|
||||
* Default texts, styling, and category generation from data points.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CookieBannerCategory,
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerTexts,
|
||||
CookieInfo,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEFAULT CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
|
||||
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
|
||||
},
|
||||
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
||||
rejectAll: { de: 'Nur notwendige', en: 'Essential Only' },
|
||||
customize: { de: 'Einstellungen', en: 'Customize' },
|
||||
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
||||
privacyPolicyLink: {
|
||||
de: 'Mehr in unserer Datenschutzerklaerung',
|
||||
en: 'More in our Privacy Policy',
|
||||
},
|
||||
}
|
||||
|
||||
export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: '#f1f5f9',
|
||||
textColor: '#1e293b',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
maxWidth: 480,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getExpiryFromRetention(retention: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'24_HOURS': '24 Stunden / 24 hours',
|
||||
'30_DAYS': '30 Tage / 30 days',
|
||||
'90_DAYS': '90 Tage / 90 days',
|
||||
'12_MONTHS': '1 Jahr / 1 year',
|
||||
'24_MONTHS': '2 Jahre / 2 years',
|
||||
'36_MONTHS': '3 Jahre / 3 years',
|
||||
'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
|
||||
'UNTIL_PURPOSE_FULFILLED': 'Session',
|
||||
'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
|
||||
}
|
||||
return mapping[retention] || 'Session'
|
||||
}
|
||||
|
||||
export function generateCookieCategories(
|
||||
dataPoints: DataPoint[]
|
||||
): CookieBannerCategory[] {
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
|
||||
const categoryDataPoints = cookieDataPoints.filter(
|
||||
(dp) => dp.cookieCategory === defaultCat.id
|
||||
)
|
||||
|
||||
const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
|
||||
name: dp.code,
|
||||
provider: 'First Party',
|
||||
purpose: dp.purpose,
|
||||
expiry: getExpiryFromRetention(dp.retentionPeriod),
|
||||
type: 'FIRST_PARTY',
|
||||
}))
|
||||
|
||||
return {
|
||||
...defaultCat,
|
||||
dataPointIds: categoryDataPoints.map((dp) => dp.id),
|
||||
cookies,
|
||||
}
|
||||
}).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
|
||||
}
|
||||
|
||||
export function generateCookieBannerConfig(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
customTexts?: Partial<CookieBannerTexts>,
|
||||
customStyling?: Partial<CookieBannerStyling>
|
||||
): CookieBannerConfig {
|
||||
const categories = generateCookieCategories(dataPoints)
|
||||
|
||||
return {
|
||||
id: `cookie-banner-${tenantId}`,
|
||||
tenantId,
|
||||
categories,
|
||||
styling: { ...DEFAULT_COOKIE_BANNER_STYLING, ...customStyling },
|
||||
texts: { ...DEFAULT_COOKIE_BANNER_TEXTS, ...customTexts },
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Cookie Banner — Embed Code Generation (CSS, HTML, JS)
|
||||
*
|
||||
* Generates the embeddable cookie banner code from configuration.
|
||||
*/
|
||||
|
||||
import {
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerEmbedCode,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// MAIN EXPORT
|
||||
// =============================================================================
|
||||
|
||||
export function generateEmbedCode(
|
||||
config: CookieBannerConfig,
|
||||
privacyPolicyUrl: string = '/datenschutz'
|
||||
): CookieBannerEmbedCode {
|
||||
const css = generateCSS(config.styling)
|
||||
const html = generateHTML(config, privacyPolicyUrl)
|
||||
const js = generateJS(config)
|
||||
|
||||
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
|
||||
|
||||
return { html, css, js, scriptTag }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CSS GENERATION
|
||||
// =============================================================================
|
||||
|
||||
function generateCSS(styling: CookieBannerStyling): string {
|
||||
const positionStyles: Record<string, string> = {
|
||||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||||
TOP: 'top: 0; left: 0; right: 0;',
|
||||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
||||
}
|
||||
|
||||
const isDark = styling.theme === 'DARK'
|
||||
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
|
||||
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
|
||||
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||||
|
||||
return `
|
||||
/* Cookie Banner Styles */
|
||||
.cookie-banner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 9998;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
${positionStyles[styling.position]}
|
||||
z-index: 9999;
|
||||
background: ${bgColor};
|
||||
color: ${textColor};
|
||||
border-radius: ${styling.borderRadius || 12}px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
max-width: ${styling.maxWidth}px;
|
||||
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cookie-banner-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 12px 20px;
|
||||
border-radius: ${(styling.borderRadius || 12) / 2}px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary {
|
||||
background: ${styling.primaryColor};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary {
|
||||
background: ${styling.secondaryColor || borderColor};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.cookie-banner-link {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: ${styling.primaryColor};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cookie-banner-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Category Details */
|
||||
.cookie-banner-details {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid ${borderColor};
|
||||
padding-top: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cookie-banner-details.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cookie-banner-category {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
.cookie-banner-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cookie-banner-category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-category-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cookie-banner-category-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
background: ${borderColor};
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active {
|
||||
background: ${styling.primaryColor};
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active::after {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cookie-banner {
|
||||
margin: 0;
|
||||
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTML GENERATION
|
||||
// =============================================================================
|
||||
|
||||
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
|
||||
const categoriesHTML = config.categories
|
||||
.map((cat) => {
|
||||
const isRequired = cat.isRequired
|
||||
return `
|
||||
<div class="cookie-banner-category" data-category="${cat.id}">
|
||||
<div class="cookie-banner-category-info">
|
||||
<div class="cookie-banner-category-name">${cat.name.de}</div>
|
||||
<div class="cookie-banner-category-desc">${cat.description.de}</div>
|
||||
</div>
|
||||
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
|
||||
data-category="${cat.id}"
|
||||
data-required="${isRequired}"></div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
|
||||
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
|
||||
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
|
||||
<div class="cookie-banner-description">${config.texts.description.de}</div>
|
||||
|
||||
<div class="cookie-banner-buttons">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
|
||||
${config.texts.rejectAll.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
|
||||
${config.texts.customize.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
|
||||
${config.texts.acceptAll.de}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cookie-banner-details" id="cookieBannerDetails">
|
||||
${categoriesHTML}
|
||||
<div class="cookie-banner-buttons" style="margin-top: 16px;">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
|
||||
${config.texts.save.de}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
|
||||
${config.texts.privacyPolicyLink.de}
|
||||
</a>
|
||||
</div>
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JS GENERATION
|
||||
// =============================================================================
|
||||
|
||||
function generateJS(config: CookieBannerConfig): string {
|
||||
const categoryIds = config.categories.map((c) => c.id)
|
||||
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
|
||||
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const COOKIE_NAME = 'cookie_consent';
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
const CATEGORIES = ${JSON.stringify(categoryIds)};
|
||||
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
|
||||
|
||||
function getConsent() {
|
||||
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveConsent(consent) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
|
||||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
|
||||
';expires=' + date.toUTCString() +
|
||||
';path=/;SameSite=Lax';
|
||||
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
|
||||
}
|
||||
|
||||
function hasConsent(category) {
|
||||
const consent = getConsent();
|
||||
if (!consent) return REQUIRED_CATEGORIES.includes(category);
|
||||
return consent[category] === true;
|
||||
}
|
||||
|
||||
function initBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
const details = document.getElementById('cookieBannerDetails');
|
||||
|
||||
if (!banner) return;
|
||||
|
||||
const consent = getConsent();
|
||||
if (consent) return;
|
||||
|
||||
setTimeout(() => {
|
||||
banner.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
}, 500);
|
||||
|
||||
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = true);
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
|
||||
details.classList.toggle('active');
|
||||
});
|
||||
|
||||
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => {
|
||||
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
|
||||
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
|
||||
});
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
|
||||
if (toggle.dataset.required === 'true') return;
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
overlay?.addEventListener('click', () => {
|
||||
// Don't close - user must make a choice
|
||||
});
|
||||
}
|
||||
|
||||
function closeBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
banner?.classList.remove('active');
|
||||
overlay?.classList.remove('active');
|
||||
}
|
||||
|
||||
window.CookieConsent = {
|
||||
getConsent,
|
||||
saveConsent,
|
||||
hasConsent,
|
||||
show: () => {
|
||||
document.getElementById('cookieBanner')?.classList.add('active');
|
||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||||
},
|
||||
hide: closeBanner
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBanner);
|
||||
} else {
|
||||
initBanner();
|
||||
}
|
||||
})();
|
||||
`.trim()
|
||||
}
|
||||
@@ -1,595 +1,18 @@
|
||||
/**
|
||||
* Cookie Banner Generator
|
||||
* Cookie Banner Generator — barrel re-export
|
||||
*
|
||||
* Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog.
|
||||
* Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet.
|
||||
* Split into:
|
||||
* - cookie-banner-config.ts (defaults, category generation, config builder)
|
||||
* - cookie-banner-embed.ts (CSS, HTML, JS embed code generation)
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CookieCategory,
|
||||
CookieBannerCategory,
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerTexts,
|
||||
CookieBannerEmbedCode,
|
||||
CookieInfo,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Holt den lokalisierten Text
|
||||
*/
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COOKIE BANNER CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Standard Cookie Banner Texte
|
||||
*/
|
||||
export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
|
||||
title: {
|
||||
de: 'Cookie-Einstellungen',
|
||||
en: 'Cookie Settings',
|
||||
},
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
|
||||
},
|
||||
acceptAll: {
|
||||
de: 'Alle akzeptieren',
|
||||
en: 'Accept All',
|
||||
},
|
||||
rejectAll: {
|
||||
de: 'Nur notwendige',
|
||||
en: 'Essential Only',
|
||||
},
|
||||
customize: {
|
||||
de: 'Einstellungen',
|
||||
en: 'Customize',
|
||||
},
|
||||
save: {
|
||||
de: 'Auswahl speichern',
|
||||
en: 'Save Selection',
|
||||
},
|
||||
privacyPolicyLink: {
|
||||
de: 'Mehr in unserer Datenschutzerklaerung',
|
||||
en: 'More in our Privacy Policy',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Styling fuer Cookie Banner
|
||||
*/
|
||||
export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1', // Indigo
|
||||
secondaryColor: '#f1f5f9', // Slate-100
|
||||
textColor: '#1e293b', // Slate-800
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
maxWidth: 480,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert Cookie-Banner Kategorien aus Datenpunkten
|
||||
*/
|
||||
export function generateCookieCategories(
|
||||
dataPoints: DataPoint[]
|
||||
): CookieBannerCategory[] {
|
||||
// Filtere nur Datenpunkte mit Cookie-Kategorie
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
// Erstelle die Kategorien basierend auf den Defaults
|
||||
return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
|
||||
// Filtere die Datenpunkte fuer diese Kategorie
|
||||
const categoryDataPoints = cookieDataPoints.filter(
|
||||
(dp) => dp.cookieCategory === defaultCat.id
|
||||
)
|
||||
|
||||
// Erstelle Cookie-Infos aus den Datenpunkten
|
||||
const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
|
||||
name: dp.code,
|
||||
provider: 'First Party',
|
||||
purpose: dp.purpose,
|
||||
expiry: getExpiryFromRetention(dp.retentionPeriod),
|
||||
type: 'FIRST_PARTY',
|
||||
}))
|
||||
|
||||
return {
|
||||
...defaultCat,
|
||||
dataPointIds: categoryDataPoints.map((dp) => dp.id),
|
||||
cookies,
|
||||
}
|
||||
}).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert Retention Period zu Cookie-Expiry String
|
||||
*/
|
||||
function getExpiryFromRetention(retention: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'24_HOURS': '24 Stunden / 24 hours',
|
||||
'30_DAYS': '30 Tage / 30 days',
|
||||
'90_DAYS': '90 Tage / 90 days',
|
||||
'12_MONTHS': '1 Jahr / 1 year',
|
||||
'24_MONTHS': '2 Jahre / 2 years',
|
||||
'36_MONTHS': '3 Jahre / 3 years',
|
||||
'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
|
||||
'UNTIL_PURPOSE_FULFILLED': 'Session',
|
||||
'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
|
||||
}
|
||||
return mapping[retention] || 'Session'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die vollstaendige Cookie Banner Konfiguration
|
||||
*/
|
||||
export function generateCookieBannerConfig(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
customTexts?: Partial<CookieBannerTexts>,
|
||||
customStyling?: Partial<CookieBannerStyling>
|
||||
): CookieBannerConfig {
|
||||
const categories = generateCookieCategories(dataPoints)
|
||||
|
||||
return {
|
||||
id: `cookie-banner-${tenantId}`,
|
||||
tenantId,
|
||||
categories,
|
||||
styling: {
|
||||
...DEFAULT_COOKIE_BANNER_STYLING,
|
||||
...customStyling,
|
||||
},
|
||||
texts: {
|
||||
...DEFAULT_COOKIE_BANNER_TEXTS,
|
||||
...customTexts,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMBED CODE GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert den Embed-Code fuer den Cookie Banner
|
||||
*/
|
||||
export function generateEmbedCode(
|
||||
config: CookieBannerConfig,
|
||||
privacyPolicyUrl: string = '/datenschutz'
|
||||
): CookieBannerEmbedCode {
|
||||
const css = generateCSS(config.styling)
|
||||
const html = generateHTML(config, privacyPolicyUrl)
|
||||
const js = generateJS(config)
|
||||
|
||||
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
|
||||
|
||||
return {
|
||||
html,
|
||||
css,
|
||||
js,
|
||||
scriptTag,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das CSS fuer den Cookie Banner
|
||||
*/
|
||||
function generateCSS(styling: CookieBannerStyling): string {
|
||||
const positionStyles: Record<string, string> = {
|
||||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||||
TOP: 'top: 0; left: 0; right: 0;',
|
||||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
||||
}
|
||||
|
||||
const isDark = styling.theme === 'DARK'
|
||||
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
|
||||
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
|
||||
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||||
|
||||
return `
|
||||
/* Cookie Banner Styles */
|
||||
.cookie-banner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 9998;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
${positionStyles[styling.position]}
|
||||
z-index: 9999;
|
||||
background: ${bgColor};
|
||||
color: ${textColor};
|
||||
border-radius: ${styling.borderRadius || 12}px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
max-width: ${styling.maxWidth}px;
|
||||
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cookie-banner-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 12px 20px;
|
||||
border-radius: ${(styling.borderRadius || 12) / 2}px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary {
|
||||
background: ${styling.primaryColor};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary {
|
||||
background: ${styling.secondaryColor || borderColor};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.cookie-banner-link {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: ${styling.primaryColor};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cookie-banner-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Category Details */
|
||||
.cookie-banner-details {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid ${borderColor};
|
||||
padding-top: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cookie-banner-details.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cookie-banner-category {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
.cookie-banner-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cookie-banner-category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-category-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cookie-banner-category-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
background: ${borderColor};
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active {
|
||||
background: ${styling.primaryColor};
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active::after {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cookie-banner {
|
||||
margin: 0;
|
||||
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das HTML fuer den Cookie Banner
|
||||
*/
|
||||
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
|
||||
const categoriesHTML = config.categories
|
||||
.map((cat) => {
|
||||
const isRequired = cat.isRequired
|
||||
return `
|
||||
<div class="cookie-banner-category" data-category="${cat.id}">
|
||||
<div class="cookie-banner-category-info">
|
||||
<div class="cookie-banner-category-name">${cat.name.de}</div>
|
||||
<div class="cookie-banner-category-desc">${cat.description.de}</div>
|
||||
</div>
|
||||
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
|
||||
data-category="${cat.id}"
|
||||
data-required="${isRequired}"></div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
|
||||
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
|
||||
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
|
||||
<div class="cookie-banner-description">${config.texts.description.de}</div>
|
||||
|
||||
<div class="cookie-banner-buttons">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
|
||||
${config.texts.rejectAll.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
|
||||
${config.texts.customize.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
|
||||
${config.texts.acceptAll.de}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cookie-banner-details" id="cookieBannerDetails">
|
||||
${categoriesHTML}
|
||||
<div class="cookie-banner-buttons" style="margin-top: 16px;">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
|
||||
${config.texts.save.de}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
|
||||
${config.texts.privacyPolicyLink.de}
|
||||
</a>
|
||||
</div>
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das JavaScript fuer den Cookie Banner
|
||||
*/
|
||||
function generateJS(config: CookieBannerConfig): string {
|
||||
const categoryIds = config.categories.map((c) => c.id)
|
||||
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
|
||||
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const COOKIE_NAME = 'cookie_consent';
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
const CATEGORIES = ${JSON.stringify(categoryIds)};
|
||||
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
|
||||
|
||||
// Get consent from cookie
|
||||
function getConsent() {
|
||||
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save consent to cookie
|
||||
function saveConsent(consent) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
|
||||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
|
||||
';expires=' + date.toUTCString() +
|
||||
';path=/;SameSite=Lax';
|
||||
|
||||
// Dispatch event
|
||||
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
|
||||
}
|
||||
|
||||
// Check if category is consented
|
||||
function hasConsent(category) {
|
||||
const consent = getConsent();
|
||||
if (!consent) return REQUIRED_CATEGORIES.includes(category);
|
||||
return consent[category] === true;
|
||||
}
|
||||
|
||||
// Initialize banner
|
||||
function initBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
const details = document.getElementById('cookieBannerDetails');
|
||||
|
||||
if (!banner) return;
|
||||
|
||||
const consent = getConsent();
|
||||
if (consent) {
|
||||
// User has already consented
|
||||
return;
|
||||
}
|
||||
|
||||
// Show banner
|
||||
setTimeout(() => {
|
||||
banner.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
}, 500);
|
||||
|
||||
// Accept all
|
||||
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = true);
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Reject all (only essential)
|
||||
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Customize
|
||||
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
|
||||
details.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Save selection
|
||||
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => {
|
||||
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
|
||||
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
|
||||
});
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Toggle handlers
|
||||
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
|
||||
if (toggle.dataset.required === 'true') return;
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay?.addEventListener('click', () => {
|
||||
// Don't close - user must make a choice
|
||||
});
|
||||
}
|
||||
|
||||
function closeBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
banner?.classList.remove('active');
|
||||
overlay?.classList.remove('active');
|
||||
}
|
||||
|
||||
// Expose API
|
||||
window.CookieConsent = {
|
||||
getConsent,
|
||||
saveConsent,
|
||||
hasConsent,
|
||||
show: () => {
|
||||
document.getElementById('cookieBanner')?.classList.add('active');
|
||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||||
},
|
||||
hide: closeBanner
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBanner);
|
||||
} else {
|
||||
initBanner();
|
||||
}
|
||||
})();
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// Note: All exports are defined inline with 'export const' and 'export function'
|
||||
export {
|
||||
DEFAULT_COOKIE_BANNER_TEXTS,
|
||||
DEFAULT_COOKIE_BANNER_STYLING,
|
||||
generateCookieCategories,
|
||||
generateCookieBannerConfig,
|
||||
} from './cookie-banner-config'
|
||||
|
||||
export {
|
||||
generateEmbedCode,
|
||||
} from './cookie-banner-embed'
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Privacy Policy Renderers & Main Generator
|
||||
*
|
||||
* Cookies section, changes section, rendering (HTML/Markdown),
|
||||
* and the main generatePrivacyPolicy entry point.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
GeneratedPrivacyPolicy,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
import { RETENTION_MATRIX } from '../catalog/loader'
|
||||
|
||||
import {
|
||||
formatDate,
|
||||
generateControllerSection,
|
||||
generateDataCollectionSection,
|
||||
generatePurposesSection,
|
||||
generateLegalBasisSection,
|
||||
generateRecipientsSection,
|
||||
generateRetentionSection,
|
||||
generateSpecialCategoriesSection,
|
||||
generateRightsSection,
|
||||
} from './privacy-policy-sections'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER
|
||||
// =============================================================================
|
||||
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS (cookies + changes)
|
||||
// =============================================================================
|
||||
|
||||
export function generateCookiesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '8. Cookies und aehnliche Technologien',
|
||||
en: '8. Cookies and Similar Technologies',
|
||||
}
|
||||
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
if (cookieDataPoints.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
||||
en: 'We do not use cookies on this website.',
|
||||
}
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: false,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
||||
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
||||
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
||||
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
||||
|
||||
const sections: string[] = []
|
||||
|
||||
if (essential.length > 0) {
|
||||
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
||||
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (performance.length > 0) {
|
||||
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
||||
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (personalization.length > 0) {
|
||||
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
||||
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (externalMedia.length > 0) {
|
||||
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
||||
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
||||
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateChangesSection(
|
||||
version: string,
|
||||
date: Date,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
||||
en: '9. Changes to this Privacy Policy',
|
||||
}
|
||||
|
||||
const formattedDate = formatDate(date, language)
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
||||
|
||||
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
||||
|
||||
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
||||
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
||||
|
||||
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
||||
|
||||
The new privacy policy will then apply for your next visit.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'changes',
|
||||
order: 9,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function generatePrivacyPolicySections(
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
version: string = '1.0.0'
|
||||
): PrivacyPolicySection[] {
|
||||
const now = new Date()
|
||||
|
||||
const sections: PrivacyPolicySection[] = [
|
||||
generateControllerSection(companyInfo, language),
|
||||
generateDataCollectionSection(dataPoints, language),
|
||||
generatePurposesSection(dataPoints, language),
|
||||
generateLegalBasisSection(dataPoints, language),
|
||||
generateRecipientsSection(dataPoints, language),
|
||||
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
||||
]
|
||||
|
||||
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
||||
if (specialCategoriesSection) {
|
||||
sections.push(specialCategoriesSection)
|
||||
}
|
||||
|
||||
sections.push(
|
||||
generateRightsSection(language),
|
||||
generateCookiesSection(dataPoints, language),
|
||||
generateChangesSection(version, now, language)
|
||||
)
|
||||
|
||||
sections.forEach((section, index) => {
|
||||
section.order = index + 1
|
||||
const titleDe = section.title.de
|
||||
const titleEn = section.title.en
|
||||
if (titleDe.match(/^\d+[a-z]?\./)) {
|
||||
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
if (titleEn.match(/^\d+[a-z]?\./)) {
|
||||
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
export function generatePrivacyPolicy(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat = 'HTML'
|
||||
): GeneratedPrivacyPolicy {
|
||||
const version = '1.0.0'
|
||||
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
||||
const content = renderPrivacyPolicy(sections, language, format)
|
||||
|
||||
return {
|
||||
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
||||
tenantId,
|
||||
language,
|
||||
sections,
|
||||
companyInfo,
|
||||
generatedAt: new Date(),
|
||||
version,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RENDERERS
|
||||
// =============================================================================
|
||||
|
||||
function renderPrivacyPolicy(
|
||||
sections: PrivacyPolicySection[],
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'HTML':
|
||||
return renderAsHTML(sections, language)
|
||||
case 'MARKDOWN':
|
||||
return renderAsMarkdown(sections, language)
|
||||
default:
|
||||
return renderAsMarkdown(sections, language)
|
||||
}
|
||||
}
|
||||
|
||||
export function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsHTML = sections
|
||||
.map((section) => {
|
||||
const content = t(section.content, language)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/### (.+)/g, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
||||
|
||||
return `
|
||||
<section id="${section.id}">
|
||||
<h2>${t(section.title, language)}</h2>
|
||||
<p>${content}</p>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
||||
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
||||
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
||||
p { margin: 1rem 0; }
|
||||
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
||||
li { margin: 0.5rem 0; }
|
||||
strong { font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
${sectionsHTML}
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
export function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsMarkdown = sections
|
||||
.map((section) => {
|
||||
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
||||
})
|
||||
.join('\n\n---\n\n')
|
||||
|
||||
return `# ${title}\n\n${sectionsMarkdown}`
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
/**
|
||||
* Privacy Policy Section Generators
|
||||
*
|
||||
* Generiert die 9 Abschnitte der Datenschutzerklaerung (DSI)
|
||||
* aus dem Datenpunktkatalog.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
SupportedLanguage,
|
||||
LocalizedText,
|
||||
RetentionMatrixEntry,
|
||||
LegalBasis,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// KONSTANTEN
|
||||
// =============================================================================
|
||||
|
||||
const ALL_CATEGORIES: DataPointCategory[] = [
|
||||
'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT',
|
||||
'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION',
|
||||
'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA',
|
||||
'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA',
|
||||
'AI_DATA', 'SECURITY',
|
||||
]
|
||||
|
||||
const ALL_LEGAL_BASES: LegalBasis[] = [
|
||||
'CONTRACT', 'CONSENT', 'EXPLICIT_CONSENT', 'LEGITIMATE_INTEREST',
|
||||
'LEGAL_OBLIGATION', 'VITAL_INTERESTS', 'PUBLIC_INTEREST',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
|
||||
const grouped = new Map<LegalBasis, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.legalBasis) || []
|
||||
grouped.set(dp.legalBasis, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
||||
const thirdParties = new Set<string>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
thirdParties.add(recipient)
|
||||
}
|
||||
}
|
||||
return Array.from(thirdParties).sort()
|
||||
}
|
||||
|
||||
export function formatDate(date: Date, language: SupportedLanguage): string {
|
||||
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS
|
||||
// =============================================================================
|
||||
|
||||
export function generateControllerSection(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '1. Verantwortlicher',
|
||||
en: '1. Data Controller',
|
||||
}
|
||||
|
||||
const dpoSection = companyInfo.dpoName
|
||||
? language === 'de'
|
||||
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
|
||||
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
|
||||
: ''
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
en: `The controller responsible for data processing on this website is:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'controller',
|
||||
order: 1,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateDataCollectionSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '2. Erhobene personenbezogene Daten',
|
||||
en: '2. Personal Data We Collect',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const categoryData = grouped.get(category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
if (!categoryMeta) continue
|
||||
|
||||
const categoryTitle = t(categoryMeta.name, language)
|
||||
|
||||
let categoryNote = ''
|
||||
if (category === 'HEALTH_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
|
||||
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
|
||||
} else if (category === 'EMPLOYEE_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
|
||||
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
|
||||
} else if (category === 'AI_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
|
||||
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
|
||||
}
|
||||
|
||||
const dataList = categoryData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
|
||||
en: 'We collect and process the following personal data:',
|
||||
}
|
||||
|
||||
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
|
||||
? {
|
||||
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
|
||||
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
|
||||
}
|
||||
: { de: '', en: '' }
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'data-collection',
|
||||
order: 2,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePurposesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '3. Zwecke der Datenverarbeitung',
|
||||
en: '3. Purposes of Data Processing',
|
||||
}
|
||||
|
||||
const purposes = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const purpose = t(dp.purpose, language)
|
||||
const existing = purposes.get(purpose) || []
|
||||
purposes.set(purpose, [...existing, dp])
|
||||
}
|
||||
|
||||
const purposeList = Array.from(purposes.entries())
|
||||
.map(([purpose, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
|
||||
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'purposes',
|
||||
order: 3,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateLegalBasisSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '4. Rechtsgrundlagen der Verarbeitung',
|
||||
en: '4. Legal Basis for Processing',
|
||||
}
|
||||
|
||||
const grouped = groupByLegalBasis(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const basis of ALL_LEGAL_BASES) {
|
||||
const basisData = grouped.get(basis)
|
||||
if (!basisData || basisData.length === 0) continue
|
||||
|
||||
const basisInfo = LEGAL_BASIS_INFO[basis]
|
||||
if (!basisInfo) continue
|
||||
|
||||
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
|
||||
const basisDesc = t(basisInfo.description, language)
|
||||
|
||||
let additionalWarning = ''
|
||||
if (basis === 'EXPLICIT_CONSENT') {
|
||||
additionalWarning = language === 'de'
|
||||
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
|
||||
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
|
||||
}
|
||||
|
||||
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
|
||||
const dataList = basisData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
|
||||
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'legal-basis',
|
||||
order: 4,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRecipientsSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '5. Empfaenger und Datenweitergabe',
|
||||
en: '5. Recipients and Data Sharing',
|
||||
}
|
||||
|
||||
const thirdParties = extractThirdParties(dataPoints)
|
||||
|
||||
if (thirdParties.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
||||
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
||||
}
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRetentionSection(
|
||||
dataPoints: DataPoint[],
|
||||
retentionMatrix: RetentionMatrixEntry[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '6. Speicherdauer',
|
||||
en: '6. Data Retention',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const entry of retentionMatrix) {
|
||||
const categoryData = grouped.get(entry.category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryName = t(entry.categoryName, language)
|
||||
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
||||
|
||||
const dataRetention = categoryData
|
||||
.map((dp) => {
|
||||
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
||||
return `- ${t(dp.name, language)}: ${period}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
||||
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'retention',
|
||||
order: 6,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
if (specialCategoryDataPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title: LocalizedText = {
|
||||
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
||||
}
|
||||
|
||||
const dataList = specialCategoryDataPoints
|
||||
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Ihre ausdrueckliche Einwilligung
|
||||
|
||||
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
||||
|
||||
### Ihre Rechte bei Art. 9 Daten
|
||||
|
||||
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
||||
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
||||
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
||||
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
||||
|
||||
### Besondere Schutzmassnahmen
|
||||
|
||||
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
||||
- Ende-zu-Ende-Verschluesselung
|
||||
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
||||
- Audit-Logging aller Zugriffe
|
||||
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
||||
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Your Explicit Consent
|
||||
|
||||
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
||||
|
||||
### Your Rights Regarding Art. 9 Data
|
||||
|
||||
- You can **withdraw your consent at any time**
|
||||
- Withdrawal does not affect the lawfulness of previous processing
|
||||
- Upon withdrawal, your data will be deleted immediately
|
||||
- You have the right to **access, rectification, and erasure**
|
||||
|
||||
### Special Protection Measures
|
||||
|
||||
For this sensitive data, we have implemented special technical and organizational measures:
|
||||
- End-to-end encryption
|
||||
- Strict access control (need-to-know principle)
|
||||
- Audit logging of all access
|
||||
- Regular data protection impact assessments`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'special-categories',
|
||||
order: 6.5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '7. Ihre Rechte als betroffene Person',
|
||||
en: '7. Your Rights as a Data Subject',
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
||||
|
||||
### Auskunftsrecht (Art. 15 DSGVO)
|
||||
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
||||
|
||||
### Recht auf Berichtigung (Art. 16 DSGVO)
|
||||
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
||||
|
||||
### Recht auf Loeschung (Art. 17 DSGVO)
|
||||
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||
|
||||
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
||||
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
||||
|
||||
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
||||
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
||||
|
||||
### Widerspruchsrecht (Art. 21 DSGVO)
|
||||
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
||||
|
||||
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
||||
|
||||
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
||||
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
||||
|
||||
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
||||
en: `You have the following rights regarding your personal data:
|
||||
|
||||
### Right of Access (Art. 15 GDPR)
|
||||
You have the right to request information about the personal data we process about you.
|
||||
|
||||
### Right to Rectification (Art. 16 GDPR)
|
||||
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
||||
|
||||
### Right to Erasure (Art. 17 GDPR)
|
||||
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
||||
|
||||
### Right to Restriction of Processing (Art. 18 GDPR)
|
||||
You have the right to request the restriction of processing of your data.
|
||||
|
||||
### Right to Data Portability (Art. 20 GDPR)
|
||||
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
||||
|
||||
### Right to Object (Art. 21 GDPR)
|
||||
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
||||
|
||||
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
||||
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
||||
|
||||
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
||||
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
||||
|
||||
**To exercise your rights, please contact us using the contact details provided above.**`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'rights',
|
||||
order: 7,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
@@ -1,954 +1,11 @@
|
||||
/**
|
||||
* Privacy Policy Generator
|
||||
* Privacy Policy Generator — barrel re-export
|
||||
*
|
||||
* Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog.
|
||||
* Die DSI wird aus 9 Abschnitten generiert:
|
||||
*
|
||||
* 1. Verantwortlicher (companyInfo)
|
||||
* 2. Erhobene Daten (dataPoints nach Kategorie)
|
||||
* 3. Verarbeitungszwecke (dataPoints.purpose)
|
||||
* 4. Rechtsgrundlagen (dataPoints.legalBasis)
|
||||
* 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients)
|
||||
* 6. Speicherdauer (retentionMatrix)
|
||||
* 7. Betroffenenrechte (statischer Text + Links)
|
||||
* 8. Cookies (cookieCategory-basiert)
|
||||
* 9. Aenderungen (statischer Text + Versionierung)
|
||||
* Split into:
|
||||
* - privacy-policy-sections.ts (section generators 1-7)
|
||||
* - privacy-policy-renderers.ts (sections 8-9, renderers, main generator)
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
GeneratedPrivacyPolicy,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
LocalizedText,
|
||||
RetentionMatrixEntry,
|
||||
LegalBasis,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
ARTICLE_9_WARNING,
|
||||
} from '../types'
|
||||
import { RETENTION_MATRIX } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge
|
||||
// =============================================================================
|
||||
|
||||
const ALL_CATEGORIES: DataPointCategory[] = [
|
||||
'MASTER_DATA', // A
|
||||
'CONTACT_DATA', // B
|
||||
'AUTHENTICATION', // C
|
||||
'CONSENT', // D
|
||||
'COMMUNICATION', // E
|
||||
'PAYMENT', // F
|
||||
'USAGE_DATA', // G
|
||||
'LOCATION', // H
|
||||
'DEVICE_DATA', // I
|
||||
'MARKETING', // J
|
||||
'ANALYTICS', // K
|
||||
'SOCIAL_MEDIA', // L
|
||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
||||
'CONTRACT_DATA', // O
|
||||
'LOG_DATA', // P
|
||||
'AI_DATA', // Q - AI Act
|
||||
'SECURITY', // R
|
||||
]
|
||||
|
||||
// Alle Rechtsgrundlagen in der richtigen Reihenfolge
|
||||
const ALL_LEGAL_BASES: LegalBasis[] = [
|
||||
'CONTRACT',
|
||||
'CONSENT',
|
||||
'EXPLICIT_CONSENT',
|
||||
'LEGITIMATE_INTEREST',
|
||||
'LEGAL_OBLIGATION',
|
||||
'VITAL_INTERESTS',
|
||||
'PUBLIC_INTEREST',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Holt den lokalisierten Text
|
||||
*/
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Kategorie
|
||||
*/
|
||||
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Rechtsgrundlage
|
||||
*/
|
||||
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
|
||||
const grouped = new Map<LegalBasis, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.legalBasis) || []
|
||||
grouped.set(dp.legalBasis, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert alle einzigartigen Drittanbieter
|
||||
*/
|
||||
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
||||
const thirdParties = new Set<string>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
thirdParties.add(recipient)
|
||||
}
|
||||
}
|
||||
return Array.from(thirdParties).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein Datum fuer die Anzeige
|
||||
*/
|
||||
function formatDate(date: Date, language: SupportedLanguage): string {
|
||||
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Abschnitt 1: Verantwortlicher
|
||||
*/
|
||||
function generateControllerSection(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '1. Verantwortlicher',
|
||||
en: '1. Data Controller',
|
||||
}
|
||||
|
||||
const dpoSection = companyInfo.dpoName
|
||||
? language === 'de'
|
||||
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
|
||||
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
|
||||
: ''
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
en: `The controller responsible for data processing on this website is:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'controller',
|
||||
order: 1,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 2: Erhobene Daten (18 Kategorien)
|
||||
*/
|
||||
function generateDataCollectionSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '2. Erhobene personenbezogene Daten',
|
||||
en: '2. Personal Data We Collect',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
// Prüfe ob Art. 9 Daten enthalten sind
|
||||
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const categoryData = grouped.get(category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
if (!categoryMeta) continue
|
||||
|
||||
const categoryTitle = t(categoryMeta.name, language)
|
||||
|
||||
// Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten)
|
||||
let categoryNote = ''
|
||||
if (category === 'HEALTH_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
|
||||
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
|
||||
} else if (category === 'EMPLOYEE_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
|
||||
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
|
||||
} else if (category === 'AI_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
|
||||
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
|
||||
}
|
||||
|
||||
const dataList = categoryData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
|
||||
en: 'We collect and process the following personal data:',
|
||||
}
|
||||
|
||||
// Zusätzlicher Hinweis für Art. 9 Daten
|
||||
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
|
||||
? {
|
||||
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
|
||||
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
|
||||
}
|
||||
: { de: '', en: '' }
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'data-collection',
|
||||
order: 2,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 3: Verarbeitungszwecke
|
||||
*/
|
||||
function generatePurposesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '3. Zwecke der Datenverarbeitung',
|
||||
en: '3. Purposes of Data Processing',
|
||||
}
|
||||
|
||||
// Gruppiere nach Zweck (unique purposes)
|
||||
const purposes = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const purpose = t(dp.purpose, language)
|
||||
const existing = purposes.get(purpose) || []
|
||||
purposes.set(purpose, [...existing, dp])
|
||||
}
|
||||
|
||||
const purposeList = Array.from(purposes.entries())
|
||||
.map(([purpose, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
|
||||
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'purposes',
|
||||
order: 3,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen)
|
||||
*/
|
||||
function generateLegalBasisSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '4. Rechtsgrundlagen der Verarbeitung',
|
||||
en: '4. Legal Basis for Processing',
|
||||
}
|
||||
|
||||
const grouped = groupByLegalBasis(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
// Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge
|
||||
for (const basis of ALL_LEGAL_BASES) {
|
||||
const basisData = grouped.get(basis)
|
||||
if (!basisData || basisData.length === 0) continue
|
||||
|
||||
const basisInfo = LEGAL_BASIS_INFO[basis]
|
||||
if (!basisInfo) continue
|
||||
|
||||
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
|
||||
const basisDesc = t(basisInfo.description, language)
|
||||
|
||||
// Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen
|
||||
let additionalWarning = ''
|
||||
if (basis === 'EXPLICIT_CONSENT') {
|
||||
additionalWarning = language === 'de'
|
||||
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
|
||||
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
|
||||
}
|
||||
|
||||
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
|
||||
const dataList = basisData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
|
||||
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'legal-basis',
|
||||
order: 4,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 5: Empfaenger / Dritte
|
||||
*/
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Drittanbieter
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 6: Speicherdauer
|
||||
*/
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO)
|
||||
* Wird nur generiert, wenn Art. 9 Daten vorhanden sind
|
||||
*/
|
||||
function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
// Filtere Art. 9 Datenpunkte
|
||||
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, // Zwischen Speicherdauer (6) und Rechte (7)
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 7: Betroffenenrechte
|
||||
*/
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 8: Cookies
|
||||
*/
|
||||
function generateCookiesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '8. Cookies und aehnliche Technologien',
|
||||
en: '8. Cookies and Similar Technologies',
|
||||
}
|
||||
|
||||
// Filtere Datenpunkte mit Cookie-Kategorie
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
if (cookieDataPoints.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
||||
en: 'We do not use cookies on this website.',
|
||||
}
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: false,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Cookie-Kategorie
|
||||
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
||||
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
||||
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
||||
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
||||
|
||||
const sections: string[] = []
|
||||
|
||||
if (essential.length > 0) {
|
||||
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
||||
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (performance.length > 0) {
|
||||
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
||||
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (personalization.length > 0) {
|
||||
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
||||
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (externalMedia.length > 0) {
|
||||
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
||||
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
||||
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 9: Aenderungen
|
||||
*/
|
||||
function generateChangesSection(
|
||||
version: string,
|
||||
date: Date,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
||||
en: '9. Changes to this Privacy Policy',
|
||||
}
|
||||
|
||||
const formattedDate = formatDate(date, language)
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
||||
|
||||
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
||||
|
||||
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
||||
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
||||
|
||||
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
||||
|
||||
The new privacy policy will then apply for your next visit.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'changes',
|
||||
order: 9,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9)
|
||||
*/
|
||||
export function generatePrivacyPolicySections(
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
version: string = '1.0.0'
|
||||
): PrivacyPolicySection[] {
|
||||
const now = new Date()
|
||||
|
||||
const sections: PrivacyPolicySection[] = [
|
||||
generateControllerSection(companyInfo, language),
|
||||
generateDataCollectionSection(dataPoints, language),
|
||||
generatePurposesSection(dataPoints, language),
|
||||
generateLegalBasisSection(dataPoints, language),
|
||||
generateRecipientsSection(dataPoints, language),
|
||||
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
||||
]
|
||||
|
||||
// Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden
|
||||
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
||||
if (specialCategoriesSection) {
|
||||
sections.push(specialCategoriesSection)
|
||||
}
|
||||
|
||||
sections.push(
|
||||
generateRightsSection(language),
|
||||
generateCookiesSection(dataPoints, language),
|
||||
generateChangesSection(version, now, language)
|
||||
)
|
||||
|
||||
// Abschnittsnummern neu vergeben
|
||||
sections.forEach((section, index) => {
|
||||
section.order = index + 1
|
||||
// Titel-Nummer aktualisieren
|
||||
const titleDe = section.title.de
|
||||
const titleEn = section.title.en
|
||||
if (titleDe.match(/^\d+[a-z]?\./)) {
|
||||
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
if (titleEn.match(/^\d+[a-z]?\./)) {
|
||||
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die vollstaendige Privacy Policy
|
||||
*/
|
||||
export function generatePrivacyPolicy(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat = 'HTML'
|
||||
): GeneratedPrivacyPolicy {
|
||||
const version = '1.0.0'
|
||||
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
||||
|
||||
// Generiere den Inhalt
|
||||
const content = renderPrivacyPolicy(sections, language, format)
|
||||
|
||||
return {
|
||||
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
||||
tenantId,
|
||||
language,
|
||||
sections,
|
||||
companyInfo,
|
||||
generatedAt: new Date(),
|
||||
version,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Privacy Policy im gewuenschten Format
|
||||
*/
|
||||
function renderPrivacyPolicy(
|
||||
sections: PrivacyPolicySection[],
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'HTML':
|
||||
return renderAsHTML(sections, language)
|
||||
case 'MARKDOWN':
|
||||
return renderAsMarkdown(sections, language)
|
||||
default:
|
||||
return renderAsMarkdown(sections, language)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert als HTML
|
||||
*/
|
||||
function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsHTML = sections
|
||||
.map((section) => {
|
||||
const content = t(section.content, language)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/### (.+)/g, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
||||
|
||||
return `
|
||||
<section id="${section.id}">
|
||||
<h2>${t(section.title, language)}</h2>
|
||||
<p>${content}</p>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
||||
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
||||
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
||||
p { margin: 1rem 0; }
|
||||
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
||||
li { margin: 0.5rem 0; }
|
||||
strong { font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
${sectionsHTML}
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert als Markdown
|
||||
*/
|
||||
function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsMarkdown = sections
|
||||
.map((section) => {
|
||||
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
||||
})
|
||||
.join('\n\n---\n\n')
|
||||
|
||||
return `# ${title}\n\n${sectionsMarkdown}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
generateControllerSection,
|
||||
generateDataCollectionSection,
|
||||
@@ -958,8 +15,13 @@ export {
|
||||
generateRetentionSection,
|
||||
generateSpecialCategoriesSection,
|
||||
generateRightsSection,
|
||||
} from './privacy-policy-sections'
|
||||
|
||||
export {
|
||||
generateCookiesSection,
|
||||
generateChangesSection,
|
||||
generatePrivacyPolicySections,
|
||||
generatePrivacyPolicy,
|
||||
renderAsHTML,
|
||||
renderAsMarkdown,
|
||||
}
|
||||
} from './privacy-policy-renderers'
|
||||
|
||||
Reference in New Issue
Block a user