[CLAUDE] Contract+FE-User: dot-5 menu 6-leaf y-chang-Duyet-NCC + GET /contracts/deleted + bo hero + "Tong quan quy trinh"
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m38s

- BE: ListDeletedContractsQuery (IgnoreQueryFilters + Where(IsDeleted) + IDOR mirror; LUAT thay so-dem theo reviewer FLAG-10) + GET /contracts/deleted [Contracts.Read]
- Seeder: Ct x7 +3 leaf (WfView/Approved/Deleted) + labelBackfill "Dang duyet" x7 + Khkk 6 leaf + Dashboard -> "Tong quan quy trinh" + reviewKeys +23 key
- FE user: matrix +type=3 (HD chung 1 bo V2) + route /contracts/workflow-matrix + Ct regex +3 action + MyContractsPage (phase/deleted + banner + an nut Tao + FLAG-2 guard selectContract khi deleted) + MenuLeaf depth-0 UPPERCASE + UserDashboardPage BO HERO (owner: "bo cai tren di, cai duoi OK") + title "Tong quan quy trinh"
- fe-admin: staticMap +2 Khkk (FLAG-1); leaf Ct_ moi van an theo isAdminHidden (da kiem CLEAN)
- reviewer PASS_WITH_FLAGS 10 FLAG (0H/4M/6L): fix 4M, park 6L co ghi (FLAG-4 dinh nghia "Da duyet" HD -> cho owner)
- build fe x2 + BE PASS; test 562 PASS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-29 14:40:10 +07:00
parent ceeebf3c3b
commit ea962b1e59
10 changed files with 173 additions and 371 deletions

View File

@ -23,6 +23,18 @@ public class ContractsController(IMediator mediator) : ControllerBase
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListContractsQuery(phase, supplierId, projectId) { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
// [S159-đợt5] Màn "Đã xóa" HĐ (chỉ-xem) — mirror PE /deleted (S155). Read-only,
// policy root Contracts.Read (chặt hơn [Authorize] trần của class — không phá gì).
[HttpGet("deleted")]
[Authorize(Policy = "Contracts.Read")]
public async Task<ActionResult<PagedResult<ContractListItemDto>>> ListDeleted(
[FromQuery] ContractType? type = null,
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] bool sortDesc = true,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListDeletedContractsQuery(type)
{ Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
[HttpGet("inbox")]
public async Task<ActionResult<List<ContractListItemDto>>> Inbox(CancellationToken ct)
=> Ok(await mediator.Send(new GetMyInboxQuery(), ct));

View File

@ -351,6 +351,64 @@ public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser cu
}
}
// ========== LIST DELETED contracts — màn "Đã xóa" (chỉ-xem) ==========
// [S159-đợt5 — owner "y chang Duyệt NCC"] Cookie-cutter `ListDeletedPurchaseEvaluationsQuery`
// (PeSoftDeleteFeatures.cs). 🔴 LUẬT (không đếm số — reviewer FLAG-10: "chỗ THỨ HAI" là
// con số tự-lan, ApprovalWorkflowV2AdminFeatures cũng dùng Ignore ở lớp KHÁC): mọi nơi
// dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm `.Where(x => x.IsDeleted)`
// vì Ignore GỠ filter chứ KHÔNG ĐẢO (bài 8a S155). Tự kiểm:
// grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs
// IDOR scope MIRROR ListContractsQueryHandler (Drafter eligible-phase; phase ở đây =
// phase tại thời điểm xóa).
public record ListDeletedContractsQuery(ContractType? Type = null)
: PagedRequest, IRequest<PagedResult<ContractListItemDto>>;
public class ListDeletedContractsQueryHandler(IApplicationDbContext db, ICurrentUser currentUser)
: IRequestHandler<ListDeletedContractsQuery, PagedResult<ContractListItemDto>>
{
public async Task<PagedResult<ContractListItemDto>> Handle(ListDeletedContractsQuery request, CancellationToken ct)
{
var q = from c in db.Contracts.AsNoTracking().IgnoreQueryFilters()
.Where(x => x.IsDeleted)
join s in db.Suppliers.AsNoTracking() on c.SupplierId equals s.Id
join p in db.Projects.AsNoTracking() on c.ProjectId equals p.Id
select new { c, s, p };
if (!currentUser.Roles.Contains(AppRoles.Admin))
{
var userId = currentUser.UserId;
var eligiblePhases = ListContractsQueryHandler.GetEligiblePhases(currentUser.Roles);
q = q.Where(x => x.c.DrafterUserId == userId || eligiblePhases.Contains(x.c.Phase));
}
if (request.Type is not null) q = q.Where(x => x.c.Type == request.Type);
if (!string.IsNullOrWhiteSpace(request.Search))
{
var kw = request.Search.Trim();
q = q.Where(x =>
(x.c.MaHopDong != null && x.c.MaHopDong.Contains(kw)) ||
(x.c.TenHopDong != null && x.c.TenHopDong.Contains(kw)) ||
x.s.Name.Contains(kw) || x.p.Name.Contains(kw));
}
q = request.SortDesc ? q.OrderByDescending(x => x.c.DeletedAt) : q.OrderBy(x => x.c.DeletedAt);
var total = await q.CountAsync(ct);
var items = await q
.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
.Select(x => new ContractListItemDto(
x.c.Id, x.c.MaHopDong, x.c.TenHopDong, x.c.Type, x.c.Phase,
x.c.SupplierId, x.s.Name,
x.c.ProjectId, x.p.Name,
x.c.GiaTri, x.c.SlaDeadline, x.c.CreatedAt))
.ToListAsync(ct);
return new PagedResult<ContractListItemDto>(items, total, request.Page, request.PageSize);
}
}
// ========== INBOX — HĐ chờ role/tôi xử lý ==========
public record GetMyInboxQuery : IRequest<List<ContractListItemDto>>;

