Files
solution-erp/.claude/agent-memory/implementer-frontend/archive/2026-H1.md
2026-07-14 11:15:31 +07:00

25 KiB
Raw Blame History

Implementer-Frontend — L2 Archive 2026 H1

Verbatim detail moved out of HOT MEMORY.md during S97 compaction (2026-07-01). One-line hooks remain in MEMORY.md. Read here for full detail; search L3 RAG for older.


🆕 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 + <table> 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 + <table>. 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/purchaseEvaluationcù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.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 ></span> 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 || nullhoSoLink.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].

S79 (2026-06-19) — PE detail: live thousand-sep + ghi chú giá PRO/CCM + typo (×2 app)

3 task presentation+wiring PeDetailTabs.tsx (+PeWorkspaceCreateView.tsx typo). BE thêm note body song song; FE chốt UX rồi.

  • A live phân cách: chỉ 2 comp thiếu sep — VndInlineEdit (~990) + BudgetCell (~1100): đổi onChange value.replace(/[^\d.]/g,'')formatVndInput(parseVnd(e.target.value)) (format-on-keystroke, reuse helper module-level :51/52). Dialog money inputs (2111/2555/2645) ĐÃ dùng formatVndInput/parseVnd sẵn → no-op. KHÔNG đụng phone(tel)/%/qty.
  • B ghi chú giá: SuggestedPriceRows (~1509) — proPriceMut body {minPrice,maxPrice,note} + ccmPriceMut {ccmPrice,note} (absolute-set 3-field S74). 3 call-site giá echo note:ev.*SuggestedPriceNote. State proNoteText/ccmNoteText (useEffect sync ev.*). Textarea+nút "Lưu ghi chú" dưới PRO Min/Max & dưới CCM (gate canEdit; read-only <p> khi có note). hasAnyValue +!!note (early-return không nuốt note-only readonly).
  • Type DIVERGE 2 app (xác nhận S76): PeDetailBundle +proSuggestedPriceNote+ccmSuggestedPriceNote sửa CẢ 2 file riêng (sau ccmSuggestedPrice). PeDetailTabs/PeWorkspaceCreateView byte-identical → cp.
  • C typo: d. Bản so sánhd. Bảng so sánh giá PeDetailTabs:1661(<span>) + PeWorkspaceCreateView:278(label). Comment :1712 + PeAttachmentPurposeLabel[4] GIỮ (ngoài spec, không UI).
  • SHA256 IDENTICAL ×2 pair: PeDetailTabs 285ebcf9…, PeWorkspaceCreateView 99caed4b…. Build PASS ×2 (user 1935mod index-_axiw6cM.js / admin 1946mod index-BqPYp7Pt.js, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO ambiguity, full precedent. Tag [s79, pe-live-vnd-sep, suggested-price-note, sha256-2pair, type-diverge-2app].

S78 (2026-06-19) — PE "focus mode" redesign mirror fe-user→fe-admin (CONVERGE, em-main chốt UX)

Mechanical mirror-step: redesign built+reviewed-PASS ở fe-user, em-main đã chốt UX (focus overlay). fe-admin ONLY, NO BE/schema. 2 file:

  • PeWorkflowPanel.tsx PURE-ADDITIVE: +onApproved?:()=>void prop + wasReject const (byte-mirror isReject formula mutationFn) + if(!wasReject) onApproved?.() cuối onSuccess. Trả-lại/Từ-chối giữ overlay; chỉ forward-approve auto-đóng.
  • PurchaseEvaluationsListPage.tsx STRUCTURAL port: bỏ 3-panel grid → list-state mx-auto max-w-5xl panel (4-level tree giữ verbatim) + focus-state ?id = fixed inset-0 z-50 overlay slide-from-right (translate-x-full→0, 2× rAF ~200ms) che menu+TopBar, top-strip "← Thu gọn"+title+"Chế độ duyệt" pill, phiếu trái scroll-riêng + panel duyệt phải lg:w-[24rem] xl:w-[26rem] stack <lg. a11y: role=dialog aria-modal, Esc, focus-trap, scroll-lock, backdrop-click, prefers-reduced-motion. selectRow matchMedia-split → setParam('id',id) UNCONDITIONAL; MAIN comp drop navigate (→ useCallback setParam); bottom PurchaseEvaluationDetailPage GIỮ useNavigate (deep-link compat).
  • PRESERVE fe-admin props (rule): <PeDetailTabs readOnly={true}> + <PeWorkflowPanel readOnly={!pendingMe} onApproved={closeFocus}>. Tình cờ trùng fe-user → redesign CONVERGE 2 app.
  • SHA256 IDENTICAL ×2 pair (khớp tới trailing-newline }\n\n): page 011968bb…, panel 1dc24641…. LESSON: focus-overlay redesign khiến 2 app converge (props trùng) → mirror-step ra file byte-identical, đo bằng diff+sha256sum KHÔNG mắt. Build PASS (1946 mod, index-D6IyFKgP.js, 0 TS err — tsc-b clean trước vite). Pre-existing warning: @import-order CSS + >500KB chunk + realtime.ts dynamic-import. NO ambiguity, full precedent. Tag [s78, pe-focus-mode, mirror-step, sha256-2pair, converge-2app].

