feat(reports): 顯示資料源 PlayBook Verifier 缺口
This commit is contained in:
@@ -157,6 +157,24 @@ interface ReportAutomationAsset {
|
||||
next_action: string
|
||||
}
|
||||
|
||||
interface ReportSourceGapPlaybookVerifier {
|
||||
work_item_id: string
|
||||
source_id: string
|
||||
display_name: string
|
||||
route: string
|
||||
playbook_draft_id: string
|
||||
verifier_plan_id: string
|
||||
playbook_state: string
|
||||
verifier_state: string
|
||||
script_state: string
|
||||
schedule_state: string
|
||||
owner_review_required: boolean
|
||||
runtime_gate_open: boolean
|
||||
playbook_template_fields: string[]
|
||||
verifier_checks: string[]
|
||||
next_action: string
|
||||
}
|
||||
|
||||
interface ReportSourceHealthSnapshot {
|
||||
source_health: ReportSourceHealthItem[]
|
||||
all_zero_assessment: {
|
||||
@@ -168,12 +186,16 @@ interface ReportSourceHealthSnapshot {
|
||||
}
|
||||
no_send_previews: ReportNoSendPreview[]
|
||||
automation_assets: ReportAutomationAsset[]
|
||||
source_gap_playbook_verifier: ReportSourceGapPlaybookVerifier[]
|
||||
rollups: {
|
||||
source_count: number
|
||||
source_ok_count: number
|
||||
source_gap_count: number
|
||||
confidence_percent: number
|
||||
report_work_item_count: number
|
||||
source_gap_playbook_draft_count: number
|
||||
source_gap_verifier_plan_count: number
|
||||
source_gap_owner_review_required_count: number
|
||||
live_send_allowed_count: number
|
||||
runtime_gate_count: number
|
||||
}
|
||||
@@ -303,6 +325,7 @@ export default function ReportsPage({ params }: { params: { locale: string } })
|
||||
() => new Map((sourceHealth?.no_send_previews ?? []).map(preview => [preview.cadence_id, preview])),
|
||||
[sourceHealth]
|
||||
)
|
||||
const sourceGapCards = sourceHealth?.source_gap_playbook_verifier ?? []
|
||||
|
||||
const pcts = useMemo(() => {
|
||||
if (!ds || ds.total === 0) return null
|
||||
@@ -452,6 +475,35 @@ export default function ReportsPage({ params }: { params: { locale: string } })
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[7px] border border-border bg-card p-4 shadow-sm">
|
||||
<SectionHeader icon={<ShieldCheck className="h-4 w-4" />} title={t('sourceGapActions.title')} subtitle={t('sourceGapActions.subtitle')} />
|
||||
<div className="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
{sourceGapCards.map(card => (
|
||||
<div key={card.work_item_id} className="rounded-[7px] border border-border bg-background p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-foreground">{card.display_name}</p>
|
||||
<p className="mt-1 break-all text-[11px] leading-5 text-muted-foreground">{card.work_item_id}</p>
|
||||
</div>
|
||||
<StatusPill ok={!card.runtime_gate_open} okLabel={t('sourceGapActions.gateClosed')} failLabel={t('sourceGapActions.gateOpen')} />
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
<AssetMiniLine label={t('sourceGapActions.playbook')} value={card.playbook_state} detail={card.playbook_draft_id} />
|
||||
<AssetMiniLine label={t('sourceGapActions.verifier')} value={card.verifier_state} detail={card.verifier_plan_id} />
|
||||
<AssetMiniLine label={t('sourceGapActions.script')} value={card.script_state} detail={card.route} />
|
||||
<AssetMiniLine label={t('sourceGapActions.schedule')} value={card.schedule_state} detail={card.owner_review_required ? t('sourceGapActions.ownerRequired') : t('sourceGapActions.ownerOptional')} />
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<ChecklistBlock title={t('sourceGapActions.fields')} items={card.playbook_template_fields.slice(0, 5)} />
|
||||
<ChecklistBlock title={t('sourceGapActions.checks')} items={card.verifier_checks.slice(0, 5)} />
|
||||
</div>
|
||||
<p className="mt-3 text-xs leading-5 text-muted-foreground">{card.next_action}</p>
|
||||
</div>
|
||||
))}
|
||||
{!sourceGapCards.length && <EmptyPanel label={t('sourceGapActions.empty')} />}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[1.05fr_0.95fr]">
|
||||
<div className="rounded-[7px] border border-border bg-card p-4 shadow-sm">
|
||||
<SectionHeader icon={<Layers3 className="h-4 w-4" />} title={t('funnel.title')} subtitle={t('funnel.subtitle')} />
|
||||
@@ -699,6 +751,32 @@ function AssetCard({ label, value, detail, danger = false }: { label: string; va
|
||||
)
|
||||
}
|
||||
|
||||
function AssetMiniLine({ label, value, detail }: { label: string; value: string; detail: string }) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-[7px] border border-border bg-card p-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 text-xs font-semibold text-foreground">{value}</p>
|
||||
<p className="mt-1 break-all text-[10px] leading-4 text-muted-foreground">{detail}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChecklistBlock({ title, items }: { title: string; items: string[] }) {
|
||||
return (
|
||||
<div className="rounded-[7px] border border-border bg-card p-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">{title}</p>
|
||||
<div className="mt-2 grid gap-1">
|
||||
{items.map(item => (
|
||||
<div key={item} className="flex min-w-0 items-start gap-1.5 text-[10px] leading-4 text-muted-foreground">
|
||||
<CheckCircle2 className="mt-0.5 h-3 w-3 flex-shrink-0 text-blue-500" />
|
||||
<span className="break-words">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentCard({ agent, chips }: { agent: AgentStatusReport; chips: string[] }) {
|
||||
const progress = agent.work_units_total > 0 ? Math.round((agent.work_units_done / agent.work_units_total) * 100) : 0
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user