[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
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:
@ -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));
|
||||
|
||||
@ -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>>;
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user