S76 (2026-06-19) — PE budget MA TRẬN 3 cột + bảng lưới <table> + badge quyền NS (anh Kiệt FDC)

  • Part 1 mirror (fe-user→fe-admin byte-identical, Python difflib slice-region): PeBudgetSummaryTable Block A → ma trận 3 cột (DỰ ÁN hiển-thị-only / PRO canEditPro / CCM canEditCcm). Type PeBudgetSummary +proInitialAmount+proAdjustmentAmount cạnh proEstimateAmount(LEGACY). proMut body {proInitialAmount,proAdjustmentAmount,proNote}. proFull=proInit+proAdj.
  • Part 3 mirror: PeWorkflowPanel approver .join(' / ') → map từng approver + badge ✎ NS PRO(amber)/✎ NS CCM(sky) gate a.canEditProBudget/canEditCcmBudget. Type PeCurrentApprovalLevelApprover +2 cờ CẢ 2 app (purchaseEvaluation.ts types DIFFER giữa 2 app → sửa cả 2; PeDetailTabs/PeWorkflowPanel byte-identical).
  • Bảng lưới (em-main, anh phản hồi "chưa chia cột giống Excel"): Block A flex→<table border-collapse> viền ô. BudgetCell xếp dọc (input full ô + Lưu dưới). BudgetNoteRowBudgetNoteCell (td colSpan=3). LESSON: flex+gap KHÔNG ra "bảng" — phải <table> viền ô mới giống spreadsheet.
  • cwd-misland: Part-1 cd fe-admin npm build → MEMORY rơi fe-user/.claude stray → em-main harvest về + .gitignore guard fe-*/.claude/ (S76).

