From a4df3201db4cb02d8832e21662e9c8c2f85b9c66 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 3 Mar 2026 15:58:50 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Obligations-Modul=20auf=20100%=20?= =?UTF-8?q?=E2=80=94=20vollst=C3=A4ndige=20CRUD-Implementierung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: compliance_obligations Tabelle (Migration 013) - Backend: obligation_routes.py — GET/POST/PUT/DELETE + Stats-Endpoint - Backend: obligation_router in __init__.py registriert - Frontend: obligations/page.tsx — ObligationModal, ObligationDetail, ObligationCard, alle Buttons verdrahtet - Proxy: PATCH-Methode in compliance catch-all route ergänzt - Tests: 39/39 Obligation-Tests (Schemas, Helpers, Business Logic) Co-Authored-By: Claude Sonnet 4.6 --- .../app/(sdk)/sdk/obligations/page.tsx | 980 +++++++++++++----- .../sdk/v1/compliance/[[...path]]/route.ts | 10 +- backend-compliance/compliance/api/__init__.py | 3 + .../compliance/api/obligation_routes.py | 318 ++++++ .../migrations/013_obligations.sql | 31 + .../tests/test_obligation_routes.py | 325 ++++++ 6 files changed, 1382 insertions(+), 285 deletions(-) create mode 100644 backend-compliance/compliance/api/obligation_routes.py create mode 100644 backend-compliance/migrations/013_obligations.sql create mode 100644 backend-compliance/tests/test_obligation_routes.py diff --git a/admin-compliance/app/(sdk)/sdk/obligations/page.tsx b/admin-compliance/app/(sdk)/sdk/obligations/page.tsx index f3aa4f1..f2ce136 100644 --- a/admin-compliance/app/(sdk)/sdk/obligations/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/obligations/page.tsx @@ -1,11 +1,10 @@ 'use client' -import React, { useState, useEffect } from 'react' -import { useSDK } from '@/lib/sdk' +import React, { useState, useEffect, useCallback } from 'react' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' // ============================================================================= -// TYPES +// Types // ============================================================================= interface Obligation { @@ -13,364 +12,777 @@ interface Obligation { title: string description: string source: string - sourceArticle: string - deadline: Date | null + source_article: string + deadline: string | null status: 'pending' | 'in-progress' | 'completed' | 'overdue' priority: 'critical' | 'high' | 'medium' | 'low' responsible: string - linkedSystems: string[] + linked_systems: string[] + assessment_id?: string + rule_code?: string + notes?: string + created_at?: string + updated_at?: string +} + +interface ObligationStats { + pending: number + in_progress: number + overdue: number + completed: number + total: number + critical: number + high: number +} + +interface ObligationFormData { + title: string + description: string + source: string + source_article: string + deadline: string + status: string + priority: string + responsible: string + linked_systems: string + notes: string +} + +const EMPTY_FORM: ObligationFormData = { + title: '', + description: '', + source: 'DSGVO', + source_article: '', + deadline: '', + status: 'pending', + priority: 'medium', + responsible: '', + linked_systems: '', + notes: '', +} + +const API = '/api/sdk/v1/compliance/obligations' + +// ============================================================================= +// Status helpers +// ============================================================================= + +const PRIORITY_COLORS: Record = { + critical: 'bg-red-100 text-red-700', + high: 'bg-orange-100 text-orange-700', + medium: 'bg-yellow-100 text-yellow-700', + low: 'bg-green-100 text-green-700', +} + +const PRIORITY_LABELS: Record = { + critical: 'Kritisch', + high: 'Hoch', + medium: 'Mittel', + low: 'Niedrig', +} + +const STATUS_COLORS: Record = { + pending: 'bg-gray-100 text-gray-600', + 'in-progress':'bg-blue-100 text-blue-700', + completed: 'bg-green-100 text-green-700', + overdue: 'bg-red-100 text-red-700', +} + +const STATUS_LABELS: Record = { + pending: 'Ausstehend', + 'in-progress':'In Bearbeitung', + completed: 'Abgeschlossen', + overdue: 'Ueberfaellig', +} + +const STATUS_NEXT: Record = { + pending: 'in-progress', + 'in-progress':'completed', + completed: 'pending', + overdue: 'in-progress', } // ============================================================================= -// COMPONENTS +// Create/Edit Modal // ============================================================================= -function ObligationCard({ obligation }: { obligation: Obligation }) { - const priorityColors = { - critical: 'bg-red-100 text-red-700', - high: 'bg-orange-100 text-orange-700', - medium: 'bg-yellow-100 text-yellow-700', - low: 'bg-green-100 text-green-700', - } +function ObligationModal({ + initial, + onClose, + onSave, +}: { + initial?: Partial + onClose: () => void + onSave: (data: ObligationFormData) => Promise +}) { + const [form, setForm] = useState({ ...EMPTY_FORM, ...initial }) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) - const statusColors = { - pending: 'bg-gray-100 text-gray-600 border-gray-200', - 'in-progress': 'bg-blue-100 text-blue-700 border-blue-200', - completed: 'bg-green-100 text-green-700 border-green-200', - overdue: 'bg-red-100 text-red-700 border-red-200', - } + const update = (field: keyof ObligationFormData, value: string) => + setForm(prev => ({ ...prev, [field]: value })) - const statusLabels = { - pending: 'Ausstehend', - 'in-progress': 'In Bearbeitung', - completed: 'Abgeschlossen', - overdue: 'Ueberfaellig', + const handleSave = async () => { + if (!form.title.trim()) { setError('Titel ist erforderlich'); return } + setSaving(true) + setError(null) + try { + await onSave(form) + onClose() + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Speichern') + } finally { + setSaving(false) + } } - const daysUntilDeadline = obligation.deadline - ? Math.ceil((obligation.deadline.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) - : null - return ( -
-
-
-
- - {obligation.priority === 'critical' ? 'Kritisch' : - obligation.priority === 'high' ? 'Hoch' : - obligation.priority === 'medium' ? 'Mittel' : 'Niedrig'} - - - {statusLabels[obligation.status]} - - - {obligation.source} {obligation.sourceArticle} - +
e.target === e.currentTarget && onClose()}> +
+
+

+ {initial?.title ? 'Pflicht bearbeiten' : 'Neue Pflicht erstellen'} +

+ +
+
+ {error &&
{error}
} + +
+ + update('title', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500" + placeholder="z.B. Datenschutz-Folgenabschaetzung durchfuehren" + />
-

{obligation.title}

-

{obligation.description}

-
-
-
-
- Verantwortlich: - {obligation.responsible} -
- {obligation.deadline && ( -
- Frist: - - {obligation.deadline.toLocaleDateString('de-DE')} - {daysUntilDeadline !== null && ( - - ({daysUntilDeadline < 0 ? `${Math.abs(daysUntilDeadline)} Tage ueberfaellig` : `${daysUntilDeadline} Tage`}) - - )} - +
+ +