fix(web): show owner review single item gates
This commit is contained in:
@@ -3312,6 +3312,37 @@
|
||||
"batchWrites": "批次寫入允許:{value}"
|
||||
}
|
||||
},
|
||||
"singleItemRail": {
|
||||
"title": "單筆 Owner Review 處理",
|
||||
"subtitle": "先乾跑取得 plan fingerprint;Owner 確認後才允許寫 KM、寫 audit 並排比例回測。",
|
||||
"outcome": "策略:{outcome}",
|
||||
"writeGate": "確認寫 KM={writes};可確認={confirm}",
|
||||
"required": "必要欄位:{fields}",
|
||||
"blockers": "卡點:{blockers}",
|
||||
"noBlockers": "卡點:無;可先做 dry-run preview。",
|
||||
"writeGateDetail": "後端會拒絕缺 fingerprint 或未 owner_approved 的寫入;讀取與乾跑不會改 KM。",
|
||||
"step": {
|
||||
"dispatch": "排入審核",
|
||||
"dryRun": "乾跑預覽",
|
||||
"confirm": "Owner 確認",
|
||||
"recheck": "比例回測"
|
||||
},
|
||||
"detail": {
|
||||
"dispatch": "Dispatch {dispatch}",
|
||||
"dryRunReady": "已取得 fingerprint,可進入 owner confirm gate",
|
||||
"dryRunPending": "按單筆乾跑取得 fingerprint",
|
||||
"dryRunBlocked": "需先排入 owner review",
|
||||
"confirmReady": "確認後會寫 KM / audit,並排 recheck",
|
||||
"confirmWaiting": "等待 dry-run fingerprint",
|
||||
"confirmDone": "owner review 已完成",
|
||||
"recheckDone": "Recheck {recheck}",
|
||||
"recheckWaiting": "寫回完成後才會產生 recheck"
|
||||
},
|
||||
"actions": {
|
||||
"preview": "單筆乾跑",
|
||||
"confirm": "Owner 確認寫回"
|
||||
}
|
||||
},
|
||||
"ownerReviewInbox": {
|
||||
"title": "Owner review 工作台",
|
||||
"subtitle": "顯示已排入 waiting_owner_review 的 P0/P1 KM,逐筆乾跑與確認完成。",
|
||||
|
||||
@@ -3312,6 +3312,37 @@
|
||||
"batchWrites": "批次寫入允許:{value}"
|
||||
}
|
||||
},
|
||||
"singleItemRail": {
|
||||
"title": "單筆 Owner Review 處理",
|
||||
"subtitle": "先乾跑取得 plan fingerprint;Owner 確認後才允許寫 KM、寫 audit 並排比例回測。",
|
||||
"outcome": "策略:{outcome}",
|
||||
"writeGate": "確認寫 KM={writes};可確認={confirm}",
|
||||
"required": "必要欄位:{fields}",
|
||||
"blockers": "卡點:{blockers}",
|
||||
"noBlockers": "卡點:無;可先做 dry-run preview。",
|
||||
"writeGateDetail": "後端會拒絕缺 fingerprint 或未 owner_approved 的寫入;讀取與乾跑不會改 KM。",
|
||||
"step": {
|
||||
"dispatch": "排入審核",
|
||||
"dryRun": "乾跑預覽",
|
||||
"confirm": "Owner 確認",
|
||||
"recheck": "比例回測"
|
||||
},
|
||||
"detail": {
|
||||
"dispatch": "Dispatch {dispatch}",
|
||||
"dryRunReady": "已取得 fingerprint,可進入 owner confirm gate",
|
||||
"dryRunPending": "按單筆乾跑取得 fingerprint",
|
||||
"dryRunBlocked": "需先排入 owner review",
|
||||
"confirmReady": "確認後會寫 KM / audit,並排 recheck",
|
||||
"confirmWaiting": "等待 dry-run fingerprint",
|
||||
"confirmDone": "owner review 已完成",
|
||||
"recheckDone": "Recheck {recheck}",
|
||||
"recheckWaiting": "寫回完成後才會產生 recheck"
|
||||
},
|
||||
"actions": {
|
||||
"preview": "單筆乾跑",
|
||||
"confirm": "Owner 確認寫回"
|
||||
}
|
||||
},
|
||||
"ownerReviewInbox": {
|
||||
"title": "Owner review 工作台",
|
||||
"subtitle": "顯示已排入 waiting_owner_review 的 P0/P1 KM,逐筆乾跑與確認完成。",
|
||||
|
||||
@@ -1723,6 +1723,263 @@ function staleCandidateFromOwnerReviewItem(
|
||||
};
|
||||
}
|
||||
|
||||
function ownerReviewRailTone(state: "done" | "ready" | "waiting" | "blocked") {
|
||||
switch (state) {
|
||||
case "done":
|
||||
return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
|
||||
case "ready":
|
||||
return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
|
||||
case "blocked":
|
||||
return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
|
||||
default:
|
||||
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
|
||||
}
|
||||
}
|
||||
|
||||
function KnowledgeOwnerReviewSingleItemRail({
|
||||
candidate,
|
||||
action,
|
||||
queuedDispatchId,
|
||||
canPreview,
|
||||
canConfirmAfterPreview = true,
|
||||
writesKmOnConfirm = true,
|
||||
requiredOwnerFields = [],
|
||||
blockers = [],
|
||||
onPreview,
|
||||
onConfirm,
|
||||
}: {
|
||||
candidate: KnowledgeStaleCandidate;
|
||||
action?: KnowledgeStaleOwnerReviewCompleteAction;
|
||||
queuedDispatchId?: string | null;
|
||||
canPreview: boolean;
|
||||
canConfirmAfterPreview?: boolean;
|
||||
writesKmOnConfirm?: boolean;
|
||||
requiredOwnerFields?: string[];
|
||||
blockers?: string[];
|
||||
onPreview: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const t = useTranslations("awooop.workItems.knowledgeGovernance");
|
||||
const preview = action?.previewResult ?? null;
|
||||
const result = action?.result ?? null;
|
||||
const previewReady = Boolean(preview?.dry_run_plan_fingerprint);
|
||||
const completed = result?.status === "completed" || result?.status === "already_completed";
|
||||
const previewStatusKey = kmStaleReviewCompleteStatusKey(preview?.status);
|
||||
const resultStatusKey = kmStaleReviewCompleteStatusKey(result?.status);
|
||||
const outcome = preview?.review_outcome ?? kmStaleReviewOutcomeForCandidate(candidate);
|
||||
const dryRunState = previewReady ? "done" : queuedDispatchId && canPreview ? "ready" : "waiting";
|
||||
const confirmState = completed
|
||||
? "done"
|
||||
: !queuedDispatchId || blockers.length > 0
|
||||
? "blocked"
|
||||
: previewReady && canConfirmAfterPreview
|
||||
? "ready"
|
||||
: "waiting";
|
||||
const recheckState = result?.stale_ratio_recheck_dispatch_id ? "done" : "waiting";
|
||||
const steps = [
|
||||
{
|
||||
key: "dispatch",
|
||||
icon: ClipboardList,
|
||||
state: queuedDispatchId ? "done" : "blocked",
|
||||
detail: t("staleCandidates.singleItemRail.detail.dispatch", {
|
||||
dispatch: queuedDispatchId ?? "--",
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "dryRun",
|
||||
icon: SearchCheck,
|
||||
state: dryRunState,
|
||||
detail: previewReady
|
||||
? t("staleCandidates.singleItemRail.detail.dryRunReady")
|
||||
: queuedDispatchId
|
||||
? t("staleCandidates.singleItemRail.detail.dryRunPending")
|
||||
: t("staleCandidates.singleItemRail.detail.dryRunBlocked"),
|
||||
},
|
||||
{
|
||||
key: "confirm",
|
||||
icon: ShieldCheck,
|
||||
state: confirmState,
|
||||
detail: completed
|
||||
? t("staleCandidates.singleItemRail.detail.confirmDone")
|
||||
: previewReady
|
||||
? t("staleCandidates.singleItemRail.detail.confirmReady")
|
||||
: t("staleCandidates.singleItemRail.detail.confirmWaiting"),
|
||||
},
|
||||
{
|
||||
key: "recheck",
|
||||
icon: RefreshCw,
|
||||
state: recheckState,
|
||||
detail: result?.stale_ratio_recheck_dispatch_id
|
||||
? t("staleCandidates.singleItemRail.detail.recheckDone", {
|
||||
recheck: result.stale_ratio_recheck_dispatch_id,
|
||||
})
|
||||
: t("staleCandidates.singleItemRail.detail.recheckWaiting"),
|
||||
},
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div className="mt-2 border border-[#e0ddd4] bg-white px-3 py-2 text-[11px] leading-5 text-[#5f5b52]">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-[#141413]">
|
||||
{t("staleCandidates.singleItemRail.title")}
|
||||
</p>
|
||||
<p className="mt-1 text-[#77736a]">
|
||||
{t("staleCandidates.singleItemRail.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 font-mono">
|
||||
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
|
||||
{t("staleCandidates.singleItemRail.outcome", {
|
||||
outcome: t(`staleCandidates.completeActions.outcomes.${outcome}` as never),
|
||||
})}
|
||||
</span>
|
||||
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5">
|
||||
{t("staleCandidates.singleItemRail.writeGate", {
|
||||
writes: String(writesKmOnConfirm),
|
||||
confirm: String(canConfirmAfterPreview),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-1 gap-px border border-[#e0ddd4] bg-[#e0ddd4] sm:grid-cols-2 xl:grid-cols-4">
|
||||
{steps.map((step) => {
|
||||
const Icon = step.icon;
|
||||
const tone = ownerReviewRailTone(step.state);
|
||||
return (
|
||||
<div key={step.key} className="bg-[#faf9f3] px-2 py-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className={cn("flex h-7 w-7 shrink-0 items-center justify-center border", tone)}>
|
||||
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</div>
|
||||
<span className={cn("border px-2 py-0.5 text-[10px] font-semibold", tone)}>
|
||||
{t(`staleCandidates.operationRail.state.${step.state}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[#77736a]">
|
||||
{t(`staleCandidates.singleItemRail.step.${step.key}` as never)}
|
||||
</p>
|
||||
<p className="mt-1 line-clamp-2 text-[11px] text-[#5f5b52]">
|
||||
{step.detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 grid gap-2 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.65fr)]">
|
||||
<div className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-1.5">
|
||||
<p>
|
||||
{t("staleCandidates.singleItemRail.required", {
|
||||
fields: requiredOwnerFields.length > 0 ? requiredOwnerFields.join(", ") : "--",
|
||||
})}
|
||||
</p>
|
||||
{blockers.length > 0 ? (
|
||||
<p className="text-[#9f2f25]">
|
||||
{t("staleCandidates.singleItemRail.blockers", {
|
||||
blockers: blockers.join(", "),
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{t("staleCandidates.singleItemRail.noBlockers")}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-1">
|
||||
{t("staleCandidates.singleItemRail.writeGateDetail")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPreview}
|
||||
disabled={!queuedDispatchId || !canPreview || action?.previewLoading || action?.confirmLoading}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#d9b36f] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{action?.previewLoading
|
||||
? t("staleCandidates.completeActions.previewing")
|
||||
: t("staleCandidates.singleItemRail.actions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={
|
||||
!queuedDispatchId ||
|
||||
!previewReady ||
|
||||
!canConfirmAfterPreview ||
|
||||
action?.previewLoading ||
|
||||
action?.confirmLoading
|
||||
}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{action?.confirmLoading
|
||||
? t("staleCandidates.completeActions.confirming")
|
||||
: t("staleCandidates.singleItemRail.actions.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{action?.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[#9f2f25]">
|
||||
{action.error}
|
||||
</p>
|
||||
) : null}
|
||||
{preview ? (
|
||||
<div className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1.5 text-[#8a5a08]">
|
||||
<p className="font-semibold">
|
||||
{t(`staleCandidates.completeActions.statuses.${previewStatusKey}` as never)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.previewResult", {
|
||||
outcome: t(`staleCandidates.completeActions.outcomes.${preview.review_outcome}` as never),
|
||||
writesKm: String(preview.writes_km),
|
||||
writesAudit: String(preview.writes_governance_audit),
|
||||
})}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.planFingerprint", {
|
||||
fingerprint: preview.dry_run_plan_fingerprint ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{preview.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: preview.stale_ratio_snapshot.stale_count,
|
||||
total: preview.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(preview.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(preview.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{result ? (
|
||||
<div className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1.5 text-[#17602a]">
|
||||
<p className="font-semibold">
|
||||
{t(`staleCandidates.completeActions.statuses.${resultStatusKey}` as never)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.result", {
|
||||
audit: result.audit_dispatch_id ?? "--",
|
||||
recheck: result.stale_ratio_recheck_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{result.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: result.stale_ratio_snapshot.stale_count,
|
||||
total: result.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(result.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(result.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function kmCorrelationSourceKey(value: string | null | undefined) {
|
||||
switch (value) {
|
||||
case "incident":
|
||||
@@ -4676,8 +4933,6 @@ function KnowledgeGovernancePanel({
|
||||
{completionQueueItems.slice(0, 6).map((item) => {
|
||||
const completionCandidate = staleCandidateFromOwnerReviewItem(item);
|
||||
const completionAction = staleReviewCompletionActions[item.entry_id];
|
||||
const completionPreview = completionAction?.previewResult ?? null;
|
||||
const completionResult = completionAction?.result ?? null;
|
||||
const readinessKey = kmStaleCompletionReadinessKey(item.readiness);
|
||||
return (
|
||||
<article
|
||||
@@ -4735,55 +4990,18 @@ function KnowledgeGovernancePanel({
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{item.can_preview ? (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => previewStaleReviewCompletion(completionCandidate)}
|
||||
disabled={completionAction?.previewLoading || completionAction?.confirmLoading}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#d9b36f] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<ClipboardList className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.previewLoading
|
||||
? t("staleCandidates.completeActions.previewing")
|
||||
: t("staleCandidates.completeActions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => confirmStaleReviewCompletion(completionCandidate)}
|
||||
disabled={
|
||||
completionAction?.confirmLoading
|
||||
|| !completionPreview?.dry_run_plan_fingerprint
|
||||
}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.confirmLoading
|
||||
? t("staleCandidates.completeActions.confirming")
|
||||
: t("staleCandidates.completeActions.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{completionAction?.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] text-[#9f2f25]">
|
||||
{completionAction.error}
|
||||
</p>
|
||||
) : null}
|
||||
{completionPreview ? (
|
||||
<p className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 font-mono text-[11px] text-[#8a5a08]">
|
||||
{t("staleCandidates.completeActions.planFingerprint", {
|
||||
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{completionResult ? (
|
||||
<p className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1 text-[11px] text-[#17602a]">
|
||||
{t("staleCandidates.completeActions.result", {
|
||||
audit: completionResult.audit_dispatch_id ?? "--",
|
||||
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<KnowledgeOwnerReviewSingleItemRail
|
||||
candidate={completionCandidate}
|
||||
action={completionAction}
|
||||
queuedDispatchId={item.dispatch_id}
|
||||
canPreview={item.can_preview}
|
||||
canConfirmAfterPreview={item.can_confirm_after_preview}
|
||||
writesKmOnConfirm={item.writes_km_on_confirm}
|
||||
requiredOwnerFields={item.required_owner_fields}
|
||||
blockers={item.blockers}
|
||||
onPreview={() => previewStaleReviewCompletion(completionCandidate)}
|
||||
onConfirm={() => confirmStaleReviewCompletion(completionCandidate)}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
@@ -4830,11 +5048,6 @@ function KnowledgeGovernancePanel({
|
||||
{ownerReviewItems.slice(0, 6).map((item) => {
|
||||
const inboxCandidate = staleCandidateFromOwnerReviewItem(item);
|
||||
const completionAction = staleReviewCompletionActions[item.entry_id];
|
||||
const completionPreview = completionAction?.previewResult ?? null;
|
||||
const completionResult = completionAction?.result ?? null;
|
||||
const completionPreviewReady = Boolean(completionPreview?.dry_run_plan_fingerprint);
|
||||
const completionPreviewStatusKey = kmStaleReviewCompleteStatusKey(completionPreview?.status);
|
||||
const completionResultStatusKey = kmStaleReviewCompleteStatusKey(completionResult?.status);
|
||||
return (
|
||||
<article
|
||||
key={item.dispatch_id}
|
||||
@@ -4880,60 +5093,16 @@ function KnowledgeGovernancePanel({
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => previewStaleReviewCompletion(inboxCandidate)}
|
||||
disabled={completionAction?.previewLoading || completionAction?.confirmLoading}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.previewLoading
|
||||
? t("staleCandidates.completeActions.previewing")
|
||||
: t("staleCandidates.completeActions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => confirmStaleReviewCompletion(inboxCandidate)}
|
||||
disabled={
|
||||
!completionPreviewReady ||
|
||||
completionAction?.previewLoading ||
|
||||
completionAction?.confirmLoading
|
||||
}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#d9b36f] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.confirmLoading
|
||||
? t("staleCandidates.completeActions.confirming")
|
||||
: t("staleCandidates.completeActions.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
{completionAction?.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[#9f2f25]">
|
||||
{completionAction.error}
|
||||
</p>
|
||||
) : null}
|
||||
{completionPreview ? (
|
||||
<p className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 text-[#8a5a08]">
|
||||
{t(
|
||||
`staleCandidates.completeActions.statuses.${completionPreviewStatusKey}` as never
|
||||
)}{" "}
|
||||
{t("staleCandidates.completeActions.planFingerprint", {
|
||||
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{completionResult ? (
|
||||
<p className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1 text-[#17602a]">
|
||||
{t(
|
||||
`staleCandidates.completeActions.statuses.${completionResultStatusKey}` as never
|
||||
)}{" "}
|
||||
{t("staleCandidates.completeActions.result", {
|
||||
audit: completionResult.audit_dispatch_id ?? "--",
|
||||
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<KnowledgeOwnerReviewSingleItemRail
|
||||
candidate={inboxCandidate}
|
||||
action={completionAction}
|
||||
queuedDispatchId={item.dispatch_id}
|
||||
canPreview={true}
|
||||
canConfirmAfterPreview={true}
|
||||
writesKmOnConfirm={true}
|
||||
onPreview={() => previewStaleReviewCompletion(inboxCandidate)}
|
||||
onConfirm={() => confirmStaleReviewCompletion(inboxCandidate)}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
@@ -4958,11 +5127,6 @@ function KnowledgeGovernancePanel({
|
||||
const persistedReviewStatusKey = governanceKmDispatchStatusKey(candidate.owner_review_status);
|
||||
const persistedReviewStageKey = governanceKmStageKey(candidate.owner_review_stage);
|
||||
const completionAction = staleReviewCompletionActions[candidate.entry_id];
|
||||
const completionPreview = completionAction?.previewResult ?? null;
|
||||
const completionResult = completionAction?.result ?? null;
|
||||
const completionPreviewReady = Boolean(completionPreview?.dry_run_plan_fingerprint);
|
||||
const completionPreviewStatusKey = kmStaleReviewCompleteStatusKey(completionPreview?.status);
|
||||
const completionResultStatusKey = kmStaleReviewCompleteStatusKey(completionResult?.status);
|
||||
return (
|
||||
<article
|
||||
key={candidate.entry_id}
|
||||
@@ -5046,37 +5210,6 @@ function KnowledgeGovernancePanel({
|
||||
? t("staleCandidates.queueingReview")
|
||||
: t("staleCandidates.queueReview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => previewStaleReviewCompletion(candidate)}
|
||||
disabled={
|
||||
!queuedDispatchId ||
|
||||
completionAction?.previewLoading ||
|
||||
completionAction?.confirmLoading
|
||||
}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.previewLoading
|
||||
? t("staleCandidates.completeActions.previewing")
|
||||
: t("staleCandidates.completeActions.preview")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => confirmStaleReviewCompletion(candidate)}
|
||||
disabled={
|
||||
!queuedDispatchId ||
|
||||
!completionPreviewReady ||
|
||||
completionAction?.previewLoading ||
|
||||
completionAction?.confirmLoading
|
||||
}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#d9b36f] hover:bg-[#fff7e8] hover:text-[#8a5a08] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{completionAction?.confirmLoading
|
||||
? t("staleCandidates.completeActions.confirming")
|
||||
: t("staleCandidates.completeActions.confirm")}
|
||||
</button>
|
||||
<Link
|
||||
href={`/knowledge-base?q=${encodeURIComponent(candidate.title)}`}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
|
||||
@@ -5099,69 +5232,16 @@ function KnowledgeGovernancePanel({
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{completionAction?.error ? (
|
||||
<p className="mt-2 border border-[#e2a29b] bg-[#fff0ef] px-2 py-1 text-[11px] leading-5 text-[#9f2f25]">
|
||||
{completionAction.error}
|
||||
</p>
|
||||
) : null}
|
||||
{completionPreview ? (
|
||||
<div className="mt-2 border border-[#d9b36f] bg-[#fff7e8] px-2 py-1.5 text-[11px] leading-5 text-[#8a5a08]">
|
||||
<p className="font-semibold">
|
||||
{t(
|
||||
`staleCandidates.completeActions.statuses.${completionPreviewStatusKey}` as never
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.previewResult", {
|
||||
outcome: t(
|
||||
`staleCandidates.completeActions.outcomes.${completionPreview.review_outcome}` as never
|
||||
),
|
||||
writesKm: String(completionPreview.writes_km),
|
||||
writesAudit: String(completionPreview.writes_governance_audit),
|
||||
})}
|
||||
</p>
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.planFingerprint", {
|
||||
fingerprint: completionPreview.dry_run_plan_fingerprint ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{completionPreview.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: completionPreview.stale_ratio_snapshot.stale_count,
|
||||
total: completionPreview.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(completionPreview.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(completionPreview.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{completionResult ? (
|
||||
<div className="mt-2 border border-[#9bc7a4] bg-[#f0faf2] px-2 py-1.5 text-[11px] leading-5 text-[#17602a]">
|
||||
<p className="font-semibold">
|
||||
{t(
|
||||
`staleCandidates.completeActions.statuses.${completionResultStatusKey}` as never
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.result", {
|
||||
audit: completionResult.audit_dispatch_id ?? "--",
|
||||
recheck: completionResult.stale_ratio_recheck_dispatch_id ?? "--",
|
||||
})}
|
||||
</p>
|
||||
{completionResult.stale_ratio_snapshot ? (
|
||||
<p className="mt-1 text-[#5f5b52]">
|
||||
{t("staleCandidates.completeActions.snapshot", {
|
||||
stale: completionResult.stale_ratio_snapshot.stale_count,
|
||||
total: completionResult.stale_ratio_snapshot.total_count,
|
||||
ratio: formatStaleRatio(completionResult.stale_ratio_snapshot.stale_ratio),
|
||||
threshold: formatStaleRatio(completionResult.stale_ratio_snapshot.threshold),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<KnowledgeOwnerReviewSingleItemRail
|
||||
candidate={candidate}
|
||||
action={completionAction}
|
||||
queuedDispatchId={queuedDispatchId}
|
||||
canPreview={Boolean(queuedDispatchId)}
|
||||
canConfirmAfterPreview={true}
|
||||
writesKmOnConfirm={true}
|
||||
onPreview={() => previewStaleReviewCompletion(candidate)}
|
||||
onConfirm={() => confirmStaleReviewCompletion(candidate)}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user