From 43e1bd7bc2f34ac4a063581405bcb577b601f788 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 5 Aug 2026 19:30:13 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Contract:=20UAT=20S174=20dot-B=20?= =?UTF-8?q?=E2=80=94=20be=20style=20PE=20sang=20KHKK=20(FE-only,=200-mig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 + + 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 : 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 . === 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 --- .../src/components/khkk/KhkkDetailContent.tsx | 80 ++++-- .../src/components/khkk/KhkkListPanel.tsx | 47 +++- .../src/components/khkk/KhkkWorkListPanel.tsx | 8 +- .../src/components/khkk/KhkkWorkflowPanel.tsx | 263 ++++++++++-------- .../components/pe/AttachmentPreviewDialog.tsx | 10 +- .../components/pipeline/PipelineTreePanel.tsx | 29 +- fe-admin/src/pages/khkk/KhkkCreatePage.tsx | 10 +- .../src/components/khkk/KhkkDetailContent.tsx | 80 ++++-- fe-user/src/components/khkk/KhkkListPanel.tsx | 47 +++- .../src/components/khkk/KhkkWorkListPanel.tsx | 8 +- .../src/components/khkk/KhkkWorkflowPanel.tsx | 263 ++++++++++-------- .../components/pe/AttachmentPreviewDialog.tsx | 10 +- .../components/pipeline/PipelineTreePanel.tsx | 29 +- fe-user/src/pages/khkk/KhkkCreatePage.tsx | 10 +- 14 files changed, 570 insertions(+), 324 deletions(-) diff --git a/fe-admin/src/components/khkk/KhkkDetailContent.tsx b/fe-admin/src/components/khkk/KhkkDetailContent.tsx index 67fb9b6..c247e9d 100644 --- a/fe-admin/src/components/khkk/KhkkDetailContent.tsx +++ b/fe-admin/src/components/khkk/KhkkDetailContent.tsx @@ -21,7 +21,7 @@ import { useState, type ReactNode } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { - ClipboardList, FileCheck, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users, + ClipboardList, FileCheck, Lock, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users, } from 'lucide-react' import { toast } from 'sonner' import { PePipelineStrip } from '@/components/pe/PePipelineStrip' @@ -48,6 +48,21 @@ function formatVnd(n: number | null | undefined): string { return n.toLocaleString('vi-VN') + ' đ' } +// [S175 · B2 · P0] Ô RỖNG phải MỜ HƠN dữ-liệu thật. Trước đây em-dash thừa-hưởng đúng +// màu chữ của ô (`text-slate-600/700`) ⇒ lướt bảng thì "—" trông y như một GIÁ-TRỊ. +// Gom về một chỗ để các ô rỗng không mỗi nơi một kiểu. +const DASH = + +/** Chuỗi có thể rỗng → em-dash mờ. */ +function orDash(v: string | null | undefined): ReactNode { + return v == null || v === '' ? DASH : v +} + +/** Tiền: `formatVnd` trả CHUỖI "—" khi rỗng nên không tô mờ được — tách nhánh rỗng ra. */ +function moneyOrDash(n: number | null | undefined): ReactNode { + return n == null ? DASH : formatVnd(n) +} + function formatDateTime(iso: string): string { return new Date(iso).toLocaleString('vi-VN') } @@ -362,17 +377,32 @@ export function KhkkDetailContent({ chipFg="var(--color-teal-700)" actions={ plan.budgetFrozen ? ( - - 🔒 Giá đã chốt tại thời điểm duyệt + // [S175 · B2 · P0] emoji 🔒 → icon lucide: emoji render khác nhau theo font hệ + // điều-hành (Windows/macOS/Android ra 3 hình), và nó KHÔNG ăn `currentColor` + // nên luôn lệch tông với chữ amber bên cạnh. + + Giá đã chốt tại thời điểm duyệt ) : undefined } > {plan.lines.length === 0 ? ( -
Phiếu nguồn chưa có NCC trúng thầu.
+
Phiếu nguồn chưa có NCC trúng thầu.
) : ( + // [S175 · B2 · P0] `table-fixed` + `` + `min-w-[880px]`: bảng co giãn tự-do + // trước đây để cột tiền nhảy bề rộng theo độ dài tên NCC ⇒ 3 cột số không thẳng hàng + // giữa các phiếu. Chốt bề rộng rồi thì hẹp màn hình sẽ CUỘN NGANG (đúng ý đồ của + // `overflow-x-auto` bọc ngoài) thay vì bóp chữ.
- +
+ + + + + + + + @@ -385,16 +415,16 @@ export function KhkkDetailContent({ {plan.lines.map((l) => ( - + - - - - - + + + + ))} @@ -462,10 +492,18 @@ export function KhkkDetailContent({ } > {plan.dossierItems.length === 0 ? ( -
Chưa khai căn cứ nào.
+
Chưa khai căn cứ nào.
) : (
-
Nhà cung cấp
- {l.supplierName ?? '—'} + {orDash(l.supplierName)} {l.contractId && ( ✓ HĐ )} + {isDraft && !l.contractId ? ( <> {formatVnd(l.peReferenceAmount)}{formatVnd(l.proposedAmount)}{formatVnd(l.approvedAmount)}{l.note ?? '—'}{moneyOrDash(l.peReferenceAmount)}{moneyOrDash(l.proposedAmount)}{moneyOrDash(l.approvedAmount)}{orDash(l.note)}
+
+ + + + + + + {isDraft && } + @@ -478,19 +516,19 @@ export function KhkkDetailContent({ {plan.dossierItems.map((d: KhkkDossierItemDto) => ( - - + + - - + {isDraft && (
Loại
{DOSSIER_KIND_LABELS[d.kind] ?? '—'}
{orDash(DOSSIER_KIND_LABELS[d.kind])} {d.name} {DOSSIER_STATUS_LABELS[d.status]} - {d.tvgsName ?? '—'} + + {orDash(d.tvgsName)} {d.tvgsResultAt && ({formatDateTime(d.tvgsResultAt)})} {d.note ?? '—'}{orDash(d.note)}
@@ -544,7 +582,7 @@ export function KhkkDetailContent({ } > {planAttachments.length === 0 ? ( -
Chưa có file đính kèm.
+
Chưa có file đính kèm.
) : (
    {planAttachments.map((a) => ( diff --git a/fe-admin/src/components/khkk/KhkkListPanel.tsx b/fe-admin/src/components/khkk/KhkkListPanel.tsx index 7088dbd..a4373ca 100644 --- a/fe-admin/src/components/khkk/KhkkListPanel.tsx +++ b/fe-admin/src/components/khkk/KhkkListPanel.tsx @@ -23,7 +23,7 @@ // (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo // "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt. import { useQuery } from '@tanstack/react-query' -import { FileCheck, Inbox, Search } from 'lucide-react' +import { ChevronLeft, ChevronRight, FileCheck, Inbox, ListTree, Search } from 'lucide-react' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel' @@ -144,8 +144,12 @@ export function KhkkPlanCard({
{plan.maKeHoach ?? '(chưa cấp mã)'} + {/* [S175 · B3] Chip nhóm: `text-[9px]` là nấc thứ TƯ của thang chữ (13/11/10/9) — + gom về 3 nấc chuẩn 13/11/10. Và `inline` + cỡ chữ khác dòng chứa ⇒ hộp chip cao + khác chữ bên cạnh nên tụt baseline; `inline-flex items-center leading-none` ghim + nó thẳng hàng thay vì dựa vào baseline của 2 cỡ chữ khác nhau. */} {plan.approvalGroup != null && ( - + N{plan.approvalGroup} )} @@ -227,7 +231,7 @@ export function KhkkListPanel({ const shown = group === null ? plans : plans.filter(k => k.approvalGroup === group) if (shown.length === 0) { return ( -

+

{group === null ? 'Gói này chưa có kế hoạch ký kết.' : `Gói này chưa có phiếu nhóm N${group}.`}

) @@ -275,23 +279,42 @@ export function KhkkListPanel({ ))} {flatMode && !deletedView && canPe && ( -

+

Đang lọc — danh sách lấy thẳng từ máy chủ (không giới hạn 200 phiếu của cây).

)} {!canPe && ( -

+

Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình không dựng được — danh sách kế hoạch ký kết vẫn liệt kê đầy đủ ở dưới.

)}
+ {/* [S175 · B3] ĐẦU CỘT — trước đây chế-độ CÂY đi thẳng vào thân, không tiêu-đề cũng + không số đếm ⇒ cột "cụt đầu": lệch cả với màn Thao tác (`KhkkWorkListPanel` có + `Tier` header) LẪN với chính chế-độ phẳng (có dòng đếm ở chân). Mirror đúng khuôn + `Tier` để 2 màn KHKK cùng một hình. */} +
+ + {flatMode ? : } + + + {deletedView ? 'Phiếu đã xóa' : flatMode ? 'Danh sách kế hoạch' : 'Cây toàn trình'} + + {flatMode && !list.isLoading && !list.isError && ( + + {total} + + )} +
+
{!flatMode && ( @@ -350,22 +373,28 @@ export function KhkkListPanel({ {total} kế hoạch — Trang {page} / {totalPages} + {/* [S175 · B3] Ký-tự trần `←`/`→` → icon: mũi tên dạng chữ đổi hình theo font hệ + điều-hành và KHÔNG có tên cho trình đọc màn hình. Icon + `aria-label` sửa cả hai. */}
diff --git a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx index 58d24ac..e1086e5 100644 --- a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx +++ b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx @@ -171,7 +171,7 @@ export function KhkkWorkListPanel({ )} {!candidates.isLoading && !candidates.isError && awaiting.length === 0 && ( -

+

Mọi phiếu đã duyệt đều đã có kế hoạch ký kết.

)} @@ -181,7 +181,7 @@ export function KhkkWorkListPanel({ {projectName} - {items.length} + {items.length}
    {items.map((pe) => ( @@ -241,7 +241,7 @@ export function KhkkWorkListPanel({ )} {!plans.isLoading && !plans.isError && planItems.length === 0 && ( -

    +

    Không còn kế hoạch nào đang xử lý. Phiếu đã duyệt không hiện ở đây — xem tại menu Kế hoạch ký kết HĐ.

    @@ -287,7 +287,7 @@ export function KhkkWorkListPanel({ sẽ đọc thành "đang cắt bớt" trong khi thật ra là "đang lọc bỏ". Nói đúng việc đang làm, và chỉ nói khi thật sự có cái bị giấu. */} {plans.data && plans.data.items.length > planItems.length && ( -

    +

    Đã ẩn kế hoạch ở trạng thái “Đã duyệt” — xem đủ ở menu Kế hoạch ký kết HĐ.

    )} diff --git a/fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx b/fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx index 5601048..2c5c900 100644 --- a/fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx +++ b/fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx @@ -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([]) const fileInputRef = useRef(null) const [confirmDelete, setConfirmDelete] = useState(false) + // [S175 · B1] File đang xem-trước inline (khuôn PE `PeWorkflowPanel.tsx:60`). + const [previewAtt, setPreviewAtt] = useState(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(`/contract-signing-plans/${plan.id}/changelogs`, { params: { take: 200 }, })).data, - enabled: showChangelog, }) const transition = useMutation({ @@ -267,41 +273,44 @@ export function KhkkWorkflowPanel({ } return ( -
    + // [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). +
    {/* ── 1. Header + meta {code} v{NN} · {name} ───────────────────────────── */} -
    - - - -

    Quy trình duyệt

    +
    +

    Quy trình duyệt

    {plan.workflowCode && ( - +

    {plan.workflowCode} - {plan.workflowVersion != null && ` v${plan.workflowVersion}`} - {plan.workflowName && · {plan.workflowName}} - + {plan.workflowVersion != null && ` v${String(plan.workflowVersion).padStart(2, '0')}`} + {plan.workflowName && <> · {plan.workflowName}} +

    )} -
    +
    -
    - {/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */} - {plan.endedByLevelFinalize && ( -
    -
    - ⚑ Kết thúc tại {plan.finalizeStepName ?? 'cấp được cấu hình'} - {plan.finalizeLevelName ? ` · ${plan.finalizeLevelName}` : ''} -
    -
    - {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.'} -
    -
    - )} + {/* ── 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 && ( +
    + {isApproved ? ( + + ✅ Kế hoạch đã kết thúc tại{' '} + {plan.finalizeStepName ?? 'cấp duyệt'} + {plan.finalizeLevelName ? <> · {plan.finalizeLevelName} : null} — không qua CEO. + + ) : ( + + ⚑ Quy trình rút gọn: duyệt đến{' '} + {plan.finalizeStepName ?? 'cấp này'} + {plan.finalizeLevelName ? <> · {plan.finalizeLevelName} : null} là KẾT THÚC, + không trình CEO. Các cấp trước vẫn duyệt như thường. + + )} +
    + )} {/* ── 3. Sơ đồ Bước → Cấp → NV ───────────────────────────────────────── */} {steps.length > 0 ? ( @@ -317,9 +326,10 @@ export function KhkkWorkflowPanel({ {STATUS_ICON[st]} Bước {step.order} — {step.name} - {/* 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 && ( - + {step.departmentName} )} @@ -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', )} > - Duyệt + ✓ Duyệt {/* 🧊 [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({ @@ -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` +`

    ` +nút `` 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 && ( -
    -
    - - File đính kèm khi duyệt - - {approvalFiles.length} - -
    -
      +
      +

      📎 File đính kèm khi duyệt

      +

      File do người duyệt tải lên trong quá trình duyệt.

      +
      {approvalFiles.map(a => ( -
    • -
      -
      {a.fileName}
      -
      {formatSize(a.fileSize)} · {formatDateTime(a.createdAt)}
      -
      +
      + + {a.fileName} + {formatSize(a.fileSize)} + {isPreviewable(a.fileName) && ( + + )} -
    • +
      ))} -
    +
    + {previewAtt && ( + setPreviewAtt(null)} + /> + )}

    )} @@ -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. */} -
    -
    - - Lịch sử duyệt - - {approvals.length} - -
    + {/* [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. */} +
    +

    Lịch sử duyệt ({approvals.length})

    {approvals.length === 0 ? (

    Chưa có lượt duyệt nào.

    ) : ( -
      - {approvals.map(a => ( -
    • -
      - - {DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'} - - {a.approvedByFullName ?? '(hệ thống)'} - · - {formatDateTime(a.approvedAt)} -
      -
      - {a.stepName ? `Bước ${a.stepOrder} — ${a.stepName}` : 'Cấp duyệt'} - {a.levelOrder != null && ` · Cấp ${a.levelOrder}`} - {a.levelName ? ` (${a.levelName})` : ''} - - {' '}· {KHKK_PHASE_LABELS[a.fromPhase]} → {KHKK_PHASE_LABELS[a.toPhase]} - -
      - {a.comment?.trim() && ( -
      {a.comment}
      - )} -
    • - ))} +
        + {approvals.map(a => { + const isReject = a.toPhase === KhkkPhase.TuChoi + const isReturn = a.toPhase === KhkkPhase.TraLai + return ( +
      • +
        +
        + + {DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'} + + {a.approvedByFullName ?? '(hệ thống)'} +
        + {formatDateTime(a.approvedAt)} +
        +
        + {a.stepName ? `Bước ${a.stepOrder} — ${a.stepName}` : 'Cấp duyệt'} + {a.levelOrder != null && ` · Cấp ${a.levelOrder}`} + {a.levelName ? ` (${a.levelName})` : ''} + + {' '}· {KHKK_PHASE_LABELS[a.fromPhase]} → {KHKK_PHASE_LABELS[a.toPhase]} + +
        + {a.comment?.trim() && ( +
        {a.comment}
        + )} +
      • + ) + })}
      )}
    {/* ── 7. Lịch sử thay đổi ────────────────────────────────────────────── */} -
    - - {showChangelog && ( -
    + +
    {changelogs.isLoading &&

    Đang tải…

    } {changelogs.isError &&

    Không tải được lịch sử thay đổi.

    } {changelogs.data && changelogs.data.length === 0 && ( @@ -614,10 +647,8 @@ export function KhkkWorkflowPanel({ ))}
- - )} + - {action !== null && ( setAction(null)} title={dialogTitle} footer={<> - + @@ -663,22 +694,26 @@ export function KhkkWorkflowPanel({ )} + {/* [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. */}