[CLAUDE] PurchaseEvaluation · FE-User · FE-Admin · Tests: PE note "Duyệt là KẾT THÚC quy trình (không trình CEO)" — 3 surface
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m58s

Phiếu mà quy trình duyệt có cấp bật AllowApproverFinalize (Mig 58) sẽ kết thúc tại cấp đó,
KHÔNG trình CEO. Hiển thị note cho MỌI NGƯỜI NẮM ở 3 surface (anh Kiệt FDC). KHÔNG migration
(cột AllowApproverFinalize đã có Mig 58, prod ở Mig 59).

BE (3 file, computed DTO):
- PurchaseEvaluationDtos.cs: +bool EndsBeforeCeo +string? FinalizeStepName +string? FinalizeLevelName
  trên DetailBundle + ListItem; flow-level +bool AllowApproverFinalize.
- PurchaseEvaluationFeatures.cs: detail compute first-reachable (Step.Order↑→Level.Order↑, dept-name
  in-memory); list + inbox projection qua 3 subquery EF-safe (lv.Step!.Name). V1 (workflowId==null) → false.
- CreateContractFromEvaluationFeatures.cs: ListApproved projection cùng DTO → +3 subquery y hệt.

FE 2-app (fe-user + fe-admin SHA-mirror component):
- PeFinalizeChip.tsx (MỚI, chip tím) ở 3 danh sách: PeListPanel + PurchaseEvaluationsListPage (cây) + InboxPage.
- PeWorkflowPanel.tsx: note callout trong phiếu, branch DaDuyet (past-tense) / đang-chạy; copy boundary-aware
  "...là KẾT THÚC, không trình CEO. Các cấp trước vẫn duyệt như thường." (tránh cấp giữa hiểu nhầm).
- UserDashboardPage.tsx (chỉ fe-user): dải "Phiếu Duyệt NCC gần đây" + chip (admin dashboard để nguyên).
- types: PeListItem/PeDetailBundle/PeApprovalFlowLevel +field.

Tests: +PeFinalizeProjectionTests (EF-translate smoke 3 subquery + first-reachable + V1-false). Full suite 421→422.
em-main self-gate sau workflow (impl-frontend return rỗng #53 → mirror fe-admin + verify 5 site recover-disk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-25 14:58:54 +07:00
parent a71753bde8
commit f0bca7737c
16 changed files with 417 additions and 6 deletions

View File

@ -189,6 +189,19 @@ public class ListApprovedPurchaseEvaluationsQueryHandler(IApplicationDbContext d
where sw.PurchaseEvaluationId == e.Id && sw.IsWinner
join sup in db.Suppliers on sw.SupplierId equals sup.Id
orderby sup.Name
select sup.Name).ToList())).ToListAsync(ct); // [S69] cờ gấp + [Mig 58] winner names
select sup.Name).ToList(), // [S69] cờ gấp + [Mig 58] winner names
// [S89 Mig 58] "Duyệt là KẾT THÚC (không trình CEO)" — 3 subquery riêng mirror
// winner-names (EF-safe). Cùng DTO PurchaseEvaluationListItemDto. V1 → false/null.
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == e.ApprovalWorkflowId
select 1).Any(),
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == e.ApprovalWorkflowId
orderby lv.Step!.Order, lv.Order
select lv.Step!.Name).FirstOrDefault(),
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == e.ApprovalWorkflowId
orderby lv.Step!.Order, lv.Order
select lv.Name).FirstOrDefault())).ToListAsync(ct);
}
}

View File

