[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

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:
pqhuy1987
2026-08-01 10:10:11 +07:00
parent bc1e7c937d
commit 88e7ecf263
14 changed files with 880 additions and 63 deletions

View File

@ -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);