bb9aacc3d3
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>
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
'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 & 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>
|
||
)
|
||
}
|