Files
breakpilot-compliance/admin-compliance/app/sdk/isms/_components/AuditsTab.tsx
Sharang Parnerkar ddcd89f26d refactor(admin): split isms page.tsx into colocated components
Split 1260-LOC client page into _types.ts and six tab components under
_components/ (Overview, Policies, SoA, Objectives, Audits, Reviews) plus
a shared helpers module. Behavior preserved exactly; page.tsx is now a
thin wiring shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:47:01 +02:00

153 lines
7.3 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { API, AuditFinding, CAPA, InternalAudit } from '../_types'
import { EmptyState, LoadingSpinner, StatCard, StatusBadge } from './shared'
// =============================================================================
// TAB: AUDITS (Internal Audits + Findings + CAPA)
// =============================================================================
export function AuditsTab() {
const [audits, setAudits] = useState<InternalAudit[]>([])
const [findings, setFindings] = useState<AuditFinding[]>([])
const [capas, setCAPAs] = useState<CAPA[]>([])
const [loading, setLoading] = useState(true)
const [subTab, setSubTab] = useState<'audits' | 'findings' | 'capa'>('audits')
const load = useCallback(async () => {
setLoading(true)
try {
const [aRes, fRes, cRes] = await Promise.all([
fetch(`${API}/internal-audits`),
fetch(`${API}/findings`),
fetch(`${API}/capa`),
])
if (aRes.ok) { const d = await aRes.json(); setAudits(d.audits || []) }
if (fRes.ok) { const d = await fRes.json(); setFindings(d.findings || []) }
if (cRes.ok) { const d = await cRes.json(); setCAPAs(d.actions || []) }
} catch { /* ignore */ }
setLoading(false)
}, [])
useEffect(() => { load() }, [load])
if (loading) return <LoadingSpinner />
const openFindings = findings.filter(f => f.status !== 'closed')
const majors = findings.filter(f => f.finding_type === 'major')
return (
<div className="space-y-4">
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<StatCard label="Interne Audits" value={audits.length} color="blue" />
<StatCard label="Offene Findings" value={openFindings.length} color={openFindings.length > 0 ? 'red' : 'green'} />
<StatCard label="Major Findings" value={majors.length} color={majors.length > 0 ? 'red' : 'green'} />
<StatCard label="CAPAs" value={capas.length} color="purple" />
</div>
{/* Sub-tabs */}
<div className="flex gap-2 border-b">
{(['audits', 'findings', 'capa'] as const).map(t => (
<button key={t} onClick={() => setSubTab(t)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${subTab === t ? 'border-purple-600 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
>
{t === 'audits' ? 'Interne Audits' : t === 'findings' ? 'Findings' : 'CAPA'}
</button>
))}
</div>
{subTab === 'audits' && (
<div className="space-y-3">
{audits.length === 0 ? <EmptyState text="Noch keine internen Audits geplant" /> : audits.map(a => (
<div key={a.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{a.audit_id}</span>
<span className="text-sm font-medium text-gray-900">{a.title}</span>
<StatusBadge status={a.status} />
</div>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>Typ: {a.audit_type}</span>
<span>Datum: {new Date(a.planned_date).toLocaleDateString('de-DE')}</span>
<span>Auditor: {a.lead_auditor}</span>
<span>Findings: {a.total_findings || 0} (Major: {a.major_findings || 0}, Minor: {a.minor_findings || 0})</span>
</div>
</div>
</div>
{a.audit_conclusion && (
<p className="text-xs text-gray-600 mt-2 bg-gray-50 rounded p-2">{a.audit_conclusion}</p>
)}
</div>
))}
</div>
)}
{subTab === 'findings' && (
<div className="space-y-3">
{findings.length === 0 ? <EmptyState text="Keine Audit-Findings vorhanden" /> : findings.map(f => (
<div key={f.id} className={`bg-white border rounded-xl p-4 ${f.is_blocking ? 'border-red-300' : ''}`}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{f.finding_id}</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
f.finding_type === 'major' ? 'bg-red-100 text-red-700' :
f.finding_type === 'minor' ? 'bg-yellow-100 text-yellow-700' :
f.finding_type === 'ofi' ? 'bg-blue-100 text-blue-700' :
'bg-green-100 text-green-700'
}`}>{f.finding_type.toUpperCase()}</span>
<span className="text-sm font-medium text-gray-900">{f.title}</span>
<StatusBadge status={f.status} />
{f.is_blocking && <span className="px-2 py-0.5 bg-red-600 text-white text-xs rounded-full">Blockiert</span>}
</div>
<p className="text-xs text-gray-600 mt-1">{f.description}</p>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>ISO: {f.iso_chapter}</span>
<span>Verantwortlich: {f.owner}</span>
<span>Auditor: {f.auditor}</span>
{f.due_date && <span>Frist: {new Date(f.due_date).toLocaleDateString('de-DE')}</span>}
</div>
</div>
</div>
</div>
))}
</div>
)}
{subTab === 'capa' && (
<div className="space-y-3">
{capas.length === 0 ? <EmptyState text="Keine Korrektur-/Vorbeugungsmassnahmen vorhanden" /> : capas.map(c => (
<div key={c.id} className="bg-white border rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-500">{c.capa_id}</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${c.capa_type === 'corrective' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}`}>
{c.capa_type === 'corrective' ? 'Korrektur' : 'Vorbeugung'}
</span>
<span className="text-sm font-medium text-gray-900">{c.title}</span>
<StatusBadge status={c.status} />
</div>
<div className="flex gap-3 text-xs text-gray-500 mt-1">
<span>Zustaendig: {c.assigned_to}</span>
<span>Ziel: {new Date(c.planned_completion).toLocaleDateString('de-DE')}</span>
{c.actual_completion && <span>Abgeschlossen: {new Date(c.actual_completion).toLocaleDateString('de-DE')}</span>}
{c.effectiveness_verified !== null && (
<span className={c.effectiveness_verified ? 'text-green-600' : 'text-red-600'}>
Wirksamkeit: {c.effectiveness_verified ? 'Bestaetigt' : 'Nicht bestaetigt'}
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)
}