diff --git a/admin-lehrer/app/(sdk)/sdk/dsb-portal/page.tsx b/admin-lehrer/app/(sdk)/sdk/dsb-portal/page.tsx
new file mode 100644
index 0000000..4cf7bd4
--- /dev/null
+++ b/admin-lehrer/app/(sdk)/sdk/dsb-portal/page.tsx
@@ -0,0 +1,2068 @@
+'use client'
+
+import React, { useState, useEffect, useCallback, useRef } from 'react'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+interface AssignmentOverview {
+ id: string
+ dsb_user_id: string
+ tenant_id: string
+ tenant_name: string
+ tenant_slug: string
+ status: string
+ contract_start: string
+ contract_end: string | null
+ monthly_hours_budget: number
+ notes: string
+ compliance_score: number
+ hours_this_month: number
+ hours_budget: number
+ open_task_count: number
+ urgent_task_count: number
+ next_deadline: string | null
+ created_at: string
+ updated_at: string
+}
+
+interface DSBDashboard {
+ assignments: AssignmentOverview[]
+ total_assignments: number
+ active_assignments: number
+ total_hours_this_month: number
+ open_tasks: number
+ urgent_tasks: number
+ generated_at: string
+}
+
+interface HourEntry {
+ id: string
+ assignment_id: string
+ date: string
+ hours: number
+ category: string
+ description: string
+ billable: boolean
+ created_at: string
+}
+
+interface Task {
+ id: string
+ assignment_id: string
+ title: string
+ description: string
+ category: string
+ priority: string
+ status: string
+ due_date: string | null
+ completed_at: string | null
+ created_at: string
+ updated_at: string
+}
+
+interface Communication {
+ id: string
+ assignment_id: string
+ direction: string
+ channel: string
+ subject: string
+ content: string
+ participants: string
+ created_at: string
+}
+
+interface HoursSummary {
+ total_hours: number
+ billable_hours: number
+ by_category: Record
+ period: string
+}
+
+// =============================================================================
+// CONSTANTS
+// =============================================================================
+
+const DSB_USER_ID = '00000000-0000-0000-0000-000000000001'
+
+const TASK_CATEGORIES = [
+ 'DSFA-Pruefung',
+ 'Betroffenenanfrage',
+ 'Vorfall-Pruefung',
+ 'Audit-Vorbereitung',
+ 'Richtlinien-Pruefung',
+ 'Schulung',
+ 'Beratung',
+ 'Sonstiges',
+]
+
+const HOUR_CATEGORIES = [
+ 'DSFA-Pruefung',
+ 'Beratung',
+ 'Audit',
+ 'Schulung',
+ 'Vorfallreaktion',
+ 'Dokumentation',
+ 'Besprechung',
+ 'Sonstiges',
+]
+
+const COMM_CHANNELS = ['E-Mail', 'Telefon', 'Besprechung', 'Portal', 'Brief']
+
+const PRIORITY_LABELS: Record = {
+ urgent: 'Dringend',
+ high: 'Hoch',
+ medium: 'Mittel',
+ low: 'Niedrig',
+}
+
+const PRIORITY_COLORS: Record = {
+ urgent: 'bg-red-100 text-red-700 border-red-200',
+ high: 'bg-orange-100 text-orange-700 border-orange-200',
+ medium: 'bg-blue-100 text-blue-700 border-blue-200',
+ low: 'bg-gray-100 text-gray-500 border-gray-200',
+}
+
+const TASK_STATUS_LABELS: Record = {
+ open: 'Offen',
+ in_progress: 'In Bearbeitung',
+ waiting: 'Wartend',
+ completed: 'Erledigt',
+ cancelled: 'Abgebrochen',
+}
+
+const TASK_STATUS_COLORS: Record = {
+ open: 'bg-blue-100 text-blue-700',
+ in_progress: 'bg-yellow-100 text-yellow-700',
+ waiting: 'bg-orange-100 text-orange-700',
+ completed: 'bg-green-100 text-green-700',
+ cancelled: 'bg-gray-100 text-gray-500',
+}
+
+const ASSIGNMENT_STATUS_COLORS: Record = {
+ active: 'bg-green-100 text-green-700 border-green-300',
+ paused: 'bg-yellow-100 text-yellow-700 border-yellow-300',
+ terminated: 'bg-red-100 text-red-700 border-red-300',
+}
+
+const ASSIGNMENT_STATUS_LABELS: Record = {
+ active: 'Aktiv',
+ paused: 'Pausiert',
+ terminated: 'Beendet',
+}
+
+// =============================================================================
+// API HELPERS
+// =============================================================================
+
+async function apiFetch(url: string, options?: RequestInit): Promise {
+ const res = await fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-User-ID': DSB_USER_ID,
+ ...(options?.headers || {}),
+ },
+ })
+ if (!res.ok) {
+ const text = await res.text().catch(() => '')
+ throw new Error(`API Error ${res.status}: ${text || res.statusText}`)
+ }
+ return res.json()
+}
+
+// =============================================================================
+// TOAST COMPONENT
+// =============================================================================
+
+interface ToastMessage {
+ id: number
+ message: string
+ type: 'success' | 'error'
+}
+
+let toastIdCounter = 0
+
+function useToast() {
+ const [toasts, setToasts] = useState([])
+
+ const addToast = useCallback((message: string, type: 'success' | 'error' = 'success') => {
+ const id = ++toastIdCounter
+ setToasts((prev) => [...prev, { id, message, type }])
+ setTimeout(() => {
+ setToasts((prev) => prev.filter((t) => t.id !== id))
+ }, 3500)
+ }, [])
+
+ return { toasts, addToast }
+}
+
+function ToastContainer({ toasts }: { toasts: ToastMessage[] }) {
+ if (toasts.length === 0) return null
+ return (
+
+ {toasts.map((t) => (
+
+ {t.message}
+
+ ))}
+
+ )
+}
+
+// =============================================================================
+// LOADING SKELETON
+// =============================================================================
+
+function Skeleton({ className = '' }: { className?: string }) {
+ return
+}
+
+function DashboardSkeleton() {
+ return (
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+ )
+}
+
+// =============================================================================
+// MODAL COMPONENT
+// =============================================================================
+
+function Modal({
+ open,
+ onClose,
+ title,
+ children,
+ maxWidth = 'max-w-lg',
+}: {
+ open: boolean
+ onClose: () => void
+ title: string
+ children: React.ReactNode
+ maxWidth?: string
+}) {
+ const overlayRef = useRef(null)
+
+ useEffect(() => {
+ if (!open) return
+ const handleEsc = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose()
+ }
+ document.addEventListener('keydown', handleEsc)
+ return () => document.removeEventListener('keydown', handleEsc)
+ }, [open, onClose])
+
+ if (!open) return null
+
+ return (
+ {
+ if (e.target === overlayRef.current) onClose()
+ }}
+ >
+
+
+ )
+}
+
+// =============================================================================
+// STAT CARD
+// =============================================================================
+
+function StatCard({
+ title,
+ value,
+ icon,
+ accent = false,
+}: {
+ title: string
+ value: string | number
+ icon: React.ReactNode
+ accent?: boolean
+}) {
+ return (
+
+
+
+ {icon}
+
+
+
{title}
+
+ {value}
+
+
+
+
+ )
+}
+
+// =============================================================================
+// COMPLIANCE PROGRESS BAR
+// =============================================================================
+
+function ComplianceBar({ score }: { score: number }) {
+ const color =
+ score < 40 ? 'bg-red-500' : score < 70 ? 'bg-yellow-500' : 'bg-green-500'
+ const textColor =
+ score < 40 ? 'text-red-700' : score < 70 ? 'text-yellow-700' : 'text-green-700'
+ return (
+
+ )
+}
+
+// =============================================================================
+// HOURS PROGRESS BAR
+// =============================================================================
+
+function HoursBar({ used, budget }: { used: number; budget: number }) {
+ const pct = budget > 0 ? Math.min((used / budget) * 100, 100) : 0
+ const over = used > budget
+ return (
+
+
+
+ {used}h / {budget}h
+
+
+ )
+}
+
+// =============================================================================
+// BADGE
+// =============================================================================
+
+function Badge({ label, className = '' }: { label: string; className?: string }) {
+ return (
+
+ {label}
+
+ )
+}
+
+// =============================================================================
+// FORM COMPONENTS
+// =============================================================================
+
+function FormLabel({ children, htmlFor }: { children: React.ReactNode; htmlFor?: string }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function FormInput({
+ id,
+ type = 'text',
+ value,
+ onChange,
+ placeholder,
+ required,
+ min,
+ max,
+ step,
+}: {
+ id?: string
+ type?: string
+ value: string | number
+ onChange: (val: string) => void
+ placeholder?: string
+ required?: boolean
+ min?: string | number
+ max?: string | number
+ step?: string | number
+}) {
+ return (
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ required={required}
+ min={min}
+ max={max}
+ step={step}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
+ />
+ )
+}
+
+function FormTextarea({
+ id,
+ value,
+ onChange,
+ placeholder,
+ rows = 3,
+}: {
+ id?: string
+ value: string
+ onChange: (val: string) => void
+ placeholder?: string
+ rows?: number
+}) {
+ return (
+
+
+
Cookie Banner Preview
+
Dies ist eine Vorschau des Cookie Banners. In der produktiven Umgebung wird der Banner auf Ihrer Website angezeigt.
+
+
+ ${embedCode.html}
+
+
+-Tag:
+
+
+Alternativ koennen Sie die Dateien separat einbinden:
+- /cookie-banner.css
+- /cookie-banner.js
+`,
+ en: `
+Add the following code to your website:
+
+1. CSS in the
section:
+
+
+2. HTML before the closing tag:
+ ${embedCode.html}
+
+3. JavaScript before the closing tag:
+
+
+Alternatively, you can include the files separately:
+- /cookie-banner.css
+- /cookie-banner.js
+`,
+ },
+ })
+ }
+
+ // Combined: Alles in einem HTML-Block
+ const combinedCode = `
+
+
+
+${embedCode.html}
+
+
+
+`.trim()
+
+ return NextResponse.json({
+ embedCode: combinedCode,
+ scriptTag: embedCode.scriptTag,
+ config: {
+ tenantId: config.tenantId,
+ categories: config.categories.map((c) => ({
+ id: c.id,
+ name: c.name,
+ isRequired: c.isRequired,
+ defaultEnabled: c.defaultEnabled,
+ })),
+ styling: config.styling,
+ },
+ instructions: {
+ de: `Fuegen Sie den folgenden Code vor dem schliessenden -Tag Ihrer Website ein.`,
+ en: `Add the following code before the closing tag of your website.`,
+ },
+ })
+ } catch (error) {
+ console.error('Error generating embed code:', error)
+ return NextResponse.json(
+ { error: 'Failed to generate embed code' },
+ { status: 500 }
+ )
+ }
+}
+
+/**
+ * POST /api/sdk/v1/einwilligungen/cookie-banner/embed-code
+ *
+ * Generiert Embed-Code mit benutzerdefinierten Optionen
+ */
+export async function POST(request: NextRequest) {
+ try {
+ const tenantId = request.headers.get('X-Tenant-ID')
+
+ if (!tenantId) {
+ return NextResponse.json(
+ { error: 'Tenant ID required' },
+ { status: 400 }
+ )
+ }
+
+ const body = await request.json()
+ const {
+ privacyPolicyUrl = '/datenschutz',
+ styling,
+ texts,
+ language = 'de',
+ } = body
+
+ // Hole oder erstelle Konfiguration
+ let config = configStorage.get(tenantId)
+
+ if (!config) {
+ config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS, texts, styling)
+ } else {
+ // Wende temporaere Anpassungen an
+ if (styling) {
+ config = {
+ ...config,
+ styling: { ...config.styling, ...styling },
+ }
+ }
+ if (texts) {
+ config = {
+ ...config,
+ texts: { ...config.texts, ...texts },
+ }
+ }
+ }
+
+ const embedCode = generateEmbedCode(config, privacyPolicyUrl)
+
+ // Generiere Preview HTML
+ const previewHtml = `
+
+
+