@ -42,7 +42,13 @@ public record PurchaseEvaluationListItemDto(
bool IsUrgentByCcm,
// [Mig 58 multi-winner] Tên các đơn vị IsWinner (FE list card join ", "; ≥2 = liên-danh).
// Thay nhãn từ SelectedSupplierName (null khi ≥2 cùng trúng → card trống). [] khi chưa chọn.
List<string> WinnerSupplierNames);
List<string> WinnerSupplierNames,
// [S89 Mig 58 anh Kiệt FDC] Quy trình pin có Cấp "Duyệt là KẾT THÚC (không trình
// CEO)" → FE render chip/note trên card list + sidebar. "first-reachable finalize"
// theo Step.Order tăng → Level.Order tăng. V1 (ApprovalWorkflowId==null) → false/null.
bool EndsBeforeCeo,
string? FinalizeStepName,
string? FinalizeLevelName);
public record PurchaseEvaluationSupplierDto(
Guid Id,
@ -155,7 +161,10 @@ public record PurchaseEvaluationApprovalFlowLevelDto(
int Order, // 1/2/3 trong Step
string? Name,
List<PurchaseEvaluationApprovalLevelApproverDto> Approvers,
string Status); // "Done" | "Current" | "Pending"
string Status, // "Done" | "Current" | "Pending"
// [S89 Mig 58 anh Kiệt FDC] Cấp này (OR-of-N rows cùng Order) có NV nào bật cờ
// "Duyệt là KẾT THÚC quy trình" → FE render NOTE/chip trong flow Bước/Cấp.
bool AllowApproverFinalize);
public record PurchaseEvaluationApprovalFlowStepDto(
int Order, // 1-based
@ -297,6 +306,13 @@ public record PurchaseEvaluationDetailBundleDto(
// Mig 26 (Session 19) — Section 5 V2 dynamic. Empty list cho phiếu V1
// legacy hoặc phiếu V2 chưa có cấp nào duyệt → FE fallback message.
List<PurchaseEvaluationLevelOpinionDto> LevelOpinions,
// [S89 Mig 58 anh Kiệt FDC] Quy trình pin có Cấp bật "Duyệt là KẾT THÚC (không
// trình CEO)" → FE hiển thị NOTE cho MỌI người ở detail. "first-reachable
// finalize" = Step.Order tăng → Level.Order tăng (đúng nơi flow THỰC kết thúc).
// V1 (ApprovalWorkflowId==null) → EndsBeforeCeo=false, names null.
bool EndsBeforeCeo,
string? FinalizeStepName,
string? FinalizeLevelName,
PurchaseEvaluationWorkflowSummaryDto Workflow);
// [S61 Mig 50] Bảng "TỔNG HỢP NGÂN SÁCH TRÌNH KÝ" theo Excel anh Kiệt — BE compute

View File

@ -633,7 +633,22 @@ public class ListPurchaseEvaluationsQueryHandler(
where sw.PurchaseEvaluationId == x.e.Id && sw.IsWinner
join sup in db.Suppliers on sw.SupplierId equals sup.Id
orderby sup.Name
select sup.Name).ToList()))
select sup.Name).ToList(),
// [S89 Mig 58] "Duyệt là KẾT THÚC (không trình CEO)" qua 3 subquery riêng
// (mirror winner-names — EF-safe; record-ctor projection KHÔNG cho biến trung
// gian nên không gộp tuple). lv.Step!.Name = simple navigation chắc translate.
// ApprovalWorkflowId==null → subquery rỗng → false/null (V1 tự đúng).
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
select 1).Any(),
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
orderby lv.Step!.Order, lv.Order
select lv.Step!.Name).FirstOrDefault(),
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
orderby lv.Step!.Order, lv.Order
select lv.Name).FirstOrDefault()))
.ToListAsync(ct);
return new PagedResult<PurchaseEvaluationListItemDto>(items, total, request.Page, request.PageSize);
@ -735,7 +750,21 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
where sw.PurchaseEvaluationId == x.e.Id && sw.IsWinner
join sup in db.Suppliers on sw.SupplierId equals sup.Id
orderby sup.Name
select sup.Name).ToList()))
select sup.Name).ToList(),
// [S89 Mig 58] "Duyệt là KẾT THÚC (không trình CEO)" — 3 subquery riêng mirror
// winner-names (EF-safe). lv.Step!.Name simple navigation. ApprovalWorkflowId
// ==null → subquery rỗng → false/null (V1 tự đúng).
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
select 1).Any(),
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
orderby lv.Step!.Order, lv.Order
select lv.Step!.Name).FirstOrDefault(),
(from lv in db.ApprovalWorkflowLevels
where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == x.e.ApprovalWorkflowId
orderby lv.Step!.Order, lv.Order
select lv.Name).FirstOrDefault()))
.Take(100)
.ToListAsync(ct);
}
@ -951,6 +980,13 @@ public class GetPurchaseEvaluationQueryHandler(
ApprovalWorkflowOptionsDto? currentLevelOptions = null;
PurchaseEvaluationCurrentApprovalDto? currentApproval = null;
PurchaseEvaluationApprovalFlowDto? approvalFlow = null;
// [S89 Mig 58 anh Kiệt FDC] "Duyệt là KẾT THÚC quy trình (không trình CEO)" —
// first-reachable Cấp bật AllowApproverFinalize theo Step.Order tăng → Level.Order
// tăng. V1 (aw==null) giữ default false/null. Names dùng dept-name thân-thiện
// (mirror step-label build flow ~:1084), fallback Step.Name.
bool endsBeforeCeo = false;
string? finalizeStepName = null;
string? finalizeLevelName = null;
if (e.ApprovalWorkflowId is Guid awId)
{
var aw = await db.ApprovalWorkflows.AsNoTracking()
@ -1075,7 +1111,10 @@ public class GetPurchaseEvaluationQueryHandler(
var levelName = g.FirstOrDefault()?.Name;
return new PurchaseEvaluationApprovalFlowLevelDto(
g.Key, levelName, approvers,
ComputeLevelStatus(i, g.Key));
ComputeLevelStatus(i, g.Key),
// [S89 Mig 58] OR-of-N rows cùng Order: bất kỳ NV trong cấp bật cờ
// → cấp này là điểm KẾT THÚC (FE render note trong flow).
g.Any(l => l.AllowApproverFinalize));
}).ToList();
flowSteps.Add(new PurchaseEvaluationApprovalFlowStepDto(
@ -1088,6 +1127,20 @@ public class GetPurchaseEvaluationQueryHandler(
approvalFlow = new PurchaseEvaluationApprovalFlowDto(currentIdx, currentLevel, flowSteps);
// [S89 Mig 58] First-reachable finalize theo Step.Order tăng → Level.Order
// tăng (steps đã OrderBy Order). Tên Bước ưu tiên dept-name thân-thiện
// (mirror flow step-label ~:1084), fallback Step.Name.
var finalizeStep = steps.FirstOrDefault(s => s.Levels.Any(l => l.AllowApproverFinalize));
if (finalizeStep is not null)
{
var finalizeLevel = finalizeStep.Levels
.Where(l => l.AllowApproverFinalize).OrderBy(l => l.Order).FirstOrDefault();
endsBeforeCeo = true;
finalizeStepName = finalizeStep.DepartmentId is Guid fsd
&& stepDeptNames.TryGetValue(fsd, out var fsdn) ? fsdn : finalizeStep.Name;
finalizeLevelName = finalizeLevel?.Name;
}
// CurrentApproval (legacy banner) — chỉ populate nếu Phase=ChoDuyet
if (phase == PurchaseEvaluationPhase.ChoDuyet
&& currentIdx is int idxCur && currentLevel is int lvlCur
@ -1185,6 +1238,7 @@ public class GetPurchaseEvaluationQueryHandler(
o.Opinion, o.SignedAt, o.UserId, o.UserName))
.ToList(),
await BuildLevelOpinionsAsync(e, ct),
endsBeforeCeo, finalizeStepName, finalizeLevelName, // [S89 Mig 58] Duyệt = KẾT THÚC (không trình CEO)
new PurchaseEvaluationWorkflowSummaryDto(
policy.Name, policy.Description,
policy.ActivePhases.ToList(),