[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:
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
Reference in New Issue
Block a user