View File

@ -181,7 +181,14 @@ public class ListDeletedPurchaseEvaluationsQueryHandler(
{
// 🔴 8a — `IgnoreQueryFilters()` **GỠ** filter chứ KHÔNG ĐẢO nó. Thiếu
// `.Where(x => x.IsDeleted)` ⇒ màn "Đã xóa" liệt kê TOÀN BỘ phiếu sống lẫn chết.
// 🔴 D3 — đây là chỗ DUY NHẤT trong `src/Backend` dùng `IgnoreQueryFilters`:
// 🔴 D3 (S159-đợt5, sửa lần 2 theo reviewer FLAG-10 — BỎ CON SỐ ĐẾM, nêu LUẬT):
// MỌI nơi dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm
// `.Where(x => x.IsDeleted)`. Lớp list-deleted hiện: đây + `ListDeletedContractsQueryHandler`
// (ContractFeatures.cs). `ApprovalWorkflowV2AdminFeatures.cs` cũng dùng Ignore ở nhiều
// vị trí nhưng thuộc LỚP KHÁC (đọc *LevelOpinions + dò phiếu-cha-mồ-côi, không list-deleted).
// Tự kiểm: grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs
// (Bài: "chỗ DUY NHẤT/THỨ HAI" là con số tự-lan — S155 ghi DUY NHẤT đã sai từ khi
// ApprovalWorkflowV2AdminFeatures dùng; đừng thay số sai cũ bằng số sai mới.)
// CẤM đưa vào helper dùng chung trả `IQueryable`, CẤM sửa
// `PurchaseEvaluationConfiguration.cs:84`, CẤM thêm cờ `includeDeleted` vào query list
// hiện có (cờ sẽ lan tới `PeBudgetAccumulator` = phá đúng thứ owner muốn).

View File

@ -1753,7 +1753,7 @@ public static class DbInitializer
var tree = new List<(string Key, string Label, string? Parent, int Order, string Icon)>
{
(MenuKeys.Dashboard, "Tổng quan", null, 10, "LayoutDashboard"),
(MenuKeys.Dashboard, "Tổng quan quy trình", null, 10, "LayoutDashboard"), // [S159-đợt5] owner: "đổi thành gì dễ hình dung? Tổng quan quy trình"
// [S58 2026-06-11] "Danh mục" 20→80: anh yêu cầu đưa xuống CUỐI sidebar
// (sau Reports 50, trước vùng System 90 admin). Main upsert tự re-set
// Order trên DB cũ. Con (21-25) giữ nguyên — order chỉ so trong siblings.
@ -1777,10 +1777,13 @@ public static class DbInitializer
// Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render
// MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app).
("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
("Khkk_List", "Danh sách", "Khkk_G1", 1, "List"),
("Khkk_Create", "Thao tác", "Khkk_G1", 2, "Plus"),
("Khkk_Pending", "Đang duyệt", "Khkk_G1", 3, "CheckCircle2"),
("Khkk_Approved", "Đã duyệt", "Khkk_G1", 4, "CheckCheck"),
// [S159-đợt5 owner] đủ 6 leaf "y chang Duyệt NCC" (WfView đầu + Deleted cuối).
("Khkk_WfView", "Luồng duyệt", "Khkk_G1", 1, "Network"),
("Khkk_List", "Danh sách", "Khkk_G1", 2, "List"),
("Khkk_Create", "Thao tác", "Khkk_G1", 3, "Plus"),
("Khkk_Pending", "Đang duyệt", "Khkk_G1", 4, "CheckCircle2"),
("Khkk_Approved", "Đã duyệt", "Khkk_G1", 5, "CheckCheck"),
("Khkk_Deleted", "Đã xóa", "Khkk_G1", 6, "Trash2"),
(MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"),
(MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"),
(MenuKeys.System, "Hệ thống", null, 90, "Settings"),
@ -1863,9 +1866,14 @@ public static class DbInitializer
// đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4).
tree.Add(("Hdc_" + code, label + " - Bảng cứng", MenuKeys.HopDongCung, hdcOrder++, "FileText")); // [S159-đợt4] owner vẽ: "01. HĐ Thầu phụ - Bảng cứng"
tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText"));
// [S159-đợt5 owner "y chang Duyệt NCC"] mỗi loại HĐ đủ 6 leaf. Label "Đang duyệt"
// cho key CŨ đi qua labelBackfill (gotcha #11 — sửa chữ ở đây vô tác dụng DB đang chạy).
tree.Add(("Ct_" + code + "_WfView", "Luồng duyệt", MenuKeys.ContractTypeGroup(code), order++, "Network"));
tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List"));
tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus"));
tree.Add((MenuKeys.ContractTypePending(code),"Duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2"));
tree.Add((MenuKeys.ContractTypePending(code),"Đang duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2"));
tree.Add(("Ct_" + code + "_Approved","Đã duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCheck"));
tree.Add(("Ct_" + code + "_Deleted", "Đã xóa", MenuKeys.ContractTypeGroup(code), order++, "Trash2"));
}
// Per-type workflow admin leaves dưới `Workflows` — mỗi loại là 1
@ -1958,6 +1966,16 @@ public static class DbInitializer
// [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod).
[MenuKeys.PurchaseEvaluations] = "Duyệt NCC",
[MenuKeys.Contracts] = "Duyệt hợp đồng",
// [S159-đợt5] Sidebar "Tổng quan" → "Tổng quan quy trình" (owner đề tên, lead lấy).
[MenuKeys.Dashboard] = "Tổng quan quy trình",
// [S159-đợt5] 7 leaf Ct_X_Pending "Duyệt" → "Đang duyệt" (đồng khuôn PE — gotcha #11).
[MenuKeys.ContractTypePending("ThauPhu")] = "Đang duyệt",
[MenuKeys.ContractTypePending("GiaoKhoan")] = "Đang duyệt",
[MenuKeys.ContractTypePending("NhaCungCap")] = "Đang duyệt",
[MenuKeys.ContractTypePending("DichVu")] = "Đang duyệt",
[MenuKeys.ContractTypePending("MuaBan")] = "Đang duyệt",
[MenuKeys.ContractTypePending("NguyenTacNcc")] = "Đang duyệt",
[MenuKeys.ContractTypePending("NguyenTacDv")] = "Đang duyệt",
// [S159-đợt4] 7 leaf Hdc_ thêm hậu tố " - Bảng cứng" (owner vẽ; prod SQL cùng ngày).
["Hdc_ThauPhu"] = "01. HĐ Thầu phụ - Bảng cứng",
["Hdc_GiaoKhoan"] = "02. HĐ Giao khoán - Bảng cứng",
@ -2142,9 +2160,12 @@ public static class DbInitializer
foreach (var c in MenuKeys.ContractTypeCodes)
{
yield return MenuKeys.ContractTypeGroup(c);
yield return "Ct_" + c + "_WfView"; // [S159-đợt5]
yield return MenuKeys.ContractTypeList(c);
yield return MenuKeys.ContractTypeCreate(c);
yield return MenuKeys.ContractTypePending(c);
yield return "Ct_" + c + "_Approved"; // [S159-đợt5]
yield return "Ct_" + c + "_Deleted"; // [S159-đợt5]
}
}
static IEnumerable<string> MasterMenuKeys() => new[]
@ -2155,7 +2176,8 @@ public static class DbInitializer
};
static IEnumerable<string> KhkkKeys() => new[]
{
"Khkk_G1", "Khkk_List", "Khkk_Create", "Khkk_Pending", "Khkk_Approved",
"Khkk_G1", "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
"Khkk_Approved", "Khkk_Deleted",
};
static IEnumerable<string> HdcKeys()
=> MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);