// Detail content cho 1 phiếu Duyệt NCC. Flat render (no tabs): Thông tin + // NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình. // Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel // → PeApprovalsSection + PeHistorySection). import { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' import { Check, ChevronDown, ChevronRight, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' import { Select } from '@/components/ui/Select' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' import { PeAttachmentPurpose, PeAttachmentPurposeLabel, PeDepartmentKind, PeDepartmentKindLabel, PeDisplayStatusColor, PeDisplayStatusLabel, PurchaseEvaluationPhase, PurchaseEvaluationPhaseColor, PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, getPeDisplayStatus, isEditablePhase, type PeAttachment, type PeChangelog, type PeDepartmentOpinion, type PeDetailBundle, type PeDetailRow, type PeLevelOpinion, type PeQuote, type PeSupplier, } from '@/types/purchaseEvaluation' import { BudgetPhase, type BudgetListItem } from '@/types/budget' import type { Paged, Supplier } from '@/types/master' const fmtMoney = (v: number) => v.toLocaleString('vi-VN') // Main detail content — flat render 3 section không tabs. // Tên giữ PeDetailTabs để không break callsite (rename gây churn). // // `mode` (2026-05-07): // - 'detail' (default): full UX — Section 5 Ý kiến 4PB editable theo readOnly. // Dùng ở leaf "Danh sách" + "Duyệt" (3-panel pages). // - 'workspace': dùng ở leaf "Thao tác" (2-panel workspace). Section 5 LUÔN // disabled (Q5 user — ý kiến nhập khi duyệt, không phải workspace nhập liệu). // Workflow Panel + Approvals + History KHÔNG render trong PeDetailTabs (luôn // ở caller PeWorkflowPanel — workspace caller skip render Panel 3 hoàn toàn). export function PeDetailTabs({ evaluation, onBack, onDelete, readOnly = false, mode = 'detail', autoEditHeader = false, }: { evaluation: PeDetailBundle onBack: () => void onDelete: () => void /** Menu "Duyệt" (pendingMe=1) — ẩn mọi action thêm/sửa/xóa, chỉ xem + duyệt phase. */ readOnly?: boolean /** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */ mode?: 'detail' | 'workspace' /** Auto open Section 1 InfoTab in edit mode khi mount — triggered từ pencil icon Panel 1 */ autoEditHeader?: boolean }) { const qc = useQueryClient() // canEditPhase: bao gồm cả TraLai (user 2026-05-07). Header bar action // buttons "Sửa header" + "Xóa" + "Đóng" workspace mode đã chuyển xuống bottom // action bar (B11+ user 2026-05-07). const canEditPhase = isEditablePhase(evaluation.phase) const opinionsReadOnly = readOnly || mode === 'workspace' // "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition // sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing // (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace. const submitForApproval = useMutation({ mutationFn: async () => { const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai) if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt') return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, { targetPhase: next, decision: 1, comment: null, }) }, onSuccess: () => { toast.success('Đã gửi duyệt phiếu — chuyển sang quy trình duyệt.') qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] }) qc.invalidateQueries({ queryKey: ['pe-list'] }) onBack() }, onError: e => toast.error(getErrorMessage(e)), }) const forwardPhase = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai) const canSubmitForApproval = mode === 'workspace' && canEditPhase && !readOnly && forwardPhase != null // Tooltip reason cho button disabled (giúp diagnose tại sao "Lưu & Gửi Duyệt" // không bấm được — user feedback 2026-05-07). const submitDisabledReason = !canEditPhase ? `Phiếu đã ở phase ${PurchaseEvaluationPhaseLabel[evaluation.phase]} — chỉ Bản nháp / Trả lại mới sửa + gửi được.` : readOnly ? 'Chế độ chỉ đọc.' : !forwardPhase ? `Workflow không có phase tiếp theo từ ${PurchaseEvaluationPhaseLabel[evaluation.phase]}. Liên hệ admin kiểm tra cấu hình quy trình.` : null return (

{evaluation.tenGoiThau}

