diff --git a/admin-compliance/app/sdk/compliance-hub/_components/MappingsAndFindings.tsx b/admin-compliance/app/sdk/compliance-hub/_components/MappingsAndFindings.tsx new file mode 100644 index 0000000..1fb5d1a --- /dev/null +++ b/admin-compliance/app/sdk/compliance-hub/_components/MappingsAndFindings.tsx @@ -0,0 +1,91 @@ +'use client' + +import Link from 'next/link' +import { DashboardData, MappingsData, FindingsData } from '../_hooks/useComplianceHub' + +export function MappingsAndFindings({ + dashboard, + mappings, + findings, +}: { + dashboard: DashboardData | null + mappings: MappingsData | null + findings: FindingsData | null +}) { + return ( +
+
+
+

Control-Mappings

+ + Alle anzeigen → + +
+
+
+

{mappings?.total || 0}

+

Mappings gesamt

+
+
+

Nach Verordnung

+
+ {mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => ( + + {reg}: {count} + + ))} + {!mappings?.by_regulation && ( + Keine Mappings vorhanden + )} +
+
+
+

+ Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 0} Controls + und {dashboard?.total_requirements || 0} Anforderungen aus {dashboard?.total_regulations || 0} Verordnungen. +

+
+ +
+
+

Audit Findings

+ + Audit Checkliste → + +
+
+
+
+
+ Hauptabweichungen +
+

{findings?.open_majors || 0}

+

offen (blockiert Zertifizierung)

+
+
+
+
+ Nebenabweichungen +
+

{findings?.open_minors || 0}

+

offen (erfordert CAPA)

