# Session 20 turn 7 — Admin Ẩn/Hiện + Đổi tên menu eOffice (Mig 27) **Date:** 2026-05-11 **Commits:** `2ea2d27` (A schema) → `ef394f8` (B API) → `059bfcb` (C admin page) → `1ed6530` (D user layout) → this (E docs) ## Bối cảnh User UAT live yêu cầu thêm tính năng admin "Ẩn/Hiện và Đổi tên hiển thị của các Menu bên ngoài Office" — trang quản lý menu eOffice (fe-user) thực hiện trong Admin Page (fe-admin). User confirm "Hình như chưa có?" — đúng, chưa có. User clarify quan trọng: **"edit hiển thị bên ngoài nhé. Chỉ của eOffice thôi"** → Q2=b: DisplayLabel CHỈ áp khi render fe-user. fe-admin sidebar luôn dùng Label gốc (admin dễ debug, không nhầm). ## Q&A trước khi code | # | Câu hỏi | Chốt | |---|---|---| | Q1 | Scope visibility (global vs per-role)? | **a** Global — permission matrix đã handle per-role | | Q2 | DisplayLabel áp đâu? | **b** Chỉ fe-user, admin sidebar giữ Label gốc | | Q3 | Init defaults — keep `USER_HIDDEN_KEYS` hardcode 4 root? | **a** Giữ hardcode + tầng `IsVisible` dynamic combine | | Q4 | Verify mode? | Phase 9 UAT iteration: skip dotnet test, vẫn `npm run build` | ## Chunk A — Schema + Migration 27 (`2ea2d27`) **Domain `MenuItem.cs`:** ```csharp public bool IsVisible { get; set; } = true; public string? DisplayLabel { get; set; } ``` **EF Configuration:** `HasDefaultValue(true)` + `HasMaxLength(200)`. **Migration 27 `AddVisibilityAndDisplayLabelToMenuItems`:** - `AddColumn IsVisible bit NOT NULL DEFAULT 1` (backfill existing rows = true) - `AddColumn DisplayLabel nvarchar(200) NULL` - 3-file rule **Verify:** - `dotnet build` 0 err - `dotnet ef database update --connection SolutionErp_Dev` applied - `dotnet ef database update SolutionErp_Design` applied (catchup Mig 25/26/27) ## Chunk B — BE API (`ef394f8`) **DTOs `MenuDtos.cs`:** - `MenuNodeDto` +`IsVisible bool` +`DisplayLabel string?` (sau CRUD flags, trước Children) - `MenuItemDto` +`IsVisible` +`DisplayLabel` **`GetMyMenuTreeQueryHandler`:** pass `m.IsVisible` + `m.DisplayLabel` vào DTO record. KHÔNG filter `IsVisible` server-side — 2 FE app tự quyết render gì (fe-admin show all, fe-user filter). **`ListMenuItemsQueryHandler`:** projection thêm 2 field. **NEW `UpdateMenuItemCommand` + Validator + Handler** (PermissionFeatures.cs): ```csharp public record UpdateMenuItemCommand(string Key, bool IsVisible, string? DisplayLabel) : IRequest; // Handler: load by Key, set 2 field, trim DisplayLabel → null nếu whitespace, SaveAsync ``` **`MenusController` +PATCH endpoint:** ```csharp [HttpPatch("{key}")] [Authorize(Policy = "Permissions.Update")] public async Task Update(string key, [FromBody] UpdateMenuItemRequest body, CancellationToken ct) { await mediator.Send(new UpdateMenuItemCommand(key, body.IsVisible, body.DisplayLabel), ct); return NoContent(); } ``` Policy reuse `Permissions.Update` (admin matrix — cùng scope quản trị). ## Chunk C — FE Admin MenuVisibilityPage (`059bfcb`) **Domain `MenuKeys.cs`:** +`MenuVisibility = "MenuVisibility"` + thêm vào `All[]`. **DbInitializer `SeedMenuTreeAsync`:** +leaf `(MenuVisibility, "Menu eOffice", System, 94, "Eye")`. Workflows shift Order 94 → 95. Idempotent. Manual seed Mig 27 LocalDB Dev: ```sql INSERT INTO MenuItems ([Key], Label, ParentKey, [Order], Icon, IsVisible, DisplayLabel) VALUES ('MenuVisibility', N'Menu eOffice', 'System', 94, 'Eye', 1, NULL); INSERT INTO Permissions (Id, RoleId, MenuKey, CanRead, CanCreate, CanUpdate, CanDelete) SELECT NEWID(), Id, 'MenuVisibility', 1, 1, 1, 1 FROM Roles WHERE Name = 'Admin'; ``` **FE Admin updates:** - `types/menu.ts`: MenuItem + MenuNode +`isVisible` +`displayLabel` - `lib/menuKeys.ts`: +`MenuVisibility` const - `components/Layout.tsx` resolver: +`MenuVisibility: '/system/menu-visibility'` - `App.tsx`: +Route + import `MenuVisibilityPage` **NEW `pages/system/MenuVisibilityPage.tsx` (~210 LOC):** Layout pattern reuse PermissionsPage: - `PageHeader` + description nhắc admin sidebar dùng Tên gốc - 4 StatCard: Tổng / Hiển thị (eOffice) / Đã ẩn / Đã đổi tên - Search input — filter theo `key | label | displayLabel` - Table 5 cột: | Key (mono + parentKey ↳) | Tên gốc | Input "Tên hiển thị" inline (placeholder "Mặc định: {label}") | Toggle Hiển thị/Ẩn (button emerald/amber) | Hành động | - Per-row state qua `DraftMap` — dirty detect = `draft[key] !== ev` - Save button hiện khi dirty: `PATCH /menus/{key}` body `{ isVisible, displayLabel }` (trim empty → null) - "Khôi phục" button khi đã custom (hidden hoặc renamed): force `isVisible=true`, `displayLabel=null` - onSuccess: invalidate `['menus', 'all']` + `['my-menu']` + clear draft entry → live update sidebar - Row hidden: `bg-amber-50/40` highlight, input custom label `bg-brand-50/40` ## Chunk D — FE User Layout filter + render (`1ed6530`) **`fe-user/types/menu.ts`:** mirror fe-admin (MenuItem + MenuNode +isVisible +displayLabel). **`fe-user/components/Layout.tsx`:** ```tsx function filterForUser(nodes: MenuNode[]): MenuNode[] { // Filter 2 tầng: // 1. USER_HIDDEN_KEYS hardcode (Master/System/Forms/Reports — structural never-show) // 2. !isVisible dynamic (Mig 27 admin toggle) return nodes .filter(n => !USER_HIDDEN_KEYS.has(n.key) && n.isVisible !== false) .map(n => ({ ...n, children: filterForUser(n.children) })) } function effectiveLabel(n: { label: string; displayLabel?: string | null }): string { return (n.displayLabel && n.displayLabel.trim()) || n.label } ``` Replace 3 callsite `{node.label}` → `{effectiveLabel(node)}` (Group header / Leaf NavLink / nested NavLink). USER_FIXED_TOP "__inbox" entry +`isVisible:true` +`displayLabel:null` cho type check pass. **fe-admin Layout KHÔNG đụng** — admin sidebar luôn render Label gốc + show hết menu, kể cả `isVisible=false` (cho admin biết menu nào đã ẩn để toggle bật lại). Đây chính là điểm khác biệt user yêu cầu Q2=b. ## Verify chain mỗi chunk | Chunk | BE build | FE-admin build | FE-user build | DB migrate | Push | |---|---|---|---|---|---| | A | ✅ 0 warn / 0 err | (no FE change) | (no FE change) | ✅ Dev + Design applied | `2ea2d27` | | B | ✅ pass | (no FE change) | (no FE change) | (no DB change) | `ef394f8` | | C | (no BE change) | ✅ pass | (no FE change) | manual SQL seed Mig 27 | `059bfcb` | | D | (no BE change) | ✅ pass (re-verify) | ✅ pass | (no DB change) | `1ed6530` | ## Stats Session 20 turn 7 | Metric | Trước | Sau | Delta | |---|---|---|---| | DB tables | 59 | 59 | 0 | | Migrations | 26 | **27** | +1 (`AddVisibilityAndDisplayLabelToMenuItems`) | | Endpoints | ~141 | **~142** | +1 (`PATCH /api/menus/{key}`) | | FE pages | 33 | **34** | +1 (`MenuVisibilityPage`) | | Menu keys | ~60 | ~61 | +1 (`MenuVisibility`) | | Unit tests | 81 pass | 81 pass | 0 (Q4 UAT iteration skip) | | Gotchas | 44 | 44 | 0 | | Commits | — | 5 | A/B/C/D + E docs | ## Cross-ref - Memory `feedback_audit_reuse_before_clone.md` — pattern reuse PermissionsPage UI cho MenuVisibilityPage (table + inline edit + state map) - Memory `feedback_per_chunk_commit.md` — A/B/C/D/E chunk discipline - Memory `feedback_designtime_runtime_db.md` — apply Mig 27 lên cả `_Dev` (runtime) + `_Design` (ef tooling) qua --connection override - Memory `feedback_uat_skip_verify.md` — Q4 Phase 9 UAT: skip test, vẫn `npm run build` - Skill `permission-matrix` — sẽ cross-ref menu visibility section ở audit 2026-06-01 ## Pending S21+ Carry over từ HANDOFF S19+S20: - Test V2 Service wire + Section gộp - Test B4 silent 403 (HIGH §7) - **Contract V2 wire (Mig 28+29 mirror PE pattern)** — biggest pending - Phân quyền strict V2 - Drop legacy V1 + Mig 15 cleanup - Cron audit 2026-06-01 (skill stale + schema-diagram §16-21) Mới sau S20 turn 7: - Test PATCH `/api/menus/{key}` validate Key required + DisplayLabel trim - Skill `permission-matrix` thêm section "menu visibility" — defer audit 2026-06-01 - Test edge case: admin ẩn menu cha → children có ẩn theo không? (hiện FE filter chỉ check `!n.isVisible`, child có thể vẫn hiện nếu parent vẫn visible — verify UX trong UAT)