diff --git a/.claude/agent-memory/frontend-designer/MEMORY.md b/.claude/agent-memory/frontend-designer/MEMORY.md index 0b7e478..b97b11e 100644 --- a/.claude/agent-memory/frontend-designer/MEMORY.md +++ b/.claude/agent-memory/frontend-designer/MEMORY.md @@ -28,12 +28,8 @@ - Fallback khi stack chưa chạy: static component preview / screenshot `/login` — **KHÔNG bỏ soi** (FD2 cấm ship-unseen). ## Component inventory (built/verified — chống reinvent) -- **3 shared UI cho Văn phòng số / E-Office (S69, 2026-06-17) — PURO-style + HRM visual language. ALL build-PASS 0 TS:** - - `fe-user/src/components/ui/PageHeader.tsx` — **richer page header** (eyebrow/title/subtitle/icon/accent/actions/breadcrumb). ⚠️ KHÁC `@/components/PageHeader` (constrained {title,description,actions}) — module path `@/components/ui/PageHeader` riêng, KHÔNG collision. icon-chip accent-tinted + title `text-xl font-bold` accent-head. Local `ACCENT` map {chipBg,chipFg,head} (brand=text-brand-800, rest -700). Title trên nền SÁNG → KHÔNG cần `text-white!`. - - `fe-user/src/components/ui/KpiCard.tsx` — **clickable stat card = FILTER chip** (PURO: row KpiCards thay tabs). icon-chip + `.stat-value text-2xl` accent + `.label-eyebrow`. active = `bg-{x}-50` + `border-{x}-300` + `ring-{x}-500`. a11y FULL: `onClick`→`role=button`+`tabIndex=0`+Enter/Space (`e.key===' '`)+`aria-pressed=active`+focus-visible ring; no-onClick = inert div. hover `-translate-y-0.5` + `motion-reduce:transform-none`. - - `fe-user/src/components/ui/WidgetCard.tsx` — **dashboard widget container** (PURO HomePage). Wrap `.card-accent` (rail via inline `--accent`). Header: brand=`.app-gradient-brand text-white` · non-brand=tinted `bg-{x}-50` bar. **gotcha 66 APPLIED:** gradient `

