[CLAUDE] Contract: K4a+K4b SP-002 sidebar 8 nhom x 6 leaf + noi ?group= end-to-end (0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
K4a (gate PASS-WITH-FLAGS 4, sub-reviewer-gate-k4a.md): 49 key Khkk_G{2-8} bo-sinh
by-construction 3-phia (All/seed/grant) MenuKeys.cs + DbInitializer 49 row + labelBackfill
G1 nhan A1 + site-2 Concat + va 7/10/11/13/15 (nhan N2 co "pha do"; comment 1893 -> mo-neo
grep). All 64->113, Policies 256->452 (test T2 khoa runtime). Test +7 KhkkGroupMenuSeedTests
(T7 revoker-khong-dung-Khkk: probe vi-phan 112->0, gotcha #84).
K4b (gate PASS-WITH-FLAGS 4M/5m 0-chan, sub-reviewer-gate-k4b.md): KhkkListPage x2 derive
phase+group tu searchParams (leaf->leaf khong F5, navKey reset) + queryKey {group,phase,
search,page,deletedView} + KhkkCreatePage preset ?group= + staticMap G1 6-leaf group=1 +
regex Khkk_G([1-8])_* fe-user (fe-admin GHI RANG BUOC isAdminHidden) + FLAG-1 lead-fix:
GET /deleted +group mirror List (Controller+Query+handler SAU-IDOR) + FE noi 2-nhanh.
2 wave 1 commit CO KHAI: Layout.tsx dinh ca 2 wave khong tach hunk phi-tuong-tac; moi dong
deu qua gate rieng. STATUS canonical 5-row dong bo (71/97/452/113/620->621) + bundle :479.
No K4c: MAJOR-A test /deleted+group filter + MAJOR-C WfView doc group. Suite 621/0
(45D+576I) + filter 41/41 + npm x2 + dotnet 0E.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -55,16 +55,17 @@ public class ContractSigningPlansController(
|
||||
public async Task<ActionResult<List<ContractSigningPlanListItemDto>>> Inbox(CancellationToken ct)
|
||||
=> Ok(await mediator.Send(new GetMyContractSigningPlanInboxQuery(), ct));
|
||||
|
||||
/// Màn "Đã xoá" — CHỈ XEM.
|
||||
/// Màn "Đã xoá" — CHỈ XEM. [K4b S166] +`group` mirror List `:42` (8 leaf Đã-xóa per-nhóm).
|
||||
[HttpGet("deleted")]
|
||||
[Authorize(Policy = "KeHoachKyKet.Read")]
|
||||
public async Task<ActionResult<PagedResult<ContractSigningPlanListItemDto>>> ListDeleted(
|
||||
[FromQuery(Name = "group")] int? group = 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 ListDeletedContractSigningPlansQuery
|
||||
=> Ok(await mediator.Send(new ListDeletedContractSigningPlansQuery(group)
|
||||
{ Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
|
||||
|
||||
/// Picker màn tạo: phiếu Duyệt NCC đã duyệt, có NCC trúng thầu, chưa có kế hoạch sống.
|
||||
|
||||
@ -862,7 +862,10 @@ public class GetMyContractSigningPlanInboxQueryHandler(IApplicationDbContext db,
|
||||
}
|
||||
|
||||
/// Màn "Đã xoá" — CHỈ XEM (mirror `ListDeletedPurchaseEvaluationsQuery`).
|
||||
public record ListDeletedContractSigningPlansQuery
|
||||
// [K4b S166 — gate-K4b FLAG-1] +ApprovalGroup mirror list SỐNG (`:756-757`): 8 leaf "Đã xóa"
|
||||
// per-nhóm trỏ `?group=n` mà endpoint này không đọc ⇒ 8 URL trả CÙNG tập mọi-nhóm; lọc client-side
|
||||
// thì total/số-trang nói dối (server đếm trên tập chưa lọc) ⇒ phải lọc server-side như list sống.
|
||||
public record ListDeletedContractSigningPlansQuery(int? ApprovalGroup = null)
|
||||
: PagedRequest, IRequest<PagedResult<ContractSigningPlanListItemDto>>;
|
||||
|
||||
public class ListDeletedContractSigningPlansQueryHandler(IApplicationDbContext db, ICurrentUser currentUser)
|
||||
@ -889,6 +892,10 @@ public class ListDeletedContractSigningPlansQueryHandler(IApplicationDbContext d
|
||||
&& userWfIds.Contains(p.ApprovalWorkflowId.Value)));
|
||||
}
|
||||
|
||||
// [K4b S166] Lọc nhóm — mirror list sống `:756-757`, SAU vế IDOR (lọc chỉ thu hẹp).
|
||||
if (request.ApprovalGroup is not null)
|
||||
q = q.Where(p => p.ApprovalGroup == request.ApprovalGroup);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
var kw = request.Search.Trim();
|
||||
|
||||
@ -46,6 +46,43 @@ public static class MenuKeys
|
||||
public const string KeHoachKyKet = "KeHoachKyKet";
|
||||
public const string HopDongCung = "HopDongCung";
|
||||
|
||||
// ============================================================
|
||||
// [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT × 6 leaf. Danh mục SOL-CCM-SP-002
|
||||
// (Mig 70) chia 86 dòng thành 8 nhóm duyệt; mỗi nhóm 1 workflow `KHKK-N{n}`
|
||||
// type-10 (K3) ⇒ sidebar cũng tách 8 nhóm.
|
||||
// - NHÓM 1 GIỮ NGUYÊN 7 key CŨ KHÔNG-infix (`Khkk_G1` + `Khkk_WfView` …
|
||||
// `Khkk_Deleted`, seed từ S159 — xem SeedMenuTreeAsync). Đổi tên key =
|
||||
// mồ côi permission-row đã cấp (bài S155) ⇒ CẤM đổi.
|
||||
// - NHÓM 2..8 dùng khuôn CÓ infix: `Khkk_G{n}` + `Khkk_G{n}_{action}`.
|
||||
// 🔴 49 key (7 group + 42 leaf) VÀO `All` ⇒ sinh thêm 196 policy
|
||||
// `Khkk_G*.{Read,Create,Update,Delete}` (registered-unused: endpoint KHKK
|
||||
// vẫn CHỈ dùng `KeHoachKyKet.*` — gotcha #85, đừng OR key con vào gate)
|
||||
// ⇒ 2 số canonical `docs/STATUS.md` đổi: Menu keys +49 · Policies +196
|
||||
// (Policies = |All| × |Actions|, DERIVED — khai DELTA, không neo mốc tuyệt đối).
|
||||
// 🔴 Vào `All` thì BẮT BUỘC có menu-row tương ứng trong `SeedMenuTreeAsync`
|
||||
// (thiếu row = Permission FK nổ ngay lần boot đầu — invariant T6 K1).
|
||||
// `KhkkGroupMenuKeys` dưới là NGUỒN CHUNG cho cả `All` LẪN vòng seed
|
||||
// menu-row + vòng grant site-2 ⇒ 3 phía khớp by-construction, KHÔNG mirror tay.
|
||||
// ⚠️ 2 KHUÔN menu-key cùng sống trong file này (đọc 1 khuôn đừng tưởng khuôn kia sai):
|
||||
// (a) `Ct_*` / `Pe_*` leaf CỐ Ý NGOÀI `All` — sinh động qua factory, Admin
|
||||
// grant qua seeder riêng (xem chú thích tại PurchaseEvaluationApproved).
|
||||
// (b) catalog / `AwV2_*` / `Khkk_G*` TRONG `All` — Admin tự có row qua
|
||||
// loop `MenuKeys.All` ở SeedAdminPermissionsAsync. K4a đi khuôn (b).
|
||||
// ============================================================
|
||||
public static readonly int[] KhkkGroupNumbers = [2, 3, 4, 5, 6, 7, 8];
|
||||
public static readonly string[] KhkkLeafActions =
|
||||
["WfView", "List", "Create", "Pending", "Approved", "Deleted"];
|
||||
|
||||
public static string KhkkGroup(int n) => $"Khkk_G{n}";
|
||||
public static string KhkkLeaf(int n, string action) => $"Khkk_G{n}_{action}";
|
||||
|
||||
// 49 key theo thứ tự: group n → 6 leaf của n → group n+1 → …
|
||||
// (khai báo TRƯỚC `All` vì static field initializer chạy theo thứ tự văn bản).
|
||||
public static readonly string[] KhkkGroupMenuKeys = KhkkGroupNumbers
|
||||
.SelectMany(n => new[] { KhkkGroup(n) }
|
||||
.Concat(KhkkLeafActions.Select(a => KhkkLeaf(n, a))))
|
||||
.ToArray();
|
||||
|
||||
public static string ContractTypeGroup(string typeCode) => $"Ct_{typeCode}";
|
||||
public static string ContractTypeList(string typeCode) => $"Ct_{typeCode}_List";
|
||||
public static string ContractTypeCreate(string typeCode) => $"Ct_{typeCode}_Create";
|
||||
@ -200,6 +237,11 @@ public static class MenuKeys
|
||||
OffDatXe, OffItTicket, OffChamCong, OffAttendanceReport, HrmDashboard, // Phase 10.3-10.4 — G-O5/G-O6/G-P1/G-H3 + P11-E report
|
||||
Personal, // [S57] Cá nhân (Puro grouping — Chấm công re-parent)
|
||||
KeHoachKyKet, // [W1 Mig 69 — S161] GĐ2 KHKK: const :41 vào All ⇒ sinh policy KeHoachKyKet.{Read,Create,Update,Delete}
|
||||
// [K4a — S166] 49 key sidebar KHKK nhóm 2..8 (7 group + 42 leaf) — nguồn sinh
|
||||
// `KhkkGroupMenuKeys` (dùng chung với seed menu-row + grant site-2). 7 key nhóm 1
|
||||
// KHÔNG-infix (`Khkk_G1`/`Khkk_WfView`…) CỐ Ý ngoài `All` như từ S159 — giữ nguyên
|
||||
// trạng thái cũ, quyền của chúng do site-2/site-3 phát.
|
||||
.. KhkkGroupMenuKeys,
|
||||
System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows,
|
||||
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22
|
||||
// [K3 nhịp-1 — S164/S165] 8 leaf Designer cho 8 nhóm duyệt KHKK (+8 key ⇒ +32 policy).
|
||||
|
||||
@ -1953,7 +1953,11 @@ public static class DbInitializer
|
||||
// [S159-đợt3] skeleton menu GĐ2/GĐ4 (owner vẽ: "viết hoa + phân list như
|
||||
// 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"),
|
||||
// [K4a — S166] Nhóm 1 trong 8 NHÓM DUYỆT (danh mục SP-002): nhãn skeleton S159
|
||||
// "1. Kế hoạch ký kết HĐ (NCC-TP)" → nhãn nhóm A1. Key `Khkk_G1` GIỮ NGUYÊN;
|
||||
// DB đang chạy hội tụ qua `labelBackfill` bên dưới (dòng này chỉ ăn với DB trống).
|
||||
// 7 nhóm còn lại (G2..G8) seed ở vòng lặp phía dưới (sau vòng `PeWf_*`).
|
||||
("Khkk_G1", "1. Thiết bị, máy móc, vật tư phụ (A1)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
|
||||
// [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"),
|
||||
@ -2092,8 +2096,8 @@ public static class DbInitializer
|
||||
tree.Add((MenuKeys.PurchaseEvaluationList(code), "Danh sách", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "List"));
|
||||
tree.Add((MenuKeys.PurchaseEvaluationCreate(code), "Thao tác", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "Plus"));
|
||||
// [S155 đợt 2] Nhãn ở đây CHỈ có tác dụng cho DB TRỐNG. Với DB đang chạy, upsert
|
||||
// `:1893-1905` gặp key đã tồn tại thì chỉ update `Order` rồi `continue` — KHÔNG
|
||||
// đụng Label ⇒ rename "Duyệt" → "Đang duyệt" phải qua `labelBackfill` bên dưới.
|
||||
// `:2162-2179` (grep `existingItems.TryGetValue`) gặp key đã tồn tại thì chỉ update
|
||||
// `Order` rồi `continue` ⇒ rename "Duyệt" → "Đang duyệt" phải qua `labelBackfill`.
|
||||
tree.Add((MenuKeys.PurchaseEvaluationPending(code), "Đang duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCircle2"));
|
||||
// 2 mục MỚI (owner chốt 2026-07-27) — key mới ⇒ đi nhánh INSERT bình thường.
|
||||
tree.Add((MenuKeys.PurchaseEvaluationApproved(code), "Đã duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCheck"));
|
||||
@ -2108,6 +2112,50 @@ public static class DbInitializer
|
||||
tree.Add((MenuKeys.PeWorkflowTypeLeaf(code), label, MenuKeys.PeWorkflows, peWfOrder++, "FileCheck"));
|
||||
}
|
||||
|
||||
// [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT × 6 leaf — 49 row MỚI cho nhóm 2..8
|
||||
// (nhóm 1 = 7 row CŨ KHÔNG-infix nằm trong tree initializer trên: `Khkk_G1` +
|
||||
// `Khkk_WfView`…`Khkk_Deleted`, key GIỮ NGUYÊN — đổi = mồ côi permission-row, S155).
|
||||
// Nhãn 8 nhóm = bảng owner đã soát `danh-muc-sp002-transcribe.md:22-29` (nguồn: danh
|
||||
// mục SOL-CCM-SP-002 Mig 70 chia 86 dòng thành 8 nhóm duyệt). Nhãn/icon/Order của 6
|
||||
// leaf MIRROR Y nhóm 1 (Luồng duyệt/Danh sách/Thao tác/Đang duyệt/Đã duyệt/Đã xóa).
|
||||
// 🔴 Key sinh bằng CHÍNH `MenuKeys.KhkkGroup/KhkkLeaf` + duyệt CHÍNH `KhkkGroupNumbers`/
|
||||
// `KhkkLeafActions` ⇒ mọi key trong `MenuKeys.All` đều có menu-row by-construction
|
||||
// (thiếu row = Permission FK nổ lần boot đầu — invariant T6 K1). Thêm nhóm/action mà
|
||||
// quên nhãn ⇒ KeyNotFoundException NGAY lúc seed (fail-fast, hơn là FK nổ về sau).
|
||||
// ⚠️ Đổi NHÃN sau khi đã seed: BẮT BUỘC thêm entry vào `labelBackfill` bên dưới +
|
||||
// restart — sửa chữ tại đây VÔ TÁC DỤNG trên DB đang chạy (upsert chỉ re-set Order).
|
||||
var khkkGroupLabels = new Dictionary<int, string>
|
||||
{
|
||||
[2] = "2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)",
|
||||
[3] = "3. Hoạt động văn phòng (A4)",
|
||||
[4] = "4. Vật tư (B1)",
|
||||
[5] = "5. Thầu phụ xây dựng (B2)",
|
||||
[6] = "6. Thầu phụ MEP (B3)",
|
||||
[7] = "7. BCH công trường (C1-C3)",
|
||||
[8] = "8. HĐ chưa ủy quyền — CEO (5.2)",
|
||||
};
|
||||
var khkkLeafMeta = new Dictionary<string, (string Label, string Icon)>
|
||||
{
|
||||
["WfView"] = ("Luồng duyệt", "Network"),
|
||||
["List"] = ("Danh sách", "List"),
|
||||
["Create"] = ("Thao tác", "Plus"),
|
||||
["Pending"] = ("Đang duyệt", "CheckCircle2"),
|
||||
["Approved"] = ("Đã duyệt", "CheckCheck"),
|
||||
["Deleted"] = ("Đã xóa", "Trash2"),
|
||||
};
|
||||
foreach (var n in MenuKeys.KhkkGroupNumbers)
|
||||
{
|
||||
var groupKey = MenuKeys.KhkkGroup(n);
|
||||
// Order nhóm = n (nhóm 1 đang Order 1 ⇒ 8 nhóm xếp 1..8 dưới root KeHoachKyKet).
|
||||
tree.Add((groupKey, khkkGroupLabels[n], MenuKeys.KeHoachKyKet, n, "FileCheck"));
|
||||
var khkkLeafOrder = 1;
|
||||
foreach (var action in MenuKeys.KhkkLeafActions)
|
||||
{
|
||||
var (leafLabel, leafIcon) = khkkLeafMeta[action];
|
||||
tree.Add((MenuKeys.KhkkLeaf(n, action), leafLabel, groupKey, khkkLeafOrder++, leafIcon));
|
||||
}
|
||||
}
|
||||
|
||||
var existingItems = await db.MenuItems.ToDictionaryAsync(m => m.Key);
|
||||
var added = 0;
|
||||
var reordered = 0;
|
||||
@ -2146,7 +2194,7 @@ public static class DbInitializer
|
||||
[MenuKeys.PeWorkflowTypeLeaf("DuyetNccPhuongAn")] = "Duyệt NCC và Giải pháp",
|
||||
// [S155 đợt 2] 🔴 Rename "Duyệt" → "Đang duyệt" (owner, ảnh menu 2026-07-27).
|
||||
// BẮT BUỘC đi qua đây: sửa chữ trong `tree.Add(...)` là VÔ TÁC DỤNG trên DB đang
|
||||
// chạy vì upsert `:1893-1905` chỉ update `Order` cho key đã tồn tại.
|
||||
// chạy vì upsert `:2162-2179` (grep `existingItems.TryGetValue`) chỉ re-set `Order`.
|
||||
[MenuKeys.PurchaseEvaluationPending("DuyetNcc")] = "Đang duyệt",
|
||||
[MenuKeys.PurchaseEvaluationPending("DuyetNccPhuongAn")] = "Đang duyệt",
|
||||
// [S159] 🔴 UAT anh Kiệt: đánh số 01.-07. nhóm HĐ (verbatim "01 . HĐ thầu phụ").
|
||||
@ -2180,6 +2228,11 @@ public static class DbInitializer
|
||||
[MenuKeys.ContractTypeGroup("MuaBan")] = "05. HĐ Mua bán",
|
||||
[MenuKeys.ContractTypeGroup("NguyenTacNcc")] = "06. HĐ Nguyên tắc NCC",
|
||||
[MenuKeys.ContractTypeGroup("NguyenTacDv")] = "07. HĐ Nguyên tắc Dịch vụ",
|
||||
// [K4a — S166] Nhóm 1 KHKK đổi nhãn skeleton S159 ("1. Kế hoạch ký kết HĐ
|
||||
// (NCC-TP)") → nhãn nhóm duyệt A1, khớp bảng 8 nhãn owner đã soát. CHỈ đổi
|
||||
// Label, KHÔNG đổi key ⇒ 0 row mới, 0 permission-row mồ côi. 7 nhóm G2..G8 là
|
||||
// key MỚI nên đi nhánh INSERT của upsert, không cần entry backfill.
|
||||
["Khkk_G1"] = "1. Thiết bị, máy móc, vật tư phụ (A1)",
|
||||
};
|
||||
var updatedLabels = 0;
|
||||
foreach (var (key, expectedLabel) in labelBackfill)
|
||||
@ -2282,6 +2335,9 @@ public static class DbInitializer
|
||||
// Cá nhân (Personal) khỏi MỌI role TRỪ Admin. Menu tự ẩn cả 2 app
|
||||
// (GetMyMenuTree lọc CanRead). Chạy SAU grant seed để revoke thắng.
|
||||
// Mở lại sau golive: gỡ prefix khỏi revoke + thêm lại vào InReviewScope.
|
||||
// [K4a — S166] Đã soi revoker cho 49 key KHKK mới: filter chỉ bắt prefix `Hrm*`/`Off*`
|
||||
// + key `Personal` ⇒ **0 match `Khkk_*`** ⇒ 49 row CanRead vừa cấp KHÔNG bị lật. Phép
|
||||
// thử của khẳng định này = restart API ×2 rồi đếm lại row `Khkk%` (acceptance A5).
|
||||
await RevokeTemporarilyHiddenModulesAsync(db, roleManager, logger);
|
||||
|
||||
// [S118 2026-07-14 — anh Kiệt FDC] CHẠY SAU revoke để THẮNG: NGOẠI-LỆ role Procurement
|
||||
@ -2370,11 +2426,24 @@ public static class DbInitializer
|
||||
MenuKeys.CatalogServices, MenuKeys.CatalogWorkItems, MenuKeys.Departments,
|
||||
MenuKeys.Projects, MenuKeys.Suppliers,
|
||||
};
|
||||
// [S159] 7 key nhóm 1 (không-infix) + [K4a S166] 49 key nhóm 2..8 (7 group + 42 leaf,
|
||||
// sinh từ `MenuKeys.KhkkGroupMenuKeys` — CÙNG bộ sinh với `MenuKeys.All` và vòng seed
|
||||
// menu-row ⇒ không mirror tay). CanRead mọi role, skip-existing.
|
||||
// 🔴 LÝ DO GIỮ site này (đính chính lý-do sai đang lưu hành "group-key cần row để
|
||||
// render"): group KHÔNG cần row CanRead của chính nó — `GetMyMenuTreeQuery.cs:96`
|
||||
// `HasAccess(n) = n.CanRead || n.Children.Any(HasAccess)`. Cái BẮT BUỘC là 42 LEAF
|
||||
// phải có row CanRead cho non-Admin: site-1 (loop MenuKeys.All) CHỈ cấp Admin, còn
|
||||
// site-3 `SeedKeHoachKyKetAccessAsync` CHỈ đụng root + 6 leaf nhóm 1. Group vẫn liệt
|
||||
// kê ở đây để có hàng tường minh trên ma trận Phân quyền (admin tự bật/tắt).
|
||||
// ⚠️ CanRead là ĐỦ cho 42 leaf mới, KHÔNG cần nhét chúng vào site-3: mọi endpoint KHKK
|
||||
// gate bằng policy `KeHoachKyKet.{Read,Create,Update,Delete}` (gotcha #85 — gate FE
|
||||
// cũng phải dùng đúng key đó, KHÔNG OR key con), và cờ GHI của root do site-3 phát
|
||||
// (13/13 role C+U, Delete = Drafter+Admin). Leaf chỉ là điểm điều hướng.
|
||||
static IEnumerable<string> KhkkKeys() => new[]
|
||||
{
|
||||
"Khkk_G1", "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
|
||||
"Khkk_Approved", "Khkk_Deleted",
|
||||
};
|
||||
}.Concat(MenuKeys.KhkkGroupMenuKeys);
|
||||
static IEnumerable<string> HdcKeys()
|
||||
=> MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user