[CLAUDE] PE Panel 3: bỏ phase cards + render flow workflow V2 thực tế
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m14s

User feedback: "bỏ luôn cái quy trình phía trên đi nhé, vì nó là trạng
thái rồi (đã có badge), update cái flow quy trình mới vào bên panel 3
đang đến ai".

BE — ApprovalFlow DTO mới (full snapshot Bước → Cấp → NV với Status):
- PurchaseEvaluationApprovalFlowDto { CurrentStepIndex, CurrentLevelOrder,
  Steps[] }
- PurchaseEvaluationApprovalFlowStepDto { Order, Name, DepartmentId/Name,
  Status, Levels[] }
- PurchaseEvaluationApprovalFlowLevelDto { Order, Name, Approvers[], Status }
- Status: "Done" | "Current" | "Pending"

Handler GetById compute Status logic:
  - Phase=DaDuyet  → tất cả Steps/Levels "Done"
  - Phase=Nháp/Trả lại/Từ chối → tất cả "Pending"
  - Phase=ChoDuyet:
    * Step.Index < currentIdx          → all Levels "Done"
    * Step.Index == currentIdx:
        Level.Order < currentLevelOrder → "Done"
        Level.Order == currentLevelOrder → "Current"
        Level.Order > currentLevelOrder → "Pending"
    * Step.Index > currentIdx           → all "Pending"
- Load Approvers info (FullName + Email) qua UserManager batch query

FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeApprovalFlow + Step + Level + Status union
  PeDetail.approvalFlow optional
- PeWorkflowPanel:
  * BỎ phase cards section (4 ô Nháp/TraLai/ChoDuyet/DaDuyet) — đã
    duplicate với status badge ở header
  * Header mới: "Quy trình duyệt" + Code + Version + Name workflow pin
  * Render Flow vertical: Bước (icon ✓/●/○) → border + bg theo status
    + dept badge → list Cấp (icon nhỏ) với label "đang chờ" / "đã
    duyệt" + tên NV duyệt
  * Phiếu V1 legacy (no flow): show note "dùng quy trình cũ — không
    khả dụng chi tiết"
  * Bỏ helper isPastPhase() (orphan sau khi xóa cards)

Verify: BE build 0 error · 2 FE builds OK.

Test eoffice:
1. Mở phiếu V2 đang ChoDuyet → thấy flow Bước 1 (Phòng A):
   ✓ Cấp 1 NV X (đã duyệt)
   ● Cấp 2 NV Y (đang chờ)  ← highlight
   ○ Cấp 3 NV Z (chưa)
2. Phase=DaDuyet → all Steps/Levels green ✓
3. Phase=Nháp/TraLai → all greyed ○
4. V1 legacy → fallback note
This commit is contained in:
pqhuy1987
2026-05-08 16:16:40 +07:00
parent 74745a77a7
commit de0f38dd25
6 changed files with 331 additions and 100 deletions

View File