{/* Display status meta (Bản nháp / Đã gửi duyệt / Đã duyệt / Từ chối) — phase chi tiết hiện ở Workflow timeline Panel 3. */} {PeDisplayStatusLabel[getPeDisplayStatus(evaluation.phase)]} ({PurchaseEvaluationPhaseLabel[evaluation.phase]}) {readOnly && ( chế độ duyệt )}
{evaluation.maPhieu ?? '—'} · {PurchaseEvaluationTypeLabel[evaluation.type]} · {evaluation.projectName} {evaluation.drafterName && <>·Soạn: {evaluation.drafterName}}
{/* Header bar actions: User 2026-05-07 chốt bỏ "Sửa header" + "Xóa" + "Đóng" (workspace mode actions chuyển xuống bottom action bar). Vẫn giữ Đóng cho non-workspace view (Danh sách + Duyệt — readOnly). */} {(readOnly || mode !== 'workspace') && (
)}
{/* Section layout (Session 20 Chunk B): Hạng mục nested expand chứa NCC (tầng 1 = hạng mục, tầng 2 = NCC tham gia + báo giá inline). NCC tham gia section riêng bỏ — gộp vào Section 2 expand panel. Tên hạng mục + giá trị auto từ gói thầu (Chunk A BE seed). */}
{mode === 'workspace' && (
Ý kiến + chữ ký auto đồng bộ khi NV duyệt phiếu — vào menu “Duyệt” để ký.
)} {/* Mig 26 — V2 dynamic theo ApprovalWorkflowLevel. V1 phiếu cũ fallback render 4 box CỨNG readOnly (data legacy giữ Mig 15). */} {evaluation.approvalWorkflowId ? : }
{/* Action bar bottom — workspace mode + canEdit + !readOnly. 3 nút: - Xóa phiếu (CHỈ Bản nháp, soft-delete BE) — bên trái red - Lưu (toast confirm, KHÔNG đóng workspace) — chính giữa ghost - Lưu & Gửi Duyệt → (POST /transitions → next phase) — bên phải brand User 2026-05-07. */} {mode === 'workspace' && canEditPhase && !readOnly && (
{/* Xóa phiếu — CHỈ DangSoanThao (bản nháp). TraLai không cho xóa (đã có lịch sử workflow). Soft-delete qua DELETE /pe/:id endpoint (AuditableEntity IsDeleted=true, không xóa hoàn toàn DB). */} {evaluation.phase === PurchaseEvaluationPhase.DangSoanThao && ( )} ✓ Các thay đổi đã tự động lưu khi chỉnh sửa từng phần.
)}
) } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } // ===== Section 5 — Ý kiến 4 phòng ban ===== // Render 2x2 grid 4 box (Phê duyệt / CCM / MuaHàng / SM-PM). Mỗi box hiển // thị Opinion text + chữ ký (UserName + SignedAt) nếu đã ký, hoặc form nhập // + 2 button "Lưu" + "Lưu & Ký" khi chưa ký / readOnly=false. function DepartmentOpinionsSection({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) { const KINDS: { kind: number; label: string }[] = [ { kind: PeDepartmentKind.PheDuyet, label: PeDepartmentKindLabel[PeDepartmentKind.PheDuyet] }, { kind: PeDepartmentKind.Ccm, label: PeDepartmentKindLabel[PeDepartmentKind.Ccm] }, { kind: PeDepartmentKind.MuaHang, label: PeDepartmentKindLabel[PeDepartmentKind.MuaHang] }, { kind: PeDepartmentKind.SmPm, label: PeDepartmentKindLabel[PeDepartmentKind.SmPm] }, ] return (
{KINDS.map(k => { const existing = ev.departmentOpinions.find(o => o.kind === k.kind) ?? null return ( ) })}
) } function OpinionBox({ evaluationId, kind, kindLabel, existing, readOnly, }: { evaluationId: string kind: number kindLabel: string existing: PeDepartmentOpinion | null readOnly: boolean }) { const qc = useQueryClient() const [text, setText] = useState(existing?.opinion ?? '') const isSigned = !!existing?.signedAt const save = useMutation({ mutationFn: async (sign: boolean) => api.post(`/purchase-evaluations/${evaluationId}/opinions`, { kind, opinion: text || null, sign, }), onSuccess: () => { toast.success('Đã lưu ý kiến.') qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }) }, onError: e => toast.error(getErrorMessage(e)), }) return (

{kindLabel}

{isSigned && ( Đã ký )}
{readOnly ? ( <>
{existing?.opinion ?? — chưa có ý kiến}
{isSigned && (
Ký bởi {existing?.userName ?? '—'} · {new Date(existing!.signedAt!).toLocaleString('vi-VN')}
)} ) : ( <>