'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { Atom, Search, ChevronRight, ChevronLeft, Filter, BarChart3, ChevronsLeft, ChevronsRight, ArrowUpDown, Clock, RefreshCw, } from 'lucide-react' import { CanonicalControl, BACKEND_URL, SeverityBadge, StateBadge, CategoryBadge, TargetAudienceBadge, GenerationStrategyBadge, ObligationTypeBadge, RegulationCountBadge, CATEGORY_OPTIONS, } from '../control-library/components/helpers' import { ControlDetail } from '../control-library/components/ControlDetail' // ============================================================================= // TYPES // ============================================================================= interface AtomicStats { total_active: number total_duplicate: number by_domain: Array<{ domain: string; count: number }> by_regulation: Array<{ regulation: string; count: number }> avg_regulation_coverage: number } // ============================================================================= // ATOMIC CONTROLS PAGE // ============================================================================= const PAGE_SIZE = 50 export default function AtomicControlsPage() { const [controls, setControls] = useState([]) const [totalCount, setTotalCount] = useState(0) const [stats, setStats] = useState(null) const [selectedControl, setSelectedControl] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filters const [searchQuery, setSearchQuery] = useState('') const [debouncedSearch, setDebouncedSearch] = useState('') const [severityFilter, setSeverityFilter] = useState('') const [domainFilter, setDomainFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') const [sortBy, setSortBy] = useState<'id' | 'newest' | 'oldest'>('id') // Pagination const [currentPage, setCurrentPage] = useState(1) // Mode const [mode, setMode] = useState<'list' | 'detail'>('list') // Debounce search const searchTimer = useRef | null>(null) useEffect(() => { if (searchTimer.current) clearTimeout(searchTimer.current) searchTimer.current = setTimeout(() => setDebouncedSearch(searchQuery), 400) return () => { if (searchTimer.current) clearTimeout(searchTimer.current) } }, [searchQuery]) // Build query params const buildParams = useCallback((extra?: Record) => { const p = new URLSearchParams() p.set('control_type', 'atomic') // Exclude duplicates — show only active masters if (!extra?.release_state) { // Don't filter by state for count queries that already have it } if (severityFilter) p.set('severity', severityFilter) if (domainFilter) p.set('domain', domainFilter) if (categoryFilter) p.set('category', categoryFilter) if (debouncedSearch) p.set('search', debouncedSearch) if (extra) for (const [k, v] of Object.entries(extra)) p.set(k, v) return p.toString() }, [severityFilter, domainFilter, categoryFilter, debouncedSearch]) // Load stats const loadStats = useCallback(async () => { try { const res = await fetch(`${BACKEND_URL}?endpoint=atomic-stats`) if (res.ok) setStats(await res.json()) } catch { /* ignore */ } }, []) // Load controls page const loadControls = useCallback(async () => { try { setLoading(true) const sortField = sortBy === 'id' ? 'control_id' : 'created_at' const sortOrder = sortBy === 'newest' ? 'desc' : 'asc' const offset = (currentPage - 1) * PAGE_SIZE const qs = buildParams({ sort: sortField, order: sortOrder, limit: String(PAGE_SIZE), offset: String(offset), }) const countQs = buildParams() const [ctrlRes, countRes] = await Promise.all([ fetch(`${BACKEND_URL}?endpoint=controls&${qs}`), fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`), ]) if (ctrlRes.ok) setControls(await ctrlRes.json()) if (countRes.ok) { const data = await countRes.json() setTotalCount(data.total || 0) } } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { setLoading(false) } }, [buildParams, sortBy, currentPage]) // Initial load useEffect(() => { loadStats() }, [loadStats]) useEffect(() => { loadControls() }, [loadControls]) useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, categoryFilter, debouncedSearch, sortBy]) const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) // Loading if (loading && controls.length === 0) { return (
) } if (error) { return (

{error}

) } // DETAIL MODE if (mode === 'detail' && selectedControl) { return (
{ setMode('list'); setSelectedControl(null) }} onEdit={() => {}} onDelete={() => {}} onReview={() => {}} onNavigateToControl={async (controlId: string) => { try { const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`) if (res.ok) { const data = await res.json() setSelectedControl(data) } } catch { /* ignore */ } }} />
) } // ========================================================================= // LIST VIEW // ========================================================================= return (
{/* Header */}

Atomare Controls

Deduplizierte atomare Controls mit Herkunftsnachweis

{/* Stats Bar */} {stats && (
{stats.total_active.toLocaleString('de-DE')}
Master Controls
{stats.total_duplicate.toLocaleString('de-DE')}
Duplikate (entfernt)
{stats.by_regulation.length}
Regulierungen
{stats.avg_regulation_coverage}
Avg. Regulierungen / Control
)} {/* Filters */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-violet-500" />
|
{/* Pagination Header */}
{totalCount} Controls gefunden {stats && totalCount !== stats.total_active && ` (von ${stats.total_active.toLocaleString('de-DE')} Master Controls)`} {loading && Lade...} Seite {currentPage} von {totalPages}
{/* Control List */}
{controls.map((ctrl) => ( ))} {controls.length === 0 && !loading && (
Keine atomaren Controls gefunden.
)}
{/* Pagination Controls */} {totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => i + 1) .filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2) .reduce<(number | 'dots')[]>((acc, p, i, arr) => { if (i > 0 && p - (arr[i - 1] as number) > 1) acc.push('dots') acc.push(p) return acc }, []) .map((p, i) => p === 'dots' ? ( ... ) : ( ) ) }
)}
) }