[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
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:
@ -82,41 +82,94 @@ 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>
|
||||
)}
|
||||
|
||||
{!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 cũ — workflow chi tiết không khả dụng.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mig 24 — V2 banner Bước/Cấp + danh sách NV duyệt */}
|
||||
{isV2Pending && evaluation.currentApproval && !readOnly && (
|
||||
@ -318,9 +371,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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user