Recent activity FIFO (pre-2026-06-17, archived verbatim)

  • 2026-06-17 (Văn phòng số "Bảng điều khiển" — mirror fe-user→fe-admin + 4-place wiring ×2 app): Mechanical mirror+wire step (frontend-designer built in fe-user, em main chốt UX). (A) index.css SYNC prereq: fe-admin STALE pre-S66 → cp fe-user/src/index.css fe-admin/ (no admin-only class to merge — utility sets identical, chỉ S66 gotcha-66 diffs: h1-h4 #0b1220/wt700 + .label-eyebrow brand-600 + accent palette comments) → SHA256 e8631471… identical. (B) Mirror 4 file cp user→admin SHA256 IDENTICAL: ui/PageHeader 6ff5303f…(Văn-phòng-số richer header — eyebrow/icon/accent/breadcrumb, NOT @/components/PageHeader constrained one), ui/KpiCard f8042ade…(clickable filter chip a11y role=button), ui/WidgetCard 9221cbed…(gotcha66 gradient header text-white!), pages/office/OfficeDashboardPage c6d9dc08…(composes 3 ui over EXISTING hooks, NO new API). All @/ imports + Button/api/cn + types/{proposal,workflowApps,meeting} đã có ở fe-admin → 0 import fix. (C) 4-place ×2 app: page(mirror) + App.tsx import+<Route path="/office/dashboard"> (NEW /office/ prefix convention — page file ở pages/office/, không xung đột; flat office routes giữ nguyên) + menuKeys OffDashboard:'Off_Dashboard' (mirror BE MenuKeys.OffDashboard, đặt sau Off trước OffDanhBa đúng thứ tự BE) + Layout staticMap Off_Dashboard:'/office/dashboard' (4th place gotcha#50, trước Off_DanhBa). Leaf admin auto via BE All[]. Build PASS ×2 (user 1934mod index-BYj_ew5Q.js, admin 1945mod index-Cn1flmn6.js, 0 TS err). CSS @import-order warning + >500KB chunk = pre-existing. NO ambiguity, full precedent (Pattern 16-bis 4-place + SHA256 mirror). Tag [office-dashboard, mirror-step, 4-place, sha256-5file, office-route-prefix].
  • 2026-06-16 (S65 PE mục E "Link hồ sơ" FE ×2 app — em-main PROXY, PE-Workflow FE-stage died-empty #53): Thêm mục "e. Link hồ sơ" hyperlink NAS dưới mục "d. Bản so sánh" + rename "Dự trù PRO"→"Ngân sách PRO". 4-file ×{user,admin} SHA256-mirror: PeDetailTabs.tsx (HoSoLinkRow :1353/1386 — useState(ev.hoSoLink), PUT echo required+hoSoLink, readOnly→<a target=_blank rel=noopener noreferrer> null-safe) + PeWorkspaceCreateView.tsx + types/purchaseEvaluation.ts (+hoSoLink). Ship Run #293 PASS, bundle both-rotate, GET phiếu thật "hoSoLink":null backward-compat ✓. SURPRISE: render landed on disk despite empty-return — work COMPLETE, chỉ MEMORY-update bị cắt (#53, lần này trong Workflow fan-out); em main self-gate bắt badge "DỰ TRÙ PRO" sót rename (agent chỉ đổi row label 1120/1126, sót badge 1078) → vá nốt ×2 app. Tag [s65, pe-section-e-link-FE, hosolink-row, em-main-proxy-truncated-53, workflow-fanout].
  • 2026-06-16 (Department parentId — cây tổ chức, fe-admin ONLY): Case 1 master-data enrich (NO 4-place, NO menu/route/Layout — chỉ Place-1 page+type). BE đã sẵn (local 0f44d97): DepartmentDto.parentId:Guid? + POST/PUT nhận parentId? + cycle-guard 409 ConflictException. fe-admin ONLY (intentional — KHÔNG fe-user, KHÔNG SHA256). 2 file: (1) types/master.ts Department +parentId:string|null (sau name, trước managerUserId) — DepartmentInput auto-inherit via Omit · (2) DepartmentsPage.tsx: import Select + FormState/emptyForm +parentId:string ('' khi rỗng) + load-all query ['departments-all'] pageSize:200 (reuse pattern proven UsersPage/Workflows/AttendanceReport) + deptNameById Map từ allDepts cho cột "Thuộc" + mutate payload parentId:d.parentId||null + openEdit parentId:d.parentId??'' + Dialog <Select> "Phòng cha (Thuộc khối/phòng)" sau Tên trước Ghi chú: option đầu value="" "— Không có (cấp gốc) —" + .filter(d=>d.id!==form.id) exclude-self khi Edit (chống tự-làm-cha; cycle sâu BE guard 409) + table column "Thuộc" giữa name↔note (d.parentId?deptNameById.get()??'—':'—'). Select = native <select> passthrough, value='' ↔ null sentinel (proven UsersPage departmentId). Build PASS (1941 mod, 0 TS err — tsc -b clean trước vite). NO ambiguity, full precedent (S55 enrich + UsersPage Select).
  • 2026-06-11 (S57bis PE WorkItem FE ×2 app — PARTIAL, on-behalf em main ghi hộ, H2-proposed): Task = PeWorkspaceCreateView select "c. Hạng mục công việc *" sau Dự án + PeHeaderForm (select + load existing + PUT/POST workItemId) + PeDetailTabs (subtitle "Dự án Hạng mục" + FormRow + inline-edit khóa) + types +3 field. Return-truncated #53 GIỮA mirror fe-admin → em main solo mirror 7 edits PeHeaderForm + 3 edits PeDetailTabs ×2 app; PeHeaderForm SHA256 IDENTICAL. LEARNED: mirror đo bằng SHA256 (không diff mắt); option label [Category] Code — Name + canSubmit require; route reuse /catalogs/work-items (KHÔNG endpoint mới). SURPRISE: điểm gãy lặp tại mirror-2-app-trong-1-spawn → cân nhắc per-app stage khi slice lớn. Tag [s57bis, truncated-53, sha256-mirror, on-behalf].
  • 2026-06-09 (S55 HMW P2 — Project +4 optional master fields): Case 1 master-data enrich (NO 4-place, NO menu/route/Layout — chỉ Place-1 page+type). BE adds 4 nullable Project fields parallel (implementer-backend). 2 file × 2 app: (1) types/master.ts Project +year:number|null+investor/location/package:string|null (sau note) +ProjectInput auto-inherit via Omit · (2) ProjectsPage.tsx (single-Dialog CRUD, NO separate pages): FormState +4 string (form dùng string, convert on submit) + emptyForm +4 '' + mutate payload year:d.year?Number():null, investor/location/package:d.x||null + openEdit year:p.year?.toString()??'', x:p.x??'' + Dialog 4 Input sau "Ngày kết thúc" trước "Ghi chú" (Năm type=number, Chủ đầu tư, Địa điểm col-span-2, Gói thầu col-span-2) + table column "Chủ đầu tư" (p.investor??'—') giữa name↔startDate. package = valid TS object KEY (reserved chỉ khi binding-identifier) → form.package/{...form,package:x} build sạch, KHÔNG cần rename. cp admin→user SHA256 IDENTICAL: master.ts 93ac1b0f…, ProjectsPage b002061…. Build PASS ×2 (admin 1945mod, user 1934mod, 0 TS err — tsc -b clean trước vite). Reuse S42 enrich-pattern (string-form + convert-on-submit). NO ambiguity, full precedent.
  • 2026-06-08 (S54 ItTicket reassign → CONVERGE 2 app, REVERSE S53 divergence) [harvested by em main — agent MEMORY write mis-landed, B2/B3]: S53 đã tách fe-admin-only (admin reassign). S54 cho tổ IT tự reassign → cả 2 app cần nút. Pattern mới: BE capability-flag gate thay vì FE đoán role. Dropdown đổi GET /usersGET /it-tickets/assignable-staff trả {canReassign:bool, staff:[{id,fullName}]} ([Authorize] any-auth, KHÔNG 403 → chống gotcha #44). canReassign = staffQ.data?.canReassign ?? false (fetch on mount, KHÔNG enabled) → nút Pencil bọc {canReassign && …}. types/workflowApps.ts +AssignableStaff+AssignableStaffResult (mirror cả 2). fe-admin rewrite (bỏ UserOption/Paged) + fe-user full-add → SHA256 IDENTICAL 4bcaf2f… (viết admin canonical → cp → verify; cùng @/... import + shadcn Dialog/Select/Button identical 2 app). Build PASS ×2 (0 TS err). Gotcha (pre-existing, NOT từ change này): types/workflowApps.ts 2 app NOT SHA-identical — fe-admin có AttendanceReportDto (P11-E) mà fe-user thiếu; 2 type S54 mirror đúng cả 2. Lesson: BE-computed capability flag = single-source → 2 app converge lại sau intentional divergence.
  • 2026-06-08 (S52 Task C+D-FE — ItTicket admin reassign + AttendanceReport menu, fe-admin ONLY): Both intentional mirror-break (admin-only, NO fe-user touch, NO SHA256). Task D-FE menu wiring: Page+App.tsx route /attendance/report ALREADY exist (S52 prior). Only 2 of 4-place needed: (1) menuKeys.ts +OffAttendanceReport='Off_AttendanceReport' (mirror BE string exact, after OffChamCong) · (2) Layout.tsx staticMap +Off_AttendanceReport:'/attendance/report' (4th place gotcha #50). types/menu.ts = MenuNode tree type, key:string NOT typed-union → NO mirror there (resolvePath(key:string)). Leaf perm-gated via BE All[]→admin auto. Task C reassign: ItTicketsPage.tsx top-comment updated DIVERGES fe-user. Per-card Pencil button (cạnh 👤 assignee) → Dialog (size sm) + Select user. Users source = GET /users {params:{page:1,pageSize:200}}→{items:UserOption{id,fullName,email}} (reuse, proven PeWorkflows/Workflows/MeetingCalendar — enabled:target!==null lazy fetch). useMutation api.put(/it-tickets/${id}/assign,{assignedToUserId})→204 (NO json read)→invalidate['it-tickets']+toast.success+close. preselect t.assignedToUserId. UI deps: Dialog(open/onClose/title/children/footer?/size) + Select(native passthrough) + Button(variant=outline) + toast(sonner) + getErrorMessage(@/lib/apiError). Build PASS (0 err, 1945 mod). git: only 3 fe-admin file, fe-user untouched.