From 25d5da78ef12e7d0a5e6a16e09162fe0168c14c0 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 3 Mar 2026 18:04:53 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Alle=205=20verbleibenden=20SDK-Module?= =?UTF-8?q?=20auf=20100%=20=E2=80=94=20RAG,=20Security-Backlog,=20Quality,?= =?UTF-8?q?=20Notfallplan,=20Loeschfristen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paket A — RAG Proxy: - NEU: admin-compliance/app/api/sdk/v1/rag/[[...path]]/route.ts → Proxy zu ai-compliance-sdk:8090, GET+POST, UUID-Validierung - UPDATE: rag/page.tsx — setTimeout Mock → echte API-Calls GET /regulations → dynamische suggestedQuestions POST /search → Qdrant-Ergebnisse mit score, title, reference Paket B — Security-Backlog + Quality: - NEU: migrations/014_security_backlog.sql + 015_quality.sql - NEU: compliance/api/security_backlog_routes.py — CRUD + Stats - NEU: compliance/api/quality_routes.py — Metrics + Tests CRUD + Stats - UPDATE: security-backlog/page.tsx — mockItems → API - UPDATE: quality/page.tsx — mockMetrics/mockTests → API - UPDATE: compliance/api/__init__.py — Router-Registrierung - NEU: tests/test_security_backlog_routes.py (48 Tests — 48/48 bestanden) - NEU: tests/test_quality_routes.py (67 Tests — 67/67 bestanden) Paket C — Notfallplan Incidents + Templates: - NEU: migrations/016_notfallplan_incidents.sql compliance_notfallplan_incidents + compliance_notfallplan_templates - UPDATE: notfallplan_routes.py — GET/POST/PUT/DELETE für /incidents + /templates - UPDATE: notfallplan/page.tsx — Incidents-Tab + Templates-Tab → API - UPDATE: tests/test_notfallplan_routes.py (+76 neue Tests — alle bestanden) Paket D — Loeschfristen localStorage → API: - NEU: migrations/017_loeschfristen.sql (JSONB: legal_holds, storage_locations, ...) - NEU: compliance/api/loeschfristen_routes.py — CRUD + Stats + Status-Update - UPDATE: loeschfristen/page.tsx — vollständige localStorage → API Migration createNewPolicy → POST (API-UUID als id), deletePolicy → DELETE, handleSaveAndClose → PUT, adoptGeneratedPolicies → POST je Policy apiToPolicy() + policyToPayload() Mapper, saving-State für Buttons - NEU: tests/test_loeschfristen_routes.py (58 Tests — alle bestanden) Gesamt: 253 neue Tests, alle bestanden (48 + 67 + 76 + 58 + bestehende) Co-Authored-By: Claude Opus 4.6 --- .../app/(sdk)/sdk/loeschfristen/page.tsx | 253 ++++- .../app/(sdk)/sdk/notfallplan/page.tsx | 242 ++++- .../app/(sdk)/sdk/quality/page.tsx | 518 ++++++---- admin-compliance/app/(sdk)/sdk/rag/page.tsx | 147 ++- .../app/(sdk)/sdk/security-backlog/page.tsx | 582 +++++++---- .../app/api/sdk/v1/rag/[[...path]]/route.ts | 100 ++ backend-compliance/compliance/api/__init__.py | 9 + .../compliance/api/loeschfristen_routes.py | 354 +++++++ .../compliance/api/notfallplan_routes.py | 319 ++++++ .../compliance/api/quality_routes.py | 378 +++++++ .../compliance/api/security_backlog_routes.py | 270 +++++ .../migrations/014_security_backlog.sql | 28 + backend-compliance/migrations/015_quality.sql | 36 + .../migrations/016_notfallplan_incidents.sql | 43 + .../migrations/017_loeschfristen.sql | 45 + .../tests/test_loeschfristen_routes.py | 630 ++++++++++++ .../tests/test_notfallplan_routes.py | 653 +++++++++++- .../tests/test_quality_routes.py | 937 ++++++++++++++++++ .../tests/test_security_backlog_routes.py | 698 +++++++++++++ 19 files changed, 5718 insertions(+), 524 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/rag/[[...path]]/route.ts create mode 100644 backend-compliance/compliance/api/loeschfristen_routes.py create mode 100644 backend-compliance/compliance/api/quality_routes.py create mode 100644 backend-compliance/compliance/api/security_backlog_routes.py create mode 100644 backend-compliance/migrations/014_security_backlog.sql create mode 100644 backend-compliance/migrations/015_quality.sql create mode 100644 backend-compliance/migrations/016_notfallplan_incidents.sql create mode 100644 backend-compliance/migrations/017_loeschfristen.sql create mode 100644 backend-compliance/tests/test_loeschfristen_routes.py create mode 100644 backend-compliance/tests/test_quality_routes.py create mode 100644 backend-compliance/tests/test_security_backlog_routes.py diff --git a/admin-compliance/app/(sdk)/sdk/loeschfristen/page.tsx b/admin-compliance/app/(sdk)/sdk/loeschfristen/page.tsx index dc10ccf..4d903dc 100644 --- a/admin-compliance/app/(sdk)/sdk/loeschfristen/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/loeschfristen/page.tsx @@ -13,8 +13,7 @@ import { ReviewInterval, DeletionTriggerLevel, RetentionUnit, LegalHoldStatus, createEmptyPolicy, createEmptyLegalHold, createEmptyStorageLocation, formatRetentionDuration, isPolicyOverdue, getActiveLegalHolds, - getEffectiveDeletionTrigger, LOESCHFRISTEN_STORAGE_KEY, - generatePolicyId, + getEffectiveDeletionTrigger, } from '@/lib/sdk/loeschfristen-types' import { BASELINE_TEMPLATES, templateToPolicy, getTemplateById, getAllTemplateTags } from '@/lib/sdk/loeschfristen-baseline-catalog' import { @@ -35,8 +34,6 @@ import { type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' -const STORAGE_KEY = 'bp_loeschfristen' - // --------------------------------------------------------------------------- // Helper: TagInput // --------------------------------------------------------------------------- @@ -127,45 +124,127 @@ export default function LoeschfristenPage() { // ---- Legal Hold management ---- const [managingLegalHolds, setManagingLegalHolds] = useState(false) + // ---- Saving state ---- + const [saving, setSaving] = useState(false) + // ---- VVT data ---- const [vvtActivities, setVvtActivities] = useState([]) // -------------------------------------------------------------------------- - // Persistence + // Persistence (API-backed) // -------------------------------------------------------------------------- + const LOESCHFRISTEN_API = '/api/sdk/v1/compliance/loeschfristen' + useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY) - if (stored) { - try { - const parsed = JSON.parse(stored) as LoeschfristPolicy[] - setPolicies(parsed) - } catch (e) { - console.error('Failed to parse stored policies:', e) - } - } - setLoaded(true) + loadPolicies() }, []) - useEffect(() => { - if (loaded && policies.length > 0) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(policies)) - } else if (loaded && policies.length === 0) { - localStorage.removeItem(STORAGE_KEY) - } - }, [policies, loaded]) - - // Load VVT activities from localStorage - useEffect(() => { + async function loadPolicies() { try { - const raw = localStorage.getItem('bp_vvt') - if (raw) { - const parsed = JSON.parse(raw) - if (Array.isArray(parsed)) setVvtActivities(parsed) + const res = await fetch(`${LOESCHFRISTEN_API}?limit=500`) + if (res.ok) { + const data = await res.json() + const fetched = Array.isArray(data.policies) + ? data.policies.map(apiToPolicy) + : [] + setPolicies(fetched) } - } catch { - // ignore + } catch (e) { + console.error('Failed to load Loeschfristen from API:', e) } + setLoaded(true) + } + + function apiToPolicy(raw: any): LoeschfristPolicy { + // Map snake_case API response to camelCase LoeschfristPolicy + const base = createEmptyPolicy() + return { + ...base, + id: raw.id, // DB UUID — used for API calls + policyId: raw.policy_id || base.policyId, // Display ID like "LF-2026-001" + dataObjectName: raw.data_object_name || '', + description: raw.description || '', + affectedGroups: raw.affected_groups || [], + dataCategories: raw.data_categories || [], + primaryPurpose: raw.primary_purpose || '', + deletionTrigger: raw.deletion_trigger || 'PURPOSE_END', + retentionDriver: raw.retention_driver || null, + retentionDriverDetail: raw.retention_driver_detail || '', + retentionDuration: raw.retention_duration ?? null, + retentionUnit: raw.retention_unit || null, + retentionDescription: raw.retention_description || '', + startEvent: raw.start_event || '', + hasActiveLegalHold: raw.has_active_legal_hold || false, + legalHolds: raw.legal_holds || [], + storageLocations: raw.storage_locations || [], + deletionMethod: raw.deletion_method || 'MANUAL_REVIEW_DELETE', + deletionMethodDetail: raw.deletion_method_detail || '', + responsibleRole: raw.responsible_role || '', + responsiblePerson: raw.responsible_person || '', + releaseProcess: raw.release_process || '', + linkedVVTActivityIds: raw.linked_vvt_activity_ids || [], + status: raw.status || 'DRAFT', + lastReviewDate: raw.last_review_date || base.lastReviewDate, + nextReviewDate: raw.next_review_date || base.nextReviewDate, + reviewInterval: raw.review_interval || 'ANNUAL', + tags: raw.tags || [], + createdAt: raw.created_at || base.createdAt, + updatedAt: raw.updated_at || base.updatedAt, + } + } + + function policyToPayload(p: LoeschfristPolicy): any { + return { + policy_id: p.policyId, + data_object_name: p.dataObjectName, + description: p.description, + affected_groups: p.affectedGroups, + data_categories: p.dataCategories, + primary_purpose: p.primaryPurpose, + deletion_trigger: p.deletionTrigger, + retention_driver: p.retentionDriver || null, + retention_driver_detail: p.retentionDriverDetail, + retention_duration: p.retentionDuration || null, + retention_unit: p.retentionUnit || null, + retention_description: p.retentionDescription, + start_event: p.startEvent, + has_active_legal_hold: p.hasActiveLegalHold, + legal_holds: p.legalHolds, + storage_locations: p.storageLocations, + deletion_method: p.deletionMethod, + deletion_method_detail: p.deletionMethodDetail, + responsible_role: p.responsibleRole, + responsible_person: p.responsiblePerson, + release_process: p.releaseProcess, + linked_vvt_activity_ids: p.linkedVVTActivityIds, + status: p.status, + last_review_date: p.lastReviewDate || null, + next_review_date: p.nextReviewDate || null, + review_interval: p.reviewInterval, + tags: p.tags, + } + } + + // Load VVT activities from API + useEffect(() => { + fetch('/api/sdk/v1/compliance/vvt?limit=200') + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data && Array.isArray(data.activities)) { + setVvtActivities(data.activities) + } + }) + .catch(() => { + // fallback: try localStorage + try { + const raw = localStorage.getItem('bp_vvt') + if (raw) { + const parsed = JSON.parse(raw) + if (Array.isArray(parsed)) setVvtActivities(parsed) + } + } catch { /* ignore */ } + }) }, [tab, editingId]) // -------------------------------------------------------------------------- @@ -222,19 +301,45 @@ export default function LoeschfristenPage() { [], ) - const createNewPolicy = useCallback(() => { - const newP = createEmptyPolicy() - setPolicies((prev) => [...prev, newP]) - setEditingId(newP.policyId) - setTab('editor') + const createNewPolicy = useCallback(async () => { + setSaving(true) + try { + const empty = createEmptyPolicy() + const res = await fetch(LOESCHFRISTEN_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(policyToPayload(empty)), + }) + if (res.ok) { + const raw = await res.json() + const newP = apiToPolicy(raw) + setPolicies((prev) => [...prev, newP]) + setEditingId(newP.policyId) + setTab('editor') + } + } catch (e) { + console.error('Failed to create policy:', e) + } finally { + setSaving(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const deletePolicy = useCallback( - (id: string) => { - setPolicies((prev) => prev.filter((p) => p.policyId !== id)) - if (editingId === id) setEditingId(null) + async (policyId: string) => { + const policy = policies.find((p) => p.policyId === policyId) + if (!policy) return + try { + const res = await fetch(`${LOESCHFRISTEN_API}/${policy.id}`, { method: 'DELETE' }) + if (res.ok || res.status === 204 || res.status === 404) { + setPolicies((prev) => prev.filter((p) => p.policyId !== policyId)) + if (editingId === policyId) setEditingId(null) + } + } catch (e) { + console.error('Failed to delete policy:', e) + } }, - [editingId], + [editingId, policies], ) const addLegalHold = useCallback( @@ -302,17 +407,41 @@ export default function LoeschfristenPage() { }, [profilingAnswers]) const adoptGeneratedPolicies = useCallback( - (onlySelected: boolean) => { + async (onlySelected: boolean) => { const toAdopt = onlySelected ? generatedPolicies.filter((p) => selectedGenerated.has(p.policyId)) : generatedPolicies - setPolicies((prev) => [...prev, ...toAdopt]) + setSaving(true) + try { + const created: LoeschfristPolicy[] = [] + for (const p of toAdopt) { + try { + const res = await fetch(LOESCHFRISTEN_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(policyToPayload(p)), + }) + if (res.ok) { + const raw = await res.json() + created.push(apiToPolicy(raw)) + } else { + created.push(p) // fallback: keep generated policy in state + } + } catch { + created.push(p) + } + } + setPolicies((prev) => [...prev, ...created]) + } finally { + setSaving(false) + } setGeneratedPolicies([]) setSelectedGenerated(new Set()) setProfilingStep(0) setProfilingAnswers([]) setTab('uebersicht') }, + // eslint-disable-next-line react-hooks/exhaustive-deps [generatedPolicies, selectedGenerated], ) @@ -321,6 +450,36 @@ export default function LoeschfristenPage() { setComplianceResult(result) }, [policies]) + const handleSaveAndClose = useCallback(async () => { + if (!editingPolicy) { + setEditingId(null) + setTab('uebersicht') + return + } + setSaving(true) + try { + const res = await fetch(`${LOESCHFRISTEN_API}/${editingPolicy.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(policyToPayload(editingPolicy)), + }) + if (res.ok) { + const raw = await res.json() + const updated = apiToPolicy(raw) + setPolicies((prev) => + prev.map((p) => (p.policyId === editingPolicy.policyId ? updated : p)), + ) + } + } catch (e) { + console.error('Failed to save policy:', e) + } finally { + setSaving(false) + } + setEditingId(null) + setTab('uebersicht') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editingPolicy]) + // -------------------------------------------------------------------------- // Tab definitions // -------------------------------------------------------------------------- @@ -1385,13 +1544,11 @@ export default function LoeschfristenPage() { Zurueck zur Uebersicht diff --git a/admin-compliance/app/(sdk)/sdk/notfallplan/page.tsx b/admin-compliance/app/(sdk)/sdk/notfallplan/page.tsx index f23b12e..97fbcc7 100644 --- a/admin-compliance/app/(sdk)/sdk/notfallplan/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/notfallplan/page.tsx @@ -387,10 +387,9 @@ export default function NotfallplanPage() { const [savingExercise, setSavingExercise] = useState(false) useEffect(() => { + // Only load config and exercises from localStorage (incidents/templates use API) const data = loadFromStorage() setConfig(data.config) - setIncidents(data.incidents) - setTemplates(data.templates) setExercises(data.exercises) }, []) @@ -401,10 +400,12 @@ export default function NotfallplanPage() { async function loadApiData() { setApiLoading(true) try { - const [contactsRes, scenariosRes, exercisesRes] = await Promise.all([ + const [contactsRes, scenariosRes, exercisesRes, incidentsRes, templatesRes] = await Promise.all([ fetch(`${NOTFALLPLAN_API}/contacts`), fetch(`${NOTFALLPLAN_API}/scenarios`), fetch(`${NOTFALLPLAN_API}/exercises`), + fetch(`${NOTFALLPLAN_API}/incidents`), + fetch(`${NOTFALLPLAN_API}/templates`), ]) if (contactsRes.ok) { const data = await contactsRes.json() @@ -418,6 +419,14 @@ export default function NotfallplanPage() { const data = await exercisesRes.json() setApiExercises(Array.isArray(data) ? data : []) } + if (incidentsRes.ok) { + const data = await incidentsRes.json() + setIncidents(Array.isArray(data) ? data.map(apiToIncident) : []) + } + if (templatesRes.ok) { + const data = await templatesRes.json() + setTemplates(Array.isArray(data) && data.length > 0 ? data : DEFAULT_TEMPLATES) + } } catch (err) { console.error('Failed to load Notfallplan API data:', err) } finally { @@ -425,6 +434,106 @@ export default function NotfallplanPage() { } } + function apiToIncident(raw: any): Incident { + return { + id: raw.id, + title: raw.title, + description: raw.description || '', + detectedAt: raw.detected_at, + detectedBy: raw.detected_by || 'Admin', + status: raw.status, + severity: raw.severity, + affectedDataCategories: raw.affected_data_categories || [], + estimatedAffectedPersons: raw.estimated_affected_persons || 0, + measures: raw.measures || [], + art34Required: raw.art34_required || false, + art34Justification: raw.art34_justification || '', + reportedToAuthorityAt: raw.reported_to_authority_at, + notifiedAffectedAt: raw.notified_affected_at, + closedAt: raw.closed_at, + closedBy: raw.closed_by, + lessonsLearned: raw.lessons_learned, + } + } + + async function apiAddIncident(newIncident: Partial) { + try { + const res = await fetch(`${NOTFALLPLAN_API}/incidents`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: newIncident.title, + description: newIncident.description, + severity: newIncident.severity || 'medium', + estimated_affected_persons: newIncident.estimatedAffectedPersons || 0, + art34_required: newIncident.art34Required || false, + art34_justification: newIncident.art34Justification || '', + }), + }) + if (res.ok) { + const created = await res.json() + setIncidents(prev => [apiToIncident(created), ...prev]) + } + } catch (err) { + console.error('Failed to create incident:', err) + } + } + + async function apiUpdateIncidentStatus(id: string, status: IncidentStatus) { + try { + const body: any = { status } + if (status === 'reported') body.reported_to_authority_at = new Date().toISOString() + if (status === 'closed') { body.closed_at = new Date().toISOString(); body.closed_by = 'Admin' } + const res = await fetch(`${NOTFALLPLAN_API}/incidents/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + if (res.ok) { + const updated = await res.json() + setIncidents(prev => prev.map(inc => inc.id === id ? apiToIncident(updated) : inc)) + } + } catch (err) { + console.error('Failed to update incident status:', err) + } + } + + async function apiDeleteIncident(id: string) { + try { + const res = await fetch(`${NOTFALLPLAN_API}/incidents/${id}`, { method: 'DELETE' }) + if (res.ok || res.status === 204) { + setIncidents(prev => prev.filter(inc => inc.id !== id)) + } + } catch (err) { + console.error('Failed to delete incident:', err) + } + } + + async function apiSaveTemplate(template: MeldeTemplate) { + try { + const isNew = !template.id.startsWith('tpl-') + const method = isNew || template.id.length < 10 ? 'POST' : 'PUT' + const url = method === 'POST' + ? `${NOTFALLPLAN_API}/templates` + : `${NOTFALLPLAN_API}/templates/${template.id}` + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: template.type, title: template.title, content: template.content }), + }) + if (res.ok) { + const saved = await res.json() + setTemplates(prev => { + const existing = prev.find(t => t.id === template.id) + if (existing) return prev.map(t => t.id === template.id ? { ...t, ...saved } : t) + return [...prev, saved] + }) + } + } catch (err) { + console.error('Failed to save template:', err) + } + } + async function handleCreateContact() { if (!newContact.name) return setSavingContact(true) @@ -529,10 +638,11 @@ export default function NotfallplanPage() { } const handleSave = useCallback(() => { - saveToStorage({ config, incidents, templates, exercises }) + // Only config and exercises are persisted to localStorage; incidents/templates use API + saveToStorage({ config, incidents: [], templates: DEFAULT_TEMPLATES, exercises }) setSaved(true) setTimeout(() => setSaved(false), 2000) - }, [config, incidents, templates, exercises]) + }, [config, exercises]) const tabs: { id: Tab; label: string; count?: number }[] = [ { id: 'config', label: 'Notfallplan' }, @@ -697,10 +807,13 @@ export default function NotfallplanPage() { setIncidents={setIncidents} showAdd={showAddIncident} setShowAdd={setShowAddIncident} + onAdd={apiAddIncident} + onStatusChange={apiUpdateIncidentStatus} + onDelete={apiDeleteIncident} /> )} {activeTab === 'templates' && ( - + )} {activeTab === 'exercises' && ( <> @@ -1252,11 +1365,17 @@ function IncidentsTab({ setIncidents, showAdd, setShowAdd, + onAdd, + onStatusChange, + onDelete, }: { incidents: Incident[] setIncidents: React.Dispatch> showAdd: boolean setShowAdd: (v: boolean) => void + onAdd?: (incident: Partial) => Promise + onStatusChange?: (id: string, status: IncidentStatus) => Promise + onDelete?: (id: string) => Promise }) { const [newIncident, setNewIncident] = useState>({ title: '', @@ -1269,23 +1388,27 @@ function IncidentsTab({ art34Justification: '', }) - function addIncident() { + async function addIncident() { if (!newIncident.title) return - const incident: Incident = { - id: `INC-${Date.now()}`, - title: newIncident.title || '', - description: newIncident.description || '', - detectedAt: new Date().toISOString(), - detectedBy: 'Admin', - status: 'detected', - severity: newIncident.severity as IncidentSeverity || 'medium', - affectedDataCategories: newIncident.affectedDataCategories || [], - estimatedAffectedPersons: newIncident.estimatedAffectedPersons || 0, - measures: newIncident.measures || [], - art34Required: newIncident.art34Required || false, - art34Justification: newIncident.art34Justification || '', + if (onAdd) { + await onAdd(newIncident) + } else { + const incident: Incident = { + id: `INC-${Date.now()}`, + title: newIncident.title || '', + description: newIncident.description || '', + detectedAt: new Date().toISOString(), + detectedBy: 'Admin', + status: 'detected', + severity: newIncident.severity as IncidentSeverity || 'medium', + affectedDataCategories: newIncident.affectedDataCategories || [], + estimatedAffectedPersons: newIncident.estimatedAffectedPersons || 0, + measures: newIncident.measures || [], + art34Required: newIncident.art34Required || false, + art34Justification: newIncident.art34Justification || '', + } + setIncidents(prev => [incident, ...prev]) } - setIncidents(prev => [incident, ...prev]) setShowAdd(false) setNewIncident({ title: '', description: '', severity: 'medium', @@ -1294,17 +1417,21 @@ function IncidentsTab({ }) } - function updateStatus(id: string, status: IncidentStatus) { - setIncidents(prev => prev.map(inc => - inc.id === id - ? { - ...inc, - status, - ...(status === 'reported' ? { reportedToAuthorityAt: new Date().toISOString() } : {}), - ...(status === 'closed' ? { closedAt: new Date().toISOString(), closedBy: 'Admin' } : {}), - } - : inc - )) + async function updateStatus(id: string, status: IncidentStatus) { + if (onStatusChange) { + await onStatusChange(id, status) + } else { + setIncidents(prev => prev.map(inc => + inc.id === id + ? { + ...inc, + status, + ...(status === 'reported' ? { reportedToAuthorityAt: new Date().toISOString() } : {}), + ...(status === 'closed' ? { closedAt: new Date().toISOString(), closedBy: 'Admin' } : {}), + } + : inc + )) + } } return ( @@ -1464,6 +1591,14 @@ function IncidentsTab({ Abschliessen )} + {onDelete && ( + + )} )} @@ -1481,10 +1616,24 @@ function IncidentsTab({ function TemplatesTab({ templates, setTemplates, + onSave, }: { templates: MeldeTemplate[] setTemplates: React.Dispatch> + onSave?: (template: MeldeTemplate) => Promise }) { + const [saving, setSaving] = useState(null) + + async function handleSave(template: MeldeTemplate) { + if (!onSave) return + setSaving(template.id) + try { + await onSave(template) + } finally { + setSaving(null) + } + } + return (
@@ -1496,15 +1645,26 @@ function TemplatesTab({ {templates.map(template => (
-
- - {template.type === 'art33' ? 'Art. 33' : 'Art. 34'} - -

{template.title}

+
+
+ + {template.type === 'art33' ? 'Art. 33' : 'Art. 34'} + +

{template.title}

+
+ {onSave && ( + + )}