diff --git a/admin-compliance/app/sdk/obligations/_components/FilterBar.tsx b/admin-compliance/app/sdk/obligations/_components/FilterBar.tsx new file mode 100644 index 0000000..be1ea3b --- /dev/null +++ b/admin-compliance/app/sdk/obligations/_components/FilterBar.tsx @@ -0,0 +1,64 @@ +'use client' + +import React from 'react' +import { REGULATION_CHIPS } from '../_types' + +export default function FilterBar({ + filter, + regulationFilter, + searchQuery, + onFilter, + onRegulationFilter, + onSearch, +}: { + filter: string + regulationFilter: string + searchQuery: string + onFilter: (v: string) => void + onRegulationFilter: (v: string) => void + onSearch: (v: string) => void +}) { + return ( + <> + {/* Regulation Filter Chips */} +
+ Regulation: + {REGULATION_CHIPS.map(r => ( + + ))} +
+ + {/* Search + Status Filter */} +
+ onSearch(e.target.value)} + placeholder="Suche nach Pflichten..." + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500" + /> +
+ {['all', 'overdue', 'pending', 'in-progress', 'completed', 'critical'].map(f => ( + + ))} +
+
+ + ) +} diff --git a/admin-compliance/app/sdk/obligations/_components/InfoBanners.tsx b/admin-compliance/app/sdk/obligations/_components/InfoBanners.tsx new file mode 100644 index 0000000..8a82d2d --- /dev/null +++ b/admin-compliance/app/sdk/obligations/_components/InfoBanners.tsx @@ -0,0 +1,77 @@ +'use client' + +import React from 'react' +import type { ApplicableRegulation } from '@/lib/sdk/compliance-scope-types' +import type { ObligationStats } from '../_types' + +export function ApplicableRegsBanner({ regs }: { regs: ApplicableRegulation[] }) { + if (regs.length === 0) return null + return ( +
+

Anwendbare Regulierungen (aus Auto-Profiling)

+
+ {regs.map(reg => ( + + + + + {reg.name} + {reg.classification && ({reg.classification})} + {reg.obligation_count} Pflichten + + ))} +
+
+ ) +} + +export function NoProfileWarning() { + return ( +
+ Kein Unternehmensprofil vorhanden. Auto-Profiling verwendet Beispieldaten.{' '} + Profil anlegen → +
+ ) +} + +export function OverdueAlert({ stats }: { stats: ObligationStats | null }) { + if ((stats?.overdue ?? 0) <= 0) return null + return ( +
+
+ + + +
+
+

Achtung: {stats?.overdue} ueberfaellige Pflicht(en)

+

Diese Pflichten erfordern sofortige Aufmerksamkeit.

+
+
+ ) +} + +export function EmptyList({ onCreate }: { onCreate: () => void }) { + return ( +
+
+ + + +
+

Keine Pflichten gefunden

+

+ Klicken Sie auf "Pflicht hinzufuegen", um die erste Compliance-Pflicht zu erfassen. +

+ +
+ ) +} diff --git a/admin-compliance/app/sdk/obligations/_components/ObligationCard.tsx b/admin-compliance/app/sdk/obligations/_components/ObligationCard.tsx new file mode 100644 index 0000000..404d655 --- /dev/null +++ b/admin-compliance/app/sdk/obligations/_components/ObligationCard.tsx @@ -0,0 +1,111 @@ +'use client' + +import React, { useState } from 'react' +import TOMControlPanel from '@/components/sdk/obligations/TOMControlPanel' +import { + PRIORITY_COLORS, + PRIORITY_LABELS, + STATUS_COLORS, + STATUS_LABELS, + STATUS_NEXT, + type Obligation, +} from '../_types' + +export default function ObligationCard({ + obligation, + onStatusChange, + onDetails, +}: { + obligation: Obligation + onStatusChange: (id: string, status: string) => Promise + onDetails: () => void +}) { + const [saving, setSaving] = useState(false) + const [showTOM, setShowTOM] = useState(false) + + const daysUntil = obligation.deadline + ? Math.ceil((new Date(obligation.deadline).getTime() - Date.now()) / 86400000) + : null + + const handleMark = async (e: React.MouseEvent) => { + e.stopPropagation() + setSaving(true) + await onStatusChange(obligation.id, STATUS_NEXT[obligation.status]) + setSaving(false) + } + + return ( +
+
+
+
+ + {PRIORITY_LABELS[obligation.priority]} + + + {STATUS_LABELS[obligation.status]} + + {obligation.source_article && ( + + {obligation.source} {obligation.source_article} + + )} +
+

{obligation.title}

+ {obligation.description && ( +

{obligation.description}

+ )} +
+
+ +
+
+ {obligation.responsible && 👤 {obligation.responsible}} + {obligation.deadline && ( + + 📅 {new Date(obligation.deadline).toLocaleDateString('de-DE')} + {daysUntil !== null && ` (${daysUntil < 0 ? `${Math.abs(daysUntil)}d überfällig` : `${daysUntil}d`})`} + + )} +
+
e.stopPropagation()}> + + + {obligation.status !== 'completed' && ( + + )} +
+
+ + {showTOM && ( +
e.stopPropagation()}> + setShowTOM(false)} /> +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx b/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx new file mode 100644 index 0000000..10d392a --- /dev/null +++ b/admin-compliance/app/sdk/obligations/_components/ObligationDetail.tsx @@ -0,0 +1,136 @@ +'use client' + +import React, { useState } from 'react' +import { + PRIORITY_COLORS, + PRIORITY_LABELS, + STATUS_COLORS, + STATUS_LABELS, + STATUS_NEXT, + type Obligation, +} from '../_types' + +export default function ObligationDetail({ obligation, onClose, onStatusChange, onEdit, onDelete }: { + obligation: Obligation + onClose: () => void + onStatusChange: (id: string, status: string) => Promise + onEdit: () => void + onDelete: (id: string) => Promise +}) { + const [saving, setSaving] = useState(false) + + const handleStatusCycle = async () => { + setSaving(true) + await onStatusChange(obligation.id, STATUS_NEXT[obligation.status]) + setSaving(false) + } + + const handleDelete = async () => { + if (!confirm('Pflicht wirklich loeschen?')) return + await onDelete(obligation.id) + onClose() + } + + const daysUntil = obligation.deadline + ? Math.ceil((new Date(obligation.deadline).getTime() - Date.now()) / 86400000) + : null + + return ( +
e.target === e.currentTarget && onClose()}> +
+
+
+
+ {PRIORITY_LABELS[obligation.priority]} + {STATUS_LABELS[obligation.status]} + {obligation.source && ( + {obligation.source} {obligation.source_article} + )} +
+

{obligation.title}

+
+ +
+ +
+ {obligation.description && ( +

{obligation.description}

+ )} + +
+
+ Verantwortlich +

{obligation.responsible || '—'}

+
+
+ Frist +

+ {obligation.deadline + ? `${new Date(obligation.deadline).toLocaleDateString('de-DE')}${daysUntil !== null ? ` (${daysUntil < 0 ? `${Math.abs(daysUntil)}d ueberfaellig` : `${daysUntil}d`})` : ''}` + : '—'} +

+
+
+ + {obligation.linked_systems?.length > 0 && ( +
+ Betroffene Systeme +
+ {obligation.linked_systems.map(s => ( + {s} + ))} +
+
+ )} + + {obligation.notes && ( +
+ Notizen +

{obligation.notes}

+
+ )} + + {obligation.rule_code && ( +
+ Aus UCCA Assessment abgeleitet · Regel {obligation.rule_code} +
+ )} + + {obligation.created_at && ( +

+ Erstellt: {new Date(obligation.created_at).toLocaleDateString('de-DE')} + {obligation.updated_at && obligation.updated_at !== obligation.created_at + ? ` · Geaendert: ${new Date(obligation.updated_at).toLocaleDateString('de-DE')}` + : ''} +

+ )} +
+ +
+
+ +
+
+ + {obligation.status !== 'completed' && ( + + )} +
+
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/obligations/_components/ObligationModal.tsx b/admin-compliance/app/sdk/obligations/_components/ObligationModal.tsx new file mode 100644 index 0000000..0e63738 --- /dev/null +++ b/admin-compliance/app/sdk/obligations/_components/ObligationModal.tsx @@ -0,0 +1,181 @@ +'use client' + +import React, { useState } from 'react' +import { EMPTY_FORM, type ObligationFormData } from '../_types' + +export default 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 update = (field: keyof ObligationFormData, value: string) => + setForm(prev => ({ ...prev, [field]: value })) + + 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) + } + } + + return ( +
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" + /> +
+ +
+ +