diff --git a/admin-compliance/app/sdk/isms/_components/AssetsTab.tsx b/admin-compliance/app/sdk/isms/_components/AssetsTab.tsx new file mode 100644 index 0000000..556655a --- /dev/null +++ b/admin-compliance/app/sdk/isms/_components/AssetsTab.tsx @@ -0,0 +1,315 @@ +'use client' + +import React, { useState, useMemo, useCallback } from 'react' +import { + type InformationAsset, + type AssetCategory, + type AssetClassification, + type ProtectionLevel, + ASSET_CATEGORY_LABELS, + CLASSIFICATION_LABELS, + PROTECTION_LABELS, +} from '../_types' + +// ============================================================================ +// Local storage key (persisted in SDK state via JSONB) +// ============================================================================ + +const STORAGE_KEY = 'isms_assets' + +function loadAssets(): InformationAsset[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + return raw ? JSON.parse(raw) : [] + } catch { return [] } +} + +function saveAssets(assets: InformationAsset[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(assets)) +} + +// ============================================================================ +// Protection level colors +// ============================================================================ + +const protectionColors: Record = { + normal: 'bg-green-100 text-green-800', + high: 'bg-amber-100 text-amber-800', + very_high: 'bg-red-100 text-red-800', +} + +const classificationColors: Record = { + PUBLIC: 'bg-gray-100 text-gray-600', + INTERNAL: 'bg-blue-100 text-blue-700', + CONFIDENTIAL: 'bg-amber-100 text-amber-800', + STRICTLY_CONFIDENTIAL: 'bg-red-100 text-red-800', +} + +// ============================================================================ +// Component +// ============================================================================ + +export function AssetsTab() { + const [assets, setAssets] = useState(() => loadAssets()) + const [showForm, setShowForm] = useState(false) + const [filterCategory, setFilterCategory] = useState('ALL') + const [editingId, setEditingId] = useState(null) + + // Form state + const [form, setForm] = useState>({ + category: 'SOFTWARE', + classification: 'INTERNAL', + protectionNeed: { confidentiality: 'normal', integrity: 'normal', availability: 'normal' }, + }) + + const filtered = useMemo(() => { + if (filterCategory === 'ALL') return assets + return assets.filter((a) => a.category === filterCategory) + }, [assets, filterCategory]) + + const stats = useMemo(() => ({ + total: assets.length, + byCategory: Object.entries(ASSET_CATEGORY_LABELS).map(([cat, label]) => ({ + category: cat, + label, + count: assets.filter((a) => a.category === cat).length, + })), + highProtection: assets.filter( + (a) => + a.protectionNeed.confidentiality === 'very_high' || + a.protectionNeed.integrity === 'very_high' || + a.protectionNeed.availability === 'very_high' + ).length, + }), [assets]) + + const handleSave = useCallback(() => { + if (!form.name || !form.category || !form.owner) return + + const now = new Date().toISOString() + const asset: InformationAsset = { + id: editingId || `asset_${Date.now()}`, + name: form.name || '', + category: form.category as AssetCategory, + description: form.description || '', + owner: form.owner || '', + location: form.location || '', + classification: form.classification as AssetClassification || 'INTERNAL', + protectionNeed: form.protectionNeed || { confidentiality: 'normal', integrity: 'normal', availability: 'normal' }, + vendor: form.vendor, + notes: form.notes, + createdAt: editingId ? (assets.find((a) => a.id === editingId)?.createdAt || now) : now, + updatedAt: now, + } + + const updated = editingId + ? assets.map((a) => (a.id === editingId ? asset : a)) + : [...assets, asset] + + setAssets(updated) + saveAssets(updated) + setShowForm(false) + setEditingId(null) + setForm({ + category: 'SOFTWARE', + classification: 'INTERNAL', + protectionNeed: { confidentiality: 'normal', integrity: 'normal', availability: 'normal' }, + }) + }, [form, editingId, assets]) + + const handleDelete = useCallback((id: string) => { + const updated = assets.filter((a) => a.id !== id) + setAssets(updated) + saveAssets(updated) + }, [assets]) + + const handleEdit = useCallback((asset: InformationAsset) => { + setForm(asset) + setEditingId(asset.id) + setShowForm(true) + }, []) + + const handleExport = useCallback(() => { + const csv = [ + ['Name', 'Kategorie', 'Eigentuemer', 'Standort', 'Klassifizierung', 'C', 'I', 'A', 'Beschreibung'].join(';'), + ...assets.map((a) => + [a.name, ASSET_CATEGORY_LABELS[a.category], a.owner, a.location, + CLASSIFICATION_LABELS[a.classification], + PROTECTION_LABELS[a.protectionNeed.confidentiality], + PROTECTION_LABELS[a.protectionNeed.integrity], + PROTECTION_LABELS[a.protectionNeed.availability], + a.description].join(';') + ), + ].join('\n') + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `asset-register-${new Date().toISOString().slice(0, 10)}.csv` + a.click() + URL.revokeObjectURL(url) + }, [assets]) + + return ( +
+ {/* Stats */} +
+
+
Gesamt
+
{stats.total}
+
+ {stats.byCategory.filter((s) => s.count > 0).slice(0, 2).map((s) => ( +
+
{s.label}
+
{s.count}
+
+ ))} +
+
Sehr hoher Schutzbedarf
+
{stats.highProtection}
+
+
+ + {/* Actions */} +
+
+ {(['ALL', ...Object.keys(ASSET_CATEGORY_LABELS)] as const).map((cat) => ( + + ))} +
+
+ + +
+
+ + {/* Form */} + {showForm && ( +
+

{editingId ? 'Asset bearbeiten' : 'Neues Asset'}

+
+
+ + setForm({ ...form, name: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="z.B. PostgreSQL Produktions-DB" /> +
+
+ + +
+
+ + setForm({ ...form, owner: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="Person oder Abteilung" /> +
+
+ + setForm({ ...form, location: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="z.B. Hetzner Cloud EU" /> +
+
+ + +
+
+ + setForm({ ...form, vendor: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="Optional" /> +
+
+ + {/* Protection need */} +
+ +
+ {(['confidentiality', 'integrity', 'availability'] as const).map((dim) => ( +
+ + +
+ ))} +
+
+ +
+ +