[CLAUDE] Docs: S97 closeout — PE finalize wording + default-flip (STATUS/HANDOFF/log + memory harvest)
- Session log 2026-07-01-S97-pe-finalize-wording-default-flip.md - STATUS mega-line S97 blurb + bundle admin Cf0o8dx8/user x8rppRAK (Run #481) + In Progress - HANDOFF S97 brief - Memory harvest: implementer-frontend (+archive/2026-H1.md compaction) + reviewer + cicd-monitor (Run #481 verdict recovered from diary — #53 return-garble lần-4) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -6,6 +6,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 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.
|
||||
@ -21,26 +30,10 @@
|
||||
- **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ánh` → `d. 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). `BudgetNoteRow`→`BudgetNoteCell` (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).
|
||||
## 📦 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) → `<table border-collapse>` grid + badge `✎ NS PRO/CCM`. LESSON: flex+gap ≠ bảng, phải `<table>`. cwd-misland `fe-*/.claude/` gitignore-guard. `[s76]`
|
||||
|
||||
---
|
||||
|
||||
@ -78,15 +71,14 @@ Dynamic class purged. PALETTE array full literal `as const` cycle `index % lengt
|
||||
## ✅ 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 (last 10 FIFO)
|
||||
|
||||
- **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 /users` → `GET /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<T>) + 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.
|
||||
## 📅 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)
|
||||
|
||||
39
.claude/agent-memory/implementer-frontend/archive/2026-H1.md
Normal file
39
.claude/agent-memory/implementer-frontend/archive/2026-H1.md
Normal file
@ -0,0 +1,39 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 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ánh` → `d. 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). `BudgetNoteRow`→`BudgetNoteCell` (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 /users` → `GET /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<T>) + 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.
|
||||
Reference in New Issue
Block a user