+
+
+
+ + Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI) + + {(findings?.open_majors || 0) === 0 ? ( + + Zertifizierung moeglich + + ) : ( + + Zertifizierung blockiert + + )} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/compliance-hub/_components/QuickActions.tsx b/admin-compliance/app/sdk/compliance-hub/_components/QuickActions.tsx new file mode 100644 index 0000000..445da78 --- /dev/null +++ b/admin-compliance/app/sdk/compliance-hub/_components/QuickActions.tsx @@ -0,0 +1,91 @@ +'use client' + +import Link from 'next/link' +import { DashboardData } from '../_hooks/useComplianceHub' + +export function QuickActions({ dashboard }: { dashboard: DashboardData | null }) { + return ( +
+

Schnellzugriff

+
+ +
+ + + +
+

Audit Checkliste

+

{dashboard?.total_requirements || '...'} Anforderungen

+ + + +
+ + + +
+

Controls

+

{dashboard?.total_controls || '...'} Massnahmen

+ + + +
+ + + +
+

Evidence

+

Nachweise

+ + + +
+ + + +
+

Risk Matrix

+

5x5 Risiken

+ + + +
+ + + +
+

Service Registry

+

Module

+ + + +
+ + + +
+

Audit Report

+

PDF Export

+ +
+
+ ) +} diff --git a/admin-compliance/app/sdk/compliance-hub/_components/RegulationsTable.tsx b/admin-compliance/app/sdk/compliance-hub/_components/RegulationsTable.tsx new file mode 100644 index 0000000..3e2098c --- /dev/null +++ b/admin-compliance/app/sdk/compliance-hub/_components/RegulationsTable.tsx @@ -0,0 +1,62 @@ +'use client' + +import { Regulation } from '../_hooks/useComplianceHub' + +export function RegulationsTable({ + regulations, + onRefresh, +}: { + regulations: Regulation[] + onRefresh: () => void +}) { + return ( +
+
+

Verordnungen & Standards ({regulations.length})

+ +
+
+ + + + + + + + + + + {regulations.slice(0, 15).map((reg) => ( + + + + + + + ))} + +
CodeNameTypAnforderungen
+ {reg.code} + +

{reg.name}

+
+ + {reg.regulation_type === 'eu_regulation' ? 'EU-VO' : + reg.regulation_type === 'eu_directive' ? 'EU-RL' : + reg.regulation_type === 'bsi_standard' ? 'BSI' : + reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type} + + + {reg.requirement_count} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/compliance-hub/_components/StatsRow.tsx b/admin-compliance/app/sdk/compliance-hub/_components/StatsRow.tsx new file mode 100644 index 0000000..3d2f226 --- /dev/null +++ b/admin-compliance/app/sdk/compliance-hub/_components/StatsRow.tsx @@ -0,0 +1,142 @@ +'use client' + +import { DashboardData } from '../_hooks/useComplianceHub' + +const DOMAIN_LABELS: Record = { + gov: 'Governance', + priv: 'Datenschutz', + iam: 'Identity & Access', + crypto: 'Kryptografie', + sdlc: 'Secure Dev', + ops: 'Operations', + ai: 'KI-spezifisch', + cra: 'Supply Chain', + aud: 'Audit', +} + +export function StatsRow({ + dashboard, + scoreColor, + scoreBgColor, + score, +}: { + dashboard: DashboardData | null + scoreColor: string + scoreBgColor: string + score: number +}) { + return ( +
+
+

Compliance Score

+
+ {score.toFixed(0)}% +
+
+
+
+

+ {dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden +

+
+ +
+
+
+

Verordnungen

+

{dashboard?.total_regulations || 0}

+
+
+ + + +
+
+

{dashboard?.total_requirements || 0} Anforderungen

+
+ +
+
+
+

Controls

+

{dashboard?.total_controls || 0}

+
+
+ + + +
+
+

{dashboard?.controls_by_status?.pass || 0} bestanden

+
+ +
+
+
+

Nachweise

+

{dashboard?.total_evidence || 0}

+
+
+ + + +
+
+

{dashboard?.evidence_by_status?.valid || 0} aktiv

+
+ +
+
+
+

Risiken

+

{dashboard?.total_risks || 0}

+
+
+ + + +
+
+

+ {(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch +

+
+
+ ) +} + +export function DomainChart({ dashboard }: { dashboard: DashboardData | null }) { + return ( +
+

Controls nach Domain

+
+ {Object.entries(dashboard?.controls_by_domain || {}).map(([domain, stats]) => { + const total = stats.total || 0 + const pass = stats.pass || 0 + const partial = stats.partial || 0 + const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0 + + return ( +
+
+ + {DOMAIN_LABELS[domain] || domain.toUpperCase()} + + + {pass}/{total} ({passPercent.toFixed(0)}%) + +
+
+
+
+
+
+ ) + })} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/compliance-hub/_hooks/useComplianceHub.ts b/admin-compliance/app/sdk/compliance-hub/_hooks/useComplianceHub.ts new file mode 100644 index 0000000..5e347ef --- /dev/null +++ b/admin-compliance/app/sdk/compliance-hub/_hooks/useComplianceHub.ts @@ -0,0 +1,116 @@ +'use client' + +import { useState, useEffect } from 'react' + +export interface DashboardData { + compliance_score: number + total_regulations: number + total_requirements: number + total_controls: number + controls_by_status: Record + controls_by_domain: Record> + total_evidence: number + evidence_by_status: Record + total_risks: number + risks_by_level: Record +} + +export interface Regulation { + id: string + code: string + name: string + full_name: string + regulation_type: string + effective_date: string | null + description: string + requirement_count: number +} + +export interface MappingsData { + total: number + by_regulation: Record +} + +export interface FindingsData { + major_count: number + minor_count: number + ofi_count: number + total: number + open_majors: number + open_minors: number +} + +export function useComplianceHub() { + const [dashboard, setDashboard] = useState(null) + const [regulations, setRegulations] = useState([]) + const [mappings, setMappings] = useState(null) + const [findings, setFindings] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [seeding, setSeeding] = useState(false) + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + setError(null) + try { + const [dashboardRes, regulationsRes, mappingsRes, findingsRes] = await Promise.all([ + fetch('/api/sdk/v1/compliance/dashboard'), + fetch('/api/sdk/v1/compliance/regulations'), + fetch('/api/sdk/v1/compliance/mappings'), + fetch('/api/sdk/v1/isms/findings?status=open'), + ]) + + if (dashboardRes.ok) { + setDashboard(await dashboardRes.json()) + } + if (regulationsRes.ok) { + const data = await regulationsRes.json() + setRegulations(data.regulations || []) + } + if (mappingsRes.ok) { + const data = await mappingsRes.json() + setMappings(data) + } + if (findingsRes.ok) { + const data = await findingsRes.json() + setFindings(data) + } + } catch (err) { + console.error('Failed to load compliance data:', err) + setError('Verbindung zum Backend fehlgeschlagen') + } finally { + setLoading(false) + } + } + + const seedDatabase = async () => { + setSeeding(true) + try { + const res = await fetch('/api/sdk/v1/compliance/seed', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ force: false }), + }) + + if (res.ok) { + const result = await res.json() + alert(`Datenbank erfolgreich initialisiert!\n\nRegulations: ${result.counts?.regulations || 0}\nControls: ${result.counts?.controls || 0}\nRequirements: ${result.counts?.requirements || 0}`) + loadData() + } else { + const errorText = await res.text() + alert(`Fehler beim Seeding: ${errorText}`) + } + } catch (err) { + console.error('Seeding failed:', err) + alert('Fehler beim Initialisieren der Datenbank') + } finally { + setSeeding(false) + } + } + + return { dashboard, regulations, mappings, findings, loading, error, seeding, loadData, seedDatabase } +} diff --git a/admin-compliance/app/sdk/compliance-hub/page.tsx b/admin-compliance/app/sdk/compliance-hub/page.tsx index c089bd8..50330b8 100644 --- a/admin-compliance/app/sdk/compliance-hub/page.tsx +++ b/admin-compliance/app/sdk/compliance-hub/page.tsx @@ -11,131 +11,18 @@ * - Regulations overview */ -import { useState, useEffect } from 'react' -import Link from 'next/link' - -// Types -interface DashboardData { - compliance_score: number - total_regulations: number - total_requirements: number - total_controls: number - controls_by_status: Record - controls_by_domain: Record> - total_evidence: number - evidence_by_status: Record - total_risks: number - risks_by_level: Record -} - -interface Regulation { - id: string - code: string - name: string - full_name: string - regulation_type: string - effective_date: string | null - description: string - requirement_count: number -} - -interface MappingsData { - total: number - by_regulation: Record -} - -interface FindingsData { - major_count: number - minor_count: number - ofi_count: number - total: number - open_majors: number - open_minors: number -} - -const DOMAIN_LABELS: Record = { - gov: 'Governance', - priv: 'Datenschutz', - iam: 'Identity & Access', - crypto: 'Kryptografie', - sdlc: 'Secure Dev', - ops: 'Operations', - ai: 'KI-spezifisch', - cra: 'Supply Chain', - aud: 'Audit', -} +import { useComplianceHub } from './_hooks/useComplianceHub' +import { QuickActions } from './_components/QuickActions' +import { StatsRow, DomainChart } from './_components/StatsRow' +import { MappingsAndFindings } from './_components/MappingsAndFindings' +import { RegulationsTable } from './_components/RegulationsTable' export default function ComplianceHubPage() { - const [dashboard, setDashboard] = useState(null) - const [regulations, setRegulations] = useState([]) - const [mappings, setMappings] = useState(null) - const [findings, setFindings] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [seeding, setSeeding] = useState(false) - - useEffect(() => { - loadData() - }, []) - - const loadData = async () => { - setLoading(true) - setError(null) - try { - const [dashboardRes, regulationsRes, mappingsRes, findingsRes] = await Promise.all([ - fetch('/api/sdk/v1/compliance/dashboard'), - fetch('/api/sdk/v1/compliance/regulations'), - fetch('/api/sdk/v1/compliance/mappings'), - fetch('/api/sdk/v1/isms/findings?status=open'), - ]) - - if (dashboardRes.ok) { - setDashboard(await dashboardRes.json()) - } - if (regulationsRes.ok) { - const data = await regulationsRes.json() - setRegulations(data.regulations || []) - } - if (mappingsRes.ok) { - const data = await mappingsRes.json() - setMappings(data) - } - if (findingsRes.ok) { - const data = await findingsRes.json() - setFindings(data) - } - } catch (err) { - console.error('Failed to load compliance data:', err) - setError('Verbindung zum Backend fehlgeschlagen') - } finally { - setLoading(false) - } - } - - const seedDatabase = async () => { - setSeeding(true) - try { - const res = await fetch('/api/sdk/v1/compliance/seed', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ force: false }), - }) - - if (res.ok) { - const result = await res.json() - alert(`Datenbank erfolgreich initialisiert!\n\nRegulations: ${result.counts?.regulations || 0}\nControls: ${result.counts?.controls || 0}\nRequirements: ${result.counts?.requirements || 0}`) - loadData() - } else { - const error = await res.text() - alert(`Fehler beim Seeding: ${error}`) - } - } catch (err) { - console.error('Seeding failed:', err) - alert('Fehler beim Initialisieren der Datenbank') - } finally { - setSeeding(false) - } - } + const { + dashboard, regulations, mappings, findings, + loading, error, seeding, + loadData, seedDatabase, + } = useComplianceHub() const score = dashboard?.compliance_score || 0 const scoreColor = score >= 80 ? 'text-green-600' : score >= 60 ? 'text-yellow-600' : 'text-red-600' @@ -184,88 +71,7 @@ export default function ComplianceHubPage() { )} {/* Quick Actions */} -
-

Schnellzugriff

-
- -
- - - -
-

Audit Checkliste

-

{dashboard?.total_requirements || '...'} Anforderungen

- - - -
- - - -
-

Controls

-

{dashboard?.total_controls || '...'} Massnahmen

- - - -
- - - -
-

Evidence

-

Nachweise

- - - -
- - - -
-

Risk Matrix

-

5x5 Risiken

- - - -
- - - -
-

Service Registry

-

Module

- - - -
- - - -
-

Audit Report

-

PDF Export

- -
-
+ {loading ? (
@@ -273,242 +79,15 @@ export default function ComplianceHubPage() {
) : ( <> - {/* Score and Stats Row */} -
-
-

Compliance Score

-
- {score.toFixed(0)}% -
-
-
-
-

- {dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden -

-
- -
-
-
-

Verordnungen

-

{dashboard?.total_regulations || 0}

-
-
- - - -
-
-

{dashboard?.total_requirements || 0} Anforderungen

-
- -
-
-
-

Controls

-

{dashboard?.total_controls || 0}

-
-
- - - -
-
-

{dashboard?.controls_by_status?.pass || 0} bestanden

-
- -
-
-
-

Nachweise

-

{dashboard?.total_evidence || 0}

-
-
- - - -
-
-

{dashboard?.evidence_by_status?.valid || 0} aktiv

-
- -
-
-
-

Risiken

-

{dashboard?.total_risks || 0}

-
-
- - - -
-
-

- {(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch -

-
-
- - {/* Control-Mappings & Findings Row */} -
-
-
-

Control-Mappings

- - Alle anzeigen → - -
-
-
-

{mappings?.total || 0}

-

Mappings gesamt

-
-
-

Nach Verordnung

-
- {mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => ( - - {reg}: {count} - - ))} - {!mappings?.by_regulation && ( - Keine Mappings vorhanden - )} -
-
-
-

- Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 0} Controls - und {dashboard?.total_requirements || 0} Anforderungen aus {dashboard?.total_regulations || 0} Verordnungen. -

-
- -
-
-

Audit Findings

- - Audit Checkliste → - -
-
-
-
-
- Hauptabweichungen -
-

{findings?.open_majors || 0}

-

offen (blockiert Zertifizierung)

-
-
-
-
- Nebenabweichungen -
-

{findings?.open_minors || 0}

-

offen (erfordert CAPA)

-
-
-
- - Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI) - - {(findings?.open_majors || 0) === 0 ? ( - - Zertifizierung moeglich - - ) : ( - - Zertifizierung blockiert - - )} -
-
-
- - {/* Domain Chart */} -
-

Controls nach Domain

-
- {Object.entries(dashboard?.controls_by_domain || {}).map(([domain, stats]) => { - const total = stats.total || 0 - const pass = stats.pass || 0 - const partial = stats.partial || 0 - const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0 - - return ( -
-
- - {DOMAIN_LABELS[domain] || domain.toUpperCase()} - - - {pass}/{total} ({passPercent.toFixed(0)}%) - -
-
-
-
-
-
- ) - })} -
-
- - {/* Regulations Table */} -
-
-

Verordnungen & Standards ({regulations.length})

- -
-
- - - - - - - - - - - {regulations.slice(0, 15).map((reg) => ( - - - - - - - ))} - -
CodeNameTypAnforderungen
- {reg.code} - -

{reg.name}

-
- - {reg.regulation_type === 'eu_regulation' ? 'EU-VO' : - reg.regulation_type === 'eu_directive' ? 'EU-RL' : - reg.regulation_type === 'bsi_standard' ? 'BSI' : - reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type} - - - {reg.requirement_count} -
-
-
+ + + + )}
diff --git a/admin-compliance/app/sdk/cookie-banner/_components/BannerPreview.tsx b/admin-compliance/app/sdk/cookie-banner/_components/BannerPreview.tsx new file mode 100644 index 0000000..7ccd456 --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/_components/BannerPreview.tsx @@ -0,0 +1,50 @@ +'use client' + +import { BannerConfig, BannerTexts, CookieCategory } from '../_hooks/useCookieBanner' + +export function BannerPreview({ + config, + categories, + bannerTexts, +}: { + config: BannerConfig + categories: CookieCategory[] + bannerTexts: BannerTexts +}) { + return ( +
+
+ Website-Vorschau +
+
+

{bannerTexts.title}

+

+ {bannerTexts.description} +

+
+ + {config.showDeclineAll && ( + + )} + {config.showSettings && ( + + )} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/cookie-banner/_components/CategoryCard.tsx b/admin-compliance/app/sdk/cookie-banner/_components/CategoryCard.tsx new file mode 100644 index 0000000..0350fb1 --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/_components/CategoryCard.tsx @@ -0,0 +1,80 @@ +'use client' + +import { useState } from 'react' +import { CookieCategory } from '../_hooks/useCookieBanner' + +export function CategoryCard({ + category, + onToggle, +}: { + category: CookieCategory + onToggle: (enabled: boolean) => void +}) { + const [expanded, setExpanded] = useState(false) + + return ( +
+
+
+
+

{category.name}

+ {category.required && ( + Erforderlich + )} + + {category.cookies.length} Cookies + +
+

{category.description}

+
+
+ +
+ + {expanded && ( +
+ + + + + + + + + + + {category.cookies.map(cookie => ( + + + + + + + ))} + +
CookieAnbieterZweckAblauf
{cookie.name}{cookie.provider}{cookie.purpose}{cookie.expiry}
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts b/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts new file mode 100644 index 0000000..1b7db88 --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts @@ -0,0 +1,184 @@ +'use client' + +import React, { useState } from 'react' + +export interface Cookie { + name: string + provider: string + purpose: string + expiry: string + type: 'first-party' | 'third-party' +} + +export interface CookieCategory { + id: string + name: string + description: string + required: boolean + enabled: boolean + cookies: Cookie[] +} + +export interface BannerConfig { + position: 'bottom' | 'top' | 'center' + style: 'bar' | 'popup' | 'modal' + primaryColor: string + showDeclineAll: boolean + showSettings: boolean + blockScripts: boolean +} + +export interface BannerTexts { + title: string + description: string + privacyLink: string +} + +const DEFAULT_COOKIE_CATEGORIES: CookieCategory[] = [ + { + id: 'necessary', + name: 'Notwendig', + description: 'Diese Cookies sind fuer die Grundfunktionen der Website erforderlich.', + required: true, + enabled: true, + cookies: [ + { name: 'session_id', provider: 'Eigene', purpose: 'Session-Verwaltung', expiry: 'Session', type: 'first-party' }, + { name: 'csrf_token', provider: 'Eigene', purpose: 'Sicherheit', expiry: 'Session', type: 'first-party' }, + ], + }, + { + id: 'analytics', + name: 'Analyse', + description: 'Diese Cookies helfen uns, die Nutzung der Website zu verstehen.', + required: false, + enabled: true, + cookies: [ + { name: '_ga', provider: 'Google Analytics', purpose: 'Nutzeranalyse', expiry: '2 Jahre', type: 'third-party' }, + { name: '_gid', provider: 'Google Analytics', purpose: 'Nutzeranalyse', expiry: '24 Stunden', type: 'third-party' }, + ], + }, + { + id: 'marketing', + name: 'Marketing', + description: 'Diese Cookies werden fuer personalisierte Werbung verwendet.', + required: false, + enabled: false, + cookies: [ + { name: '_fbp', provider: 'Meta (Facebook)', purpose: 'Werbung', expiry: '3 Monate', type: 'third-party' }, + { name: 'IDE', provider: 'Google Ads', purpose: 'Werbung', expiry: '1 Jahr', type: 'third-party' }, + ], + }, + { + id: 'preferences', + name: 'Praeferenzen', + description: 'Diese Cookies speichern Ihre Einstellungen und Praeferenzen.', + required: false, + enabled: true, + cookies: [ + { name: 'language', provider: 'Eigene', purpose: 'Spracheinstellung', expiry: '1 Jahr', type: 'first-party' }, + { name: 'theme', provider: 'Eigene', purpose: 'Design-Einstellung', expiry: '1 Jahr', type: 'first-party' }, + ], + }, +] + +const defaultConfig: BannerConfig = { + position: 'bottom', + style: 'bar', + primaryColor: '#6366f1', + showDeclineAll: true, + showSettings: true, + blockScripts: true, +} + +const defaultBannerTexts: BannerTexts = { + title: 'Wir verwenden Cookies', + description: 'Wir nutzen Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.', + privacyLink: '/datenschutz', +} + +export function useCookieBanner() { + const [categories, setCategories] = useState([]) + const [config, setConfig] = useState(defaultConfig) + const [bannerTexts, setBannerTexts] = useState(defaultBannerTexts) + const [isSaving, setIsSaving] = useState(false) + const [exportToast, setExportToast] = useState(null) + + React.useEffect(() => { + const loadConfig = async () => { + try { + const response = await fetch('/api/sdk/v1/einwilligungen/cookie-banner/config') + if (response.ok) { + const data = await response.json() + setCategories(data.categories?.length > 0 ? data.categories : DEFAULT_COOKIE_CATEGORIES) + if (data.config && Object.keys(data.config).length > 0) { + setConfig(prev => ({ ...prev, ...data.config })) + const savedTexts = data.config.banner_texts || data.config.texts + if (savedTexts) { + setBannerTexts(prev => ({ ...prev, ...savedTexts })) + } + } + } else { + setCategories(DEFAULT_COOKIE_CATEGORIES) + } + } catch { + setCategories(DEFAULT_COOKIE_CATEGORIES) + } + } + loadConfig() + }, []) + + const handleCategoryToggle = async (categoryId: string, enabled: boolean) => { + setCategories(prev => + prev.map(cat => cat.id === categoryId ? { ...cat, enabled } : cat) + ) + try { + await fetch('/api/sdk/v1/einwilligungen/cookie-banner/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ categoryId, enabled }), + }) + } catch { + // Silently ignore — local state already updated + } + } + + const handleExportCode = async () => { + try { + const res = await fetch('/api/sdk/v1/einwilligungen/cookie-banner/embed-code') + if (res.ok) { + const data = await res.json() + const code = data.embed_code || data.script || '' + await navigator.clipboard.writeText(code) + setExportToast('Embed-Code in Zwischenablage kopiert!') + setTimeout(() => setExportToast(null), 3000) + } else { + setExportToast('Fehler beim Laden des Embed-Codes') + setTimeout(() => setExportToast(null), 3000) + } + } catch { + setExportToast('Fehler beim Kopieren in die Zwischenablage') + setTimeout(() => setExportToast(null), 3000) + } + } + + const handleSaveConfig = async () => { + setIsSaving(true) + try { + await fetch('/api/sdk/v1/einwilligungen/cookie-banner/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ categories, config: { ...config, banner_texts: bannerTexts } }), + }) + } catch { + // Silently ignore + } finally { + setIsSaving(false) + } + } + + return { + categories, config, bannerTexts, isSaving, exportToast, + setConfig, setBannerTexts, + handleCategoryToggle, handleExportCode, handleSaveConfig, + } +} diff --git a/admin-compliance/app/sdk/cookie-banner/page.tsx b/admin-compliance/app/sdk/cookie-banner/page.tsx index 0e82ebd..1733ff0 100644 --- a/admin-compliance/app/sdk/cookie-banner/page.tsx +++ b/admin-compliance/app/sdk/cookie-banner/page.tsx @@ -1,315 +1,19 @@ 'use client' -import React, { useState } from 'react' +import React from 'react' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' - -// ============================================================================= -// TYPES -// ============================================================================= - -interface CookieCategory { - id: string - name: string - description: string - required: boolean - enabled: boolean - cookies: Cookie[] -} - -interface Cookie { - name: string - provider: string - purpose: string - expiry: string - type: 'first-party' | 'third-party' -} - -interface BannerConfig { - position: 'bottom' | 'top' | 'center' - style: 'bar' | 'popup' | 'modal' - primaryColor: string - showDeclineAll: boolean - showSettings: boolean - blockScripts: boolean -} - -// ============================================================================= -// DEFAULT DATA (Fallback wenn DB leer oder nicht erreichbar) -// ============================================================================= - -const DEFAULT_COOKIE_CATEGORIES: CookieCategory[] = [ - { - id: 'necessary', - name: 'Notwendig', - description: 'Diese Cookies sind fuer die Grundfunktionen der Website erforderlich.', - required: true, - enabled: true, - cookies: [ - { name: 'session_id', provider: 'Eigene', purpose: 'Session-Verwaltung', expiry: 'Session', type: 'first-party' }, - { name: 'csrf_token', provider: 'Eigene', purpose: 'Sicherheit', expiry: 'Session', type: 'first-party' }, - ], - }, - { - id: 'analytics', - name: 'Analyse', - description: 'Diese Cookies helfen uns, die Nutzung der Website zu verstehen.', - required: false, - enabled: true, - cookies: [ - { name: '_ga', provider: 'Google Analytics', purpose: 'Nutzeranalyse', expiry: '2 Jahre', type: 'third-party' }, - { name: '_gid', provider: 'Google Analytics', purpose: 'Nutzeranalyse', expiry: '24 Stunden', type: 'third-party' }, - ], - }, - { - id: 'marketing', - name: 'Marketing', - description: 'Diese Cookies werden fuer personalisierte Werbung verwendet.', - required: false, - enabled: false, - cookies: [ - { name: '_fbp', provider: 'Meta (Facebook)', purpose: 'Werbung', expiry: '3 Monate', type: 'third-party' }, - { name: 'IDE', provider: 'Google Ads', purpose: 'Werbung', expiry: '1 Jahr', type: 'third-party' }, - ], - }, - { - id: 'preferences', - name: 'Praeferenzen', - description: 'Diese Cookies speichern Ihre Einstellungen und Praeferenzen.', - required: false, - enabled: true, - cookies: [ - { name: 'language', provider: 'Eigene', purpose: 'Spracheinstellung', expiry: '1 Jahr', type: 'first-party' }, - { name: 'theme', provider: 'Eigene', purpose: 'Design-Einstellung', expiry: '1 Jahr', type: 'first-party' }, - ], - }, -] - -const defaultConfig: BannerConfig = { - position: 'bottom', - style: 'bar', - primaryColor: '#6366f1', - showDeclineAll: true, - showSettings: true, - blockScripts: true, -} - -// ============================================================================= -// COMPONENTS -// ============================================================================= - -interface BannerTexts { - title: string - description: string - privacyLink: string -} - -function BannerPreview({ config, categories, bannerTexts }: { config: BannerConfig; categories: CookieCategory[]; bannerTexts: BannerTexts }) { - return ( -
-
- Website-Vorschau -
-
-

{bannerTexts.title}

-

- {bannerTexts.description} -

-
- - {config.showDeclineAll && ( - - )} - {config.showSettings && ( - - )} -
-
-
- ) -} - -function CategoryCard({ - category, - onToggle, -}: { - category: CookieCategory - onToggle: (enabled: boolean) => void -}) { - const [expanded, setExpanded] = useState(false) - - return ( -
-
-
-
-

{category.name}

- {category.required && ( - Erforderlich - )} - - {category.cookies.length} Cookies - -
-

{category.description}

-
-
- -
- - {expanded && ( -
- - - - - - - - - - - {category.cookies.map(cookie => ( - - - - - - - ))} - -
CookieAnbieterZweckAblauf
{cookie.name}{cookie.provider}{cookie.purpose}{cookie.expiry}
-
- )} -
- ) -} - -// ============================================================================= -// MAIN PAGE -// ============================================================================= - -const defaultBannerTexts: BannerTexts = { - title: 'Wir verwenden Cookies', - description: 'Wir nutzen Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.', - privacyLink: '/datenschutz', -} +import { useCookieBanner } from './_hooks/useCookieBanner' +import { BannerPreview } from './_components/BannerPreview' +import { CategoryCard } from './_components/CategoryCard' export default function CookieBannerPage() { const { state } = useSDK() - const [categories, setCategories] = useState([]) - const [config, setConfig] = useState(defaultConfig) - const [bannerTexts, setBannerTexts] = useState(defaultBannerTexts) - const [isSaving, setIsSaving] = useState(false) - const [exportToast, setExportToast] = useState(null) - - React.useEffect(() => { - const loadConfig = async () => { - try { - const response = await fetch('/api/sdk/v1/einwilligungen/cookie-banner/config') - if (response.ok) { - const data = await response.json() - // DB-Kategorien haben immer Vorrang — Defaults nur wenn DB wirklich leer - setCategories(data.categories?.length > 0 ? data.categories : DEFAULT_COOKIE_CATEGORIES) - if (data.config && Object.keys(data.config).length > 0) { - setConfig(prev => ({ ...prev, ...data.config })) - const savedTexts = data.config.banner_texts || data.config.texts - if (savedTexts) { - setBannerTexts(prev => ({ ...prev, ...savedTexts })) - } - } - } else { - setCategories(DEFAULT_COOKIE_CATEGORIES) - } - } catch { - setCategories(DEFAULT_COOKIE_CATEGORIES) - } - } - loadConfig() - }, []) - - const handleCategoryToggle = async (categoryId: string, enabled: boolean) => { - setCategories(prev => - prev.map(cat => cat.id === categoryId ? { ...cat, enabled } : cat) - ) - try { - await fetch('/api/sdk/v1/einwilligungen/cookie-banner/config', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ categoryId, enabled }), - }) - } catch { - // Silently ignore — local state already updated - } - } - - const handleExportCode = async () => { - try { - const res = await fetch('/api/sdk/v1/einwilligungen/cookie-banner/embed-code') - if (res.ok) { - const data = await res.json() - const code = data.embed_code || data.script || '' - await navigator.clipboard.writeText(code) - setExportToast('Embed-Code in Zwischenablage kopiert!') - setTimeout(() => setExportToast(null), 3000) - } else { - setExportToast('Fehler beim Laden des Embed-Codes') - setTimeout(() => setExportToast(null), 3000) - } - } catch { - setExportToast('Fehler beim Kopieren in die Zwischenablage') - setTimeout(() => setExportToast(null), 3000) - } - } - - const handleSaveConfig = async () => { - setIsSaving(true) - try { - await fetch('/api/sdk/v1/einwilligungen/cookie-banner/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ categories, config: { ...config, banner_texts: bannerTexts } }), - }) - } catch { - // Silently ignore - } finally { - setIsSaving(false) - } - } + const { + categories, config, bannerTexts, isSaving, exportToast, + setConfig, setBannerTexts, + handleCategoryToggle, handleExportCode, handleSaveConfig, + } = useCookieBanner() const totalCookies = categories.reduce((sum, cat) => sum + cat.cookies.length, 0) const thirdPartyCookies = categories.reduce( diff --git a/admin-compliance/app/sdk/dsr/new/_components/SourceSelector.tsx b/admin-compliance/app/sdk/dsr/new/_components/SourceSelector.tsx new file mode 100644 index 0000000..a91b5c6 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/new/_components/SourceSelector.tsx @@ -0,0 +1,74 @@ +'use client' + +import { DSRSource } from '@/lib/sdk/dsr/types' + +const SOURCES: { value: DSRSource; label: string; icon: string }[] = [ + { value: 'web_form', label: 'Webformular', icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' }, + { value: 'email', label: 'E-Mail', icon: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' }, + { value: 'letter', label: 'Brief', icon: 'M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5' }, + { value: 'phone', label: 'Telefon', icon: 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z' }, + { value: 'in_person', label: 'Persoenlich', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' }, + { value: 'other', label: 'Sonstiges', icon: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' } +] + +export function SourceSelector({ + selectedSource, + sourceDetails, + onSourceChange, + onDetailsChange +}: { + selectedSource: DSRSource | '' + sourceDetails: string + onSourceChange: (source: DSRSource) => void + onDetailsChange: (details: string) => void +}) { + return ( +
+ +
+ {SOURCES.map(source => ( + + ))} +
+ {selectedSource && ( + onDetailsChange(e.target.value)} + placeholder={ + selectedSource === 'web_form' ? 'z.B. Kontaktformular auf website.de' : + selectedSource === 'email' ? 'z.B. info@firma.de' : + selectedSource === 'phone' ? 'z.B. Anruf am 22.01.2025' : + 'Weitere Details zur Quelle' + } + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> + )} +
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/new/_components/TypeSelector.tsx b/admin-compliance/app/sdk/dsr/new/_components/TypeSelector.tsx new file mode 100644 index 0000000..3609b0b --- /dev/null +++ b/admin-compliance/app/sdk/dsr/new/_components/TypeSelector.tsx @@ -0,0 +1,70 @@ +'use client' + +import { DSRType, DSR_TYPE_INFO } from '@/lib/sdk/dsr/types' + +export function TypeSelector({ + selectedType, + onSelect +}: { + selectedType: DSRType | '' + onSelect: (type: DSRType) => void +}) { + return ( +
+ +
+ {Object.entries(DSR_TYPE_INFO).map(([type, info]) => ( + + ))} +
+ {selectedType && ( +
+
+ {DSR_TYPE_INFO[selectedType].label} +
+

+ {DSR_TYPE_INFO[selectedType].description} +

+
+ Standardfrist: {DSR_TYPE_INFO[selectedType].defaultDeadlineDays} Tage + {DSR_TYPE_INFO[selectedType].maxExtensionMonths > 0 && ( + <> | Verlaengerbar um {DSR_TYPE_INFO[selectedType].maxExtensionMonths} Monate + )} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/new/_hooks/useNewDSRForm.ts b/admin-compliance/app/sdk/dsr/new/_hooks/useNewDSRForm.ts new file mode 100644 index 0000000..34b9172 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/new/_hooks/useNewDSRForm.ts @@ -0,0 +1,111 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { + DSRType, + DSRSource, + DSRPriority, + DSRCreateRequest +} from '@/lib/sdk/dsr/types' +import { createSDKDSR } from '@/lib/sdk/dsr/api' + +export interface FormData { + type: DSRType | '' + requesterName: string + requesterEmail: string + requesterPhone: string + requesterAddress: string + source: DSRSource | '' + sourceDetails: string + requestText: string + priority: DSRPriority + customerId: string +} + +export function useNewDSRForm() { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = useState(false) + const [errors, setErrors] = useState>({}) + + const [formData, setFormData] = useState({ + type: '', + requesterName: '', + requesterEmail: '', + requesterPhone: '', + requesterAddress: '', + source: '', + sourceDetails: '', + requestText: '', + priority: 'normal', + customerId: '' + }) + + const updateField = (field: K, value: FormData[K]) => { + setFormData(prev => ({ ...prev, [field]: value })) + if (errors[field]) { + setErrors(prev => { + const newErrors = { ...prev } + delete newErrors[field] + return newErrors + }) + } + } + + const validate = (): boolean => { + const newErrors: Record = {} + + if (!formData.type) { + newErrors.type = 'Bitte waehlen Sie den Anfragetyp' + } + if (!formData.requesterName.trim()) { + newErrors.requesterName = 'Name ist erforderlich' + } + if (!formData.requesterEmail.trim()) { + newErrors.requesterEmail = 'E-Mail ist erforderlich' + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.requesterEmail)) { + newErrors.requesterEmail = 'Bitte geben Sie eine gueltige E-Mail-Adresse ein' + } + if (!formData.source) { + newErrors.source = 'Bitte waehlen Sie die Quelle der Anfrage' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validate()) return + + setIsSubmitting(true) + try { + const request: DSRCreateRequest = { + type: formData.type as DSRType, + requester: { + name: formData.requesterName, + email: formData.requesterEmail, + phone: formData.requesterPhone || undefined, + address: formData.requesterAddress || undefined, + customerId: formData.customerId || undefined + }, + source: formData.source as DSRSource, + sourceDetails: formData.sourceDetails || undefined, + requestText: formData.requestText || undefined, + priority: formData.priority + } + + await createSDKDSR(request) + + router.push('/sdk/dsr') + } catch (error) { + console.error('Failed to create DSR:', error) + setErrors({ submit: 'Fehler beim Erstellen der Anfrage. Bitte versuchen Sie es erneut.' }) + } finally { + setIsSubmitting(false) + } + } + + return { formData, errors, isSubmitting, updateField, handleSubmit } +} diff --git a/admin-compliance/app/sdk/dsr/new/page.tsx b/admin-compliance/app/sdk/dsr/new/page.tsx index 42a3c04..9691113 100644 --- a/admin-compliance/app/sdk/dsr/new/page.tsx +++ b/admin-compliance/app/sdk/dsr/new/page.tsx @@ -1,266 +1,14 @@ 'use client' -import React, { useState } from 'react' +import React from 'react' import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { - DSRType, - DSRSource, - DSRPriority, - DSR_TYPE_INFO, - DSRCreateRequest -} from '@/lib/sdk/dsr/types' -import { createSDKDSR } from '@/lib/sdk/dsr/api' - -// ============================================================================= -// TYPES -// ============================================================================= - -interface FormData { - type: DSRType | '' - requesterName: string - requesterEmail: string - requesterPhone: string - requesterAddress: string - source: DSRSource | '' - sourceDetails: string - requestText: string - priority: DSRPriority - customerId: string -} - -// ============================================================================= -// COMPONENTS -// ============================================================================= - -function TypeSelector({ - selectedType, - onSelect -}: { - selectedType: DSRType | '' - onSelect: (type: DSRType) => void -}) { - return ( -
- -
- {Object.entries(DSR_TYPE_INFO).map(([type, info]) => ( - - ))} -
- {selectedType && ( -
-
- {DSR_TYPE_INFO[selectedType].label} -
-

- {DSR_TYPE_INFO[selectedType].description} -

-
- Standardfrist: {DSR_TYPE_INFO[selectedType].defaultDeadlineDays} Tage - {DSR_TYPE_INFO[selectedType].maxExtensionMonths > 0 && ( - <> | Verlaengerbar um {DSR_TYPE_INFO[selectedType].maxExtensionMonths} Monate - )} -
-
- )} -
- ) -} - -function SourceSelector({ - selectedSource, - sourceDetails, - onSourceChange, - onDetailsChange -}: { - selectedSource: DSRSource | '' - sourceDetails: string - onSourceChange: (source: DSRSource) => void - onDetailsChange: (details: string) => void -}) { - const sources: { value: DSRSource; label: string; icon: string }[] = [ - { value: 'web_form', label: 'Webformular', icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' }, - { value: 'email', label: 'E-Mail', icon: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' }, - { value: 'letter', label: 'Brief', icon: 'M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5' }, - { value: 'phone', label: 'Telefon', icon: 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z' }, - { value: 'in_person', label: 'Persoenlich', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' }, - { value: 'other', label: 'Sonstiges', icon: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' } - ] - - return ( -
- -
- {sources.map(source => ( - - ))} -
- {selectedSource && ( - onDetailsChange(e.target.value)} - placeholder={ - selectedSource === 'web_form' ? 'z.B. Kontaktformular auf website.de' : - selectedSource === 'email' ? 'z.B. info@firma.de' : - selectedSource === 'phone' ? 'z.B. Anruf am 22.01.2025' : - 'Weitere Details zur Quelle' - } - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - /> - )} -
- ) -} - -// ============================================================================= -// MAIN PAGE -// ============================================================================= +import { DSRType, DSRPriority } from '@/lib/sdk/dsr/types' +import { TypeSelector } from './_components/TypeSelector' +import { SourceSelector } from './_components/SourceSelector' +import { useNewDSRForm } from './_hooks/useNewDSRForm' export default function NewDSRPage() { - const router = useRouter() - const [isSubmitting, setIsSubmitting] = useState(false) - const [errors, setErrors] = useState>({}) - - const [formData, setFormData] = useState({ - type: '', - requesterName: '', - requesterEmail: '', - requesterPhone: '', - requesterAddress: '', - source: '', - sourceDetails: '', - requestText: '', - priority: 'normal', - customerId: '' - }) - - const updateField = (field: K, value: FormData[K]) => { - setFormData(prev => ({ ...prev, [field]: value })) - // Clear error when field is updated - if (errors[field]) { - setErrors(prev => { - const newErrors = { ...prev } - delete newErrors[field] - return newErrors - }) - } - } - - const validate = (): boolean => { - const newErrors: Record = {} - - if (!formData.type) { - newErrors.type = 'Bitte waehlen Sie den Anfragetyp' - } - if (!formData.requesterName.trim()) { - newErrors.requesterName = 'Name ist erforderlich' - } - if (!formData.requesterEmail.trim()) { - newErrors.requesterEmail = 'E-Mail ist erforderlich' - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.requesterEmail)) { - newErrors.requesterEmail = 'Bitte geben Sie eine gueltige E-Mail-Adresse ein' - } - if (!formData.source) { - newErrors.source = 'Bitte waehlen Sie die Quelle der Anfrage' - } - - setErrors(newErrors) - return Object.keys(newErrors).length === 0 - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - if (!validate()) return - - setIsSubmitting(true) - try { - // Create DSR request - const request: DSRCreateRequest = { - type: formData.type as DSRType, - requester: { - name: formData.requesterName, - email: formData.requesterEmail, - phone: formData.requesterPhone || undefined, - address: formData.requesterAddress || undefined, - customerId: formData.customerId || undefined - }, - source: formData.source as DSRSource, - sourceDetails: formData.sourceDetails || undefined, - requestText: formData.requestText || undefined, - priority: formData.priority - } - - await createSDKDSR(request) - - // Redirect to DSR list - router.push('/sdk/dsr') - } catch (error) { - console.error('Failed to create DSR:', error) - setErrors({ submit: 'Fehler beim Erstellen der Anfrage. Bitte versuchen Sie es erneut.' }) - } finally { - setIsSubmitting(false) - } - } + const { formData, errors, isSubmitting, updateField, handleSubmit } = useNewDSRForm() return (
@@ -300,7 +48,6 @@ export default function NewDSRPage() {

Antragsteller

- {/* Name */}
- {/* Email */}
- {/* Phone */}
- {/* Customer ID */}
- {/* Address */}