Files
solution-erp/.claude/agent-memory/implementer-frontend/MEMORY.md
2026-07-12 18:12:03 +07:00

114 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.
---
## 🆕 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ã" `<td>{r.code}``<Input value={r.code??''} onChange updateRow(rowIndex,{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 + `<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/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``.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 || 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) → `<table border-collapse>` grid + badge `✎ NS PRO/CCM`. LESSON: flex+gap ≠ bảng, phải `<table>`. 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<Kind, {fields, columns, icon, label}>` + 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 `<div className="rounded-lg border bg-card">`
## ✅ 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`.