[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
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:
@ -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} là 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 “Đã xóa”)"
|
||||
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 có 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 “Ý kiến cấp duyệt” của kế hoạch (1 ô/Cấp — duyệt lại
|
||||
Ghi chú sẽ được ghi vào mục “Ý kiến cấp duyệt” của kế hoạch (1 ô/Cấp — duyệt lại
|
||||
thì ghi đè ô cũ).
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user