Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/RemediationPlan.tsx
T
Benjamin Admin bb9aacc3d3 feat(agent): Abstellmaßnahmen + Ticket-Formulierung (Schritt 3)
RemediationPlan: aus den offenen Punkten (result.results, Haupt-Engine) je
Finding eine Massnahme + fertigen Ticket-Text ableiten, nach Prioritaet
sortiert, mit Kopieren + JSON-Export als Uebergabe. SCOPE: BreakPilot
formuliert nur — Ticketsystem/Jira/Feedback-Loop baut ein anderes Team.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 00:12:35 +02:00

141 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* RemediationPlan — Abstellmaßnahmen + Ticket-Formulierung.
*
* Aus den offenen Punkten (result.results, Haupt-Engine) je Finding eine
* Maßnahme + einen fertigen Ticket-Text ableiten und übergabebereit machen
* (Kopieren / JSON-Export). SCOPE: BreakPilot formuliert NUR — Ticketsystem,
* Jira-Sync und Feedback-Loop baut ein anderes Team. Keine zweite Engine.
*/
import React, { useState } from 'react'
import { DOC_TYPE_LABELS, type DocResult } from './ChecklistView'
type Priority = 'Hoch' | 'Mittel' | 'Niedrig'
interface Remediation {
docType: string
docLabel: string
checkLabel: string
action: string
ticketTitle: string
ticketBody: string
priority: Priority
}
const PRIO_RANK: Record<Priority, number> = { Hoch: 0, Mittel: 1, Niedrig: 2 }
const PRIO_COLOR: Record<Priority, string> = {
Hoch: 'bg-red-100 text-red-700',
Mittel: 'bg-amber-100 text-amber-700',
Niedrig: 'bg-blue-100 text-blue-700',
}
function toPriority(sev: string): Priority {
const s = (sev || '').toUpperCase()
if (s === 'HIGH' || s === 'CRITICAL') return 'Hoch'
if (s === 'MEDIUM') return 'Mittel'
return 'Niedrig'
}
function buildRemediations(docs: DocResult[]): Remediation[] {
const out: Remediation[] = []
for (const d of docs) {
if (d.error) continue
const docLabel = DOC_TYPE_LABELS[d.doc_type] || d.doc_type
const failed = d.checks.filter(
c => !c.passed && !c.skipped && c.severity !== 'INFO',
)
for (const c of failed) {
const action = c.hint || `${c.label} im ${docLabel} ergänzen.`
out.push({
docType: d.doc_type,
docLabel,
checkLabel: c.label,
action,
ticketTitle: `Compliance: ${docLabel} ${c.label}`,
ticketBody:
`Dokument: ${docLabel}\nPrüfpunkt: ${c.label}\n` +
`Status: nicht erfüllt\nMaßnahme: ${action}`,
priority: toPriority(c.severity),
})
}
}
return out.sort((a, b) => PRIO_RANK[a.priority] - PRIO_RANK[b.priority])
}
export function RemediationPlan({ results }: { results: any }) {
const items = buildRemediations(results.results || [])
const [copied, setCopied] = useState<number | null>(null)
if (items.length === 0) {
return (
<div className="border rounded-lg p-4 text-sm text-green-700 bg-green-50">
Keine offenen Pflichtangaben kein Handlungsbedarf.
</div>
)
}
function copyTicket(i: number, body: string) {
navigator.clipboard?.writeText(body)
setCopied(i)
window.setTimeout(() => setCopied(null), 1500)
}
function exportAll() {
const payload = items.map(it => ({
title: it.ticketTitle,
body: it.ticketBody,
priority: it.priority,
doc_type: it.docType,
}))
const blob = new Blob([JSON.stringify(payload, null, 2)], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'breakpilot-tickets.json'
a.click()
URL.revokeObjectURL(url)
}
return (
<div className="border rounded-lg overflow-hidden">
<div className="px-4 py-2.5 bg-slate-50 border-b flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-gray-800">
Abstellmaßnahmen &amp; Tickets ({items.length})
</h3>
<button
onClick={exportAll}
className="text-xs px-2.5 py-1 rounded border border-gray-200 hover:bg-gray-100 text-gray-600"
>
Alle als JSON exportieren
</button>
</div>
<div className="divide-y divide-gray-100">
{items.map((it, i) => (
<div key={i} className="px-4 py-3 space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${PRIO_COLOR[it.priority]}`}>
{it.priority}
</span>
<span className="text-sm font-medium text-gray-800">
{it.docLabel}: {it.checkLabel}
</span>
</div>
<div className="text-xs text-gray-600">{it.action}</div>
<button
onClick={() => copyTicket(i, it.ticketBody)}
className="text-xs px-2 py-1 rounded bg-purple-50 text-purple-700 border border-purple-200 hover:bg-purple-100"
>
{copied === i ? 'Kopiert ✓' : 'Ticket-Text kopieren'}
</button>
</div>
))}
</div>
</div>
)
}