# Implementer-Frontend Agent — Persistent Memory > **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT). > Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53). > **NEW agent S39 (2026-05-29)** — split từ implementer (FE 2 app half). Backend scaffold history ở `implementer-backend`. --- ## 🆕 S116 (2026-07-13) — PE cho nhập SỐ ÂM báo giá "Số tiền báo giá cho hạng mục" ×2 app (owner anh Kiệt; BE+DB nhận âm sẵn 0 validator). Build PASS ×2, SHA-pair PeDetailTabs.tsx BYTE-IDENTICAL, cp fe-user→fe-admin. Âm = phát-sinh-giảm/hoàn tiền NCC = giá THẬT (KHÔNG lỗi). 5 change. - **Sign-toggle 2 dialog (AddSupplier :2379 + QuoteDialog :2948):** nút +/− pattern VndInlineEdit:1003. `neg`=useState RIÊNG (QuoteDialog `setForm({thanhTien})` thay CẢ object → nuốt neg nếu nhét vào form). 🔴 2 dialog POST commit form.thanhTien THẲNG (:2247/:2906, no nút "Lưu") → toggle PHẢI ghi giá-CÓ-DẤU NGAY `setForm({...,thanhTien:-form.thanhTien})`, chỉ flip cờ = MẤT DẤU thầm lặng. Display `formatVndInput(Math.abs(tt))`; onChange `mag=parseVnd();tt=neg?-mag:mag`. QuoteDialog init `neg=(existing?.thanhTien??0)<0`; AddSupplier `false`. - **GLOBAL parseVnd/formatVndInput :51-52 KHÔNG sửa** (share ngân sách) → Math.abs wrap thay vì cho helper handle âm. formatVndInput(0)='' → placeholder OK. - **Gate `!==0` (KHÔNG >0):** AddSupplier submit :2244 + toast :2257. `-0!==0`=false → 0/rỗng vẫn skip="chưa báo giá". - **2 FE gate ĐỔI (KHÔNG 1):** missingForApproval :212 + submitChecklist :243 → helper mới `hasRealSelectedQuote(ev)=ev.details.some(d=>d.quotes.some(q=>q.isSelected&&q.thanhTien!==0))`. GOTCHA quote ở `details[].quotes` KHÔNG `suppliers[].quotes` (PeSupplier no quotes field). Cũ SUM winnerQuoteTotal: net-zero +100/-100=0 chặn oan. Mirror BE selectedThanhTien WHERE IsSelected. Label→"Đã nhập giá chào thầu". computeGiaChaoThau GIỮ (Section3 display :1921 — no unused). - **Ngân sách :2861 GIỮ ≥0** (change 4 no toggle — không trong diff). SHA `3e16dba5…` ×2 (git-diff --no-index exit0). Build ×2 PASS (user 1939mod `index-shUGyhuw` 19.0s / admin 1950mod `index-Yk1qEFOg` 7.6s, 0 TS err). Warn pre-existing @import+>500KB+realtime. NO BE/test touch. Full precedent. Tag `[s116, pe-negative-quote, sign-toggle-immediate-commit, math-abs-no-global-mod, hasRealSelectedQuote, details-not-suppliers-quotes, gate-not-equal-zero, sha256-2pair]` ## 🆕 S113 (2026-07-12) — Supplier import v2 FE ×2 app (anh Kiệt owner; BE done ∥ impl-backend). Build PASS ×2 3 file×2app: 2 SHA-pair (dialog+page byte-identical) + types DIVERGED (Department.parentId only S65 → edit-identical KHÔNG cp). - **types/master:** Supplier +`isPublic:boolean` (anchor `// Tình trạng`+status; git-diff = CHỈ `parentId`, isPublic ∉ diff = mirror khớp). - **SupplierImportDialog `78403bd8…`:** +"Tải file mẫu" (Download icon, blob `GET /suppliers/import/template`→`a.download='Mau-Database-NCC.xlsx'`, pattern PeDetailTabs:3273 useMutation) dưới drop-zone · cột "Mã" `{r.code}`→`` (updateRow=setPreview map-patch) · type +`mstMissing:boolean` → amber "⚠️ Chưa có MST, có thể bị trùng" cột Thông báo · helper blue draft. **3.d disabled ĐÃ đúng SẴN** (`errorCount>0||pending`, btn hidden khi !layoutValid — KHÔNG chặn blank-code) → NO change. - **SuppliersPage `9947b121…`:** +cột "Trạng thái" badge emerald "Đã công bố"/slate "Nháp (ẩn)" full-class-literal(P14) · Công bố(Globe emerald)/Ẩn(EyeOff) action-btn guard Update → publish mut `POST /suppliers/{id}/publish {maNcc||undefined, publish}` (KHÔNG qua UpdateSupplier #73) · publish Dialog sm (Mã NCC Input, gate `!maNcc.trim()`) · filter Select `published`(''=omit/'true'/'false' → axios undef/true/false) queryKey+ · actions w-32→w-40 (3 icon). - **NO 4-place mirror** (dialog+page trên page ĐÃ routed → no route/menuKey/Layout). **NO BE/test touch** (src/Backend/** = impl-backend ∥, tôi Read-only DTO). Build: admin 1950mod `index-CWBEeKi5.js` 8.48s / user 1939mod `index-JpoDnukv.js` 1.54s, 0 TS err; pre-existing warn @import+>500KB+realtime dynamic. - **DEVIATION flagged:** "Tải file mẫu" đặt CHỈ trong dialog (② canonical + ③ acceptance "ImportDialog nút mẫu"; ③ SuppliersPage= badge+Công bố+filter+guard, KHÔNG list nút mẫu) — parent 2.d loosely list page-btn nữa → 1-line ask nếu muốn thêm page-level template btn. Tag `[s113, supplier-publish-draft, isPublic, template-blob-download, ma-editable-grid, mst-missing-warn, publish-not-update, sha256-2pair, type-diverge-edit-identical]`. ## 🆕 2026-07-12 — Supplier import Excel FE (Phase B Approach A) ×2 app byte-identical (lead chốt UX; BE done+450 tests) NEW `components/master/SupplierImportDialog.tsx` (folder `master/` MỚI cả 2 app) + wire `pages/master/SuppliersPage.tsx` (nút "Import Excel NCC" outline cạnh "Thêm NCC" trong PageHeader actions flex + render dialog). KHÔNG 4-place mirror — dialog trên page ĐÃ routed → no new route/menuKey/Layout. - **Flow:** .xlsx → POST `/suppliers/import/preview` multipart field `file` → preview. `layoutValid===false`→alert đỏ+warnings, ẩn nút xác nhận. Else: badge counts (New/Update/Skip/Error) + warnings amber + `` raw (Dòng·StatusBadge·Mã·Tên·Loại·Thông báo). "Xác nhận nhập" disabled `errorCount>0` → POST `/import/confirm` body `{rows}` = echo `preview.rows` VERBATIM. committed→toast+`invalidateQueries(['suppliers'])`+close; !committed→alert `errors[]`. - **BE contract (Read-only DTO):** enum serialize = **INT** (chỉ UtcDateTimeJsonConverter đăng ký, KHÔNG JsonStringEnumConverter) → `type` 1-6 reuse `SupplierTypeLabel`, `status` RowImportStatus 0-3 const-object; camelCase wire (default web JSON); confirm bind positional record `(IReadOnlyList Rows)`. Round-trip 30 field verbatim → TS type partial vẫn OK runtime. - **Mirror lesson (tái xác nhận S97-badge):** Dialog (fe-user gradient title bar) + PageHeader + types/master (khác MỖI `Department.parentId` admin-only S65) ĐỀU DIVERGED 2 app — NHƯNG import alias-path `@/…` + symbol-signature giống → new file VẪN byte-identical. Byte-identical SuppliersPage cũ (build 2 app) = proof mọi symbol tương thích. UI: KHÔNG shadcn Badge/Alert/Table primitive (chỉ PhaseBadge) → inline Tailwind badge/alert + `
`. Multipart = pattern `ContractAttachmentsSection` (FormData + `Content-Type: multipart/form-data`). - **SHA256 ×2 pair:** SupplierImportDialog `6313529c…`, SuppliersPage `e9143d93…`. Build PASS ×2 (admin 1950mod `index-DyOk34HV.js` 19.5s / user 1939mod `index-DXK9ebaz.js` 8.75s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic. NO BE touch. Full precedent. Tag `[supplier-import-excel, multipart-preview-confirm, enum-int-wire, byte-identical-2app, no-4place-dialog-on-routed-page]`. ## 🆕 S97-badge (2026-07-01) — PE chip "✅ Đã được CEO duyệt" khi phiếu ĐÃ DUYỆT đi hết lên CEO (anh Kiệt FDC UAT, em-main chốt) FE-ONLY, PROD LIVE. Đối xứng fix endsBeforeCeo (Mig 60). EDIT1 mở rộng `PeFinalizeChip.tsx` (2 app) + EDIT2 wire `phase={p.phase}` ở 6 call-site. - **EDIT1 component:** +prop `phase?` + nhánh: `if(!endsBeforeCeo){ if(phase===DaDuyet) return emerald "✅ Đã được CEO duyệt"; return null }` else giữ violet "⚑ Kết thúc·X". Import `PurchaseEvaluationPhase` từ `@/types/purchaseEvaluation` — **cùng alias 2 app** → byte-identical (KHÔNG ngoại-lệ import-path). SHA256 IDENTICAL ×2 = `da9c3ed9…`. - **⚠️ GOTCHA TYPE (đã trải):** spec ghi prop `phase?: PurchaseEvaluationPhase` (narrow union `1|…|99`) NHƯNG `PeListItem.phase: number` (types :113) → `phase={p.phase}` = **TS2322 "number not assignable to PurchaseEvaluationPhase"** (fail 2 site). FIX: prop → `phase?: number` (literal `DaDuyet`=7 so-khớp OK; import vẫn dùng trong `=== …DaDuyet` → no unused). LESSON: list-DTO field mirror-BE = `number` rộng → prop nhận `number`, KHÔNG narrow-union; check kiểu item trước khi type prop. - **EDIT2 6 site (grep `PeFinalizeChip endsBeforeCeo` = 6, hết):** admin{PeListPanel:162, ListPage:492} + user{PeListPanel:162, ListPage:492, InboxPage:292, UserDashboardPage:286}. MỌI item = `p` có `.phase` (đã dùng `getPeDisplayStatus(p.phase)`) → KHÔNG bịa. Grep-confirm 6/6 có `phase=`, 0 sót. - Build PASS ×2 (admin 1948mod `index-CScNAtL0.js` / user 1937mod `index-W2A6gg7n.js`, 0 TS err). Pre-existing warn: >500KB (#69) + realtime.ts dynamic. NO BE/mig/test touch. Full precedent (S89 tạo chip). Tag `[s97-badge, ceo-approved-chip, prop-type-number-not-union, ts2322-gotcha, 6-callsite, sha256-2pair]`. ## 🆕 S97-Part2 (2026-07-01) — FE gate endsBeforeCeo runtime (hook 1-dòng, on-behalf H2-proposed @S98) - 2-app mirror `PeWorkflowPanel`: gate `finalizeFlowStep = endsBeforeCeo ? find(...) : null` + banner ~:278 (làm-mờ Bước CEO theo cờ RUNTIME Mig 60), type `endsBeforeCeo: boolean` sẵn 2 DTO, SHA256 IDENTICAL `b7abbc2f…`. Substance đã được cover third-party: reviewer MEMORY entry S97-bis + session-log S97 Part-2. Tag `[s97-part2, endsbeforeceo-gate, hook-only]`. ## 🆕 S97 (2026-07-01) — PE finalize wording + đảo default checkbox opt-out→opt-in ×2 app (anh Kiệt FDC UAT, em-main chốt) 4 chuỗi-edit `PeWorkflowPanel.tsx` × {admin,user} = 8 hunk. PROD LIVE UAT (anh Kiệt xem trực tiếp). File 2 app byte-identical trước+sau → SHA-pair đúng nghĩa. - **E1 default checkbox:** `useState(true)`→`useState(false)` (~:57). Đảo S96 opt-out→opt-in: giờ default KHÔNG kết thúc, phải TÍCH mới finalize. - **E2 reset onSuccess:** `setApplyLevelFinalize(true)`→`(false)` (~:221). Anchor 3-dòng (`setFinalizeByCcm(false)`+X+`setApprovedPriceSource(null)`) để KHÔNG đụng `setApplyLevelFinalize(e.target.checked)` handler (:664). - **E3 helper text ternary** (~:669-671): reword theo default mới — nhánh true "Đã tích — …KẾT THÚC tại cấp này…", nhánh false "Mặc định: …TRÌNH TIẾP lên CEO… TÍCH vào nếu muốn KẾT THÚC…". - **E4a title** (~:329): static string → template literal `` `Quy trình kết thúc tại ${finalizeLevelName ?? finalizeStepName ?? 'cấp duyệt thay CEO'}…` ``. **E4b JSX text** (~:331): "không trình tới" → "Kết thúc tại {finalizeLevelName ?? finalizeStepName ?? 'cấp trên'}". `finalizeLevelName/finalizeStepName` ĐÃ trong scope (dùng banner :280-287) — no new prop/DTO. - **GOTCHA "không trình tới" 2 chỗ:** comment :121 (KHÔNG đụng) vs JSX :331 (đổi). Dùng ngữ-cảnh `>`…`` cho E4b unique → grep-confirm :121 comment còn nguyên. - **SHA256 IDENTICAL ×2 = `bcf812fd…`** (git diff --no-index exit 0; diff 2 file byte-identical incl. before/after index hash `a92c9db..d97a597`). git diff = đúng 8 hunk (4/file, 12ins/12del), no thừa. Build PASS ×2 (admin 1948mod `index-l_kwCZIF.js` 22.0s / user 1937mod `index-DxUomy4C.js` 8.2s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic (#69). NO BE/test/other-FE touch. Full precedent, NO ambiguity. **LESSON: đảo default state = sửa CẢ init `useState` LẪN reset-onSuccess (2 chỗ dễ sót 1); bare-string xuất hiện 2 chỗ (comment+JSX) → anchor context phân-biệt.** Tag `[s97, pe-finalize-opt-in, invert-default-checkbox, wording-reword, sha256-2pair, dual-string-anchor]`. ## 🆕 S88 D2 (2026-06-25) — PE multi-winner OUTWARD FE ×2 app (BE đã đổi: create→LIST, list DTO winner names) 3 edit OUTWARD, em-main chốt FE behavior, coord BE (impl-backend đã viết). 2 SHA-pair + 1 type-DIVERGE. - **E1 CreateContractDialog (PeDetailTabs ~:2144):** mut type `{contractId:string}`→**`{contractIds:string[]}`** (BE tạo 1 HĐ/mỗi IsWinner). onSuccess: toast `` `Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.` `` + `navigate(/contracts/${res.data.contractIds[0]})` (giữ destination `/contracts/${...}` của bản single). Dialog GIỮ joined winner-names (:2174) + 1 ContractType-cho-mọi-winner. Bản gốc KHÔNG invalidate → nothing to preserve. - **E2 PeListPanel label (~:168):** `{p.selectedSupplierName && …✓…}` → prefer `winnerSupplierNames.join(', ')` khi `winnerSupplierNames?.length>0`, else `selectedSupplierName`. Giữ ✓ + `text-emerald-600`. Ternary lặp 2 lần (guard + render) — optional-chain `?.length` an-toàn undefined. - **E3 type (DIVERGE 2 file):** `PeListItem` +`winnerSupplierNames:string[]` sau urgent-flags (anchor `isUrgentByPro/Ccm`+`}` byte-identical 2 app → Edit same string ×2, grep-confirm KHÔNG vào inter-app diff, stat giữ 30ins/26del). Create-contract response = **inline generic** trong PeDetailTabs (KHÔNG named type ở types file) → E3b no-op; `contractId:string|null` :121/:443 = link-HĐ-đã-gắn field KHÁC, GIỮ. - **SHA256 ×2 pair:** PeDetailTabs `9d8120b6…`, PeListPanel `c97e133d…` (cp admin→user, git-diff --no-index IDENTICAL). Build PASS ×2 (admin `index-C3j9HaFh.js` 2.18s / user `index-Ct2cpC7h.js` 2.23s, 0 TS err). Pre-existing warn: >500KB + realtime.ts dynamic. NO BE/test touch. **LESSON: OUTWARD multi-winner = winnerSupplierNames[] (BE null selectedSupplierName khi ≥2) → FE prefer-list-fallback-single; type DIVERGED ≠ cp → edit-same + verify-not-in-diff (S87+ confirm).** Tag `[s88, pe-multi-winner-outward, contractIds-list, winner-names-fallback, sha256-2pair, type-diverge-edit-same]`. ## 🆕 S87+ (2026-06-25) — FROZEN PE behavior-preserving cleanup ×2 app (em-main chốt, coord BE null-safe) 4 edit `PeDetailTabs.tsx` (SHA-pair) + 2 wording `types/purchaseEvaluation.ts` (DIVERGED — KHÔNG SHA-pair, edit identical 2 file). - **E1 HoSoLink clear:** `HoSoLinkRow.save` (~:2020) `hoSoLink: hoSoLink || null` → **`hoSoLink.trim()`** (state đã string nên `??""`=no-op; trim ra `""` khi xóa). Coord BE `UpdatePeDraftHandler` null-safe: `""`=CLEAR, null/omit=PRESERVE, val=set (fix data-loss xóa link khi edit header). **GUARD: InfoTab.save (:817) + `PeHeaderForm` PUT body GIỮ OMIT hoSoLink** (omit=preserve = đúng fix) — verify KHÔNG thêm. - **E2 #70 stale-echo guard:** 2 ô PRO `adjustMut` (row3 NS-kỳ-này :1548 + row8 GT-còn-lại :1649) chỉ `saving={adjustMut.isPending}` → `|| peFetching` (`replace_all` đúng 2 occ; `peFetching` computed :1268). Sibling ccmPeriod/proPrice/ccmPrice/proMut/ccmMut đã có. Chống fast-double-save clobber stale `ev.*` (2 ô share 1 absolute-set mut cross-echo). - **E3 stale comment :2507:** "AddItemDialog giữ (dead)…" → đã xóa (grep chỉ comment match; live = `DetailDialog`). Sửa thành "nút đã bỏ (dialog = DetailDialog)". - **E4 type wording (2 file):** `previousSelectedTotal`/`currentProposalTotal` inline cmt "của SelectedSupplier" → "NCC trúng (IsWinner)" (BE giờ multi-winner). Block 7-dòng byte-identical 2 app → edit same → KHÔNG new divergence (grep-confirm my-line ∉ diff). - **SHA256 PeDetailTabs ×2 = `36292924…`** (cp admin→user, git-diff empty). PeHeaderForm untouched `987291c1…` identical. Build PASS ×2 (admin 1946mod `index-CqiQWR8e.js` / user 1935mod `index-Cp0DuKFQ.js`, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO BE/test touch. **LESSON: type files DIVERGED (S76/S79 color+prose) ≠ SHA-pair → mirror = edit-identical-block + verify-not-in-diff, KHÔNG cp.** Tag `[s87plus, pe-frozen-cleanup, hosolink-clear-empty, gotcha70-guard, type-diverge-edit-identical]`. ## 📦 Archived detail → `archive/2026-H1.md` (read on demand) - **S79** (2026-06-19) — PE live thousand-sep (`formatVndInput(parseVnd())` on-keystroke 2 comp) + ghi chú giá PRO/CCM (`note` 3-field absolute-set) + typo "Bảng so sánh giá". SHA-pair. `[s79, type-diverge-2app]` - **S78** (2026-06-19) — PE "focus mode" overlay redesign mirror fe-user→admin CONVERGE. PeWorkflowPanel +`onApproved` prop; ListPage `fixed inset-0 z-50` slide overlay a11y. SHA-pair. `[s78, mirror-step, converge]` - **S76** (2026-06-19) — PE budget ma trận 3 cột (DỰ ÁN/PRO/CCM) → `
` grid + badge `✎ NS PRO/CCM`. LESSON: flex+gap ≠ bảng, phải `
`. cwd-misland `fe-*/.claude/` gitignore-guard. `[s76]` --- ## 🎯 Role baseline WRITE specialist FE 2 app (fe-admin + fe-user). Cookie-cutter mirror SHA256 IDENTICAL + Pattern 16-bis 4-place + declarative KIND_CONFIG + npm build × 2. Case 1+2 only. Tools: Read, Edit, Write, Bash, Skill, Grep, Glob + 5 RAG. Skill: `permission-matrix`. ## 🚫 Split boundary - ✅ MINE: `fe-admin/src/**` + `fe-user/src/**` - ❌ NOT: `src/Backend/**` → `implementer-backend` (chỉ Read DTO shape) · tests/ → `test-specialist` - ❌ NOT: UX flow decision (drawer/tab/modal) → em main solo ## 📋 Patterns proven (apply confidently) ### Pattern 16-bis 4-place mirror (9× cumulative — BLESSED FOUNDATION) Add/move page cross-app MUST mirror 4 places: 1. Page/types file · 2. `App.tsx` Route · 3. `lib/menuKeys.ts` const · 4. ⚠️ `components/Layout.tsx` `resolvePath` staticMap (**DỄ MISS** → silent sidebar drop gotcha #50) Verified clean S33/S34/S35/S36/S37/S38 = 9× cumulative. Spec MUST list 4 places explicit. ### SHA256 IDENTICAL × 2 app Viết fe-admin → `cp` fe-user → `sha256sum` verify. Khác UX (admin full sidebar vs user filter) → mirror tay + `diff`. Proven S36 (3 pair) + S37 (4 pair) + S38 (5 pair). ### Declarative KIND_CONFIG Record (S35, 3× proven) Single-page multi-kind CRUD URL `:kind` param + `Record` + renderField switch FieldType. Reuse: HrmConfigs (S35) + WorkflowApps (S38) + HrmConfigs +2 kind (S51). ADD-KIND playbook: union type + KIND_CONFIG entry + KINDS array + renderCells branch (before fallback) + import icon — `:kind`-driven page = NO new App.tsx route. ### Pattern 14 Tailwind JIT palette Dynamic class purged. PALETTE array full literal `as const` cycle `index % length`. ### TS6 / convention - `erasableSyntaxOnly` cấm enum → `const X = {...} as const` + `type X = typeof X[keyof typeof X]` - Named export only (trừ App). UI 100% tiếng Việt. - **PageHeader signature (S37):** `title/description/actions` only — KHÔNG `icon` / `children` prop (build fail TS2322) - fe-user thiếu Card/Badge shadcn → fallback inline `
` ## ✅ Verify protocol `cd fe-admin && npm run build` + `cd fe-user && npm run build` BOTH 0 TS error + `sha256sum` mirror proof. Bundle >500KB warning OK pre-existing. ## 📅 Recent activity (FIFO — one-line hooks; full detail → `archive/2026-H1.md`) - **2026-06-17** Văn phòng số "Bảng điều khiển" — mirror fe-user→admin 4 file (PageHeader/KpiCard/WidgetCard/OfficeDashboardPage) + 4-place wiring `/office/` prefix route. SHA-5file. `[office-dashboard, 4-place]` - **2026-06-16 (S65)** PE mục "e. Link hồ sơ" NAS hyperlink + rename "Dự trù PRO"→"Ngân sách PRO". SHA-mirror 4 file. #53 empty-return (render landed OK); em-main bắt sót badge rename. `[s65, hosolink]` - **2026-06-16** Department parentId cây tổ chức, **fe-admin ONLY** (no SHA). Select "Phòng cha" + load-all pageSize:200 + exclude-self khi Edit + cột "Thuộc"; BE cycle-guard 409. `[dept-parent, admin-only]` - **2026-06-11 (S57bis)** PE WorkItem FE ×2 app (select "c. Hạng mục" + PeHeaderForm/PeDetailTabs +3 field). Truncated #53 GIỮA mirror → em-main solo. LESSON: per-app stage khi slice lớn. `[s57bis, truncated-53]` - **2026-06-09 (S55)** Project +4 optional master fields (year/investor/location/package). String-form+convert-on-submit (S42 pattern). `package`=valid TS key. SHA-pair. `[s55, master-enrich]` - **2026-06-08 (S54)** ItTicket reassign → CONVERGE 2 app (reverse S53). Pattern: BE capability-flag gate (`GET /it-tickets/assignable-staff {canReassign,staff}`) thay FE đoán role. SHA `4bcaf2f`. `[s54, capability-flag]` - **2026-06-08 (S52)** ItTicket admin reassign + AttendanceReport menu, **fe-admin ONLY**. Menu = 2/4-place (menuKeys+Layout staticMap; types/menu.ts key:string no-union). `[s52, admin-only, menu-2place]` --- ## ⚠️ Anti-patterns (DO NOT) 1. ❌ Touch BE files · 2. ❌ Miss 4th Layout staticMap · 3. ❌ Skip npm build × 2 · 4. ❌ `git add -A` · 5. ❌ Push remote · 6. ❌ UX decision autonomous → REFUSE ## 🔄 Curate trigger Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): `FE-Admin` · `FE-User`.