'use client' import React, { useState, useEffect, useCallback } from 'react' interface MC { id: string master_control_id: string canonical_name: string total_controls: number phases_covered: string[] phase_control_count: Record } interface Member { control_id: string title: string objective: string severity: string phase: string action: string regulation_source: string regulation_article: string } const API = '/api/sdk/v1/master-controls' const PAGE_SIZE = 50 const SEV_COLORS: Record = { critical: 'bg-red-100 text-red-800', high: 'bg-orange-100 text-orange-800', medium: 'bg-yellow-100 text-yellow-800', low: 'bg-blue-100 text-blue-800', } export default function MasterControlsPage() { const [mcs, setMcs] = useState([]) const [total, setTotal] = useState(0) const [offset, setOffset] = useState(0) const [search, setSearch] = useState('') const [sortBy, setSortBy] = useState('total_controls') const [sortOrder, setSortOrder] = useState('DESC') const [loading, setLoading] = useState(false) // Detail view const [selectedMC, setSelectedMC] = useState(null) const [members, setMembers] = useState([]) const [membersLoading, setMembersLoading] = useState(false) const loadMCs = useCallback(async () => { setLoading(true) try { const params = new URLSearchParams({ action: 'list', limit: String(PAGE_SIZE), offset: String(offset), sort: sortBy, order: sortOrder, }) if (search) params.set('search', search) const res = await fetch(`${API}?${params}`) if (res.ok) { const data = await res.json() setMcs(data.master_controls || []) setTotal(data.total || 0) } } catch { /* ignore */ } setLoading(false) }, [offset, search, sortBy, sortOrder]) useEffect(() => { loadMCs() }, [loadMCs]) const loadMembers = async (mc: MC) => { setSelectedMC(mc) setMembersLoading(true) try { const res = await fetch(`${API}?action=members&mc_id=${mc.master_control_id}&limit=200`) if (res.ok) { const data = await res.json() setMembers(data.members || []) } } catch { /* ignore */ } setMembersLoading(false) } const handleSort = (field: string) => { if (sortBy === field) { setSortOrder(sortOrder === 'DESC' ? 'ASC' : 'DESC') } else { setSortBy(field) setSortOrder('DESC') } setOffset(0) } const totalPages = Math.ceil(total / PAGE_SIZE) const currentPage = Math.floor(offset / PAGE_SIZE) + 1 // L1 token = part before first underscore that has a sub-part const getL1Token = (name: string) => { const parts = name.split('_') // Find the longest known L1 prefix for (let i = Math.min(parts.length, 3); i >= 1; i--) { const candidate = parts.slice(0, i).join('_') if (['access_control', 'audit_logging', 'key_management', 'risk_management', 'network_security', 'network_segmentation', 'multi_factor_auth', 'transport_encryption', 'data_subject_rights', 'data_breach_notification', 'data_processing_agreement', 'data_processing_register', 'third_party_management', 'change_management', 'human_resources_security', 'physical_security', 'secure_development', 'api_security', 'input_validation', 'container_security', 'logging_configuration', 'cookie_consent', 'video_surveillance', 'supply_chain_due_diligence', 'critical_infrastructure', 'sustainability_reporting', 'financial_reporting', 'consumer_protection', 'compliance_audit', 'asset_management', 'disaster_recovery', 'patch_management', 'password_policy', 'session_management', 'privileged_access', 'certificate_management', 'personal_data', 'sensitive_data', 'health_data', 'product_safety', 'medical_device', 'payment_services', 'supervisory_authority', 'data_retention', 'data_transfer', 'data_classification', 'privacy_by_design', ].includes(candidate)) return candidate } return parts[0] } return (

Master Controls

{total.toLocaleString()} Master Controls mit {mcs.reduce((s, m) => s + m.total_controls, 0).toLocaleString()}+ Atomic Controls

{/* Search + Filters */}
{ setSearch(e.target.value); setOffset(0) }} className="flex-1 px-4 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" /> Seite {currentPage} von {totalPages}
{/* MC List */}
{loading ? ( ) : mcs.map(mc => { const l1 = getL1Token(mc.canonical_name) const l2 = mc.canonical_name.slice(l1.length + 1) || '' return ( loadMembers(mc)} className={`cursor-pointer hover:bg-purple-50 transition-colors ${ selectedMC?.id === mc.id ? 'bg-purple-100' : '' }`} > ) })}
handleSort('canonical_name')}> Name {sortBy === 'canonical_name' ? (sortOrder === 'ASC' ? '↑' : '↓') : ''} handleSort('total_controls')}> Controls {sortBy === 'total_controls' ? (sortOrder === 'ASC' ? '↑' : '↓') : ''} Phasen
Laden...
{l1} {l2 && ( {l2.replace(/_/g, ' ')} )} {mc.total_controls} {(mc.phases_covered || []).length}
{/* Pagination */}
{offset + 1}-{Math.min(offset + PAGE_SIZE, total)} von {total}
{/* Member Detail Panel */} {selectedMC && (

{selectedMC.canonical_name}

{selectedMC.total_controls} Controls, {(selectedMC.phases_covered || []).length} Phasen

{membersLoading ? (
Laden...
) : members.map((m, i) => (
{m.control_id} {m.severity && ( {m.severity} )} {m.phase}

{m.title}

{m.regulation_source && (

{m.regulation_source} {m.regulation_article}

)}
))} {members.length === 0 && !membersLoading && (
Keine Members gefunden
)}
)}
) }