` title = **`text-white!`** (bang) — plain text-white thua unlayered h1-h4 rule. Props: title/icon/accent/stats[]/onExpand/onRefresh/children/empty/emptyText. `stats[]` = clickable StatChip row (mỗi chip a11y button khi có onClick). empty → muted icon-chip + emptyText. Header IconButton (RefreshCw/Maximize2) contrast-adapt gradient↔tinted, aria-label. - - **3 đều:** NAMED export · `import type` (verbatimModuleSyntax) · `cn` from `@/lib/cn` · lucide-react · accent palettes stop 50/100/500/600/700 ONLY (no -800) → head/value -700 (brand -800 OK) · icon-chip recolor `['--chip-bg' as string]`/`['--chip-fg' as string]` inline (pattern từ HRM Card). NO new npm dep. Build `tsc -b && vite` PASS 0 TS, 24.87s (warning @import-order + chunk-size = pre-existing). fe-admin NOT mirrored (separate pass nếu cần). FD2 authed-screenshot SKIP (components chưa wired vào page nào — pure library; visual verify khi page tiêu thụ chúng). -- `fe-user/src/pages/office/OfficeDashboardPage.tsx` — **E-Office landing dashboard (S69, 2026-06-17) — PURO HomePage, COMPOSES the 3 shared ui widgets. build-PASS 0 TS, 434ms.** Layout: `ui/PageHeader` (eyebrow "Văn phòng số" / title "Bảng điều khiển" / icon LayoutDashboard / accent brand) on top → `grid grid-cols-1 lg:grid-cols-3 gap-5`: LEFT `lg:col-span-2` = stack of 4 `WidgetCard` (Đề xuất brand / Đơn từ teal / Ticket CNTT violet / Phòng họp hôm nay amberx — each body = row of 3 `KpiCard` filter-chips except Phòng họp = 1 KpiCard + next-4 booking peek list) · RIGHT `lg:col-span-1` = "Công việc của tôi" WidgetCard (brand-50 hero count `myTodo` + 3 clickable `MetricRow`) + "Thao tác nhanh" `.card-accent` panel (3 Button primary/secondary/outline). Stacks 1-col ` chứa Org-tree (TRÊN, `lg:max-h-[44%] lg:shrink-0`, cuộn riêng) + List+filter (DƯỚI, `flex-1`, cuộn riêng). **CỘT PHẢI** = Detail 5-tab (flex-1, rộng). ` controlled `useState(n===currentStage)`; prop `content` cho GĐ1 giữ leaf phiếu cũ; `locked`/`failed`/`loading`/`hideEmpty`) · `hooks/usePipelineStages.ts` (2 query index key MỚI `pipeline-khkk-index`/`pipeline-contract-index` pageSize 200 — `PagedResult.cs:20` KẸP ≤200 không 400; `buildStages(peItems)` nối GĐ2 qua `khkk.purchaseEvaluationId`, GĐ3 qua `pe.contractId`, GĐ4 = HĐ đó có `hasSealedCopy`) · `components/pipeline/PipelineTreePanel.tsx` (panel cây trái cho trang GĐ2/GĐ4; `buildTree` RIÊNG, KHÔNG đụng memo `yearGroups` trang PE). **Wire:** PE list (`currentStage=1`, khối `
  • ` phiếu giữ NGUYÊN TỪNG BYTE kể cả thụt lề ⇒ diff chỉ 2 mẩu bọc = bằng chứng không đụng behavior) · KhkkListPage (=2) · HardCopiesPage (=4), 2 trang sau bọc lưới `lg:grid-cols-[19rem_minmax(0,1fr)]` + `min-w-0` + `` bọc `overflow-x-auto` + KPI `lg:grid-cols-6`→`xl:`. **Permission 2 tầng (S118/#82):** gate TRƯỚC fetch, `enabled:false` ⇒ 0 rác 403; ⚠️ phải OR NHIỀU KEY vì `GetMyMenuTreeQuery.cs:96` trả root canRead=false khi con có quyền, `Ct_*` kế thừa `Contracts` nhưng `Khkk_*`/`Hdc_*` KHÔNG. Tag [s162, pipeline-stage-folders, 4-folder-gd, permission-or-keys, index-query-key-moi, leaf-byte-verbatim, sha-pair-6-file]. - **S159 (2026-07-29) ARC-2 "Tổng quan quy trình" ×2 lượt — `UserDashboardPage.tsx` fe-user (đợt-1 hero pipeline + đợt-4 redesign list toàn-trình).** *(Entry on-behalf bởi lead @S160 closeout M9 — VERIFY từ 2 sub-file + commit; cả 2 lượt vai ghi-đĩa-trong-lúc-làm nên #53 ×2 first-return mất 0 B.)* **Đợt-1** (`b5799fc`, `sub-frontend-designer-tongquan.md` 15.803 B): file 309 dòng → THÊM hero pipeline 4 GĐ trên cùng, GIỮ trọn 3 query (`my-dashboard`/`my-contracts-recent`/`my-pe-recent`) + 5 StatCard + 2 list (owner: *"đừng vứt"*); đổi PageHeader title "Tổng quan tất cả luồng duyệt". **Đợt-4** (`a2bbcb9`, `sub-frontend-designer-dot4.md` 13.956 B): file lúc đó 766 dòng — đọc đĩa KHÔNG dùng bản nhớ (đã qua 3 đợt sửa nhãn giữa 2 lượt); bỏ khối owner khoanh (5 StatCard + 2 list "gần đây") → **list chi tiết toàn-trình**; GIỮ hero (owner không khoanh — *đợt-5 vai khác mới bỏ hero theo lệnh anh*); nhãn 4 GĐ echo bản MỚI NHẤT từ comment đầu file (GĐ2 "Kế hoạch ký kết HĐ", KHÔNG lấy tên đợt-1 "Đề xuất ký kết hợp đồng" đã superseded); +2 query `['pipeline-pe-approved']` (phase=7 pageSize=1 lấy `.total`) + `['pe-inbox']` **dùng `select: d => d.length` để KHÔNG vỡ cache dùng chung InboxPage**; verify param BE bằng đọc controller thật. 🔴 **Môi trường spawn KHÔNG có dev-server/Playwright ⇒ FD2 visual-loop bất-khả — thay bằng `npm run build` PASS + tự-review tĩnh FD4, KHAI THẲNG không bịa screenshot** (khuôn NO-SCREENSHOT này tái dùng cho mọi spawn thiếu rig). Bài giữ lại: (i) giữa 2 lượt cùng file, dòng-đếm/nhãn đổi ⇒ **khảo sát lại từ đĩa mỗi lượt**; (ii) đổi consumer của endpoint dùng-chung → `select` transform thay đổi shape cache. Tag [s159, tong-quan-quy-trinh, hero-pipeline-4gd, dot4-list-toan-trinh, no-screenshot-khai-that, select-transform-cache-safe, doc-dia-moi-luot, on-behalf-m9]. -- **S79 (2026-06-19) PE list fe-user → DECOUPLE chọn/mở-rộng (anh chốt, annotated screenshot):** `pages/pe/PurchaseEvaluationsListPage.tsx`. S78 cũ: bấm dòng = mở overlay full-bleed, 2 panel giữa/phải LÀ placeholder vĩnh viễn. S79 đổi: **bấm dòng = inline 3-panel detail "như cũ" (8e68ed1)** + thêm nút **"Xem mở rộng"** mỗi dòng → overlay. Mechanism = param thứ-2 **`?expand=1`** cạnh `?id`: overlay render CHỈ khi `id && expand`; inline detail (panel giữa `` + panel phải ``) render khi `id && !expand`. Handlers: `selectRow`=set id+clear expand (NHƯNG `` cũ bọc trong `
    ` + `pr-9` chừa chỗ nút. **ALL logic VERBATIM** (grep ✓): 3 queryKey (`approval-workflows-v2-filter`/`pe-list`/`pe-detail`) · 3 endpoint (`/inbox`+paged+detail) · `del` mutation (onSuccess giờ clear id+expand thay `setParam`) · `yearGroups` 4-tầng tree memo · `PeUrgentChips` pill · search/filter/SLA/localStorage-expand. `PurchaseEvaluationDetailPage` route GIỮ. **slide+a11y S78 giữ NGUYÊN** (mount-rAF×2, role=dialog/aria-modal, focus-trap Tab-vòng, body scroll-lock, motion-reduce). `npm run build` (tsc -b strict + vite v8) **PASS 0 TS err, 570ms** (3 warning pre-existing). **FD2: static layout-shell harness** (backend :5443 down → authed dev-rig blocked gotcha#3) — Playwright 6 shot × 3 state: (a) inline 3-panel desktop1920+laptop1280 = list+PeDetailTabs giữa+PeWorkflowPanel phải, dòng-chọn có nút Maximize2; (b) overlay 1920+1280 = full-bleed phiếu+panel cạnh; (c) mobile390 list (1-col, nút mở-rộng) + mobile390 overlay (stack). Read tất 6 PNG — đúng hết, no overflow, pill+tree intact. Harness/driver/shots ĐÃ XÓA. Rubric PASS. fe-admin NOT mirrored (implementer-frontend SHA-mirror riêng), no BE, no commit. Tag [s79, pe-decouple-select-expand, expand-param, maximize2-row-btn, inline-restore-8e68ed1, lg-straight-overlay, collapse-keeps-id, logic-verbatim, build-pass, harness-fd2]. +- _(**S79 (2026-06-19) PE list fe-user — DECOUPLE chọn/mở-rộng: `?expand=1` cạnh `?id`, nút Maximize2 mỗi dòng, ALL logic verbatim, harness-FD2 6 shot.** verbatim → `archive/2026-06.md` §"S79 (2026-06-19)")_ - _(**S69 (2026-06-17) OfficeDashboardPage.tsx fe-user — E-Office landing PURO, COMPOSES 3 shared widgets, reuse hooks verbatim 0 new API, build PASS 0 TS.** verbatim → `archive/2026-06.md` substring:"S69 (2026-06-17) OfficeDashboardPage.tsx")_ - _(**S69 (2026-06-17) Văn phòng số / Đơn từ RE-SKIN 2 file (PURO+HRM visual, client-side filter view, logic verbatim, lucide alias-dodge Send).** verbatim → `archive/2026-06.md` substring:"S69 (2026-06-17) Văn phòng số / Đơn từ")_ diff --git a/.claude/agent-memory/frontend-designer/archive/2026-06.md b/.claude/agent-memory/frontend-designer/archive/2026-06.md index 2a86cd7..a7f613b 100644 --- a/.claude/agent-memory/frontend-designer/archive/2026-06.md +++ b/.claude/agent-memory/frontend-designer/archive/2026-06.md @@ -15,3 +15,16 @@ - **S69 (2026-06-17) OfficeDashboardPage.tsx fe-user — E-Office landing, PURO HomePage, COMPOSES 3 shared widgets:** built `pages/office/OfficeDashboardPage.tsx` (~400 LOC) over EXISTING data hooks of 4 modules. Read-first: 3 ui widgets (exact prop sigs) + 4 source pages (ProposalsListPage/WorkflowAppsListPage/ItTicketsPage/MeetingCalendarPage) to harvest queryKey+endpoint + types + App.tsx routes. **Reused hooks verbatim** → shared TanStack cache, ZERO new API/BE: `GET /proposals` (+inboxOnly query for needs-my-action) · `/leave|ot|travel-requests` (merged, countByStatus client-side) · `/it-tickets` · `/meeting-bookings` (today window). Layout = PageHeader(brand) + `lg:grid-cols-3` [LEFT col-span-2 = 4 WidgetCards w/ KpiCard filter-chip bodies | RIGHT col-span-1 = "Công việc của tôi" hero+MetricRows + "Thao tác nhanh" 3 buttons], 1-col ` + 2 `useMemo` (counts + visibleItems) DERIVED over already-fetched `items`; **KHÔNG touch query/endpoint/queryKey/navigation** (page chỉ fetch `page:1` như cũ, không có filter-state sẵn nên thêm view-layer = thuần presentation; empty-state phân biệt "chưa có data" vs "không có đơn ở trạng thái này"). **DETAIL:** ui/PageHeader teal + 4 section dùng local `Card`(accent-rail pseudo `before:bg-{x}-500` + icon-chip) + `Field`(label uppercase `text-{x}-700`, value `text-brand-800`) — copy idiom HRM EmployeesListPage (KHÔNG import HRM, helper local riêng). Accent gán: Thông tin=teal · Số dư phép=greenx · Quy trình=violet · Ý kiến=brand. Status badge giữ `WORKFLOW_APP_STATUS_BADGE`. Drop raw "⚠️" emoji trong over-budget banner→text thuần (anti-slop FD3). **ALL data logic VERBATIM** (grep-verified): 2 query `[endpoint,id]`+`['approval-workflows-v2',applicableType]` (key/endpoint/`enabled` y nguyên) · 3 mutation pinWorkflow(PUT /workflow)/submit(POST /submit)/action(POST /{k}) body+onSuccess+invalidate identical · 3 state + flags isDraft/isInWorkflow/hasWorkflow + mọi onClick/nav target bất biến. **gotcha Tailwind-v4 stop:** dùng CHỈ -50/-500/-700 cho teal/violet/amberx/greenx (no -800) — `border-l-greenx-500`/`bg-amberx-50`/`text-amberx-700` đều stop tồn tại (index.css verified). **Self-caught:** `SendHorizonal` (KpiCard "Đã gửi duyệt" icon) export-aggregation-line trong lucide d.ts → đổi `Send` (proven-safe export) tránh alias-risk. Self-review: mọi import resolve, 0 unused local (noUnusedLocals strict) — `Info`/`Wallet`/`GitBranch`/`MessageSquareText` đều dùng. **KHÔNG run npm build** (em main builds central, 7-agent parallel interference) + KHÔNG modify ui/index.css (other agents edit). FD2 authed-screenshot SKIP (ProtectedRoute + rig gotcha #3 — verify via deploy). fe-admin NOT mirrored. Tag [s69, eoffice-donutu-reskin, puro-hrm-visual, consume-ui-pageheader-kpicard, client-side-filter-view, logic-verbatim, no-build-parallel-fanout, lucide-alias-dodge]. + +## S79 (2026-06-19) PE list fe-user — DECOUPLE chọn/mở-rộng +- **S79 (2026-06-19) PE list fe-user → DECOUPLE chọn/mở-rộng (anh chốt, annotated screenshot):** `pages/pe/PurchaseEvaluationsListPage.tsx`. S78 cũ: bấm dòng = mở overlay full-bleed, 2 panel giữa/phải LÀ placeholder vĩnh viễn. S79 đổi: **bấm dòng = inline 3-panel detail "như cũ" (8e68ed1)** + thêm nút **"Xem mở rộng"** mỗi dòng → overlay. Mechanism = param thứ-2 **`?expand=1`** cạnh `?id`: overlay render CHỈ khi `id && expand`; inline detail (panel giữa `` + panel phải ``) render khi `id && !expand`. Handlers: `selectRow`=set id+clear expand (NHƯNG `` cũ bọc trong `
    ` + `pr-9` chừa chỗ nút. **ALL logic VERBATIM** (grep ✓): 3 queryKey (`approval-workflows-v2-filter`/`pe-list`/`pe-detail`) · 3 endpoint (`/inbox`+paged+detail) · `del` mutation (onSuccess giờ clear id+expand thay `setParam`) · `yearGroups` 4-tầng tree memo · `PeUrgentChips` pill · search/filter/SLA/localStorage-expand. `PurchaseEvaluationDetailPage` route GIỮ. **slide+a11y S78 giữ NGUYÊN** (mount-rAF×2, role=dialog/aria-modal, focus-trap Tab-vòng, body scroll-lock, motion-reduce). `npm run build` (tsc -b strict + vite v8) **PASS 0 TS err, 570ms** (3 warning pre-existing). **FD2: static layout-shell harness** (backend :5443 down → authed dev-rig blocked gotcha#3) — Playwright 6 shot × 3 state: (a) inline 3-panel desktop1920+laptop1280 = list+PeDetailTabs giữa+PeWorkflowPanel phải, dòng-chọn có nút Maximize2; (b) overlay 1920+1280 = full-bleed phiếu+panel cạnh; (c) mobile390 list (1-col, nút mở-rộng) + mobile390 overlay (stack). Read tất 6 PNG — đúng hết, no overflow, pill+tree intact. Harness/driver/shots ĐÃ XÓA. Rubric PASS. fe-admin NOT mirrored (implementer-frontend SHA-mirror riêng), no BE, no commit. Tag [s79, pe-decouple-select-expand, expand-param, maximize2-row-btn, inline-restore-8e68ed1, lg-straight-overlay, collapse-keeps-id, logic-verbatim, build-pass, harness-fd2]. + +## S69 (2026-06-17) 3 shared UI cho Văn phòng số / E-Office +- **3 shared UI cho Văn phòng số / E-Office (S69, 2026-06-17) — PURO-style + HRM visual language. ALL build-PASS 0 TS:** + - `fe-user/src/components/ui/PageHeader.tsx` — **richer page header** (eyebrow/title/subtitle/icon/accent/actions/breadcrumb). ⚠️ KHÁC `@/components/PageHeader` (constrained {title,description,actions}) — module path `@/components/ui/PageHeader` riêng, KHÔNG collision. icon-chip accent-tinted + title `text-xl font-bold` accent-head. Local `ACCENT` map {chipBg,chipFg,head} (brand=text-brand-800, rest -700). Title trên nền SÁNG → KHÔNG cần `text-white!`. + - `fe-user/src/components/ui/KpiCard.tsx` — **clickable stat card = FILTER chip** (PURO: row KpiCards thay tabs). icon-chip + `.stat-value text-2xl` accent + `.label-eyebrow`. active = `bg-{x}-50` + `border-{x}-300` + `ring-{x}-500`. a11y FULL: `onClick`→`role=button`+`tabIndex=0`+Enter/Space (`e.key===' '`)+`aria-pressed=active`+focus-visible ring; no-onClick = inert div. hover `-translate-y-0.5` + `motion-reduce:transform-none`. + - `fe-user/src/components/ui/WidgetCard.tsx` — **dashboard widget container** (PURO HomePage). Wrap `.card-accent` (rail via inline `--accent`). Header: brand=`.app-gradient-brand text-white` · non-brand=tinted `bg-{x}-50` bar. **gotcha 66 APPLIED:** gradient `

    ` title = **`text-white!`** (bang) — plain text-white thua unlayered h1-h4 rule. Props: title/icon/accent/stats[]/onExpand/onRefresh/children/empty/emptyText. `stats[]` = clickable StatChip row (mỗi chip a11y button khi có onClick). empty → muted icon-chip + emptyText. Header IconButton (RefreshCw/Maximize2) contrast-adapt gradient↔tinted, aria-label. + - **3 đều:** NAMED export · `import type` (verbatimModuleSyntax) · `cn` from `@/lib/cn` · lucide-react · accent palettes stop 50/100/500/600/700 ONLY (no -800) → head/value -700 (brand -800 OK) · icon-chip recolor `['--chip-bg' as string]`/`['--chip-fg' as string]` inline (pattern từ HRM Card). NO new npm dep. Build `tsc -b && vite` PASS 0 TS, 24.87s (warning @import-order + chunk-size = pre-existing). fe-admin NOT mirrored (separate pass nếu cần). FD2 authed-screenshot SKIP (components chưa wired vào page nào — pure library; visual verify khi page tiêu thụ chúng). + +## S69 (2026-06-17) OfficeDashboardPage.tsx — E-Office landing +- `fe-user/src/pages/office/OfficeDashboardPage.tsx` — **E-Office landing dashboard (S69, 2026-06-17) — PURO HomePage, COMPOSES the 3 shared ui widgets. build-PASS 0 TS, 434ms.** Layout: `ui/PageHeader` (eyebrow "Văn phòng số" / title "Bảng điều khiển" / icon LayoutDashboard / accent brand) on top → `grid grid-cols-1 lg:grid-cols-3 gap-5`: LEFT `lg:col-span-2` = stack of 4 `WidgetCard` (Đề xuất brand / Đơn từ teal / Ticket CNTT violet / Phòng họp hôm nay amberx — each body = row of 3 `KpiCard` filter-chips except Phòng họp = 1 KpiCard + next-4 booking peek list) · RIGHT `lg:col-span-1` = "Công việc của tôi" WidgetCard (brand-50 hero count `myTodo` + 3 clickable `MetricRow`) + "Thao tác nhanh" `.card-accent` panel (3 Button primary/secondary/outline). Stacks 1-col One line per archived entry. Pointer = substring:"..." (Ctrl-F in `2026-06.md`; chuẩn-hoá @S152 A4 từ dạng `→ find` để lọt mắt A7 :284). NO line-hints (additive appends shift lines). Built @S96 (2026-07-01). Verbatim → `2026-06.md` · 4-field distill → `2026-06.gist.md`. -## 2026-06 (5 entries · S66→S47) +## 2026-06 (8 entries · S79→S47) +- **S79 pe-decouple-select-expand** (2026-06-19) — bấm dòng = inline 3-panel "như cũ", nút Maximize2 = overlay; param thứ-2 `?expand=1`; ALL logic verbatim; harness-FD2 6 shot. Archive @S162 (L1 quá ngân sách). · substring:"S79 (2026-06-19)" +- **S69 e-office-3-shared-ui** (2026-06-17) — `ui/PageHeader` giàu + `ui/KpiCard` filter-chip + `ui/WidgetCard`; accent map + a11y Enter/Space + gotcha-66 `text-white!`. Archive @S162. · substring:"S69 (2026-06-17) 3 shared UI" +- **S69 office-dashboard** (2026-06-17) — OfficeDashboardPage compose 3 widget, reuse hook verbatim 0 API mới. Archive @S162. · substring:"S69 (2026-06-17) OfficeDashboardPage" - **S66 hrm-2col-refine** (2026-06-16) — HRM Hồ sơ 3-cột→2-cột + accent-detail 5-tone; Tailwind-v4 -800-stop self-caught ×2 + rail `before:content-['']`; atomic-Write anti-#53; 5 CRUD+15 api verbatim. · substring:"S66 (2026-06-16) HRM Hồ sơ" - **S65 hrm-3panel** (2026-06-16) — EmployeesListPage → 3-panel master-detail NamGroup-ref; ONE atomic Write anti-#53; mojibake `网络Placeholder` lucide self-caught; org-tree `/departments/tree`. · substring:"S65 (2026-06-16) HRM Hồ sơ" - **S58 fe-user-redesign** (2026-06-11) — mirror fe-admin DS → 14 file fe-user KEEP brand; truncated-#53 2nd-consecutive (em-main proxy, ship `e959f72`); LESSON emit-file-list-before-FD2-loop. · substring:"S58 (2026-06-11) fe-user redesign" diff --git a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-s162.html.txt b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-s162.html.txt new file mode 100644 index 0000000..8f2d936 --- /dev/null +++ b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-s162.html.txt @@ -0,0 +1,12 @@ + + + + + + S162 harness — cây 4 folder GĐ + + +
    + + + diff --git a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-s162.tsx.txt b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-s162.tsx.txt new file mode 100644 index 0000000..691350f --- /dev/null +++ b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-s162.tsx.txt @@ -0,0 +1,180 @@ +// THROWAWAY — harness FD2 cho S162 (cây 4 folder GĐ). XOÁ SAU KHI CHỤP. +// Render CHÍNH các component thật (không vẽ lại markup) với cache TanStack seed +// sẵn + menu quyền seed vào localStorage ⇒ soi được UI mà không cần API/SQL. +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { MemoryRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import './index.css' +import { AuthProvider } from '@/contexts/AuthContext' +import { PipelineStageFolders } from '@/components/pipeline/PipelineStageFolders' +import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel' + +const menu = [ + { key: 'PurchaseEvaluations', label: 'Duyệt NCC', parentKey: null, order: 1, icon: null, canRead: true, canCreate: true, canUpdate: true, canDelete: true, isVisible: true, displayLabel: null, children: [] }, + { key: 'KeHoachKyKet', label: 'Kế hoạch ký kết HĐ', parentKey: null, order: 2, icon: null, canRead: true, canCreate: true, canUpdate: true, canDelete: true, isVisible: true, displayLabel: null, children: [] }, + { key: 'Contracts', label: 'Duyệt hợp đồng', parentKey: null, order: 3, icon: null, canRead: true, canCreate: true, canUpdate: true, canDelete: true, isVisible: true, displayLabel: null, children: [] }, + { key: 'HopDongCung', label: 'Hợp đồng cứng', parentKey: null, order: 4, icon: null, canRead: true, canCreate: true, canUpdate: true, canDelete: true, isVisible: true, displayLabel: null, children: [] }, +] +localStorage.setItem('solution-erp-user-token', 'harness') +localStorage.setItem('solution-erp-user-user', JSON.stringify({ id: 'u1', fullName: 'Harness', email: 'h@x.vn', roles: [] })) +localStorage.setItem('solution-erp-user-menu', JSON.stringify(menu)) + +const pe = (i: number, over: Record) => ({ + id: `pe-${i}`, maPhieu: `PDG-2026-${String(i).padStart(3, '0')}`, + tenGoiThau: 'Cung cấp và lắp đặt hệ thống điện nhẹ khối A', type: 1, phase: 7, + projectId: 'prj-1', projectName: 'Khu phức hợp Sunrise Riverside', projectCode: 'SRS', + workItemId: 'wi-1', workItemName: '02 Hệ thống điện nhẹ', + selectedSupplierId: null, selectedSupplierName: null, contractId: null, slaDeadline: null, + createdAt: '2026-05-14T03:00:00Z', updatedAt: null, + drafterUserId: 'u1', drafterName: 'Nguyễn Văn Kiệt', departmentId: 'd1', departmentName: 'Phòng Mua hàng', + budgetPeriodAmount: null, expectedRemainingAmount: null, isUrgentByPro: false, isUrgentByCcm: false, + winnerSupplierNames: [], endsBeforeCeo: false, finalizeStepName: null, finalizeLevelName: null, + ...over, +}) + +const peRows = [ + pe(1, { contractId: 'ct-1' }), + pe(2, { phase: 3, tenGoiThau: 'Thi công phần thô tháp B', maPhieu: 'PDG-2026-002' }), + pe(3, { workItemId: 'wi-2', workItemName: '10 Cửa nhôm kính', tenGoiThau: 'Cửa nhôm kính mặt dựng', maPhieu: 'PDG-2026-003', createdAt: '2026-03-02T03:00:00Z' }), + pe(4, { projectId: 'prj-2', projectName: 'Nhà máy Đại An giai đoạn 2', projectCode: 'DAN', workItemId: null, workItemName: null, tenGoiThau: 'San lấp mặt bằng', maPhieu: 'PDG-2026-004', createdAt: '2025-11-20T03:00:00Z' }), +] +const khkk = [{ + id: 'kh-1', maKeHoach: 'KHKK-2026-001', phase: 3, purchaseEvaluationId: 'pe-1', + peMaPhieu: 'PDG-2026-001', peTenGoiThau: 'Cung cấp và lắp đặt hệ thống điện nhẹ khối A', + projectId: 'prj-1', projectName: 'Khu phức hợp Sunrise Riverside', departmentId: null, departmentName: null, + drafterUserId: 'u1', drafterFullName: 'Nguyễn Văn Kiệt', approvalWorkflowId: null, workflowCode: null, + currentApprovalLevelOrder: null, totalProposedAmount: 4200000000, createdAt: '2026-06-01T03:00:00Z', updatedAt: null, +}] +const contracts = [{ + id: 'ct-1', maHopDong: 'HĐ-TP-2026-014', tenHopDong: 'HĐ thi công hệ thống điện nhẹ khối A', + type: 1, phase: 9, supplierId: 's1', supplierName: 'Công ty TNHH Điện nhẹ Toàn Cầu', + projectId: 'prj-1', projectName: 'Khu phức hợp Sunrise Riverside', giaTri: 4150000000, + slaDeadline: null, createdAt: '2026-06-20T03:00:00Z', hasSealedCopy: true, +}] + +const qc = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: 600_000 } } }) +qc.setQueryData(['pipeline-pe-index'], peRows) +qc.setQueryData(['pipeline-khkk-index'], khkk) +qc.setQueryData(['pipeline-contract-index'], contracts) + +// Vỏ cây trang Duyệt NCC (sao markup từ PurchaseEvaluationsListPage để soi +// folder trong đúng ngữ cảnh). Leaf phiếu = rút gọn của dòng phiếu thật. +function PeTreeShell() { + return ( + + ) +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
    +

    {title}

    + {children} +
    + ) +} + +createRoot(document.getElementById('root')!).render( + + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + {} }] }, + { n: 2, leaves: [], locked: true }, + { n: 3, leaves: [], failed: true }, + { n: 4, leaves: [] }, + ]} + /> +
    +
    +
    +
    +
    +
    +
    , +) diff --git a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-shot-r3-desktop-1440.png b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-shot-r3-desktop-1440.png new file mode 100644 index 0000000..3fa31ad Binary files /dev/null and b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-shot-r3-desktop-1440.png differ diff --git a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-shot-r3-mobile-375.png b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-shot-r3-mobile-375.png new file mode 100644 index 0000000..9e96df7 Binary files /dev/null and b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/harness-shot-r3-mobile-375.png differ diff --git a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-frontend-designer-0.md b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-frontend-designer-0.md index 2a743a3..1930829 100644 --- a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-frontend-designer-0.md +++ b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-frontend-designer-0.md @@ -76,3 +76,53 @@ T1f build gate: - fe-user `npm run build` → **EXIT 0**, `✓ built in 21.91s`, `dist/assets/index-CaEssrop.js 1,629.67 kB │ gzip 411.70 kB`, css 91.82 kB. - fe-admin `npm run build` → **EXIT 0**, `✓ built in 10.15s`, `dist/assets/index-BtjKK7qJ.js 1,713.48 kB │ gzip 425.60 kB`, css 86.14 kB. - Warning cả 2 app = PRE-EXISTING (chunk >500 kB · INEFFECTIVE_DYNAMIC_IMPORT `src/lib/realtime.ts`). 0 lỗi TS. + +## MỐC 3 — FD2 visual-verify (3 vòng soi, KHÔNG ship-unseen) + +Rig: authed dev-rig bị chặn (gotcha #3 — API HTTPS :5443 ⟂ vite proxy HTTP + ProtectedRoute) ⇒ dùng **static harness** như S79: mount CHÍNH component thật (không vẽ lại markup) với `queryClient.setQueryData` seed 3 cache index + menu quyền seed `localStorage:solution-erp-user-menu` (AuthProvider giữ menu cache khi `/menus/me` lỗi — `AuthContext.tsx:29-31`) + `MemoryRouter`. 4 khung: A cây trang PE · B panel GĐ2 · C panel GĐ4 · D trạng thái biên (khoá/lỗi/rỗng). Chụp `desktop-1440` + `laptop-1366` + `mobile-375`, `device_scale_factor=2`, full_page. Gotcha Vite đã né: `domcontentloaded` + `wait_for_selector` (KHÔNG networkidle) + warm-up goto 60s. + +**Vòng 1 (r1) — 3 lỗi tự bắt được khi NHÌN ảnh:** +1. 🔴 Panel B/C **trông y hệt nhau**, folder giai đoạn nằm sâu 3 lớp collapsed ⇒ vào trang KHKK/HĐ-cứng KHÔNG thấy folder của mình đâu (hỏng đúng ý owner "folder trang hiện tại mở sẵn"). Kèm bug tương tác: mẹo "set rỗng = mặc định mở năm mới nhất" làm node **KHÔNG ĐÓNG ĐƯỢC** (đóng → set rỗng → mặc định bật lại). +2. 🔴 Dòng folder **khoá quyền lệch 26px** so với 3 dòng kia (thiếu khoang icon-chip) — soi rõ ở khung D mobile. +3. 🟡 Leaf: mã `font-mono 11px` bẹt ngang dòng phụ sans 11px ⇒ mất phân cấp. + +**Sửa:** (1) `defaultOpen` = mở HẲN 1 NHÁNH (năm mới nhất > dự án đầu > hạng mục đầu) + state `Set|null` (`null` = chưa từng chỉnh) thay mẹo set-rỗng + đang-lọc thì ép mở hết & `toggleExpand` no-op. (2) dòng khoá giữ đúng 3 khoang (ổ khoá thay chevron + icon-chip + nhãn). (3) mã leaf `text-[12px] font-medium`. + +**Vòng 2 (r2):** cả 3 PASS — B mở tới folder "Kế hoạch Hợp đồng" hiện leaf `KHKK-2026-001`, C mở "Hợp đồng cứng" hiện `HĐ-TP-2026-014 · Đã lưu bản cứng` ⇒ B/C giờ KHÁC nhau rõ, chứng minh `currentStage` ăn thật. Còn 1 điểm tự bắt: nhãn muted `text-slate-400` (#94a3b8) = **2.8:1 < sàn AA 4.5:1** mà folder rỗng VẪN bấm mở được (chữ đang-hoạt-động) → đổi `text-slate-500` (4.8:1); chip "Sắp"/icon giữ slate-400 (trạng thái trơ + token verbatim dải strip). +**Vòng 3 (r3):** chụp lại sau sửa contrast — PASS, không lệch layout. + +**Rubric FD4:** hierarchy ✓ (5 cấp: Năm 13px semibold → Dự án/Hạng mục 12px slate-700 → folder 12px accent → leaf mã 12px mono + phụ 11px slate-500) · spacing ✓ (py-1.5 dòng cây / py-2 leaf, rail `ml-3 border-l` mỗi cấp) · typography ✓ · contrast ✓ (brand-800 13.0:1 · violet-700 7.4:1 · amberx-700 4.9:1 · teal-700 5.6:1 · muted slate-500 4.8:1) · states ✓ (hover/focus-visible ring brand + loading skeleton + empty + error + locked) · responsive ✓ (1440/1366/375; `500 kB · INEFFECTIVE_DYNAMIC_IMPORT realtime.ts). 0 lỗi TS. +- `git status --porcelain`: đúng 12 file sản phẩm (6 × 2 app) + 4 artifact thư mục run. KHÔNG commit (em main commit). + +## 2 lần lead báo "mày chết" — ĐÍNH CHÍNH bằng đĩa +Cả 2 lần đều là **stale IDE diagnostic** (bài `feedback_stale_diagnostic_background_agent`, gotcha #68), KHÔNG phải #53: +- Lần 1 (báo `:625 ')' expected` + 4 symbol unused): đo lại `npx tsc -b --force` ngay lúc đó = **EXIT 0**, vùng `:620-627` trên đĩa khâu đúng. +- Lần 2 (báo KhkkListPage `:114/:138/:141` + HardCopiesPage `:97/:116/:250-264` rách JSX): đọc thẳng 3 vùng đó trên đĩa = nguyên vẹn, `tsc -b --force` = **EXIT 0**. +⇒ Diagnostic chụp GIỮA lúc agent đang Edit nhiều file. Chỉ tin build chạy SAU khi agent dừng tay. + +## MỐC 5 — MEMORY + đóng việc +- `.claude/agent-memory/frontend-designer/MEMORY.md`: +1 entry S162 (Activity log). L1 vượt ngân sách (19.6KB > sàn 17.1KB) ⇒ nén theo đúng khuôn archive sẵn có: chuyển VERBATIM 3 mục 06/2026 (S79 pe-decouple · S69 3-shared-UI · S69 OfficeDashboardPage) sang `archive/2026-06.md` + để lại dòng trỏ `substring:"…"`; cập nhật `archive/_INDEX.md` (5→8 entry). Sau nén **14.6KB**, integrity check 7/7 khoá value_protect còn nguyên (S162 · S159 · EmployeesListPage · #53 · Tailwind-v4-accent-stop · authed-rig-#3 · lucide-alias-dodge). +- KHÔNG commit (em main commit, scope `FE-User`/`FE-Admin`). KHÔNG đụng BE/menu/seeder/strip/tests. + +### TỰ ĐÁNH GIÁ CÒN HỞ (lead cần biết) +1. **Chưa soi được trên PROD/authed thật** — mọi ảnh là harness tĩnh với dữ liệu giả. Nút cần nghiệm thu khi deploy: cây có ra đúng gói thầu thật không, và `pageSize 200` có đủ cho lượng HĐ/KHKK thật không (>200 thì leaf GĐ3 rơi về dạng tối giản, GĐ4 mất cờ bản cứng). +2. **GĐ3/GĐ4 nối qua `pe.contractId`** — phiếu PE chưa gắn HĐ thì 2 folder đó rỗng dù HĐ có tồn tại (đi đường KHKK→HĐ mà không ghi ngược `contractId` về phiếu là cây đứt). Đây là giới hạn DỮ LIỆU, không phải UI. +3. Trang GĐ3 (cụm HĐ 7 loại) CHƯA gắn panel — đúng spec đợt-1 (để đợt-2 sau khi owner xem prod). diff --git a/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-reviewer-1.md b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-reviewer-1.md new file mode 100644 index 0000000..a1f7f18 --- /dev/null +++ b/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-reviewer-1.md @@ -0,0 +1,38 @@ +# sub-reviewer-1 — T2 review đối-kháng cây 4-folder GĐ (S162, 2026-07-30) + +Ghi-đĩa-trong-lúc-làm (chống #53). Append theo từng mục soi. Verdict cuối ở cuối file. + +## Phạm vi đo (từ đĩa, `git status --porcelain` + `git diff --stat`) +- 6 file tracked M: `fe-{user,admin}/src/pages/pe/PurchaseEvaluationsListPage.tsx` · `.../khkk/KhkkListPage.tsx` · `.../contracts/HardCopiesPage.tsx` — tổng +168/−16. +- 6 file MỚI untracked: `fe-{user,admin}/src/components/pipeline/{PipelineStageFolders,PipelineTreePanel}.tsx` + `fe-{user,admin}/src/hooks/usePipelineStages.ts`. +- Ngoài src: memory frontend-designer (MEMORY.md + archive/2026-06.md + archive/_INDEX.md), `.session-counter.json`, run-folder artifacts (2 png + 2 harness .txt + sub-frontend-designer-0.md). + +## MỤC 1 — Claim "khối `
  • ` phiếu giữ nguyên từng byte" — ĐẠT + +`git diff -- .../pe/PurchaseEvaluationsListPage.tsx` (cả 2 app, diff IDENTICAL): +- Hunk A `@@ -29,6 +29,8` — +2 import. 0 xoá. +- Hunk B `@@ -47,6 +49,14` — +8 dòng (6 comment + `usePipelineStages()` + `filterActive`). 0 xoá. +- Hunk C `@@ -507,7 +517,12` — xoá ĐÚNG 1 dòng `
      `. +- Hunk D `@@ -607,6 +622,7` — +1 dòng `) })} />`. 0 xoá. +⇒ Tổng xoá = 1 dòng. Khối `
    • ` KHÔNG có dòng `-` nào ⇒ CLAIM ĐỨNG. Class cũ không mất: `ml-3 border-l border-slate-200` → prop `className` wrapper; `divide-y divide-slate-100` giữ ở `
        ` trong. + +## MỤC 6 — Biên giới — ĐẠT +- `git diff --name-only` (tracked): 0 file `src/Backend`, 0 `tests/`, 0 seeder/menu (#84), 0 `PePipelineStrip.tsx`. Chỉ 6 file FE + memory/run-artifact ngoài src. +- `grep -rn "export default"` trong 6 file mới ⇒ NONE (named export only, đúng §3.9). +- Nhãn VERBATIM `PipelineStageFolders.tsx:22-27`: `1:'Duyệt NCC'` · `2:'Kế hoạch Hợp đồng'` · `3:'Duyệt Hợp đồng'` · `4:'Hợp đồng cứng'` — khớp lời owner từng chữ. +- Mirror SHA256 6/6 PAIR (tự đo lại, không tin bảng designer): + PipelineStageFolders `05c93c3619a9cfec` · PipelineTreePanel `76e5dafb239e402a` · usePipelineStages `e762d5eb9c3a057a` · PE page `1d8d8f75c7d2cc14` · Khkk page `40a650bf6964ab4b` · HardCopies page `d30a911e7483f96a`. +- `types/contracts.ts` lệch 2 app CHỈ ở giá trị `ContractPhaseColor` (cùng bộ key 1-10,99) ⇒ hook SHA-chung import an toàn, `badgeClass` undefined vẫn có fallback `PipelineStageFolders.tsx:189`. + +## MỤC 3 — Permission-gate 2 tầng — 1 LỖ THẬT (latent) + 1 điểm sai luận-cứ + +Key FE (`usePipelineStages.ts:41-44`) vs BE: +- `Khkk_List`/`Khkk_G1`/`KeHoachKyKet` — cả 3 tồn tại thật (`DbInitializer.cs:2184-2188` KhkkKeys + `MenuKeys.cs:41`). +- `Hdc_<7 code>` + `HopDongCung` — tồn tại thật (`DbInitializer.cs:1868/2191`). +- `Ct__List` + `Contracts` — tồn tại thật. +- `can(k)` = `node.canRead` (`usePermission.ts:20`), findNode không thấy ⇒ false ⇒ OR-nhiều-key hợp lệ về mặt đọc cây. + +🔴 **F: OR-nhiều-key ĐÁ NHAU với policy API cho GĐ2.** `ContractSigningPlansController.cs:36` = `[Authorize(Policy="KeHoachKyKet.Read")]`, và `MenuPermissionHandler.cs:40` so khớp **CHÍNH XÁC** `p.MenuKey == req.MenuKey` (KHÔNG kế thừa, KHÔNG OR con). ⇒ role có `Khkk_List=1` mà `KeHoachKyKet=0` sẽ: FE cho `canKhkk=true` → gọi API → **403** → `khkkFailed` → GĐ2 hiện "—" dưới MỌI gói thầu, và claim "0 rác 403" vỡ. Đo Dev DB: 13/13 role đều có `Contracts=1` ∧ `KeHoachKyKet=1` ⇒ HIỆN CHƯA vỡ; prod chưa đo được (chỉ localhost+secret). Là lỗ THIẾT KẾ chờ 1 lần chỉnh ma trận quyền. +🟡 luận-cứ trong comment `usePipelineStages.ts:10-13` ("gate phải OR nhiều key vì root có thể canRead=false") đúng cho TẦNG HIỂN THỊ nhưng sai đích cho tầng API: query nào cũng nên gate bằng ĐÚNG key policy của endpoint (`KeHoachKyKet` cho `/contract-signing-plans`). +🟢 `/contracts` KHÔNG có policy action-level (`ContractsController.cs:16-17`, chỉ `[Authorize]` trần class) ⇒ nhánh `enabled: canContract || canHardCopy` KHÔNG sinh 403. Đổi lại: đây là bare-`[Authorize]` sẵn có của hệ (#82), diff này không làm tệ thêm nhưng có nới hiển-thị: user chỉ được cấp `Hdc_ThauPhu` vẫn thấy leaf GĐ4 của MỌI loại HĐ (`usePipelineStages.ts:211`) vì gate là OR-toàn-nhóm chứ không theo `c.type`. +🟢 Không quyền ⇒ `enabled:false` (`:77`, `:91`, `PipelineTreePanel.tsx:108`) ⇒ KHÔNG gọi API; folder khoá render nhánh riêng KHÔNG `
        ` (`PipelineStageFolders.tsx:99-113`). Fetch lỗi ⇒ `failed` ⇒ badge "—" + body "Không tải được…" (`:140-156`) ⇒ cây không vỡ. ĐẠT.