[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

@ -101,6 +101,27 @@ public record PurchaseEvaluationCurrentApprovalDto(
string? LevelName,
List<PurchaseEvaluationApprovalLevelApproverDto> Approvers);
// Mig 22-24 V2 — full workflow flow snapshot (Bước → Cấp → NV) cho FE render
// thay 4-phase cards cũ. Mỗi Level có Status: Done/Current/Pending.
public record PurchaseEvaluationApprovalFlowLevelDto(
int Order, // 1/2/3 trong Step
string? Name,
List<PurchaseEvaluationApprovalLevelApproverDto> Approvers,
string Status); // "Done" | "Current" | "Pending"
public record PurchaseEvaluationApprovalFlowStepDto(
int Order, // 1-based
string Name,
Guid? DepartmentId,
string? DepartmentName,
string Status, // "Done" | "Current" | "Pending"
List<PurchaseEvaluationApprovalFlowLevelDto> Levels);
public record PurchaseEvaluationApprovalFlowDto(
int? CurrentStepIndex, // 0-based, null khi terminal
int? CurrentLevelOrder,
List<PurchaseEvaluationApprovalFlowStepDto> Steps);
public record PurchaseEvaluationAttachmentDto(
Guid Id,
Guid? PurchaseEvaluationSupplierId,
@ -153,6 +174,7 @@ public record PurchaseEvaluationDetailBundleDto(
string? ApprovalWorkflowName,
int? ApprovalWorkflowVersion,
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
PurchaseEvaluationApprovalFlowDto? ApprovalFlow,
List<PurchaseEvaluationSupplierDto> Suppliers,
List<PurchaseEvaluationDetailDto> Details,
List<PurchaseEvaluationApprovalDto> Approvals,

View File

@ -512,11 +512,12 @@ public class GetPurchaseEvaluationQueryHandler(
// Mig 23 — load ApprovalWorkflow V2 info nếu pin (Code/Name/Version
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
// Mig 24 — populate CurrentApproval { Bước/Cấp + N approvers } để
// FE biết user nào được duyệt cấp hiện tại → disable button đúng.
// Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full
// Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards.
string? awCode = null, awName = null;
int? awVersion = null;
PurchaseEvaluationCurrentApprovalDto? currentApproval = null;
PurchaseEvaluationApprovalFlowDto? approvalFlow = null;
if (e.ApprovalWorkflowId is Guid awId)
{
var aw = await db.ApprovalWorkflows.AsNoTracking()
@ -529,48 +530,110 @@ public class GetPurchaseEvaluationQueryHandler(
awName = aw.Name;
awVersion = aw.Version;
// Compute current approval level info nếu Phase=ChoDuyet
if (e.Phase == PurchaseEvaluationPhase.ChoDuyet
&& e.CurrentWorkflowStepIndex is int idx
&& e.CurrentApprovalLevelOrder is int levelOrder)
var steps = aw.Steps.OrderBy(s => s.Order).ToList();
// Resolve dept names cho Steps
var stepDeptIds = steps.Where(s => s.DepartmentId != null)
.Select(s => s.DepartmentId!.Value).Distinct().ToList();
var stepDeptNames = stepDeptIds.Count == 0
? new Dictionary<Guid, string>()
: await db.Departments.AsNoTracking()
.Where(d => stepDeptIds.Contains(d.Id))
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
// Resolve approver user info (all levels)
var allApproverIds = steps.SelectMany(s => s.Levels)
.Select(l => l.ApproverUserId).Distinct().ToList();
var approverInfos = allApproverIds.Count == 0
? new Dictionary<Guid, (string FullName, string? Email)>()
: await userManager.Users.AsNoTracking()
.Where(u => allApproverIds.Contains(u.Id))
.Select(u => new { u.Id, u.FullName, u.Email })
.ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct);
// Compute Status mỗi level theo Phase + currentStepIdx + currentLevelOrder
var currentIdx = e.CurrentWorkflowStepIndex;
var currentLevel = e.CurrentApprovalLevelOrder;
var phase = e.Phase;
bool isTerminalDone = phase == PurchaseEvaluationPhase.DaDuyet;
bool isPending = phase == PurchaseEvaluationPhase.DangSoanThao
|| phase == PurchaseEvaluationPhase.TraLai;
bool isLocked = phase == PurchaseEvaluationPhase.TuChoi;
string ComputeLevelStatus(int stepIdx0, int levelOrder)
{
var steps = aw.Steps.OrderBy(s => s.Order).ToList();
if (idx >= 0 && idx < steps.Count)
if (isTerminalDone) return "Done";
if (isPending || isLocked) return "Pending";
if (currentIdx is null || currentLevel is null) return "Pending";
if (stepIdx0 < currentIdx.Value) return "Done";
if (stepIdx0 == currentIdx.Value)
{
var step = steps[idx];
string? stepDeptName = null;
if (step.DepartmentId is Guid stepDeptId)
{
stepDeptName = await db.Departments.AsNoTracking()
.Where(d => d.Id == stepDeptId)
.Select(d => d.Name)
.FirstOrDefaultAsync(ct);
}
var levelGroup = step.Levels.Where(l => l.Order == levelOrder).ToList();
var approverIds = levelGroup.Select(l => l.ApproverUserId).Distinct().ToList();
var approverInfos = approverIds.Count == 0
? new Dictionary<Guid, (string FullName, string? Email)>()
: await userManager.Users.AsNoTracking()
.Where(u => approverIds.Contains(u.Id))
.Select(u => new { u.Id, u.FullName, u.Email })
.ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct);
var approvers = levelGroup
.Select(l =>
{
approverInfos.TryGetValue(l.ApproverUserId, out var info);
return new PurchaseEvaluationApprovalLevelApproverDto(
l.ApproverUserId,
info.FullName ?? l.ApproverUserId.ToString(),
info.Email);
})
.ToList();
var levelName = levelGroup.FirstOrDefault()?.Name;
currentApproval = new PurchaseEvaluationCurrentApprovalDto(
idx, step.Name, step.DepartmentId, stepDeptName,
levelOrder, levelName, approvers);
if (levelOrder < currentLevel.Value) return "Done";
if (levelOrder == currentLevel.Value) return "Current";
return "Pending";
}
return "Pending";
}
string ComputeStepStatus(int stepIdx0, int stepLevelCount)
{
if (isTerminalDone) return "Done";
if (isPending || isLocked) return "Pending";
if (currentIdx is null) return "Pending";
if (stepIdx0 < currentIdx.Value) return "Done";
if (stepIdx0 == currentIdx.Value) return "Current";
return "Pending";
}
var flowSteps = new List<PurchaseEvaluationApprovalFlowStepDto>();
for (int i = 0; i < steps.Count; i++)
{
var step = steps[i];
var levelGroups = step.Levels.OrderBy(l => l.Order).GroupBy(l => l.Order).ToList();
var flowLevels = levelGroups.Select(g =>
{
var approvers = g.Select(l =>
{
approverInfos.TryGetValue(l.ApproverUserId, out var info);
return new PurchaseEvaluationApprovalLevelApproverDto(
l.ApproverUserId,
info.FullName ?? l.ApproverUserId.ToString(),
info.Email);
}).ToList();
var levelName = g.FirstOrDefault()?.Name;
return new PurchaseEvaluationApprovalFlowLevelDto(
g.Key, levelName, approvers,
ComputeLevelStatus(i, g.Key));
}).ToList();
flowSteps.Add(new PurchaseEvaluationApprovalFlowStepDto(
step.Order, step.Name,
step.DepartmentId,
step.DepartmentId is Guid sd && stepDeptNames.TryGetValue(sd, out var sdn) ? sdn : null,
ComputeStepStatus(i, levelGroups.Count),
flowLevels));
}
approvalFlow = new PurchaseEvaluationApprovalFlowDto(currentIdx, currentLevel, flowSteps);
// CurrentApproval (legacy banner) — chỉ populate nếu Phase=ChoDuyet
if (phase == PurchaseEvaluationPhase.ChoDuyet
&& currentIdx is int idxCur && currentLevel is int lvlCur
&& idxCur >= 0 && idxCur < steps.Count)
{
var step = steps[idxCur];
var levelGroup = step.Levels.Where(l => l.Order == lvlCur).ToList();
var approvers = levelGroup.Select(l =>
{
approverInfos.TryGetValue(l.ApproverUserId, out var info);
return new PurchaseEvaluationApprovalLevelApproverDto(
l.ApproverUserId,
info.FullName ?? l.ApproverUserId.ToString(),
info.Email);
}).ToList();
var levelName = levelGroup.FirstOrDefault()?.Name;
currentApproval = new PurchaseEvaluationCurrentApprovalDto(
idxCur, step.Name, step.DepartmentId,
step.DepartmentId is Guid sd2 && stepDeptNames.TryGetValue(sd2, out var sdn2) ? sdn2 : null,
lvlCur, levelName, approvers);
}
}
}
@ -586,7 +649,7 @@ public class GetPurchaseEvaluationQueryHandler(
e.BudgetId, budgetSummary,
e.BudgetManualName, e.BudgetManualAmount,
e.ApprovalWorkflowId, awCode, awName, awVersion,
currentApproval,
currentApproval, approvalFlow,
e.Suppliers
.OrderBy(s => s.Order)
.Select(s => new PurchaseEvaluationSupplierDto(