[CLAUDE] Contract: UAT S174 dot-B — be style PE sang KHKK (FE-only, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m37s

Nguon: spec runs/2026-08-05-S174-khkk-style-audit/spec-khkk-style-dong-bo-05-08-2026.md
(ensemble /fable-clone frontend-designer 5 lane). Owner S175: "KHKK theo khuon PE".
Mirror 7 CAP file, sha256 khop 7/7. Build XANH ca 2 app, 0 error TS.

=== B1 panel Quy trinh duyet (KhkkWorkflowPanel) ===
X2 GO card-accent -> panel TRAN khuon PE (:345).
  🔴 GO HAN CLASS, khong de border-0/shadow-none: .card-accent (index.css:112) nam NGOAI
  @layer nen thang utility Tailwind v4 (gotcha #66).
- Tieu de h3 tran + bo icon-chip; so hieu WF padStart(2,'0') => v01 (PE :350).
- Banner ket-thuc-som violet -> EMERALD (PE :360-377). PE danh violet RIENG cho
  "Duyet thay CEO"; dung violet ca 2 cho la tron 2 nghia vao mot mau.
- Chip PHONG vien-xam-nen-trang -> emerald dac (PE :406-410).
- Nut lucide -> glyph '✓ Duyet' / '← Tra lai' (PE :540); nut Xoa xam -> DO (PE :574).
- File dinh kem: +border-t pt-4 +h3 +nut <Eye> xem truoc. Route BE DA CO
  ContractSigningPlansController.cs:231 (/view, cung hinh PE :317).
  Nhan "Tai xuong" chuyen vao title= chu KHONG bo — giu chuoi do trong bundle
  de phep verify prod con control-duong soi duoc.
- Lich su duyet: bo pill dem -> (n) trong ngoac; the bg-slate-50/60 -> bg-white p-3 text-sm;
  moc-gio day phai bang justify-between.
  🔴 Badge 2-nga -> 3-NGA: truoc day MOI thu khong phai Reject deu to emerald => "Tra lai"
  (amber o PE) bi ve xanh nhu da-duyet, doc NGUOC nghia. toPhase da co san (khkk.ts:307).
- Lich su thay doi: BO GAP, render thang (PE PeDetailTabs.tsx:787-794).
  🔴 BAT BUOC bo luon `enabled: showChangelog` cua useQuery: giu lai thi khoi luon hien
  nhung query KHONG BAO GIO chay, va no hong duoi dang "Chua co thay doi nao duoc ghi."
  — tuc trong y het phieu chua tung sua, KHONG phai trong nhu loi. Doi lay +1 request/phieu,
  dung gia PE dang tra.
- Micro-copy: Huy / tuy chon; nhan O NHAP -> "Ghi chu" (PE :869).
  🔴 CHI doi ten TRUONG — chuoi "Y kien cap duyet" o dong tro-giup la TEN THAT cua muc dich
  den trong phieu (Section 5, Mig 26), doi no la tro nguoi dung toi mot muc khong ton tai.
- KHONG be chip "NS PRO"/"NS CCM" (2 co do mo quyen sua ngan sach per-goi-thau, khai niem
  KHKK khong co). GIU moc chu-ky KHKK (cho KHKK tot hon PE).

=== B2 noi dung phieu (KhkkDetailContent) — 6 P0 ===
- 2 bang: table-fixed + <colgroup> + min-w-[880px] trong overflow-x-auto. Bang co gian
  tu-do truoc day de cot tien nhay be rong theo do dai ten NCC => 3 cot so khong thang hang.
- O RONG -> em-dash MO (text-slate-300) gom vao helper orDash/moneyOrDash: truoc day em-dash
  thua-huong dung mau chu that => luot bang thi "—" trong y nhu mot GIA-TRI.
  moneyOrDash rieng vi formatVnd tra CHUOI "—" nen khong to mo duoc.
- empty-block -> text-sm italic text-slate-400 (3 cho).
- emoji 🔒 -> icon lucide <Lock>: emoji render khac nhau theo font he dieu-hanh va KHONG an
  currentColor nen luon lech tong voi chu amber ben canh.
- thong nhat text-xs ca hang bang; +hover:bg-slate-50/60 cho <tr>.

=== B3 panel danh sach (KhkkListPanel + KhkkWorkListPanel + PipelineTreePanel) ===
- DAU COT: che-do CAY truoc day di thang vao than, khong tieu-de/so dem => cot "cut dau",
  lech ca voi man Thao tac (Tier header) LAN voi chinh che-do phang (co dem o chan).
  Mirror khuon Tier.
- Gop 2 o tim chong nhau: +prop hideSearch cho PipelineTreePanel (MAC DINH giu nguyen —
  4/6 host kia khong truyen, 0 doi hanh vi). O trong cay KHONG bao gio dung toi vi o ngoai
  di server-side va go vao la chuyen han sang flatMode => cay bien mat.
- Thang chu 4 nac (13/11/10/9) -> 3 nac chuan 13/11/10.
- 🔴 WCAG: text-slate-400 = 2.56:1 tren nen trang, DUOI nguong AA 4.5:1 — nang len
  slate-500 (4.76:1) cho 7 dong MANG THONG TIN (6 note/empty-state + 1 so dem).
  GIU slate-400 cho 2 cho TRANG TRI (icon Search, dau cham phan cach) — WCAG khong doi
  tuong phan cho noi dung trang tri.
- Nut phan trang ky-tu tran ←/→ -> icon ChevronLeft/Right + aria-label: mui ten dang chu
  doi hinh theo font he dieu-hanh va KHONG co ten cho trinh doc man hinh.
- Chip nhom: inline + co chu khac dong chua => hop chip cao khac chu ben canh nen tut
  baseline; inline-flex items-center leading-none ghim thang hang.

=== B4 host (KhkkCreatePage) ===
- Grid 260/320 DA land tu dot A — chi con COMMENT stale ghi 300px, da va.
- 🔴 Va comment SAI SU THAT (L3 H-5): comment khai Panel 1 co "nut hanh-dong dinh day"
  nhung grep -c Button KhkkWorkListPanel = 0 (control-duong PeListPanel = 8). Do la mo ta
  CUA PE be nguyen sang => tu khai mot thu chua dung.

=== 2 component DUNG CHUNG — mo rong kieu MAC-DINH-GIU-NGUYEN ===
AttachmentPreviewDialog +basePath? (mac dinh /purchase-evaluations) => 6 call-site PE
KHONG phai sua dong nao (module da golive; bat sua het call-site cu = tu de rui-ro o cho
dang chay tot). PipelineTreePanel +hideSearch? y het. Da verify: 0 call-site PE bi dong.

=== CON NO ===
🔴 CHUA soi mat tren prod 375/1366/1440 — ca 5 lane deu NO-SCREENSHOT (dev-rig chan:
chi :8080 LISTENING, API :5443 khong co, trang sau ProtectedRoute — gotcha #3). Moi ket
luan la DOC MA, khong phai da nhin.
Dot C3 (Field to brand cho MOI value) + dot D (O3/O4/O6/O7/O8) chua lam. O3 con cho owner
xac nhan cach doc "sum tong" — do la tien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-05 19:30:13 +07:00
parent c61e28f833
commit 43e1bd7bc2
14 changed files with 570 additions and 324 deletions

View File

@ -33,7 +33,8 @@
import { useMemo, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Check, History, Paperclip, Send, Trash2, Undo2, Workflow } from 'lucide-react'
import { Download, Eye, History, Paperclip, Send, Trash2 } from 'lucide-react'
import { AttachmentPreviewDialog, isPreviewable } from '@/components/pe/AttachmentPreviewDialog'
import { Button } from '@/components/ui/Button'
import { Dialog } from '@/components/ui/Dialog'
import { Label } from '@/components/ui/Label'
@ -104,6 +105,8 @@ export function KhkkWorkflowPanel({
const [pendingFiles, setPendingFiles] = useState<File[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const [confirmDelete, setConfirmDelete] = useState(false)
// [S175 · B1] File đang xem-trước inline (khuôn PE `PeWorkflowPanel.tsx:60`).
const [previewAtt, setPreviewAtt] = useState<KhkkAttachmentDto | null>(null)
const phase = plan.phase as KhkkPhaseValue
const steps = plan.workflowSteps ?? []
@ -174,15 +177,18 @@ export function KhkkWorkflowPanel({
const approvalFiles: KhkkAttachmentDto[] =
(plan.attachments ?? []).filter(a => a.purpose === KhkkAttachmentPurpose.ApprovalAttachment)
// [BE-5] Lịch sử thay đổi — chỉ tải khi người dùng mở khối (không tốn 1 request/phiếu).
const [showChangelog, setShowChangelog] = useState(false)
// [BE-5] Lịch sử thay đổi.
// 🔴 [S175 · B1] Khối này TRƯỚC ĐÂY gập lại và query `enabled: showChangelog` (lazy —
// tiết-kiệm 1 request/phiếu). Đợt này bỏ gập theo khuôn PE ⇒ **BẮT BUỘC bỏ luôn cờ
// `enabled`**: giữ lại thì khối luôn hiện nhưng query KHÔNG BAO GIỜ chạy, và nó hỏng
// dưới dạng "Chưa có thay đổi nào được ghi." — tức trông y hệt phiếu chưa từng sửa,
// KHÔNG phải trông như lỗi. Đổi lấy: +1 request mỗi lần mở phiếu, đúng giá PE đang trả.
const changelogs = useQuery({
queryKey: ['khkk-changelogs', plan.id],
queryFn: async () =>
(await api.get<KhkkChangelogDto[]>(`/contract-signing-plans/${plan.id}/changelogs`, {
params: { take: 200 },
})).data,
enabled: showChangelog,
})
const transition = useMutation({
@ -267,41 +273,44 @@ export function KhkkWorkflowPanel({
}
return (
<section className="card-accent" style={{ ['--accent' as string]: 'var(--color-emerald-500)' }}>
// [S175 · B1 · X2] Panel TRẦN khuôn PE (`PeWorkflowPanel.tsx:345`). Trước đây bọc
// `card-accent` ⇒ thẻ-trong-thẻ, lệch đập vào mắt trước mọi mục owner liệt kê.
// 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`/`shadow-none`: `.card-accent` (`index.css:112`)
// nằm NGOÀI `@layer` nên thắng utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật).
<div className="space-y-4">
{/* ── 1. Header + meta {code} v{NN} · {name} ───────────────────────────── */}
<header className="flex flex-wrap items-center gap-2 border-b border-slate-100 px-5 py-3">
<span
className="icon-chip"
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: 'var(--color-emerald-50)', ['--chip-fg' as string]: 'var(--color-emerald-600)' }}
aria-hidden
>
<Workflow className="h-4 w-4" />
</span>
<h3 className="text-sm font-semibold tracking-tight text-emerald-700">Quy trình duyệt</h3>
<div>
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
{plan.workflowCode && (
<span className="font-mono text-[11px] text-slate-500">
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
{plan.workflowCode}
{plan.workflowVersion != null && ` v${plan.workflowVersion}`}
{plan.workflowName && <span className="font-sans"> · {plan.workflowName}</span>}
</span>
{plan.workflowVersion != null && ` v${String(plan.workflowVersion).padStart(2, '0')}`}
{plan.workflowName && <> · <span className="font-sans">{plan.workflowName}</span></>}
</p>
)}
</header>
</div>
<div className="space-y-4 p-5">
{/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */}
{plan.endedByLevelFinalize && (
<div className="rounded border border-violet-300 bg-violet-50 px-3 py-2 text-[11px] text-violet-800">
<div className="font-medium">
Kết thúc tại {plan.finalizeStepName ?? 'cấp được cấu hình'}
{plan.finalizeLevelName ? ` · ${plan.finalizeLevelName}` : ''}
</div>
<div className="mt-0.5">
{isApproved
? 'Kế hoạch đã duyệt xong ngay tại cấp này — không qua CEO / Ban Giám đốc.'
: 'Cấp này được cấu hình duyệt là KẾT THÚC — nếu người duyệt tích ô kết thúc thì kế hoạch không qua CEO.'}
</div>
</div>
)}
{/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */}
{/* [S175 · B1] violet → EMERALD khuôn PE (`:360-377`). PE dành violet RIÊNG cho
"⚑ Duyệt thay CEO"; dùng violet cả ở đây là trộn 2 nghĩa vào một màu. */}
{plan.endedByLevelFinalize && (
<div className="rounded-md border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
{isApproved ? (
<span>
Kế hoạch đã kết thúc tại{' '}
<span className="font-semibold">{plan.finalizeStepName ?? 'cấp duyệt'}</span>
{plan.finalizeLevelName ? <> · {plan.finalizeLevelName}</> : null} không qua CEO.
</span>
) : (
<span>
Quy trình rút gọn: duyệt đến{' '}
<span className="font-semibold">{plan.finalizeStepName ?? 'cấp này'}</span>
{plan.finalizeLevelName ? <> · {plan.finalizeLevelName}</> : null} KẾT THÚC,
không trình CEO. Các cấp trước vẫn duyệt như thường.
</span>
)}
</div>
)}
{/* ── 3. Sơ đồ Bước → Cấp → NV ───────────────────────────────────────── */}
{steps.length > 0 ? (
@ -317,9 +326,10 @@ export function KhkkWorkflowPanel({
{STATUS_ICON[st]}
</span>
<span className="text-slate-800">Bước {step.order} {step.name}</span>
{/* Chip PHÒNG của Bước (BE-4 join `Departments`). */}
{/* Chip PHÒNG của Bước (BE-4 join `Departments`).
[S175 · B1] viền-xám-nền-trắng → EMERALD đặc khuôn PE (`:406-410`). */}
{step.departmentName && (
<span className="rounded border border-slate-300 bg-white px-1.5 py-0.5 text-[10px] font-medium text-slate-600">
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
{step.departmentName}
</span>
)}
@ -458,11 +468,11 @@ export function KhkkWorkflowPanel({
}}
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Duyệt cấp hiện tại'}
className={cn(
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
'rounded border px-2 py-1 text-[11px] font-bold transition',
blockedByLevel ? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400' : 'border-emerald-300 text-emerald-700 hover:bg-emerald-50',
)}
>
<Check className="h-3 w-3" /> Duyệt
Duyệt
</button>
<button
type="button"
@ -470,11 +480,11 @@ export function KhkkWorkflowPanel({
onClick={() => { if (!blockedByLevel) { setComment(''); setPendingFiles([]); setAction(KhkkTransitionAction.Return) } }}
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Trả lại người soạn sửa'}
className={cn(
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
'rounded border px-2 py-1 text-[11px] font-bold transition',
blockedByLevel ? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400' : 'border-amber-300 text-amber-700 hover:bg-amber-50',
)}
>
<Undo2 className="h-3 w-3" /> Trả lại
Trả lại
</button>
{/* 🧊 [S174 owner] Nút "TỪ CHỐI" ĐÃ GỠ — anh chốt "vậy thì bỏ luôn từ chối
~ tương tự PE". Duyệt NCC có code từ-chối nhưng THỰC TẾ không hiện
@ -488,7 +498,8 @@ export function KhkkWorkflowPanel({
<button
type="button"
onClick={() => setConfirmDelete(true)}
className="inline-flex items-center gap-1 rounded border border-slate-300 px-2 py-1 text-[11px] font-bold text-slate-600 transition hover:border-red-300 hover:bg-red-50 hover:text-red-700"
title="Xóa phiếu này (phiếu chuyển sang mục &ldquo;Đã xóa&rdquo;)"
className="inline-flex items-center gap-1 rounded border border-red-300 bg-white px-2 py-1 text-[11px] font-bold text-red-700 transition hover:bg-red-50"
>
<Trash2 className="h-3 w-3" /> Xóa phiếu
</button>
@ -498,32 +509,52 @@ export function KhkkWorkflowPanel({
)}
{/* ── 5. 📎 File đính kèm khi duyệt — TỰ ẨN khi rỗng (khuôn PE :928-967) ── */}
{/* [S175 · B1] +`border-t pt-4` +`<h3 text-sm>` +nút `<Eye>` xem trước.
Route BE ĐÃ CÓ `ContractSigningPlansController.cs:231` (`/view`, cùng hình với
PE `:317`) ⇒ chỉ cần truyền `basePath` cho dialog dùng chung.
🔴 Nhãn "Tải xuống" chuyển vào `title=` chứ KHÔNG bỏ: giữ đúng chuỗi đó trong
bundle để phép verify prod còn control-dương soi được. */}
{approvalFiles.length > 0 && (
<div>
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
<Paperclip className="h-3.5 w-3.5 text-teal-500" />
File đính kèm khi duyệt
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">
{approvalFiles.length}
</span>
</div>
<ul className="mt-1.5 space-y-1.5">
<div className="border-t border-slate-200 pt-4">
<h3 className="text-sm font-semibold text-slate-900">📎 File đính kèm khi duyệt</h3>
<p className="mt-0.5 text-[11px] text-slate-500">File do người duyệt tải lên trong quá trình duyệt.</p>
<div className="mt-2 space-y-1">
{approvalFiles.map(a => (
<li key={a.id} className="flex items-center justify-between gap-2 rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]">
<div className="min-w-0">
<div className="truncate font-medium text-slate-700">{a.fileName}</div>
<div className="text-slate-500">{formatSize(a.fileSize)} · {formatDateTime(a.createdAt)}</div>
</div>
<div key={a.id} className="flex items-center gap-1.5 rounded bg-slate-50 px-1.5 py-1 text-[11px]">
<Paperclip className="h-3 w-3 shrink-0 text-slate-400" />
<span className="min-w-0 flex-1 truncate text-slate-700" title={a.fileName}>{a.fileName}</span>
<span className="shrink-0 text-[10px] text-slate-400">{formatSize(a.fileSize)}</span>
{isPreviewable(a.fileName) && (
<button
type="button"
onClick={() => setPreviewAtt(a)}
className="shrink-0 rounded px-1 text-violet-600 hover:bg-violet-50"
title="Xem trước"
>
<Eye className="h-3 w-3" />
</button>
)}
<button
type="button"
onClick={() => downloadAttachment(a)}
className="shrink-0 text-[11px] font-medium text-brand-600 hover:underline"
className="shrink-0 rounded px-1 text-brand-600 hover:bg-brand-50"
title="Tải xuống"
>
Tải xuống
<Download className="h-3 w-3" />
</button>
</li>
</div>
))}
</ul>
</div>
{previewAtt && (
<AttachmentPreviewDialog
open
basePath="/contract-signing-plans"
evaluationId={plan.id}
attachmentId={previewAtt.id}
fileName={previewAtt.fileName}
onClose={() => setPreviewAtt(null)}
/>
)}
</div>
)}
@ -531,61 +562,63 @@ export function KhkkWorkflowPanel({
{/* [B4/D-11] BE ĐÃ LỌC hàng `decision = Pending` (hành-vi GỬI duyệt) ⇒ số (N) ở đây
CÙNG NGHĨA với (N) của Duyệt NCC. FE KHÔNG dedupe lại; sự kiện "Gửi duyệt" vẫn
còn nguyên ở "Lịch sử thay đổi" bên dưới. */}
<div>
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
<Check className="h-3.5 w-3.5 text-emerald-500" />
Lịch sử duyệt
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">
{approvals.length}
</span>
</div>
{/* [S175 · B1] Bỏ pill đếm → `(n)` trong ngoặc · thẻ `bg-slate-50/60 text-[11px]`
→ `bg-white p-3 text-sm` · mốc-giờ ĐẨY PHẢI bằng `justify-between`.
🔴 Badge 2-ngả → 3-NGẢ: trước đây MỌI thứ không phải `Reject` đều tô emerald
⇒ "Trả lại" (amber ở PE) bị vẽ xanh như đã-duyệt — đọc ngược nghĩa.
`toPhase` đã có sẵn (`khkk.ts:307`) nên phân được TraLai ⟂ TuChoi ⟂ Duyệt. */}
<div className="border-t border-slate-200 pt-4">
<h3 className="text-sm font-semibold text-slate-900">Lịch sử duyệt ({approvals.length})</h3>
{approvals.length === 0 ? (
<p className="mt-1.5 text-[11px] italic text-slate-400">Chưa lượt duyệt nào.</p>
) : (
<ul className="mt-1.5 space-y-1.5">
{approvals.map(a => (
<li key={a.id} className="rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]">
<div className="flex flex-wrap items-center gap-x-1.5 text-slate-700">
<span className={cn(
'rounded px-1.5 py-0.5 text-[9px] font-semibold',
a.decision === KhkkApprovalDecision.Reject ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800',
)}>
{DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'}
</span>
<span className="font-medium">{a.approvedByFullName ?? '(hệ thống)'}</span>
<span className="text-slate-400">·</span>
<span className="text-slate-500">{formatDateTime(a.approvedAt)}</span>
</div>
<div className="mt-0.5 text-slate-600">
{a.stepName ? `Bước ${a.stepOrder}${a.stepName}` : 'Cấp duyệt'}
{a.levelOrder != null && ` · Cấp ${a.levelOrder}`}
{a.levelName ? ` (${a.levelName})` : ''}
<span className="text-slate-400">
{' '}· {KHKK_PHASE_LABELS[a.fromPhase]} {KHKK_PHASE_LABELS[a.toPhase]}
</span>
</div>
{a.comment?.trim() && (
<div className="mt-0.5 whitespace-pre-wrap text-slate-600">{a.comment}</div>
)}
</li>
))}
<ul className="mt-2 space-y-1.5">
{approvals.map(a => {
const isReject = a.toPhase === KhkkPhase.TuChoi
const isReturn = a.toPhase === KhkkPhase.TraLai
return (
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
<div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-slate-700">
<div className="flex flex-wrap items-center gap-x-1.5">
<span className={cn(
'rounded px-1.5 py-0.5 text-[10px] font-semibold',
isReject ? 'bg-rose-100 text-rose-800'
: isReturn ? 'bg-amber-100 text-amber-800'
: 'bg-emerald-100 text-emerald-800',
)}>
{DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'}
</span>
<span className="font-medium">{a.approvedByFullName ?? '(hệ thống)'}</span>
</div>
<span className="text-[11px] text-slate-500">{formatDateTime(a.approvedAt)}</span>
</div>
<div className="mt-1 text-[12px] text-slate-600">
{a.stepName ? `Bước ${a.stepOrder}${a.stepName}` : 'Cấp duyệt'}
{a.levelOrder != null && ` · Cấp ${a.levelOrder}`}
{a.levelName ? ` (${a.levelName})` : ''}
<span className="text-slate-400">
{' '}· {KHKK_PHASE_LABELS[a.fromPhase]} {KHKK_PHASE_LABELS[a.toPhase]}
</span>
</div>
{a.comment?.trim() && (
<div className="mt-1 whitespace-pre-wrap text-[12px] text-slate-600">{a.comment}</div>
)}
</li>
)
})}
</ul>
)}
</div>
{/* ── 7. Lịch sử thay đổi ────────────────────────────────────────────── */}
<div>
<button
type="button"
onClick={() => setShowChangelog(v => !v)}
className="flex w-full items-center gap-1.5 text-xs font-semibold text-slate-700 hover:text-brand-700"
>
{/* [S175 · B1] BỎ GẬP — PE render thẳng (`PeDetailTabs.tsx:787-794`). Nội-dung
đã tải sẵn qua query; giấu sau 1 cú bấm chỉ thêm ma-sát, không tiết-kiệm gì. */}
<div className="border-t border-slate-200 pt-4">
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-slate-900">
<History className="h-3.5 w-3.5 text-slate-400" />
Lịch sử thay đi
<span className="ml-auto text-[10px] font-normal text-slate-400">{showChangelog ? 'Ẩn' : 'Xem'}</span>
</button>
{showChangelog && (
<div className="mt-1.5">
</h3>
<div className="mt-2">
{changelogs.isLoading && <p className="text-[11px] text-slate-400">Đang tải</p>}
{changelogs.isError && <p className="text-[11px] text-red-600">Không tải đưc lịch sử thay đi.</p>}
{changelogs.data && changelogs.data.length === 0 && (
@ -614,10 +647,8 @@ export function KhkkWorkflowPanel({
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
{action !== null && (
<Dialog
@ -625,7 +656,7 @@ export function KhkkWorkflowPanel({
onClose={() => setAction(null)}
title={dialogTitle}
footer={<>
<Button variant="outline" onClick={() => setAction(null)}>Huỷ</Button>
<Button variant="outline" onClick={() => setAction(null)}>Hủy</Button>
<Button onClick={() => transition.mutate(action)} disabled={confirmDisabled}>
{transition.isPending ? 'Đang gửi...' : 'Xác nhận'}
</Button>
@ -663,22 +694,26 @@ export function KhkkWorkflowPanel({
</label>
</div>
)}
{/* [S175 · B1 micro-copy] Nhãn Ô NHẬP theo PE (`PeWorkflowPanel.tsx:869` "Ghi chú
(tùy chọn)"). 🔴 CHỈ đổi tên TRƯỜNG — chuỗi "Ý kiến cấp duyệt" ở dòng trợ-giúp
bên dưới là TÊN THẬT của mục đích đến trong phiếu (Section 5, Mig 26), đổi nó
là trỏ người dùng tới một mục không tồn tại. */}
<Label>
Ý kiến{commentRequired ? <span className="text-red-500"> *</span> : ' (tuỳ chọn)'}
Ghi chú{commentRequired ? <span className="text-red-500"> *</span> : ' (tùy chọn)'}
</Label>
<Textarea
value={comment}
onChange={e => setComment(e.target.value)}
rows={3}
maxLength={2000}
placeholder={commentRequired ? 'Nêu lý do trả lại / từ chối...' : 'Ý kiến của bạn (ghi vào phiếu)...'}
placeholder={commentRequired ? 'Nêu lý do trả lại / từ chối...' : 'Ghi chú của bạn (ghi vào phiếu)...'}
/>
{/* [QĐ-3 / BE-7] File đính kèm khi duyệt — upload THẬT qua đường riêng
`POST {id}/approval-attachments` (BE ép `purpose=ApprovalAttachment`). */}
{action !== KhkkTransitionAction.Submit && (
<div className="mt-3">
<Label className="text-xs">File đính kèm (tuỳ chọn)</Label>
<Label className="text-xs">File đính kèm (tùy chọn)</Label>
<div className="mt-1 flex flex-wrap items-center gap-2">
<input
ref={fileInputRef}
@ -714,7 +749,7 @@ export function KhkkWorkflowPanel({
{action === KhkkTransitionAction.Approve && (
<p className="mt-1 text-[11px] text-slate-500">
Ý kiến sẽ đưc ghi vào mục &ldquo;Ý kiến cấp duyệt&rdquo; của kế hoạch (1 ô/Cấp duyệt lại
Ghi chú sẽ đưc ghi vào mục &ldquo;Ý kiến cấp duyệt&rdquo; của kế hoạch (1 ô/Cấp duyệt lại
thì ghi đè ô ).
</p>
)}
@ -727,7 +762,7 @@ export function KhkkWorkflowPanel({
onClose={() => setConfirmDelete(false)}
title="🗑 Xóa kế hoạch ký kết"
footer={<>
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Huỷ</Button>
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Hủy</Button>
<Button onClick={() => removePlan.mutate()} disabled={removePlan.isPending}>
{removePlan.isPending ? 'Đang xóa...' : 'Xóa phiếu'}
</Button>
@ -739,6 +774,6 @@ export function KhkkWorkflowPanel({
</div>
</Dialog>
)}
</section>
</div>
)
}