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>
This commit is contained in:
Benjamin Admin
2026-06-11 00:12:35 +02:00
parent 5da20af4fd
commit bb9aacc3d3
3 changed files with 182 additions and 0 deletions
@@ -0,0 +1,140 @@
'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>
)
}