@ -86,41 +86,95 @@ export function PeWorkflowPanel({
})
const next = evaluation.workflow.nextPhases
const flow = evaluation.approvalFlow
return (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-slate-900">Quy trình</h3>
<p className="mt-0.5 text-[11px] text-slate-500">{evaluation.workflow.policyDescription}</p>
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
{evaluation.approvalWorkflowCode && (
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
{evaluation.approvalWorkflowCode} v{String(evaluation.approvalWorkflowVersion ?? 0).padStart(2, '0')}
{evaluation.approvalWorkflowName && <> · <span className="font-sans">{evaluation.approvalWorkflowName}</span></>}
</p>
)}
</div>
<ol className="space-y-1.5">
{evaluation.workflow.activePhases
.filter(p => p !== PurchaseEvaluationPhase.TuChoi)
.map(p => {
const isCurrent = evaluation.phase === p
const isPast = isPastPhase(evaluation.phase, p, evaluation.workflow.activePhases)
{/* Mig 24 V2 — Flow render Bước → Cấp → NV thay phase cards.
Status: Done (✓ emerald) / Current (● brand) / Pending (○ slate) */}
{flow && flow.steps.length > 0 && (
<ol className="space-y-2">
{flow.steps.map(step => {
const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'
return (
<li key={p}>
<div
className={cn(
'flex items-center gap-2 rounded border px-2 py-1.5 text-xs',
isCurrent && 'border-brand-300 bg-brand-50 font-medium',
isPast && 'border-emerald-200 bg-emerald-50 text-emerald-700',
!isCurrent && !isPast && 'border-slate-200 text-slate-500',
)}
>
<span className={cn('rounded px-1.5 py-0.5 text-[10px]', PurchaseEvaluationPhaseColor[p])}>
{p}
<li
key={step.order}
className={cn(
'rounded-md border p-2',
step.status === 'Current' && 'border-brand-300 bg-brand-50/50',
step.status === 'Done' && 'border-emerald-200 bg-emerald-50/40',
step.status === 'Pending' && 'border-slate-200 bg-white',
)}
>
<div className="flex items-center gap-2 text-xs font-medium">
<span className={cn(
'flex h-5 w-5 shrink-0 items-center justify-center rounded-full font-bold',
step.status === 'Done' && 'bg-emerald-500 text-white',
step.status === 'Current' && 'bg-brand-600 text-white',
step.status === 'Pending' && 'bg-slate-200 text-slate-500',
)}>
{stepIcon}
</span>
<span className="truncate">{PurchaseEvaluationPhaseLabel[p]}</span>
{isCurrent && <span className="ml-auto text-[10px] text-brand-700"> hiện tại</span>}
{isPast && <span className="ml-auto text-[10px] text-emerald-600"></span>}
<span className="text-slate-800">Bước {step.order} {step.name}</span>
{step.departmentName && (
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
{step.departmentName}
</span>
)}
</div>
{step.levels.length > 0 && (
<ul className="mt-1.5 ml-7 space-y-1 border-l-2 border-violet-200 pl-2">
{step.levels.map(lv => {
const lvIcon = lv.status === 'Done' ? '✓' : lv.status === 'Current' ? '●' : '○'
return (
<li key={lv.order} className="text-[11px]">
<div className="flex items-start gap-1.5">
<span className={cn(
'mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[9px] font-bold',
lv.status === 'Done' && 'bg-emerald-500 text-white',
lv.status === 'Current' && 'bg-brand-600 text-white',
lv.status === 'Pending' && 'bg-slate-200 text-slate-500',
)}>
{lvIcon}
</span>
<div className="min-w-0 flex-1">
<div className="font-medium text-slate-700">
{lv.name || `Cấp ${lv.order}`}
{lv.status === 'Current' && <span className="ml-1.5 text-[10px] font-normal text-brand-700">đang chờ</span>}
{lv.status === 'Done' && <span className="ml-1.5 text-[10px] font-normal text-emerald-600">đã duyệt</span>}
</div>
<div className="text-slate-500">
{lv.approvers.map(a => a.fullName).join(' / ') || '(chưa cấu hình)'}
</div>
</div>
</div>
</li>
)
})}
</ul>
)}
</li>
)
})}
</ol>
</ol>
)}
{/* Phiếu V1 legacy không có flow → fallback hiển thị phase summary đơn giản */}
{!flow && (
<div className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] text-slate-600">
Phiếu này dùng quy trình workflow chi tiết không khả dụng.
</div>
)}
{/* Mig 24 — V2 banner: hiển thị Bước/Cấp hiện tại + danh sách NV được duyệt.
Nếu actor không có trong list → banner amber + nút Duyệt sẽ disable. */}
@ -327,9 +381,3 @@ function fmtTime(iso: string): string {
return d.toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
}
function isPastPhase(current: number, p: number, active: number[]): boolean {
const orderedIdx = active.indexOf(p)
const currentIdx = active.indexOf(current)
if (orderedIdx < 0 || currentIdx < 0) return false
return orderedIdx < currentIdx && p !== PurchaseEvaluationPhase.TuChoi
}