Compare commits
3 Commits
a1b9cbb175
...
43e1bd7bc2
| Author | SHA1 | Date | |
|---|---|---|---|
| 43e1bd7bc2 | |||
| c61e28f833 | |||
| 213eec07ed |
@ -1,41 +1,33 @@
|
||||
# WAL — auto-generated, không sửa tay
|
||||
updated: 2026-08-05 | session: S174 (phiên-LOGIC L11, window 2) | branch: main
|
||||
updated: 2026-08-05 | session: S175 (phiên-LOGIC L11, window 3) | branch: main
|
||||
|
||||
goal: nối mạch sau /pause. Vá 5 điểm treo + trả lời anh. Reconcile §3 đã sửa 3 chỗ lệch (dưới).
|
||||
goal: UAT UI module KHKK theo 9 ảnh anh khoanh. Đợt A ĐÃ ship + deploy VERIFIED 2-vế. Cửa này = đợt B → hỏi câu "sum tổng" → đợt D.
|
||||
|
||||
chain:
|
||||
- [x] bootstrap: Sàn-3 SẠCH 5/5 · scaffold session-11 · tick 46→47 squash-benign · test 644 PASS/0 Fail
|
||||
- [x] 5 vòng / 11 vai — tooling PWF-10 · harvest GATE-FAIL-6 · lead-stale 11 FLAG(SÀN) · lead-gap 4 · ring1 78Đ/4T · ring2 15Đ/3T · eval MIXED-15 · refine 1-action/15-bác · audit 50Đ/7T · ctx-audit TRUOT-7 · ring4 NO-OP-có-khai
|
||||
- [x] cổng C2 @pause KÊU (A\B=9) → lead seed 9 diary + 1 erratum → A\B=∅ (verify: mọi vai grep S173 ≥1)
|
||||
- [x] _mind-s-11 MIND-0+MIND-1; mind-check `dat=11 TRUOT=0` exit 0 · _context PAUSE-1 + §(c) 16 file
|
||||
- [x] 🔄 RECONCILE-§3 @S174: MIND-2 ĐÃ chèn @037fe63 sau ctx-curator `SUA-9` (lead vá đủ 9) — mind-check nay 3 block (2,1,0) dat=11/TRUOT=0. WAL cũ ghi `[!]` chờ-chèn = LỆCH, đã sửa.
|
||||
- [x] tick 47→48 clean @S174 · dòng-nợ: light 2/6 deep 8/15 ok | tràn-bộ-nhớ 1 | run-chưa-gom 1 | pull-cách 14 ngày
|
||||
- [!] ring2-audit/MEMORY.md = 26.440 B > autoinject_cap 25.600 (dư 840 B) — DO CHÍNH LƯỢT SEED S173. A1 (rút 3 record cũ → archive/2026-07.md, còn 20.375 B) có payload sẵn, harness-audit chấm ĐẠT, CỐ Ý hoãn khỏi cửa dừng
|
||||
- [ ] vá `STATUS:473` Gotchas 86→87 (đầu chuỗi B1 ⇒ 3 bề mặt sai theo)
|
||||
- [ ] gạch 5 slot (68)-(72) trong HANDOFF (đã ratify `6730eb4` mà sổ chưa lật — còn sống ở :16 :18 :20 :23 :25)
|
||||
- [ ] đưa `ROTATE mật-khẩu prod vrapp` trở lại HANDOFF (ctx-audit F-1 HIGH — rơi khỏi MỌI bề mặt sống)
|
||||
- [ ] re-stamp Carry 14 slug (HANDOFF:38 còn đóng dấu @S172 ⇒ bỏ tiếp = streak 12; ctx-audit F-5: lớp mềm mù hoàn toàn nghĩa vụ này)
|
||||
- [ ] slot (70) MỞ LẠI — vá anchor `^\*\*` phải neo CẢ `**`/`##`/`###` (3 khối mù `:27` `:68` `:102`)
|
||||
- [ ] monthly drift-audit (quá hạn 4 ngày; ring1 phân xử CHƯA chạy, lần cuối `881783c` 07-01)
|
||||
- [ ] 🔄 RECONCILE-§3 v2 (ctx-verifier cờ-5 sửa lead): việc = **ĐỌC/ÁP 4 + KÉO 2**, KHÔNG phải "kéo 6". 4× `2026-08-04-*upgrade-pack-phased-*` ĐÃ về `broadcasts/inbox/ai_infra/` @`3a6eb92` (chỉ chưa có adap-report) · 2× `2026-08-05-*loi-do-luong-phan-1/2` **0 hit toàn đĩa** = chưa kéo thật. Kênh se=0. (lead dò sai vì soi `inbox/<file>` — inbox lồng theo NGƯỜI GỬI `inbox/<from>/`)
|
||||
- [ ] 🔴 MỚI (ctx-verifier cờ-4 — rơi im lặng qua 2 phiên): E4 vế ➊ CHƯA land. `agent-memory/harness-eval/MEMORY.md` `## Luật cứng` **đúng 3 bullet**, thiếu luật *"0-hit CẤM rời tay khi chưa có control-dương"* — luật ĐÃ có trong THÂN nhật-ký `:9` (tự rút @S172) nhưng **không được thăng lên khối auto-inject** ⇒ không tới vai lúc spawn. Chủ = **LEAD** (`harness-refine` §10). Payload: `runs/2026-08-05-S172-bookend-close/harness-refine-return.md` §1
|
||||
- [ ] 🔄 RECONCILE-§3 (MỚI, không có trong WAL cũ): run-folder `2026-08-05-S173-bookend-open` = **ORPHAN** — 11 artifact ~245 KB + run.md giàu NHƯNG 0 file `*-synthesis.md` phẳng ⇒ Sàn-3 ① KÊU. Cần synthesis @bookend-close.
|
||||
- [ ] dọn 9 dir rỗng `runs/.../.claude/agent-memory/*` @session-end sau sub cuối (harness tạo lúc spawn, đơn-điệu tăng — ghi CƠ-CHẾ không ghi SỐ)
|
||||
- [x] nối mạch /tiep: Sàn-3 5/5 · reconcile 5 chỗ lệch · tick 47→48 · `_tiep-1.md` cân sổ ORPHAN-L
|
||||
- [x] 5 commit ship + 3 đợt deploy XANH (verify byte-level: hash ĐỔI ∧ chuỗi mới CÓ, 2 vế): `14a917f` `3f339c0` `f86f30d` `d0453ba` `a1b9cbb`
|
||||
- [x] anh BÁC chỗ đặt cột 2-tầng → REVERT `PurchaseEvaluationWorkspacePage` ×2 về ĐÚNG BYTE `1b3bcaa` (verify `git show` + grep 0 hit)
|
||||
- [x] ensemble `/fable-clone frontend-designer` `wf_016b1bed-b85` — 5 lane / 5 xong / 0 chết / 755K tok; spec 3-mục + `audit-synthesis.md` ĐÃ ghi (hết orphan dạng-1)
|
||||
- [x] đợt A ship `a1b9cbb`: vá 401 tải file · vá `<lg` ngõ cụt mobile · trả nút duyệt mobile · gỡ SoonChip (3 ca 3 cách) · gỡ nút Từ chối · grid 260/320
|
||||
- [x] cổng C2 @PAUSE-2 KÊU (A\B={frontend-designer}) → lead seed 1 entry → A\B=∅, sổ 16.373→19.869 B (control-dương: S173 có ở 10 sổ)
|
||||
- [x] deploy `a1b9cbb` VERIFIED 2-vế ×2 app @S175: hash ĐỔI (admin `BXwdUVmD`→`Dlhi3eF1` · eoffice `DdcCH2oZ`→`B9PVjJnU`) ∧ literal gỡ `Từ chối kế hoạch (khoá phiếu)`=0/0, control-dương `Tải xuống`=10/10
|
||||
- [ ] đợt B (32 mục bê style PE): panel duyệt 13 · nội dung 6 P0 + 8 P1 · panel list 13 delta (có `slate-400`=2.56:1 dưới WCAG) · host
|
||||
- [ ] đợt D: O6 bấm-card-PE-giữ-màn · O3 hạng-mục N-dòng + SUM (**0-migration**) · O4 shopdrawing (**FE-only**) · O7 nút edit · O8 cây 2 cấp
|
||||
- [ ] 🔴 HỎI ANH trước khi viết BE cho O3: cách đọc "sum tổng" — đây là TIỀN (lane L5 dặn)
|
||||
- [ ] thăng luật E4 vế ➊ lên khối auto-inject `harness-eval/MEMORY.md ## Luật cứng` (đang 3 bullet, luật nằm dưới vùng planner mù)
|
||||
- [ ] ghi synthesis cho run-folder **S173** `2026-08-05-S173-bookend-open` (vẫn ORPHAN, 0 flat synthesis)
|
||||
- [ ] 5 điểm governance đầu phiên CHƯA ĐỘNG: `STATUS:473` 86→87 · gạch 5 slot HANDOFF · `ROTATE vrapp` · re-stamp Carry 14 slug · slot (70) anchor
|
||||
- [ ] `ring2-audit/MEMORY.md` 26.440 B > cap 25.600 — A1 treo từ PAUSE-1
|
||||
- [ ] kéo 2 thư `2026-08-05-Governance-loi-do-luong-phan-1/2` — 🔄 @S175 nay ĐÃ CÓ ở `AI_INFRA/broadcasts/outbox/all/` (trước = 0 hit); 4× `upgrade-pack-phased` đã ở `inbox/ai_infra/`, chỉ thiếu adap-report
|
||||
- [ ] monthly drift-audit quá hạn · dọn 9 dir rỗng `runs/.../agent-memory/*` @session-end
|
||||
|
||||
- [x] 🆕 VIỆC ANH GIAO S174 (ngắt mạch vá-governance): 7 chỉnh UI KHKK/PE qua 6 ảnh khoanh đỏ + 4 câu trả lời — commit `14a917f` 16 file, push `1b3bcaa..14a917f`. Build 2 app PASS · mirror SHA256 8/8 · byte-level bundle local xác nhận. Chi tiết ở body commit.
|
||||
- [x] deploy `14a917f` XANH (~5 phút, poll 15/28). Bundle prod ĐỔI cả 2: admin `Ajv-MaCz`→`DpZjuFBn` · eoffice `YsXRkBSR`→`DU_NaPIg`; 5/5 chuỗi mới HIT ở CẢ HAI; `Tất cả nhóm` VẮNG (đúng). Trang chủ http=200 ×2. 🔴 KHÔNG có `GITEA_TOKEN` ⇒ verify bằng grep bundle prod (#77) thay vì Actions API — 2 vế hash-ĐỔI + chuỗi-CÓ mới đủ, mỗi vế lẻ đều nói dối được
|
||||
- [x] 🔄 VÒNG 2 S174 — anh BÁC chỗ đặt: *"bỏ phần này đi chỗ duyệt NCC đã golive và đã tốt rồi, ý tao là đang muốn mày làm giống giao diện thao tác của Duyệt NCC"*. Commit `3f339c0` (amend): REVERT `PurchaseEvaluationWorkspacePage` ×2 về ĐÚNG BYTE `1b3bcaa` + `git mv` panel `components/pe/PeKhkkSidePanel` → `components/khkk/KhkkWorkListPanel` + dựng `/khkk/create` thành HOST 2-PANEL (panel-1 2 tầng callback-driven, panel-2 form|xem). Deploy XANH poll 17: admin `DpZjuFBn`→`CcypDu1E` · eoffice `DU_NaPIg`→`6q1cAHcJ`; 5/5 chuỗi mới HIT ×2, control-ÂM `xl:grid-cols-[320px_1fr_320px]` VẮNG ×2 (chứng revert sạch tới prod)
|
||||
- [!] 🔴 BÀI HỌC vòng này — 2 lỗi CÙNG HỌ "kết-quả trông thành-công nhưng không đo thứ mình tưởng", cần cân nhắc lên gotcha/memory:
|
||||
(a) **"làm giống màn X" ≠ "sửa màn X"** — anh khoanh đỏ TRÊN màn PE để chỉ NỘI DUNG muốn có, lead đọc thành CHỖ ĐỂ SỬA ⇒ chèn nhầm cột vào màn đã golive. Ảnh sau (anh 7) mới lộ chỗ đặt thật.
|
||||
(b) **`git add` abort TRỌN lệnh khi có 1 path đã bị `git mv` xoá** ⇒ 6 file sửa nội dung KHÔNG được stage mà `git commit` vẫn báo thành công; tín-hiệu duy nhất = dòng `2 files changed, 0 insertions(+), 0 deletions(-)`. Bắt được nhờ đọc SỐ + verify `git show HEAD:` thay vì tin chữ "thành công". Cùng họ với `539ms`-build và 3 probe hỏng đầu phiên.
|
||||
- [ ] 🆕 nợ đã KHAI với anh, chưa làm: trang chi tiết KHKK trên mobile mất nút Gửi duyệt/Xóa phiếu (gỡ cùng khối Quy trình duyệt) — mở lại bằng `lg:hidden`. 🧊 Nợ "preselect `?peId=`" ĐÃ TỰ TIÊU: panel nay callback-driven, bấm PE nạp thẳng vào form, không còn điều hướng.
|
||||
|
||||
next: chờ poll deploy `b4d42pfzu` → báo anh kết quả → quay lại vá 5 điểm governance treo (STATUS:473 → HANDOFF 5 slot → vrapp → Carry re-stamp → slot 70)
|
||||
next: đợt B (bê style PE) → hỏi anh câu "sum tổng" TRƯỚC khi viết BE cho O3 → đợt D → [đuôi, đừng rơi] thăng luật E4 ➊ · synthesis S173 · 5 điểm governance
|
||||
|
||||
verify:
|
||||
- `sed -n '473p' docs/STATUS.md | grep -c 'Gotchas | \*\*86\*\*'` → 1 = CHƯA vá (🔴 literal cũ `'Gotchas | 86'` KHÔNG khớp vì text là `**86**` bold — 0-hit đọc nhầm thành đã-vá; ground-truth `grep -cE '^### [0-9]+\. ' docs/gotchas.md` = 87)
|
||||
- `grep -c 'ROTATE' docs/HANDOFF.md` → 0 = vrapp còn mất (control-dương: `grep -c ZZTEST` → 3)
|
||||
- `grep -c 'Carry @S172' docs/HANDOFF.md` → 1 = chưa re-stamp
|
||||
- `curl -s https://admin.solutions.com.vn/ | grep -oE '/assets/index-[A-Za-z0-9_-]+\.js'` → khác `index-Dlhi3eF1.js` = đợt SAU đã tới (mốc @S175)
|
||||
- `grep -c '<SoonChip' fe-admin/src/components/pipeline/PipelineStageFolders.tsx` → 0 (control-dương: `grep -c 'SoonChip'` → 2, chỉ còn trong comment)
|
||||
- `grep -c "responseType" fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx` → 2 = A1 đã vá
|
||||
- `ls .claude/workflows/runs/2026-08-05-S173-bookend-open/*-synthesis.md` → no match = S173 CÒN orphan
|
||||
- `wc -c .claude/agent-memory/ring2-audit/MEMORY.md` → 26440 (>25600 = còn nợ A1)
|
||||
- `ls .claude/workflows/runs/2026-08-05-S173-bookend-open/*-synthesis.md` → no match = còn orphan
|
||||
- `ls .claude/sessions/session-11/` → _context-s-11.md · _mind-s-11.md · _pause-1.md · _tiep-1.md
|
||||
- `sed -n '473p' docs/STATUS.md | grep -c 'Gotchas | \*\*86\*\*'` → 1 = chưa vá (literal có `**` bold)
|
||||
- `ls .claude/sessions/session-11/` → _context · _mind · _pause-1 · _tiep-1 · _pause-2
|
||||
|
||||
@ -45,6 +45,12 @@
|
||||
- **Lucide alias-dodge:** icon từ export-aggregation-line (`SendHorizonal`) / mojibake token (`网络Placeholder` autocomplete) → dùng proven-safe export (`Send`), node-check icons trước build.
|
||||
|
||||
## Activity log
|
||||
- **S174 (2026-08-05) AUDIT style KHKK(GĐ2) ⟂ Duyệt NCC(GĐ1) — ensemble 5 lane CÙNG 1 VAI qua `/fable-clone`, 5/5 xong 0 chết, 755K tok/147 tool-use/~17′.** *(Entry on-behalf bởi lead @pause cổng C2 — VERIFY từ 5 sub-file trên đĩa + tự re-đo 2 claim nặng nhất.)* Run `wf_016b1bed-b85`, artifact `runs/2026-08-05-S174-khkk-style-audit/sub-frontend-designer-L1..L5.md` (34+41+30+33+32 KB) + `spec-khkk-style-dong-bo-05-08-2026.md`. **Lăng-kính:** L1 panel-duyệt · L2 panel-list · L3 host · L4 cây · L5 nội-dung. **Verdict:** L1 15 lệch (13 bê-thẳng) · L2 CHƯA-PASS 13 delta + 3 trục rubric đỏ · L3 KHỚP-MỘT-PHẦN · L4 SHIP-SPEC-READY · L5 6 P0 + 8 P1 + 3 P2.
|
||||
🔴 **3 phát hiện owner KHÔNG nêu mà nặng hơn phần lớn danh sách owner nêu:** (a) **LỖI THẬT** `KhkkWorkflowPanel:501` tải file bằng `<a href>` trần ⇒ JWT chỉ gắn qua interceptor axios (`lib/api.ts:18`) ⇒ endpoint `[Authorize]` ăn **401, nút không chạy** — lead verify bằng đối-chứng `responseType:'blob'` ở PE `:331`; (b) vỏ `card-accent` bọc panel KHKK ⟂ PE panel TRẦN = thứ làm 2 bên "nhìn khác hệ" TRƯỚC mọi mục owner kê; (c) `<lg` màn Thao tác `<main hidden lg:block>` ⇒ **mobile không tạo được phiếu** — regression do CHÍNH LEAD gây ra cùng phiên.
|
||||
🔴 **BÀI LỚN NHẤT — lane BÁC 2 giả-định của lead, lead tự verify thì lane ĐÚNG:** `O3` hạng-mục N-dòng **KHÔNG cần migration** (`ContractSigningPlanLineConfiguration.cs:37-38` unique index **3 CỘT** `(PlanId,SupplierId,CatalogEntryId)` ⇒ 1 NCC đã được phép N dòng × N hạng mục từ Mig 71) · `O4` shopdrawing **FE-only 0-BE** (rail `dossierItemId` đủ 6 tầng từ Mig 69). Lead đã báo owner "O3 nặng nhất cần migration" — **SAI**, đã đính chính. ⇒ **Trước khi tuyên một việc là "nặng/cần migration", ĐỌC EF configuration + index; đừng suy từ hình-dạng UI.** Đây đúng khuôn `máy rộng, UI hẹp` (owner S164): schema đã tổng-quát sẵn, chỉ UI đang khoá 1-1.
|
||||
🔸 **Chỗ CỐ Ý không bê:** chip "NS PRO"/"NS CCM" (`PeWorkflowPanel:457-463`) — 2 cờ `canEditProBudget/canEditCcmBudget` mở quyền sửa **ngân sách per-gói-thầu** (`PeWorkItemBudgets`, Mig 50), **khái niệm KHKK không có** ⇒ bê sang = vẽ nhãn quyền không tồn tại. Và chỗ KHKK **TỐT HƠN** PE (mốc chữ-ký + in đậm người đã ký) ⇒ **GIỮ, đừng hạ xuống cho "đồng bộ"**.
|
||||
🔴 **NO-SCREENSHOT ×5 lane, khai thẳng:** dev-rig chặn (chỉ `:8080` LISTENING, API `:5443` không có, trang sau `ProtectedRoute` — gotcha #3) ⇒ mọi kết luận là **đọc mã, KHÔNG phải đã nhìn**. Thay bằng: đo class trên đĩa + `sha256sum` mirror + **tự tính tương-phản WCAG** (bắt được `slate-400` = **2.56:1** cho 6 dòng chữ mang thông tin). Người land BẮT BUỘC soi 375/1366/1440 trên **prod sau deploy**.
|
||||
🧪 **Luật C2 (ép ghi khung rỗng lượt 1-2 TRƯỚC khi đọc) có RĂNG THẬT:** run trước cùng khuôn 3/5 lane chết cháy-lượt; run này **0/5 chết**. Cùng model, cùng harness, khác **thứ tự ghi**. Tag [s174, ensemble-5-lane-1-vai, fable-clone, audit-khkk-vs-pe, 401-a-href-khong-mang-jwt, card-accent-ngoai-layer-gotcha66, lane-bac-gia-dinh-lead, may-rong-ui-hep, no-screenshot-khai-that, c2-khung-rong-0-lane-chet].
|
||||
- **S162 (2026-07-30) Cây 4 folder GIAI ĐOẠN dưới từng gói thầu — 3 file MỚI + 3 trang, MIRROR đủ 2 app, build PASS ×2.** Owner (screenshot khoanh đỏ dải 4 GĐ → cây trái trang Duyệt NCC) + AskUser chốt: folder đặt DƯỚI TỪNG GÓI THẦU · áp cả trang GĐ khác. Nhãn VERBATIM "Duyệt NCC · Kế hoạch Hợp đồng · Duyệt Hợp đồng · Hợp đồng cứng" (GĐ2 CỐ Ý khác nhãn strip `PePipelineStrip` = lời-mới-nhất-thắng; KHÔNG sửa strip/menu). **3 file mới:** `components/pipeline/PipelineStageFolders.tsx` (thuần trình bày; `<details>` 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 `<li>` 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` + `<table>` 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 **VỚI ĐIỀU KIỆN gate = ĐÚNG key policy của endpoint** *(reviewer F-1 @S162 bắt, lead sửa cùng lượt: GĐ2 `/contract-signing-plans` = `KeHoachKyKet.Read`, `MenuPermissionHandler.cs:40` so khớp CHÍNH XÁC 1 key — KHÔNG kế thừa/OR-con ⇒ OR key con sẽ fetch rồi ăn 403 khi role thiếu key root; gate đã đổi về `['KeHoachKyKet']`. GĐ3/4 `/contracts` `[Authorize]` trần nên OR key hiển-thị vô hại)*. ⚠️ menu-tree fallback quyền cha CHỈ khi menu con KHÔNG có row riêng (`GetMyMenuTreeQuery.cs:66`) — seeder TẠO row `Ct_*` (`DbInitializer.cs:2168-2176`) ⇒ `Ct_*` thực tế KHÔNG kế thừa `Contracts` *(câu "Ct_* kế thừa Contracts" bản đầu entry này SAI — reviewer F-8, đã sửa)*. **+5 fix hậu-review cùng lượt (lead áp):** F-1 gate-policy-key · F-3 leaf ngoài-index nhãn "(ngoài chỉ mục 200 — mở để xem)" tách nghĩa "(chưa cấp mã)" · F-4 index giữ `total` ⇒ cờ `truncated` khai cửa-sổ-200 ngay trên cây (footer folder + notice panel) · F-6 `hideEmpty` KHÔNG nuốt folder failed/loading · F-7 upload bản cứng invalidate thêm `pipeline-contract-index`. Giới hạn khai thẳng: F-2 phiếu liên-danh N-HĐ chỉ nối được HĐ[0] qua `pe.contractId` (chờ W5 link KHKK→HĐ) · F-5 lưới 19rem 2 trang chưa render thật (harness chỉ import 2 component) — nghiệm thu prod. **Evidence:** `.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-frontend-designer-0.md` + `ship-synthesis.md` · SHIP `bfc7b79` · cicd PASS 5/5 Run #432. 🔸 *H2 F-03 @S162: `ship-synthesis` trích `VERDICT: SHIP-READY` như verbatim của tao nhưng sub-file **0 hit** chuỗi đó (kết bằng "TỰ ĐÁNH GIÁ CÒN HỞ", không END-line) — nội dung khớp đĩa nên KHÔNG phải bịa, nhưng từ nay **mọi `sub-*.md` PHẢI kết bằng `END <slug> — VERDICT=<…>`*. Tag [s162, pipeline-stage-folders, 4-folder-gd, permission-policy-key-gate, index-query-key-moi, leaf-byte-verbatim, sha-pair-6-file, review-f1-f8].
|
||||
- **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: `?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)")_
|
||||
|
||||
@ -11,9 +11,9 @@
|
||||
},
|
||||
"_tick_invariant_note": "Tick invariant (hub dede7ec5 Delta-1, verbatim): moi LAN-CHOT +1; mot cap dung-noi tang DUNG +1, khong +2, khong +0. SE form = tick-at-entry-gate idempotent-per-label (hub Delta-2 recovery-gate +1 = permitted form) - NET +1/session-label EQUIV hub +1/cap on CLOSED pairs; an OPEN pair is transiently +0 until its entry gate fires (hub blessed, 9a35405b block-1 phep dung-noi). Guard song-con = label-convention (session-start 2.1.8: new conversation = new S\u003cnn\u003e label, NEVER reuse). history[] is append-unbounded BY DESIGN =\u003e absence of a marker = never-happened (safe semantics); IF a FIFO cap is ever added, eviction MUST be handled explicitly (absence-vi-bi-day != absence-vi-chua-xay-ra - log-BOUNDED design-note dede7ec5).",
|
||||
"_seed_honesty": "Seeded UNTICKED on purpose. counter=0 and last_ticked_* = null mean \u0027no tick has ever happened\u0027, which is the truth at S121 - the ritual that performs the tick lands in W3. Seeding a fake first tick here would make the very first cadence reading a lie, and H24 exists to catch exactly that kind of invented number.",
|
||||
"counter": 47,
|
||||
"last_ticked_session": "S173",
|
||||
"last_ticked_head": "1b3bcaa2e26c6c722dd2abde2f359fedf58d5ea7",
|
||||
"counter": 48,
|
||||
"last_ticked_session": "S174",
|
||||
"last_ticked_head": "7fcf0ab5e9d86b85a36d776783d30d3cc174fb3f",
|
||||
"last_ticked_at": "2026-08-05",
|
||||
"last_audit": {
|
||||
"light_at_counter": 46,
|
||||
@ -242,6 +242,11 @@
|
||||
"at": "2026-08-05",
|
||||
"session": "S173",
|
||||
"event": "squash-benign (session-counter-tick.ps1 M2, contract fail_loud_on_regress trigger-2 BENIGN branch): counter 46->47, session S172->S173, head 37f18e9->1b3bcaa. last_ticked_head 37f18e9 object EXISTS (cat-file=commit) but NOT reachable (merge-base --is-ancestor exit!=0), counter not regressed => a closeout squash lifted the ticked wal:/session commit out of history (expected drift, not tamper). Trace appended, continue, no owner alarm. Written atomically (temp + Move-Item -Force)."
|
||||
},
|
||||
{
|
||||
"at": "2026-08-05",
|
||||
"session": "S174",
|
||||
"event": "CLEAN tick (session-counter-tick.ps1 M2): counter 47->48, session S173->S174, head 1b3bcaa->7fcf0ab. Classify-before-tick: no regression (n=174 >= stored n=173); last_ticked_head reachable (merge-base --is-ancestor exit 0). 4 fields updated, 1 history entry appended, written atomically (temp + Move-Item -Force)."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -82,6 +82,36 @@
|
||||
{ "machine_block": "python scripts/session_ctx.py machine-block --session 11 --json" }
|
||||
```
|
||||
|
||||
### PAUSE-2 2026-08-05T16:54:23+07:00
|
||||
|
||||
> anh: xong thì /pause tạm nhé
|
||||
|
||||
**(1) quyết-định đã CHỐT**
|
||||
- Cửa-sổ này **KHÔNG làm việc governance** — anh ngắt mạch vá-5-điểm sang **UAT UI module KHKK**, giao qua **9 ảnh khoanh đỏ + 6 tin nhắn chốt**. Governance 5 điểm vẫn treo nguyên (xem WAL `[ ]`).
|
||||
- **5 commit ship + deploy XANH ×3 đợt:** `14a917f` · `3f339c0` · `f86f30d` · `d0453ba` · `a1b9cbb`. Prod verify byte-level mỗi đợt (bundle hash ĐỔI **∧** chuỗi mới CÓ — 2 vế, mỗi vế lẻ đều nói dối được).
|
||||
- **Anh BÁC 1 lần, lead revert sạch:** cột 2-tầng bị lead đặt nhầm vào màn *Duyệt NCC* (đã golive). Anh: *"bỏ phần này đi chỗ duyệt NCC đã golive và đã tốt rồi, ý tao là đang muốn mày làm giống giao diện thao tác của Duyệt NCC"*. `PurchaseEvaluationWorkspacePage` ×2 app trở về **ĐÚNG BYTE** `1b3bcaa` (verify `git show` + control-dương grep 0 hit). Bài ghi vào đầu file component: **"làm giống màn X" ≠ "sửa màn X"**.
|
||||
- **Ensemble `/fable-clone frontend-designer`** (`wf_016b1bed-b85`): 5 lane cùng 1 vai, **5/5 xong, 0 chết**, 755K tok. Spec 3-mục đã ghi. Luật C2 (ép ghi khung rỗng lượt 1-2) chứng có răng: run trước 3/5 chết, run này 0/5.
|
||||
- **4 câu owner chốt @đợt-C:** ① O5 giữ máy R-11 sửa hình → OK · ② bỏ chữ SẮP → OK · ③ *"cho màu sắc phân biệt ra từng loại, cùng loại thì cùng màu"* (KHÔNG phải hạ hết về xám — nguyên tắc mạnh hơn cả 2 lựa chọn lead đưa) · ④ bỏ luôn nút Từ chối ~ như PE.
|
||||
|
||||
**(2) delta còn SỐNG**
|
||||
- 🔴 **Đợt B chưa land** (bê style PE): panel duyệt (13 mục) · nội dung phiếu (6 P0 + 8 P1) · panel danh sách (13 delta, có **`slate-400` = 2.56:1** dưới ngưỡng WCAG) · host.
|
||||
- 🔴 **Đợt D chưa land:** O6 bấm-card-PE-giữ-màn · O3 hạng-mục N-dòng + SUM (**0-migration**, máy rộng sẵn) · O4 shopdrawing (**FE-only**) · O7 nút edit · O8 cây 2 cấp.
|
||||
- 🔴 **Còn 1 câu chưa hỏi anh:** cách đọc **"sum tổng"** của O3 — đây là **tiền**, lane L5 dặn xác nhận 1 dòng trước khi viết BE.
|
||||
- 🔴 **NO-SCREENSHOT ×5 lane** (dev-rig chặn: chỉ `:8080` LISTENING, API `:5443` không có, trang sau `ProtectedRoute` — gotcha #3). Mọi kết luận là **đọc mã, không phải đã nhìn** ⇒ người land BẮT BUỘC soi 375/1366/1440 trên prod.
|
||||
- 5 điểm governance đầu phiên **nguyên vẹn chưa động**: `STATUS:473` 86→87 · gạch 5 slot HANDOFF · `ROTATE vrapp` · re-stamp Carry 14 slug · slot (70) anchor.
|
||||
- `thu-moi: se=0 all=6` (4× `upgrade-pack-phased` ĐÃ ở `inbox/ai_infra/` chỉ thiếu adap-report · 2× `loi-do-luong` **0 hit toàn đĩa** = chưa kéo thật).
|
||||
- `ring2-audit/MEMORY.md` vẫn **26.440 B > 25.600** — A1 chưa thi hành (treo từ PAUSE-1).
|
||||
|
||||
**(3) con-trỏ**
|
||||
- run-folder: `.claude/workflows/runs/2026-08-05-S174-khkk-style-audit/` (`run.md` + 5 sub 34-41 KB + `spec-khkk-style-dong-bo-05-08-2026.md`)
|
||||
- prod hiện tại: admin `index-BXwdUVmD.js` · eoffice `index-DdcCH2oZ.js` (đợt A **chưa** deploy — vừa push `a1b9cbb`, chưa poll)
|
||||
- 🔴 **Cổng C2 @PAUSE-2 KÊU** — `A∖B = {frontend-designer}` (có 5 artifact, 0 entry S174); lead seed on-behalf 1 entry → `A∖B = ∅`, sổ 16.373 → **19.869 B** (dưới trần). Control-dương: nhãn S173 có ở **10** sổ ⇒ phép grep sống, không phải glob hỏng.
|
||||
- 🔴 **Lane BÁC 2 giả-định của lead, lead tự verify thì lane ĐÚNG** — O3 **không cần migration** (`ContractSigningPlanLineConfiguration.cs:37-38` unique **3 cột**) · O4 **FE-only**. Lead đã báo owner sai chiều "O3 nặng nhất cần migration", đã đính chính tại chỗ.
|
||||
|
||||
```json
|
||||
{ "machine_block": "python scripts/session_ctx.py machine-block --session 11 --json" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## (c) STOCK-touched — máy-derive
|
||||
@ -110,3 +140,16 @@
|
||||
| `…/harness-refine-return.md` | A | 40.048 B — `1 action / 15 bác` |
|
||||
| `…/harness-audit-return.md` | A | 57.039 B — `50Đ/7T` |
|
||||
| _(ngoài bảng máy — chưa commit lúc đo)_ | M×9 | 9 diary lead seed on-behalf @cổng C2 + `ring2-audit` **vượt trần 840 B** |
|
||||
|
||||
> **Máy-derive @PAUSE-2** — 🔴 `machine-block --session 11` trả `changed_count: 0` vì **anchor CHÍNH LÀ commit vừa tạo** (`a1b9cbb`) nên `git diff anchor..HEAD` rỗng. Đó là **giới-hạn của phép đo, KHÔNG phải "không đụng gì"** — đọc thẳng con số đó thành "cửa-sổ này 0 file" là sai. Lead tự đo khoảng đúng của cửa-sổ-2: `git diff --name-only 7fcf0ab..a1b9cbb` (mốc `_tiep-1` → nay) = **28 file**.
|
||||
|
||||
| Nhóm | n | Ghi-chú |
|
||||
|---|---|---|
|
||||
| `fe-admin/src/**` | 9 | khkk ×4 · pipeline ×2 · pages/khkk ×3 |
|
||||
| `fe-user/src/**` | 10 | mirror 9 file trên + `pages/InboxPage.tsx` (gỡ cây khỏi Hộp thư — chỉ fe-user có trang này) |
|
||||
| `runs/2026-08-05-S174-khkk-style-audit/**` | 7 | `run.md` + 5 `sub-frontend-designer-L*.md` + `spec-*.md` |
|
||||
| `sessions/session-11/_tiep-1.md` | 1 | biên-nhận cửa vào (cân sổ ORPHAN-L) |
|
||||
| _(loại khỏi bảng)_ | 2 | `WAL.md` · `.session-counter.json` — nhiễu mỗi turn, không mang tin |
|
||||
|
||||
> 🔴 **5 commit KHÔNG phải `wal:` trong cửa-sổ này** (đây mới là tín-hiệu "có người làm thật", hook không bao giờ sinh được loại này):
|
||||
> `14a917f` · `3f339c0` · `f86f30d` · `d0453ba` · `a1b9cbb`.
|
||||
|
||||
@ -39,6 +39,62 @@ Máy chỉ quét phần **DƯỚI** marker đóng khối luật. Khối luật
|
||||
(`_MIND_TOP_MARKER` trong scripts/session_ctx.py) -- khac khuon `FLOW-START` cua _context (khong may nao doc). -->
|
||||
<!-- MIND-TOP -->
|
||||
|
||||
## MIND-3 — 2026-08-05T16:54:23+07:00 @ a1b9cbb (window 2, @PAUSE-2)
|
||||
|
||||
### A. Gói-turn
|
||||
|
||||
- Anh ngắt mạch governance sang UAT UI KHKK, giao bằng **9 ảnh khoanh đỏ + 6 tin nhắn chốt** rải suốt cửa-sổ — không phải một đề bài đóng gói sẵn. Số commit / đợt deploy / verdict: xem `_context` PAUSE-2 mục (1).
|
||||
- 1 lần anh BÁC chỗ đặt, lead revert về đúng byte. Ensemble `/fable-clone frontend-designer` chạy xong; spec 3-mục đã ghi — con-trỏ ở PAUSE-2 mục (3).
|
||||
- `ctx-curator` chấm nháp block này: **`SUA — 9 điểm`**, lead sửa đủ 9 rồi mới chèn.
|
||||
|
||||
### B. Hướng-tiếp + nhánh-đã-loại
|
||||
|
||||
- **Đi tiếp (cửa sau):** poll deploy `a1b9cbb` → đợt B (bê style PE) → đợt D (O6 · O3 · O4 · O7 · O8) → **thăng luật E4 vế ➊ lên khối auto-inject** → **ghi synthesis cho run-folder S173** (đang ORPHAN) → rồi mới quay lại 5 điểm governance.
|
||||
- 🔴 **Phải hỏi anh 1 câu TRƯỚC khi viết BE cho O3:** cách đọc "sum tổng" — đây là tiền, lane L5 dặn xác nhận.
|
||||
- **Nhánh đã LOẠI ⑧:** *hạ khuôn Section của KHKK xuống cho bằng PE* — LOẠI: KHKK **giàu hơn** PE (icon-chip/accent/actions). Hướng đúng là **nâng PE lên**, không hạ KHKK.
|
||||
- **Nhánh đã LOẠI ⑨:** *bê chip NS PRO/NS CCM sang KHKK cho đủ bộ* — LOẠI: 2 cờ đó mở quyền sửa ngân sách per-gói-thầu, khái niệm KHKK không có ⇒ bê sang là vẽ nhãn quyền không tồn tại.
|
||||
- **Nhánh đã LOẠI ⑩:** *thay cả 3 chỗ SoonChip bằng số 0* — LOẠI: ca "chưa có QUYỀN xem" mà in 0 là nói dối về dữ-liệu. Ba ca ba cách: đếm-được → in số · khoá-quyền → bỏ trắng · failed → giữ dấu `—`.
|
||||
|
||||
### C. Kế-hoạch (delta suy-nghĩ)
|
||||
|
||||
- **Đổi so MIND-2:** lead bị vai kiểm bác **2 giả-định** và cả 2 lần lane đúng. ⇒ nhận: **trước khi tuyên một việc là "nặng", phải đọc EF configuration + index, đừng suy từ hình-dạng UI**. Chính khuôn `máy rộng, UI hẹp` anh chốt S164 — schema tổng-quát sẵn từ lâu, chỉ UI khoá 1-1.
|
||||
- **Đổi thứ hai — sửa overclaim của chính bản nháp đầu:** cửa-sổ này lặp đúng MỘT họ lỗi *"trông như đã làm nhưng không làm gì"* (grep sai hình · thiếu `cd` build nhầm app · `git add` abort sau `git mv` · `border-0` bị `.card-accent` nuốt · `JSON.parse` free-text). Nháp đầu gọi đây là "luật tự rút" — **SAI**: luật `0-hit phải có control-dương` **đã tồn tại từ S172** trong thân `harness-eval/MEMORY.md`. Delta thật = **bằng-chứng 5 ca cùng họ trong một cửa-sổ**, và việc phải làm là **THĂNG luật lên khối auto-inject** (nó đang nằm dưới vùng planner mù nên không tới tay ai), KHÔNG phải rút lại lần 2.
|
||||
- **Đổi thứ ba:** anh trả lời câu-3 hay hơn cả hai lựa chọn lead đưa ra — lead hỏi nhị-phân *giữ màu hay bỏ màu*, anh chốt một **nguyên-tắc** phân màu theo LOẠI (nguyên văn: xem PAUSE-2 mục (1)④). ⇒ khi hỏi owner, **đừng gói câu hỏi thành nhị-phân** nếu trục thật là một nguyên-tắc.
|
||||
|
||||
### D. Đang-thảo-luận
|
||||
|
||||
- Cách đọc "sum tổng" của O3 — là tiền, phải hỏi trước khi viết BE {treo-chờ-anh}
|
||||
- Đợt B — HƯỚNG bê style PE đã được anh chốt {gần-chốt}
|
||||
- Đợt B — NỘI DUNG từng mục lane đề xuất, anh chưa xem mục nào {mới-nêu}
|
||||
- `slate-400` = 2.56:1 dưới ngưỡng WCAG cho 6 dòng chữ mang thông tin {mới-nêu}
|
||||
- O6 bấm card PE giữ nguyên màn KHKK — callback-hoá `onOpenPe` mirror `onOpenKhkk` {gần-chốt}
|
||||
- O7 nút edit trên card + O8 cây thư mục 2 cấp — tới sau khi lane đã phóng, lane không thấy {mới-nêu}
|
||||
- NO-SCREENSHOT ×5 lane ⇒ mọi kết luận là đọc mã; phải soi thật 3 khổ màn trên prod {mới-nêu}
|
||||
- Nâng PE lên khuôn KHKK — run riêng có ảnh đối chứng, PE đang golive {treo-chờ-anh}
|
||||
- E4 vế ➊ chưa land — thăng luật lên khối auto-inject, chủ = LEAD {gần-chốt}
|
||||
- Run-folder S173 ORPHAN (0 `*-synthesis.md`) — Sàn-3 ① sẽ kêu lần 2 nếu bỏ {gần-chốt}
|
||||
- 2 thư `loi-do-luong` chưa kéo thật {mới-nêu}
|
||||
- `ring2-audit/MEMORY.md` vượt `autoinject_cap` — A1 có payload, treo từ PAUSE-1 {gần-chốt}
|
||||
- Nâng `autoinject_cap` thay vì archive — trần là quyền anh {treo-chờ-anh}
|
||||
- Bản thân việc ROTATE mật-khẩu prod {treo-chờ-anh}
|
||||
- Slot (67) người soạn có được upload file khi duyệt {treo-chờ-anh}
|
||||
- Nợ harvest thật vs con số HANDOFF ghi — gom hay để ngủ {treo-chờ-anh}
|
||||
- Vá `STATUS:473` Gotchas 86→87 — việc lead, đã quyết chưa thi hành {gần-chốt}
|
||||
- Gạch 5 slot (68)-(72) khỏi HANDOFF — đã ratify `6730eb4`, sổ chưa lật {gần-chốt}
|
||||
- Đưa `ROTATE vrapp` trở lại HANDOFF — việc lead {gần-chốt}
|
||||
- Re-stamp Carry 14 slug — bỏ tiếp thành streak 12 {gần-chốt}
|
||||
- Slot (70) vá anchor `^\*\*` neo cả `**`/`##`/`###` {gần-chốt}
|
||||
- 9 dir rỗng `agent-memory/<role>/` trong run-folder — dọn @session-end {mới-nêu}
|
||||
- Thước MFE thiên lệch theo độ dài diary, ngược trục `autoinject_cap` {mới-nêu}
|
||||
- Headroom: cắt `STATUS.md` + `HANDOFF.md` theo tiering hay nâng trần {mới-nêu}
|
||||
- Monthly drift-audit quá hạn {mới-nêu}
|
||||
|
||||
### E. Dòng-sống spawn/engine-run
|
||||
|
||||
- `ctx-verifier` @`/tiep` — **`NAP-LAI-SAN-SANG — 10 cờ`**; 2 cờ sửa thẳng reconcile của lead (kênh thư nhầm ledger · "kéo 6" gộp 2 trạng-thái) — con-trỏ `_context` PAUSE-2 mục (3)
|
||||
- `/fable-clone frontend-designer` ensemble — lần phóng ĐẦU **`wf_edc388bf-f8f` CHẾT** ở `JSON.parse(args)` (free-text ⟂ object; 0 agent · 0 token · 20ms); lần hai **`wf_016b1bed-b85`** 5 lane / 5 xong / 0 chết. Verdict từng lane + artifact: `.claude/workflows/runs/2026-08-05-S174-khkk-style-audit/run.md` §Synthesis
|
||||
- `ctx-curator` @PAUSE-2 — **`SUA — 9 điểm`**, verdict ghi vào `_context` entry `PAUSE-2` mục (3)
|
||||
|
||||
## MIND-2 — 2026-08-05T12:13:25+07:00 @ 037fe63 (window 1, @PAUSE-1)
|
||||
|
||||
### A. Gói-turn
|
||||
|
||||
5
.claude/sessions/session-11/_pause-2.md
Normal file
5
.claude/sessions/session-11/_pause-2.md
Normal file
@ -0,0 +1,5 @@
|
||||
ts: 2026-08-05T16:54:23+07:00
|
||||
head-sha: a1b9cbb17521f2d87d8b47c94e9b72cb37a95075
|
||||
window-ordinal: 2
|
||||
jsonl-hint: D--Dropbox-CONG-VIEC-SOLUTION-SOLUTION-ERP/e22c19fa-68f7-4269-87ab-24465b75de9d
|
||||
account-label: none
|
||||
6
.claude/sessions/session-11/_tiep-2.md
Normal file
6
.claude/sessions/session-11/_tiep-2.md
Normal file
@ -0,0 +1,6 @@
|
||||
ts: 2026-08-05T17:06:33+07:00
|
||||
head-sha: 213eec07ed257dd1a5f24229e516937464d4cb33
|
||||
window-ordinal: 3
|
||||
jsonl-hint: D--Dropbox-CONG-VIEC-SOLUTION-SOLUTION-ERP/041d0937-4486-46cb-ab80-a2a988516b25
|
||||
account-label: none
|
||||
ctx-verifier: NAP-LAI-SAN-SANG — 13 co (2 co nang tu-do-lai CONFIRMED: run.md Owner-items O2/O3 stale -> lead da flush; 2/3 con-tro MIND-3 muc E chet tang noi-dung)
|
||||
@ -0,0 +1,51 @@
|
||||
# audit-synthesis — KHKK style audit vs Duyệt NCC (S174)
|
||||
|
||||
> **Dạng-1 phẳng** (`<stage>-synthesis.md`) theo `workflows/README.md:17` — RAW `sub-*` ⟂ **VERIFIED** `-synthesis.md`.
|
||||
> Dạng-3 (chỉ có heading `## Synthesis` trong `run.md`) **đã retire @S123** ⇒ không ghi file này thì Sàn-3 ① kêu ORPHAN.
|
||||
> Engine **propose-only**; file này = **bản lead ĐÃ VERIFY**, không phải bê thẳng lane.
|
||||
|
||||
## Nguồn
|
||||
|
||||
`/fable-clone frontend-designer` — ensemble 5 lane CÙNG 1 VAI (đa-lăng-kính), run `wf_016b1bed-b85`.
|
||||
**5/5 xong · 0 chết · 0 rỗng** · 755.738 tok · 147 tool-use · ~17′.
|
||||
🧊 Lần phóng đầu `wf_edc388bf-f8f` **chết ở `JSON.parse(args)`** (lead truyền free-text, script đòi object JSON) — **0 agent · 0 token · 20ms**; guard nằm ở dòng đầu nên hỏng sạch, không đốt gì.
|
||||
|
||||
## Verdict từng lane
|
||||
|
||||
| lane | verdict (END-line của chính sub-file) |
|
||||
|---|---|
|
||||
| **L1** panel-duyệt | 15 lệch có-thể-sửa (13 bê-thẳng + 1 bán-bê kèm vá 401 + 1 chờ-owner) · 1 KHÔNG-BÊ vì thiếu khái niệm · H2+H4 đã khớp · lệch nặng nhất = **vỏ `card-accent`**, owner KHÔNG nêu |
|
||||
| **L2** panel-list | **CHƯA-PASS** — 13 delta + 3 trục rubric đỏ · O5 = **GIỮ-MÁY-R11-SỬA-HÌNH** |
|
||||
| **L3** host | **KHỚP-MỘT-PHẦN** — 2 lệch nặng (**grid 300→260** · **`<lg` ngõ cụt không tạo được phiếu**) · 3 lệch vừa · 3 mục CHỐT-KHÔNG-SỬA |
|
||||
| **L4** cây | **SHIP-SPEC-READY** — O2 + O6 đều thi-hành-được, 6 host đã liệt kê ảnh hưởng |
|
||||
| **L5** nội-dung | **AUDIT-DONE** — O3 = **N-ROW 0-migration** · O4 = **FE-ONLY 0-BE** · 6 P0 + 8 P1 + 3 P2 |
|
||||
|
||||
## Lead refute — 2 claim nặng nhất, tự đo lại
|
||||
|
||||
| claim của lane | lead đo lại | kết |
|
||||
|---|---|---|
|
||||
| O3 hạng-mục N-dòng **không cần migration** | `ContractSigningPlanLineConfiguration.cs:37-38` unique index **3 CỘT** `(PlanId, SupplierId, CatalogEntryId)` | ✅ lane ĐÚNG — lead từng báo owner "nặng nhất, cần migration", **SAI, đã đính chính** |
|
||||
| KHKK tải file **401** | KHKK `:501` `<a href>` trần ⟂ PE `:331` `responseType:'blob'`; JWT chỉ gắn qua interceptor axios | ✅ lane ĐÚNG — **lỗi thật**, không phải style |
|
||||
|
||||
## 3 phát hiện owner KHÔNG nêu mà nặng hơn phần lớn danh sách owner nêu
|
||||
|
||||
1. **X1** — nút tải file KHKK **401, không chạy**.
|
||||
2. **X2** — vỏ `card-accent` bọc panel KHKK ⟂ PE panel trần ⇒ thứ làm 2 bên "nhìn khác hệ" **trước** mọi mục owner kê.
|
||||
3. **X3** — màn Thao tác `<lg`: `<main hidden lg:block>` ⇒ **mobile không tạo được phiếu**. 🔴 **Regression do chính lead gây ra cùng phiên.**
|
||||
|
||||
## Đã LAND ngay (commit `a1b9cbb`)
|
||||
|
||||
Đợt A (3 lỗi thật) + 2 câu owner chốt: vá 401 · vá `<lg` ngõ cụt · trả nút duyệt mobile · gỡ `SoonChip` (**3 ca 3 cách**: đếm-được → in số · khoá-quyền → bỏ trắng · failed → giữ `—`) · gỡ nút Từ chối (chỉ gỡ đường bấm, **máy BE giữ nguyên**) · grid 260/320.
|
||||
|
||||
## Còn nợ
|
||||
|
||||
Đợt B (32 mục style) · đợt D (O6 · O3 · O4 · O7 · O8). Spec đầy đủ: `spec-khkk-style-dong-bo-05-08-2026.md`.
|
||||
|
||||
🔴 **KHÔNG lane nào có screenshot** — dev-rig chặn (chỉ `:8080` LISTENING · API `:5443` không có · trang sau `ProtectedRoute`, gotcha #3). Mọi kết luận là **đọc mã, KHÔNG phải đã nhìn**. Người land BẮT BUỘC soi **375 / 1366 / 1440** trên **prod sau deploy** trước khi tuyên PASS.
|
||||
Thay cho mắt, lane dùng: đo class trên đĩa · `sha256sum` mirror · **tự tính tương-phản WCAG** — bắt được `slate-400` = **2.56:1** cho 6 dòng chữ mang thông tin.
|
||||
|
||||
## Bài giữ lại
|
||||
|
||||
🧪 **Luật C2 (ép ghi khung rỗng lượt 1-2 TRƯỚC khi đọc) có RĂNG THẬT** — run trước cùng khuôn **3/5 lane chết** cháy-lượt; run này **0/5**. Cùng model, cùng harness, khác **thứ tự ghi**.
|
||||
|
||||
END audit-synthesis — VERDICT=SPEC-READY · đợt A ĐÃ LAND (`a1b9cbb`) · đợt B+D còn nợ · 0 screenshot (khai thẳng)
|
||||
@ -20,8 +20,8 @@ mỗi lệch phải có `file:line` CẢ HAI phía; đầu ra = spec thi-hành-
|
||||
| # | Việc | Trạng thái |
|
||||
|---|---|---|
|
||||
| O1 | `KhkkWorkflowPanel` style ≠ `PeWorkflowPanel` (banner · icon tròn · chip NS PRO/CCM · HÀNH ĐỘNG · file đính kèm · lịch sử duyệt) | chưa làm |
|
||||
| O2 | Badge **"SẮP"** trên cây → **đổi thành 0** ("bỏ hết đi đổi thành 0") | chưa làm |
|
||||
| O3 | Hạng mục cho chọn **NHIỀU** + **SUM tổng**; tổng giá duyệt ghi **PHÍA TRÊN** — 🔴 đổi cardinality 1→N, cần BE+migration | chưa làm |
|
||||
| O2 | Badge **"SẮP"** trên cây → **đổi thành 0** ("bỏ hết đi đổi thành 0") | ✅ **LAND `a1b9cbb`** — gỡ `SoonChip` 3 ca 3 cách (đếm-được→số · khoá-quyền→trắng · failed→`—`); deploy verify 2-vế @S175 |
|
||||
| O3 | Hạng mục cho chọn **NHIỀU** + **SUM tổng**; tổng giá duyệt ghi **PHÍA TRÊN** — ✅ **N-ROW 0-MIGRATION** (`ContractSigningPlanLineConfiguration.cs:37-38` unique **3 cột**, máy rộng sẵn từ Mig 71) | chưa làm |
|
||||
| O4 | Shopdrawing / căn cứ hồ sơ cho **đính kèm file** | chưa làm |
|
||||
| O5 | Menu Đang duyệt / Đã duyệt / Đã xóa "lọc đúng trạng thái" — lead đã truy 5/5 mắt xích, **chuỗi lọc ĐÚNG**; nghi vấn thật = `KhkkListPanel:202 flatMode` đổi CÂY→PHẲNG khi có filter | cần phán |
|
||||
| O6 | **Bấm card PE trong cây ở màn KHKK phải GIỮ NGUYÊN màn KHKK** (view tại chỗ), không văng sang phiếu gốc — "tương tự với duyệt NCC" | chưa làm |
|
||||
@ -52,10 +52,27 @@ gỡ cây khỏi Hộp thư. Prod hiện tại: admin `index-BXwdUVmD.js` · eof
|
||||
|
||||
## Stages
|
||||
|
||||
- [ ] L1..L5 chạy song song (hmw parallel)
|
||||
- [ ] lead refute + synthesize (P3/P4)
|
||||
- [ ] lead ghi `spec-khkk-style-dong-bo-05-08-2026.md` — 3 heading: ① đề-bài ② cách implement ③ checklist acceptance ĐO ĐƯỢC
|
||||
- [x] L1..L5 chạy song song (hmw parallel) — `wf_016b1bed-b85`, 5/5 xong · 0 chết · 0 rỗng · 755K tok · 147 tool-use · ~17′
|
||||
- [x] lead refute + synthesize (P3/P4) — tự verify 2 claim nặng nhất, cả 2 lane ĐÚNG (dưới)
|
||||
- [x] lead ghi `spec-khkk-style-dong-bo-05-08-2026.md` — 3 heading đủ
|
||||
|
||||
## Synthesis
|
||||
|
||||
_(lead điền sau khi verify — engine propose-only, KHÔNG tự ghi)_
|
||||
**VERDICT từng lane** (dòng-1 lấy từ END-line của chính sub-file):
|
||||
|
||||
| lane | verdict |
|
||||
|---|---|
|
||||
| L1 panel-duyệt | 15 lệch có-thể-sửa (13 bê-thẳng + 1 bán-bê kèm vá lỗi 401 + 1 chờ-owner) · 1 KHÔNG-BÊ vì thiếu khái niệm (NS PRO/CCM) · H2+H4 đã khớp · **lệch nặng nhất = vỏ `card-accent`, owner KHÔNG nêu** |
|
||||
| L2 panel-list | **CHƯA-PASS** — 13 delta + 3 trục rubric đỏ · O5 = **GIỮ-MÁY-R11-SỬA-HÌNH** (bỏ `term` khỏi `flatMode` + gộp 2 ô tìm + công-tắc Cây/Danh-sách + chip lọc) |
|
||||
| L3 host | **KHỚP-MỘT-PHẦN** — 4 trục khớp · **2 lệch nặng: grid 300→260 · `<lg` ngõ cụt không tạo được phiếu** · 3 lệch vừa · 3 mục CHỐT-KHÔNG-SỬA |
|
||||
| L4 cây | **SHIP-SPEC-READY** — O2: **không GĐ nào còn đúng nghĩa "sắp"**, `SoonChip` không export, 0 host phụ thuộc · O6: đường điều hướng = `usePipelineStages.ts:219` |
|
||||
| L5 nội-dung | **AUDIT-DONE** — O3 = **N-ROW 0-migration** (máy rộng sẵn từ Mig 71) · O4 = **FE-ONLY 0-BE** (rail `dossierItemId` đủ 6 tầng từ Mig 69) · 6 P0 + 8 P1 + 3 P2 |
|
||||
|
||||
**LEAD REFUTE — 2 claim nặng nhất tự đo lại, lane ĐÚNG cả hai:**
|
||||
- `ContractSigningPlanLineConfiguration.cs:37-38` unique index **3 CỘT** `(PlanId, SupplierId, CatalogEntryId)` ⇒ O3 **0-migration**. Lead từng báo owner "nặng nhất, cần migration" — **SAI, đã đính chính**.
|
||||
- KHKK `:501` `<a href>` trần ⟂ PE `:331` `responseType:'blob'` ⇒ **401 xác nhận**, không phải lệch style.
|
||||
|
||||
**ĐÃ LAND ngay trong phiên (đợt A + 2 câu owner chốt)** — commit `a1b9cbb`: vá 401 · vá `<lg` ngõ cụt · trả nút duyệt mobile · gỡ `SoonChip` (3 ca 3 cách) · gỡ nút Từ chối · grid 260/320.
|
||||
|
||||
🔴 **CÒN NỢ:** đợt B (32 mục style) + đợt D (O6·O3·O4·O7·O8). **KHÔNG lane nào có screenshot** — dev-rig chặn (chỉ `:8080` LISTENING, API `:5443` không có, trang sau `ProtectedRoute`, gotcha #3) ⇒ mọi kết luận là **đọc mã, không phải đã nhìn**; người land BẮT BUỘC soi 375/1366/1440 trên prod.
|
||||
🧪 **Luật C2 có răng:** run trước cùng khuôn 3/5 lane chết cháy-lượt; run này **0/5**. Cùng model, cùng harness, khác **thứ tự ghi**.
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
ClipboardList, FileCheck, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users,
|
||||
ClipboardList, FileCheck, Lock, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { PePipelineStrip } from '@/components/pe/PePipelineStrip'
|
||||
@ -48,6 +48,21 @@ function formatVnd(n: number | null | undefined): string {
|
||||
return n.toLocaleString('vi-VN') + ' đ'
|
||||
}
|
||||
|
||||
// [S175 · B2 · P0] Ô RỖNG phải MỜ HƠN dữ-liệu thật. Trước đây em-dash thừa-hưởng đúng
|
||||
// màu chữ của ô (`text-slate-600/700`) ⇒ lướt bảng thì "—" trông y như một GIÁ-TRỊ.
|
||||
// Gom về một chỗ để các ô rỗng không mỗi nơi một kiểu.
|
||||
const DASH = <span className="text-slate-300">—</span>
|
||||
|
||||
/** Chuỗi có thể rỗng → em-dash mờ. */
|
||||
function orDash(v: string | null | undefined): ReactNode {
|
||||
return v == null || v === '' ? DASH : v
|
||||
}
|
||||
|
||||
/** Tiền: `formatVnd` trả CHUỖI "—" khi rỗng nên không tô mờ được — tách nhánh rỗng ra. */
|
||||
function moneyOrDash(n: number | null | undefined): ReactNode {
|
||||
return n == null ? DASH : formatVnd(n)
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
return new Date(iso).toLocaleString('vi-VN')
|
||||
}
|
||||
@ -362,17 +377,32 @@ export function KhkkDetailContent({
|
||||
chipFg="var(--color-teal-700)"
|
||||
actions={
|
||||
plan.budgetFrozen ? (
|
||||
<span className="inline-flex items-center rounded-md border border-amber-300 bg-amber-100 px-2 py-0.5 text-[11px] font-semibold normal-case tracking-normal text-amber-800">
|
||||
🔒 Giá đã chốt tại thời điểm duyệt
|
||||
// [S175 · B2 · P0] emoji 🔒 → icon lucide: emoji render khác nhau theo font hệ
|
||||
// điều-hành (Windows/macOS/Android ra 3 hình), và nó KHÔNG ăn `currentColor`
|
||||
// nên luôn lệch tông với chữ amber bên cạnh.
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-amber-300 bg-amber-100 px-2 py-0.5 text-[11px] font-semibold normal-case tracking-normal text-amber-800">
|
||||
<Lock className="h-3 w-3" aria-hidden /> Giá đã chốt tại thời điểm duyệt
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{plan.lines.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Phiếu nguồn chưa có NCC trúng thầu.</div>
|
||||
<div className="text-sm italic text-slate-400">Phiếu nguồn chưa có NCC trúng thầu.</div>
|
||||
) : (
|
||||
// [S175 · B2 · P0] `table-fixed` + `<colgroup>` + `min-w-[880px]`: bảng co giãn tự-do
|
||||
// trước đây để cột tiền nhảy bề rộng theo độ dài tên NCC ⇒ 3 cột số không thẳng hàng
|
||||
// giữa các phiếu. Chốt bề rộng rồi thì hẹp màn hình sẽ CUỘN NGANG (đúng ý đồ của
|
||||
// `overflow-x-auto` bọc ngoài) thay vì bóp chữ.
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full min-w-[880px] table-fixed text-xs">
|
||||
<colgroup>
|
||||
<col className="w-[22%]" />
|
||||
<col className="w-[22%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-[14%]" />
|
||||
</colgroup>
|
||||
<thead className="border-b border-slate-200 bg-slate-50/70">
|
||||
<tr>
|
||||
<th className="label-eyebrow px-3 py-2 text-left">Nhà cung cấp</th>
|
||||
@ -385,16 +415,16 @@ export function KhkkDetailContent({
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.lines.map((l) => (
|
||||
<tr key={l.id} className="border-b border-slate-100 last:border-0">
|
||||
<tr key={l.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
|
||||
<td className="px-3 py-2 font-medium text-brand-800">
|
||||
{l.supplierName ?? '—'}
|
||||
{orDash(l.supplierName)}
|
||||
{l.contractId && (
|
||||
<a href={`/contracts/${l.contractId}`} className="ml-2 text-[10px] font-medium text-brand-600 hover:underline" title="Dòng này đã được đưa vào hợp đồng">
|
||||
✓ HĐ
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-700">
|
||||
<td className="px-3 py-2 text-slate-700">
|
||||
{isDraft && !l.contractId ? (
|
||||
<>
|
||||
<select
|
||||
@ -426,12 +456,12 @@ export function KhkkDetailContent({
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : l.tenHangMuc ? l.tenHangMuc : <span className="text-slate-300">—</span>}
|
||||
) : orDash(l.tenHangMuc)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-slate-600">{formatVnd(l.peReferenceAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-brand-800">{formatVnd(l.proposedAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-emerald-700">{formatVnd(l.approvedAmount)}</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-600">{l.note ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-slate-600">{moneyOrDash(l.peReferenceAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-brand-800">{moneyOrDash(l.proposedAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-emerald-700">{moneyOrDash(l.approvedAmount)}</td>
|
||||
<td className="px-3 py-2 text-slate-600">{orDash(l.note)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -462,10 +492,18 @@ export function KhkkDetailContent({
|
||||
}
|
||||
>
|
||||
{plan.dossierItems.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Chưa khai căn cứ nào.</div>
|
||||
<div className="text-sm italic text-slate-400">Chưa khai căn cứ nào.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full min-w-[880px] table-fixed text-xs">
|
||||
<colgroup>
|
||||
<col className="w-[14%]" />
|
||||
<col className={isDraft ? 'w-[26%]' : 'w-[30%]'} />
|
||||
<col className="w-[14%]" />
|
||||
<col className={isDraft ? 'w-[16%]' : 'w-[20%]'} />
|
||||
<col className={isDraft ? 'w-[18%]' : 'w-[22%]'} />
|
||||
{isDraft && <col className="w-[12%]" />}
|
||||
</colgroup>
|
||||
<thead className="border-b border-slate-200 bg-slate-50/70">
|
||||
<tr>
|
||||
<th className="label-eyebrow px-3 py-2 text-left">Loại</th>
|
||||
@ -478,19 +516,19 @@ export function KhkkDetailContent({
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.dossierItems.map((d: KhkkDossierItemDto) => (
|
||||
<tr key={d.id} className="border-b border-slate-100 last:border-0">
|
||||
<td className="px-3 py-2 text-xs text-slate-600">{DOSSIER_KIND_LABELS[d.kind] ?? '—'}</td>
|
||||
<tr key={d.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
|
||||
<td className="px-3 py-2 text-slate-600">{orDash(DOSSIER_KIND_LABELS[d.kind])}</td>
|
||||
<td className="px-3 py-2 font-medium text-brand-800">{d.name}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={cn('inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium', DOSSIER_STATUS_BADGE[d.status])}>
|
||||
{DOSSIER_STATUS_LABELS[d.status]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-600">
|
||||
{d.tvgsName ?? '—'}
|
||||
<td className="px-3 py-2 text-slate-600">
|
||||
{orDash(d.tvgsName)}
|
||||
{d.tvgsResultAt && <span className="ml-1 text-[11px] text-slate-400">({formatDateTime(d.tvgsResultAt)})</span>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-600">{d.note ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-slate-600">{orDash(d.note)}</td>
|
||||
{isDraft && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
@ -544,7 +582,7 @@ export function KhkkDetailContent({
|
||||
}
|
||||
>
|
||||
{planAttachments.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Chưa có file đính kèm.</div>
|
||||
<div className="text-sm italic text-slate-400">Chưa có file đính kèm.</div>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{planAttachments.map((a) => (
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
// (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo
|
||||
// "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt.
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { FileCheck, Inbox, Search } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, FileCheck, Inbox, ListTree, Search } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
|
||||
@ -144,8 +144,12 @@ export function KhkkPlanCard({
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
||||
<span className="font-mono">{plan.maKeHoach ?? '(chưa cấp mã)'}</span>
|
||||
{/* [S175 · B3] Chip nhóm: `text-[9px]` là nấc thứ TƯ của thang chữ (13/11/10/9) —
|
||||
gom về 3 nấc chuẩn 13/11/10. Và `inline` + cỡ chữ khác dòng chứa ⇒ hộp chip cao
|
||||
khác chữ bên cạnh nên tụt baseline; `inline-flex items-center leading-none` ghim
|
||||
nó thẳng hàng thay vì dựa vào baseline của 2 cỡ chữ khác nhau. */}
|
||||
{plan.approvalGroup != null && (
|
||||
<span className="rounded border border-violet-200 bg-violet-50 px-1 text-[9px] font-semibold text-violet-700">
|
||||
<span className="inline-flex shrink-0 items-center rounded border border-violet-200 bg-violet-50 px-1 py-0.5 text-[10px] font-semibold leading-none text-violet-700">
|
||||
N{plan.approvalGroup}
|
||||
</span>
|
||||
)}
|
||||
@ -227,7 +231,7 @@ export function KhkkListPanel({
|
||||
const shown = group === null ? plans : plans.filter(k => k.approvalGroup === group)
|
||||
if (shown.length === 0) {
|
||||
return (
|
||||
<p className="px-3 py-2 text-[11px] italic text-slate-400">
|
||||
<p className="px-3 py-2 text-[11px] italic text-slate-500">
|
||||
{group === null ? 'Gói này chưa có kế hoạch ký kết.' : `Gói này chưa có phiếu nhóm N${group}.`}
|
||||
</p>
|
||||
)
|
||||
@ -275,23 +279,42 @@ export function KhkkListPanel({
|
||||
))}
|
||||
</Select>
|
||||
{flatMode && !deletedView && canPe && (
|
||||
<p className="text-[10px] italic text-slate-400">
|
||||
<p className="text-[10px] italic text-slate-500">
|
||||
Đang lọc — danh sách lấy thẳng từ máy chủ (không giới hạn 200 phiếu của cây).
|
||||
</p>
|
||||
)}
|
||||
{!canPe && (
|
||||
<p className="text-[10px] italic text-slate-400">
|
||||
<p className="text-[10px] italic text-slate-500">
|
||||
Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình không dựng được — danh
|
||||
sách kế hoạch ký kết vẫn liệt kê đầy đủ ở dưới.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* [S175 · B3] ĐẦU CỘT — trước đây chế-độ CÂY đi thẳng vào thân, không tiêu-đề cũng
|
||||
không số đếm ⇒ cột "cụt đầu": lệch cả với màn Thao tác (`KhkkWorkListPanel` có
|
||||
`Tier` header) LẪN với chính chế-độ phẳng (có dòng đếm ở chân). Mirror đúng khuôn
|
||||
`Tier` để 2 màn KHKK cùng một hình. */}
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-slate-200 bg-slate-50/80 px-3 py-2">
|
||||
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded bg-brand-100 text-brand-700" aria-hidden>
|
||||
{flatMode ? <FileCheck className="h-3 w-3" /> : <ListTree className="h-3 w-3" />}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-[11px] font-bold uppercase tracking-wide text-slate-600">
|
||||
{deletedView ? 'Phiếu đã xóa' : flatMode ? 'Danh sách kế hoạch' : 'Cây toàn trình'}
|
||||
</span>
|
||||
{flatMode && !list.isLoading && !list.isError && (
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium tabular-nums text-slate-600">
|
||||
{total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{!flatMode && (
|
||||
<PipelineTreePanel
|
||||
currentStage={2}
|
||||
hideHeader
|
||||
hideSearch
|
||||
bodyMaxHClass="max-h-[28rem] lg:max-h-none"
|
||||
stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect }}
|
||||
/>
|
||||
@ -350,22 +373,28 @@ export function KhkkListPanel({
|
||||
<FileCheck className="mr-1 inline h-3 w-3" />
|
||||
{total} kế hoạch — Trang {page} / {totalPages}
|
||||
</span>
|
||||
{/* [S175 · B3] Ký-tự trần `←`/`→` → icon: mũi tên dạng chữ đổi hình theo font hệ
|
||||
điều-hành và KHÔNG có tên cho trình đọc màn hình. Icon + `aria-label` sửa cả hai. */}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPageChange(Math.max(1, page - 1))}
|
||||
disabled={page <= 1}
|
||||
className="rounded border border-slate-300 px-2 py-0.5 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
aria-label="Trang trước"
|
||||
title="Trang trước"
|
||||
className="rounded border border-slate-300 p-1 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
>
|
||||
←
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className="rounded border border-slate-300 px-2 py-0.5 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
aria-label="Trang sau"
|
||||
title="Trang sau"
|
||||
className="rounded border border-slate-300 p-1 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
>
|
||||
→
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -171,7 +171,7 @@ export function KhkkWorkListPanel({
|
||||
</div>
|
||||
)}
|
||||
{!candidates.isLoading && !candidates.isError && awaiting.length === 0 && (
|
||||
<p className="p-3 text-[11px] italic text-slate-400">
|
||||
<p className="p-3 text-[11px] italic text-slate-500">
|
||||
Mọi phiếu đã duyệt đều đã có kế hoạch ký kết.
|
||||
</p>
|
||||
)}
|
||||
@ -181,7 +181,7 @@ export function KhkkWorkListPanel({
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
{projectName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-slate-400">{items.length}</span>
|
||||
<span className="shrink-0 text-[10px] text-slate-500">{items.length}</span>
|
||||
</div>
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{items.map((pe) => (
|
||||
@ -241,7 +241,7 @@ export function KhkkWorkListPanel({
|
||||
</div>
|
||||
)}
|
||||
{!plans.isLoading && !plans.isError && planItems.length === 0 && (
|
||||
<p className="p-3 text-[11px] italic text-slate-400">
|
||||
<p className="p-3 text-[11px] italic text-slate-500">
|
||||
Không còn kế hoạch nào đang xử lý. Phiếu đã duyệt không hiện ở đây — xem tại
|
||||
menu Kế hoạch ký kết HĐ.
|
||||
</p>
|
||||
@ -287,7 +287,7 @@ export function KhkkWorkListPanel({
|
||||
sẽ đọc thành "đang cắt bớt" trong khi thật ra là "đang lọc bỏ". Nói đúng việc
|
||||
đang làm, và chỉ nói khi thật sự có cái bị giấu. */}
|
||||
{plans.data && plans.data.items.length > planItems.length && (
|
||||
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-400">
|
||||
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-500">
|
||||
Đã ẩn kế hoạch ở trạng thái “Đã duyệt” — xem đủ ở menu Kế hoạch ký kết HĐ.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -33,7 +33,8 @@
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { Check, History, Paperclip, Send, Trash2, Undo2, Workflow } from 'lucide-react'
|
||||
import { Download, Eye, History, Paperclip, Send, Trash2 } from 'lucide-react'
|
||||
import { AttachmentPreviewDialog, isPreviewable } from '@/components/pe/AttachmentPreviewDialog'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Dialog } from '@/components/ui/Dialog'
|
||||
import { Label } from '@/components/ui/Label'
|
||||
@ -104,6 +105,8 @@ export function KhkkWorkflowPanel({
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
// [S175 · B1] File đang xem-trước inline (khuôn PE `PeWorkflowPanel.tsx:60`).
|
||||
const [previewAtt, setPreviewAtt] = useState<KhkkAttachmentDto | null>(null)
|
||||
|
||||
const phase = plan.phase as KhkkPhaseValue
|
||||
const steps = plan.workflowSteps ?? []
|
||||
@ -174,15 +177,18 @@ export function KhkkWorkflowPanel({
|
||||
const approvalFiles: KhkkAttachmentDto[] =
|
||||
(plan.attachments ?? []).filter(a => a.purpose === KhkkAttachmentPurpose.ApprovalAttachment)
|
||||
|
||||
// [BE-5] Lịch sử thay đổi — chỉ tải khi người dùng mở khối (không tốn 1 request/phiếu).
|
||||
const [showChangelog, setShowChangelog] = useState(false)
|
||||
// [BE-5] Lịch sử thay đổi.
|
||||
// 🔴 [S175 · B1] Khối này TRƯỚC ĐÂY gập lại và query `enabled: showChangelog` (lazy —
|
||||
// tiết-kiệm 1 request/phiếu). Đợt này bỏ gập theo khuôn PE ⇒ **BẮT BUỘC bỏ luôn cờ
|
||||
// `enabled`**: giữ lại thì khối luôn hiện nhưng query KHÔNG BAO GIỜ chạy, và nó hỏng
|
||||
// dưới dạng "Chưa có thay đổi nào được ghi." — tức trông y hệt phiếu chưa từng sửa,
|
||||
// KHÔNG phải trông như lỗi. Đổi lấy: +1 request mỗi lần mở phiếu, đúng giá PE đang trả.
|
||||
const changelogs = useQuery({
|
||||
queryKey: ['khkk-changelogs', plan.id],
|
||||
queryFn: async () =>
|
||||
(await api.get<KhkkChangelogDto[]>(`/contract-signing-plans/${plan.id}/changelogs`, {
|
||||
params: { take: 200 },
|
||||
})).data,
|
||||
enabled: showChangelog,
|
||||
})
|
||||
|
||||
const transition = useMutation({
|
||||
@ -267,41 +273,44 @@ export function KhkkWorkflowPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card-accent" style={{ ['--accent' as string]: 'var(--color-emerald-500)' }}>
|
||||
// [S175 · B1 · X2] Panel TRẦN khuôn PE (`PeWorkflowPanel.tsx:345`). Trước đây bọc
|
||||
// `card-accent` ⇒ thẻ-trong-thẻ, lệch đập vào mắt trước mọi mục owner liệt kê.
|
||||
// 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`/`shadow-none`: `.card-accent` (`index.css:112`)
|
||||
// nằm NGOÀI `@layer` nên thắng utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật).
|
||||
<div className="space-y-4">
|
||||
{/* ── 1. Header + meta {code} v{NN} · {name} ───────────────────────────── */}
|
||||
<header className="flex flex-wrap items-center gap-2 border-b border-slate-100 px-5 py-3">
|
||||
<span
|
||||
className="icon-chip"
|
||||
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: 'var(--color-emerald-50)', ['--chip-fg' as string]: 'var(--color-emerald-600)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<Workflow className="h-4 w-4" />
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold tracking-tight text-emerald-700">Quy trình duyệt</h3>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
|
||||
{plan.workflowCode && (
|
||||
<span className="font-mono text-[11px] text-slate-500">
|
||||
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
|
||||
{plan.workflowCode}
|
||||
{plan.workflowVersion != null && ` v${plan.workflowVersion}`}
|
||||
{plan.workflowName && <span className="font-sans"> · {plan.workflowName}</span>}
|
||||
</span>
|
||||
{plan.workflowVersion != null && ` v${String(plan.workflowVersion).padStart(2, '0')}`}
|
||||
{plan.workflowName && <> · <span className="font-sans">{plan.workflowName}</span></>}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-5">
|
||||
{/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */}
|
||||
{plan.endedByLevelFinalize && (
|
||||
<div className="rounded border border-violet-300 bg-violet-50 px-3 py-2 text-[11px] text-violet-800">
|
||||
<div className="font-medium">
|
||||
⚑ Kết thúc tại {plan.finalizeStepName ?? 'cấp được cấu hình'}
|
||||
{plan.finalizeLevelName ? ` · ${plan.finalizeLevelName}` : ''}
|
||||
</div>
|
||||
<div className="mt-0.5">
|
||||
{isApproved
|
||||
? 'Kế hoạch đã duyệt xong ngay tại cấp này — không qua CEO / Ban Giám đốc.'
|
||||
: 'Cấp này được cấu hình duyệt là KẾT THÚC — nếu người duyệt tích ô kết thúc thì kế hoạch không qua CEO.'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */}
|
||||
{/* [S175 · B1] violet → EMERALD khuôn PE (`:360-377`). PE dành violet RIÊNG cho
|
||||
"⚑ Duyệt thay CEO"; dùng violet cả ở đây là trộn 2 nghĩa vào một màu. */}
|
||||
{plan.endedByLevelFinalize && (
|
||||
<div className="rounded-md border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
|
||||
{isApproved ? (
|
||||
<span>
|
||||
✅ Kế hoạch đã kết thúc tại{' '}
|
||||
<span className="font-semibold">{plan.finalizeStepName ?? 'cấp duyệt'}</span>
|
||||
{plan.finalizeLevelName ? <> · {plan.finalizeLevelName}</> : null} — không qua CEO.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
⚑ Quy trình rút gọn: duyệt đến{' '}
|
||||
<span className="font-semibold">{plan.finalizeStepName ?? 'cấp này'}</span>
|
||||
{plan.finalizeLevelName ? <> · {plan.finalizeLevelName}</> : null} là KẾT THÚC,
|
||||
không trình CEO. Các cấp trước vẫn duyệt như thường.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 3. Sơ đồ Bước → Cấp → NV ───────────────────────────────────────── */}
|
||||
{steps.length > 0 ? (
|
||||
@ -317,9 +326,10 @@ export function KhkkWorkflowPanel({
|
||||
{STATUS_ICON[st]}
|
||||
</span>
|
||||
<span className="text-slate-800">Bước {step.order} — {step.name}</span>
|
||||
{/* Chip PHÒNG của Bước (BE-4 join `Departments`). */}
|
||||
{/* Chip PHÒNG của Bước (BE-4 join `Departments`).
|
||||
[S175 · B1] viền-xám-nền-trắng → EMERALD đặc khuôn PE (`:406-410`). */}
|
||||
{step.departmentName && (
|
||||
<span className="rounded border border-slate-300 bg-white px-1.5 py-0.5 text-[10px] font-medium text-slate-600">
|
||||
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
|
||||
{step.departmentName}
|
||||
</span>
|
||||
)}
|
||||
@ -458,11 +468,11 @@ export function KhkkWorkflowPanel({
|
||||
}}
|
||||
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Duyệt cấp hiện tại'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
'rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
blockedByLevel ? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400' : 'border-emerald-300 text-emerald-700 hover:bg-emerald-50',
|
||||
)}
|
||||
>
|
||||
<Check className="h-3 w-3" /> Duyệt
|
||||
✓ Duyệt
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -470,11 +480,11 @@ export function KhkkWorkflowPanel({
|
||||
onClick={() => { if (!blockedByLevel) { setComment(''); setPendingFiles([]); setAction(KhkkTransitionAction.Return) } }}
|
||||
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Trả lại người soạn sửa'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
'rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
blockedByLevel ? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400' : 'border-amber-300 text-amber-700 hover:bg-amber-50',
|
||||
)}
|
||||
>
|
||||
<Undo2 className="h-3 w-3" /> Trả lại
|
||||
← Trả lại
|
||||
</button>
|
||||
{/* 🧊 [S174 owner] Nút "TỪ CHỐI" ĐÃ GỠ — anh chốt "vậy thì bỏ luôn từ chối
|
||||
~ tương tự PE". Duyệt NCC có code từ-chối nhưng THỰC TẾ không hiện
|
||||
@ -488,7 +498,8 @@ export function KhkkWorkflowPanel({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
className="inline-flex items-center gap-1 rounded border border-slate-300 px-2 py-1 text-[11px] font-bold text-slate-600 transition hover:border-red-300 hover:bg-red-50 hover:text-red-700"
|
||||
title="Xóa phiếu này (phiếu chuyển sang mục “Đã xóa”)"
|
||||
className="inline-flex items-center gap-1 rounded border border-red-300 bg-white px-2 py-1 text-[11px] font-bold text-red-700 transition hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" /> Xóa phiếu
|
||||
</button>
|
||||
@ -498,32 +509,52 @@ export function KhkkWorkflowPanel({
|
||||
)}
|
||||
|
||||
{/* ── 5. 📎 File đính kèm khi duyệt — TỰ ẨN khi rỗng (khuôn PE :928-967) ── */}
|
||||
{/* [S175 · B1] +`border-t pt-4` +`<h3 text-sm>` +nút `<Eye>` xem trước.
|
||||
Route BE ĐÃ CÓ `ContractSigningPlansController.cs:231` (`/view`, cùng hình với
|
||||
PE `:317`) ⇒ chỉ cần truyền `basePath` cho dialog dùng chung.
|
||||
🔴 Nhãn "Tải xuống" chuyển vào `title=` chứ KHÔNG bỏ: giữ đúng chuỗi đó trong
|
||||
bundle để phép verify prod còn control-dương soi được. */}
|
||||
{approvalFiles.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
|
||||
<Paperclip className="h-3.5 w-3.5 text-teal-500" />
|
||||
File đính kèm khi duyệt
|
||||
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">
|
||||
{approvalFiles.length}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-1.5 space-y-1.5">
|
||||
<div className="border-t border-slate-200 pt-4">
|
||||
<h3 className="text-sm font-semibold text-slate-900">📎 File đính kèm khi duyệt</h3>
|
||||
<p className="mt-0.5 text-[11px] text-slate-500">File do người duyệt tải lên trong quá trình duyệt.</p>
|
||||
<div className="mt-2 space-y-1">
|
||||
{approvalFiles.map(a => (
|
||||
<li key={a.id} className="flex items-center justify-between gap-2 rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-slate-700">{a.fileName}</div>
|
||||
<div className="text-slate-500">{formatSize(a.fileSize)} · {formatDateTime(a.createdAt)}</div>
|
||||
</div>
|
||||
<div key={a.id} className="flex items-center gap-1.5 rounded bg-slate-50 px-1.5 py-1 text-[11px]">
|
||||
<Paperclip className="h-3 w-3 shrink-0 text-slate-400" />
|
||||
<span className="min-w-0 flex-1 truncate text-slate-700" title={a.fileName}>{a.fileName}</span>
|
||||
<span className="shrink-0 text-[10px] text-slate-400">{formatSize(a.fileSize)}</span>
|
||||
{isPreviewable(a.fileName) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewAtt(a)}
|
||||
className="shrink-0 rounded px-1 text-violet-600 hover:bg-violet-50"
|
||||
title="Xem trước"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadAttachment(a)}
|
||||
className="shrink-0 text-[11px] font-medium text-brand-600 hover:underline"
|
||||
className="shrink-0 rounded px-1 text-brand-600 hover:bg-brand-50"
|
||||
title="Tải xuống"
|
||||
>
|
||||
Tải xuống
|
||||
<Download className="h-3 w-3" />
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{previewAtt && (
|
||||
<AttachmentPreviewDialog
|
||||
open
|
||||
basePath="/contract-signing-plans"
|
||||
evaluationId={plan.id}
|
||||
attachmentId={previewAtt.id}
|
||||
fileName={previewAtt.fileName}
|
||||
onClose={() => setPreviewAtt(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -531,61 +562,63 @@ export function KhkkWorkflowPanel({
|
||||
{/* [B4/D-11] BE ĐÃ LỌC hàng `decision = Pending` (hành-vi GỬI duyệt) ⇒ số (N) ở đây
|
||||
CÙNG NGHĨA với (N) của Duyệt NCC. FE KHÔNG dedupe lại; sự kiện "Gửi duyệt" vẫn
|
||||
còn nguyên ở "Lịch sử thay đổi" bên dưới. */}
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
|
||||
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
||||
Lịch sử duyệt
|
||||
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">
|
||||
{approvals.length}
|
||||
</span>
|
||||
</div>
|
||||
{/* [S175 · B1] Bỏ pill đếm → `(n)` trong ngoặc · thẻ `bg-slate-50/60 text-[11px]`
|
||||
→ `bg-white p-3 text-sm` · mốc-giờ ĐẨY PHẢI bằng `justify-between`.
|
||||
🔴 Badge 2-ngả → 3-NGẢ: trước đây MỌI thứ không phải `Reject` đều tô emerald
|
||||
⇒ "Trả lại" (amber ở PE) bị vẽ xanh như đã-duyệt — đọc ngược nghĩa.
|
||||
`toPhase` đã có sẵn (`khkk.ts:307`) nên phân được TraLai ⟂ TuChoi ⟂ Duyệt. */}
|
||||
<div className="border-t border-slate-200 pt-4">
|
||||
<h3 className="text-sm font-semibold text-slate-900">Lịch sử duyệt ({approvals.length})</h3>
|
||||
{approvals.length === 0 ? (
|
||||
<p className="mt-1.5 text-[11px] italic text-slate-400">Chưa có lượt duyệt nào.</p>
|
||||
) : (
|
||||
<ul className="mt-1.5 space-y-1.5">
|
||||
{approvals.map(a => (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]">
|
||||
<div className="flex flex-wrap items-center gap-x-1.5 text-slate-700">
|
||||
<span className={cn(
|
||||
'rounded px-1.5 py-0.5 text-[9px] font-semibold',
|
||||
a.decision === KhkkApprovalDecision.Reject ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800',
|
||||
)}>
|
||||
{DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'}
|
||||
</span>
|
||||
<span className="font-medium">{a.approvedByFullName ?? '(hệ thống)'}</span>
|
||||
<span className="text-slate-400">·</span>
|
||||
<span className="text-slate-500">{formatDateTime(a.approvedAt)}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-slate-600">
|
||||
{a.stepName ? `Bước ${a.stepOrder} — ${a.stepName}` : 'Cấp duyệt'}
|
||||
{a.levelOrder != null && ` · Cấp ${a.levelOrder}`}
|
||||
{a.levelName ? ` (${a.levelName})` : ''}
|
||||
<span className="text-slate-400">
|
||||
{' '}· {KHKK_PHASE_LABELS[a.fromPhase]} → {KHKK_PHASE_LABELS[a.toPhase]}
|
||||
</span>
|
||||
</div>
|
||||
{a.comment?.trim() && (
|
||||
<div className="mt-0.5 whitespace-pre-wrap text-slate-600">{a.comment}</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{approvals.map(a => {
|
||||
const isReject = a.toPhase === KhkkPhase.TuChoi
|
||||
const isReturn = a.toPhase === KhkkPhase.TraLai
|
||||
return (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-slate-700">
|
||||
<div className="flex flex-wrap items-center gap-x-1.5">
|
||||
<span className={cn(
|
||||
'rounded px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
isReject ? 'bg-rose-100 text-rose-800'
|
||||
: isReturn ? 'bg-amber-100 text-amber-800'
|
||||
: 'bg-emerald-100 text-emerald-800',
|
||||
)}>
|
||||
{DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'}
|
||||
</span>
|
||||
<span className="font-medium">{a.approvedByFullName ?? '(hệ thống)'}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-500">{formatDateTime(a.approvedAt)}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] text-slate-600">
|
||||
{a.stepName ? `Bước ${a.stepOrder} — ${a.stepName}` : 'Cấp duyệt'}
|
||||
{a.levelOrder != null && ` · Cấp ${a.levelOrder}`}
|
||||
{a.levelName ? ` (${a.levelName})` : ''}
|
||||
<span className="text-slate-400">
|
||||
{' '}· {KHKK_PHASE_LABELS[a.fromPhase]} → {KHKK_PHASE_LABELS[a.toPhase]}
|
||||
</span>
|
||||
</div>
|
||||
{a.comment?.trim() && (
|
||||
<div className="mt-1 whitespace-pre-wrap text-[12px] text-slate-600">{a.comment}</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 7. Lịch sử thay đổi ────────────────────────────────────────────── */}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowChangelog(v => !v)}
|
||||
className="flex w-full items-center gap-1.5 text-xs font-semibold text-slate-700 hover:text-brand-700"
|
||||
>
|
||||
{/* [S175 · B1] BỎ GẬP — PE render thẳng (`PeDetailTabs.tsx:787-794`). Nội-dung
|
||||
đã tải sẵn qua query; giấu sau 1 cú bấm chỉ thêm ma-sát, không tiết-kiệm gì. */}
|
||||
<div className="border-t border-slate-200 pt-4">
|
||||
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-slate-900">
|
||||
<History className="h-3.5 w-3.5 text-slate-400" />
|
||||
Lịch sử thay đổi
|
||||
<span className="ml-auto text-[10px] font-normal text-slate-400">{showChangelog ? 'Ẩn' : 'Xem'}</span>
|
||||
</button>
|
||||
{showChangelog && (
|
||||
<div className="mt-1.5">
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
{changelogs.isLoading && <p className="text-[11px] text-slate-400">Đang tải…</p>}
|
||||
{changelogs.isError && <p className="text-[11px] text-red-600">Không tải được lịch sử thay đổi.</p>}
|
||||
{changelogs.data && changelogs.data.length === 0 && (
|
||||
@ -614,10 +647,8 @@ export function KhkkWorkflowPanel({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{action !== null && (
|
||||
<Dialog
|
||||
@ -625,7 +656,7 @@ export function KhkkWorkflowPanel({
|
||||
onClose={() => setAction(null)}
|
||||
title={dialogTitle}
|
||||
footer={<>
|
||||
<Button variant="outline" onClick={() => setAction(null)}>Huỷ</Button>
|
||||
<Button variant="outline" onClick={() => setAction(null)}>Hủy</Button>
|
||||
<Button onClick={() => transition.mutate(action)} disabled={confirmDisabled}>
|
||||
{transition.isPending ? 'Đang gửi...' : 'Xác nhận'}
|
||||
</Button>
|
||||
@ -663,22 +694,26 @@ export function KhkkWorkflowPanel({
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{/* [S175 · B1 micro-copy] Nhãn Ô NHẬP theo PE (`PeWorkflowPanel.tsx:869` "Ghi chú
|
||||
(tùy chọn)"). 🔴 CHỈ đổi tên TRƯỜNG — chuỗi "Ý kiến cấp duyệt" ở dòng trợ-giúp
|
||||
bên dưới là TÊN THẬT của mục đích đến trong phiếu (Section 5, Mig 26), đổi nó
|
||||
là trỏ người dùng tới một mục không tồn tại. */}
|
||||
<Label>
|
||||
Ý kiến{commentRequired ? <span className="text-red-500"> *</span> : ' (tuỳ chọn)'}
|
||||
Ghi chú{commentRequired ? <span className="text-red-500"> *</span> : ' (tùy chọn)'}
|
||||
</Label>
|
||||
<Textarea
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={2000}
|
||||
placeholder={commentRequired ? 'Nêu lý do trả lại / từ chối...' : 'Ý kiến của bạn (ghi vào phiếu)...'}
|
||||
placeholder={commentRequired ? 'Nêu lý do trả lại / từ chối...' : 'Ghi chú của bạn (ghi vào phiếu)...'}
|
||||
/>
|
||||
|
||||
{/* [QĐ-3 / BE-7] File đính kèm khi duyệt — upload THẬT qua đường riêng
|
||||
`POST {id}/approval-attachments` (BE ép `purpose=ApprovalAttachment`). */}
|
||||
{action !== KhkkTransitionAction.Submit && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-xs">File đính kèm (tuỳ chọn)</Label>
|
||||
<Label className="text-xs">File đính kèm (tùy chọn)</Label>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@ -714,7 +749,7 @@ export function KhkkWorkflowPanel({
|
||||
|
||||
{action === KhkkTransitionAction.Approve && (
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
Ý kiến sẽ được ghi vào mục “Ý kiến cấp duyệt” của kế hoạch (1 ô/Cấp — duyệt lại
|
||||
Ghi chú sẽ được ghi vào mục “Ý kiến cấp duyệt” của kế hoạch (1 ô/Cấp — duyệt lại
|
||||
thì ghi đè ô cũ).
|
||||
</p>
|
||||
)}
|
||||
@ -727,7 +762,7 @@ export function KhkkWorkflowPanel({
|
||||
onClose={() => setConfirmDelete(false)}
|
||||
title="🗑 Xóa kế hoạch ký kết"
|
||||
footer={<>
|
||||
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Huỷ</Button>
|
||||
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Hủy</Button>
|
||||
<Button onClick={() => removePlan.mutate()} disabled={removePlan.isPending}>
|
||||
{removePlan.isPending ? 'Đang xóa...' : 'Xóa phiếu'}
|
||||
</Button>
|
||||
@ -739,6 +774,6 @@ export function KhkkWorkflowPanel({
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -15,10 +15,15 @@ export function isPreviewable(fileName: string): boolean {
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
/** Id của bản-ghi CHỦ (phiếu PE, hoặc kế-hoạch KHKK khi truyền `basePath`). */
|
||||
evaluationId: string
|
||||
attachmentId: string
|
||||
fileName: string
|
||||
onClose: () => void
|
||||
/** [S175 B1] Prefix route của module chủ. Mặc-định `/purchase-evaluations` ⇒ **6 call-site
|
||||
* PE hiện có KHÔNG phải sửa dòng nào** (module đã golive — mở trục mới mà bắt sửa hết
|
||||
* call-site cũ là tự đẻ rủi-ro ở chỗ đang chạy tốt). KHKK truyền `/contract-signing-plans`. */
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
/** Preview file inline qua BE endpoint `/view` (Content-Disposition: inline).
|
||||
@ -29,6 +34,7 @@ type Props = {
|
||||
* cỡ viewport (Dialog dùng chung chỉ có sm/md/lg → tự render overlay riêng). */
|
||||
export function AttachmentPreviewDialog({
|
||||
open, evaluationId, attachmentId, fileName, onClose,
|
||||
basePath = '/purchase-evaluations',
|
||||
}: Props) {
|
||||
const [blobUrl, setBlobUrl] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@ -58,7 +64,7 @@ export function AttachmentPreviewDialog({
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
api.get(`/purchase-evaluations/${evaluationId}/attachments/${attachmentId}/view`, {
|
||||
api.get(`${basePath}/${evaluationId}/attachments/${attachmentId}/view`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then(res => {
|
||||
@ -78,7 +84,7 @@ export function AttachmentPreviewDialog({
|
||||
if (currentUrl) window.URL.revokeObjectURL(currentUrl)
|
||||
setBlobUrl(null)
|
||||
}
|
||||
}, [open, evaluationId, attachmentId])
|
||||
}, [open, evaluationId, attachmentId, basePath])
|
||||
|
||||
const ready = blobUrl && !loading && !error
|
||||
|
||||
|
||||
@ -89,6 +89,7 @@ export function PipelineTreePanel({
|
||||
bodyMaxHClass,
|
||||
stage2Options,
|
||||
hideHeader,
|
||||
hideSearch,
|
||||
}: {
|
||||
currentStage: PipelineStageNo
|
||||
className?: string
|
||||
@ -107,6 +108,12 @@ export function PipelineTreePanel({
|
||||
// 🔴 Nút "Hiện/Ẩn cây" (mobile) sống TRONG thanh này và là đường DUY NHẤT mở thân cây khi
|
||||
// `<lg`. Vì vậy ẩn thanh ⇒ PHẢI ép thân cây hiện luôn, nếu không mobile mất trắng cây.
|
||||
hideHeader?: boolean
|
||||
// [S175 · B3] Ẩn ô lọc RIÊNG của cây. CHỈ dùng ở host đã có ô tìm của chính nó ngay trên
|
||||
// (KHKK GĐ2) — ở đó 2 ô tìm xếp chồng nhau mà nghĩa lại khác: ô ngoài đi SERVER-SIDE và
|
||||
// gõ vào là chuyển hẳn sang danh-sách phẳng (`flatMode`), nên ô trong cây KHÔNG bao giờ
|
||||
// dùng tới — người dùng gõ phát nào là cây biến mất phát đó.
|
||||
// 6 host còn lại KHÔNG truyền ⇒ giữ nguyên ô lọc cây như cũ.
|
||||
hideSearch?: boolean
|
||||
}) {
|
||||
const { canPe, buildStages } = usePipelineStages()
|
||||
const [search, setSearch] = useState('')
|
||||
@ -232,16 +239,18 @@ export function PipelineTreePanel({
|
||||
|
||||
{/* [S174] `hideHeader` ⇒ không còn nút toggle ⇒ ép `flex` để `<lg` vẫn thấy cây. */}
|
||||
<div className={cn('min-h-0 flex-col', hideHeader || mobileOpen ? 'flex' : 'hidden', 'lg:flex')}>
|
||||
<div className="relative border-b border-slate-200 p-2">
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Lọc dự án / hạng mục / mã phiếu…"
|
||||
aria-label="Lọc cây toàn trình"
|
||||
className="h-8 w-full rounded-md border border-slate-200 bg-white pl-7 pr-2 text-[12px] text-slate-700 placeholder:text-slate-400 focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
||||
/>
|
||||
</div>
|
||||
{!hideSearch && (
|
||||
<div className="relative border-b border-slate-200 p-2">
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Lọc dự án / hạng mục / mã phiếu…"
|
||||
aria-label="Lọc cây toàn trình"
|
||||
className="h-8 w-full rounded-md border border-slate-200 bg-white pl-7 pr-2 text-[12px] text-slate-700 placeholder:text-slate-400 focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cn('overflow-y-auto', bodyMaxHClass ?? 'max-h-[28rem] lg:max-h-[calc(100vh-16rem)]')}>
|
||||
{!canPe && (
|
||||
|
||||
@ -3,8 +3,14 @@
|
||||
//
|
||||
// [S174 owner] ĐỔI TỪ TRANG-GIẤY-CUỘN SANG HOST 2-PANEL — anh chốt: **"làm giống giao diện
|
||||
// thao tác của Duyệt NCC"** (`pages/pe/PurchaseEvaluationWorkspacePage.tsx`). Khuôn bê về:
|
||||
// · header 1 dòng + grid `lg:grid-cols-[300px_1fr]`;
|
||||
// · Panel 1 = danh sách (`KhkkWorkListPanel`) + nút hành-động dính đáy;
|
||||
// · header 1 dòng + grid `lg:grid-cols-[260px_1fr] xl:grid-cols-[320px_1fr]` — ĐÚNG BYTE
|
||||
// khuôn PE (`PurchaseEvaluationWorkspacePage.tsx:80`). 🧊 Comment cũ ghi `300px` là số
|
||||
// của bản nháp đầu, KHÔNG bao giờ khớp code sau khi đợt A land 260/320 (S175 vá).
|
||||
// · Panel 1 = danh sách (`KhkkWorkListPanel`) — 2 tầng, MỖI tầng có header dính ĐỈNH
|
||||
// (`sticky top-0`) gom theo dự án. 🧊 Comment cũ ghi "nút hành-động dính đáy" là MÔ TẢ
|
||||
// CỦA PE, không phải của đây: `grep -c Button KhkkWorkListPanel` = **0** (control-dương
|
||||
// `PeListPanel` = 8). Bê nguyên câu mô tả từ khuôn nguồn sang là tự khai một thứ chưa
|
||||
// dựng — S175 vá theo L3 H-5.
|
||||
// · Panel 2 = EmptyState | form tạo | phiếu đang XEM.
|
||||
// 🔴 Panel 1 chia 2 TẦNG theo đúng ảnh anh khoanh: trên = "Cột danh sách Duyệt NCC"
|
||||
// (bấm ⇒ *tạo luôn KHKK*), dưới = "Cột KHKK đã tạo" (bấm ⇒ *chỉ xem*).
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
ClipboardList, FileCheck, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users,
|
||||
ClipboardList, FileCheck, Lock, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { PePipelineStrip } from '@/components/pe/PePipelineStrip'
|
||||
@ -48,6 +48,21 @@ function formatVnd(n: number | null | undefined): string {
|
||||
return n.toLocaleString('vi-VN') + ' đ'
|
||||
}
|
||||
|
||||
// [S175 · B2 · P0] Ô RỖNG phải MỜ HƠN dữ-liệu thật. Trước đây em-dash thừa-hưởng đúng
|
||||
// màu chữ của ô (`text-slate-600/700`) ⇒ lướt bảng thì "—" trông y như một GIÁ-TRỊ.
|
||||
// Gom về một chỗ để các ô rỗng không mỗi nơi một kiểu.
|
||||
const DASH = <span className="text-slate-300">—</span>
|
||||
|
||||
/** Chuỗi có thể rỗng → em-dash mờ. */
|
||||
function orDash(v: string | null | undefined): ReactNode {
|
||||
return v == null || v === '' ? DASH : v
|
||||
}
|
||||
|
||||
/** Tiền: `formatVnd` trả CHUỖI "—" khi rỗng nên không tô mờ được — tách nhánh rỗng ra. */
|
||||
function moneyOrDash(n: number | null | undefined): ReactNode {
|
||||
return n == null ? DASH : formatVnd(n)
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
return new Date(iso).toLocaleString('vi-VN')
|
||||
}
|
||||
@ -362,17 +377,32 @@ export function KhkkDetailContent({
|
||||
chipFg="var(--color-teal-700)"
|
||||
actions={
|
||||
plan.budgetFrozen ? (
|
||||
<span className="inline-flex items-center rounded-md border border-amber-300 bg-amber-100 px-2 py-0.5 text-[11px] font-semibold normal-case tracking-normal text-amber-800">
|
||||
🔒 Giá đã chốt tại thời điểm duyệt
|
||||
// [S175 · B2 · P0] emoji 🔒 → icon lucide: emoji render khác nhau theo font hệ
|
||||
// điều-hành (Windows/macOS/Android ra 3 hình), và nó KHÔNG ăn `currentColor`
|
||||
// nên luôn lệch tông với chữ amber bên cạnh.
|
||||
<span className="inline-flex items-center gap-1 rounded-md border border-amber-300 bg-amber-100 px-2 py-0.5 text-[11px] font-semibold normal-case tracking-normal text-amber-800">
|
||||
<Lock className="h-3 w-3" aria-hidden /> Giá đã chốt tại thời điểm duyệt
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{plan.lines.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Phiếu nguồn chưa có NCC trúng thầu.</div>
|
||||
<div className="text-sm italic text-slate-400">Phiếu nguồn chưa có NCC trúng thầu.</div>
|
||||
) : (
|
||||
// [S175 · B2 · P0] `table-fixed` + `<colgroup>` + `min-w-[880px]`: bảng co giãn tự-do
|
||||
// trước đây để cột tiền nhảy bề rộng theo độ dài tên NCC ⇒ 3 cột số không thẳng hàng
|
||||
// giữa các phiếu. Chốt bề rộng rồi thì hẹp màn hình sẽ CUỘN NGANG (đúng ý đồ của
|
||||
// `overflow-x-auto` bọc ngoài) thay vì bóp chữ.
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full min-w-[880px] table-fixed text-xs">
|
||||
<colgroup>
|
||||
<col className="w-[22%]" />
|
||||
<col className="w-[22%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-[14%]" />
|
||||
</colgroup>
|
||||
<thead className="border-b border-slate-200 bg-slate-50/70">
|
||||
<tr>
|
||||
<th className="label-eyebrow px-3 py-2 text-left">Nhà cung cấp</th>
|
||||
@ -385,16 +415,16 @@ export function KhkkDetailContent({
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.lines.map((l) => (
|
||||
<tr key={l.id} className="border-b border-slate-100 last:border-0">
|
||||
<tr key={l.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
|
||||
<td className="px-3 py-2 font-medium text-brand-800">
|
||||
{l.supplierName ?? '—'}
|
||||
{orDash(l.supplierName)}
|
||||
{l.contractId && (
|
||||
<a href={`/contracts/${l.contractId}`} className="ml-2 text-[10px] font-medium text-brand-600 hover:underline" title="Dòng này đã được đưa vào hợp đồng">
|
||||
✓ HĐ
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-700">
|
||||
<td className="px-3 py-2 text-slate-700">
|
||||
{isDraft && !l.contractId ? (
|
||||
<>
|
||||
<select
|
||||
@ -426,12 +456,12 @@ export function KhkkDetailContent({
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : l.tenHangMuc ? l.tenHangMuc : <span className="text-slate-300">—</span>}
|
||||
) : orDash(l.tenHangMuc)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-slate-600">{formatVnd(l.peReferenceAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-brand-800">{formatVnd(l.proposedAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-emerald-700">{formatVnd(l.approvedAmount)}</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-600">{l.note ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-slate-600">{moneyOrDash(l.peReferenceAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-brand-800">{moneyOrDash(l.proposedAmount)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums text-emerald-700">{moneyOrDash(l.approvedAmount)}</td>
|
||||
<td className="px-3 py-2 text-slate-600">{orDash(l.note)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -462,10 +492,18 @@ export function KhkkDetailContent({
|
||||
}
|
||||
>
|
||||
{plan.dossierItems.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Chưa khai căn cứ nào.</div>
|
||||
<div className="text-sm italic text-slate-400">Chưa khai căn cứ nào.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full min-w-[880px] table-fixed text-xs">
|
||||
<colgroup>
|
||||
<col className="w-[14%]" />
|
||||
<col className={isDraft ? 'w-[26%]' : 'w-[30%]'} />
|
||||
<col className="w-[14%]" />
|
||||
<col className={isDraft ? 'w-[16%]' : 'w-[20%]'} />
|
||||
<col className={isDraft ? 'w-[18%]' : 'w-[22%]'} />
|
||||
{isDraft && <col className="w-[12%]" />}
|
||||
</colgroup>
|
||||
<thead className="border-b border-slate-200 bg-slate-50/70">
|
||||
<tr>
|
||||
<th className="label-eyebrow px-3 py-2 text-left">Loại</th>
|
||||
@ -478,19 +516,19 @@ export function KhkkDetailContent({
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.dossierItems.map((d: KhkkDossierItemDto) => (
|
||||
<tr key={d.id} className="border-b border-slate-100 last:border-0">
|
||||
<td className="px-3 py-2 text-xs text-slate-600">{DOSSIER_KIND_LABELS[d.kind] ?? '—'}</td>
|
||||
<tr key={d.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
|
||||
<td className="px-3 py-2 text-slate-600">{orDash(DOSSIER_KIND_LABELS[d.kind])}</td>
|
||||
<td className="px-3 py-2 font-medium text-brand-800">{d.name}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={cn('inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium', DOSSIER_STATUS_BADGE[d.status])}>
|
||||
{DOSSIER_STATUS_LABELS[d.status]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-600">
|
||||
{d.tvgsName ?? '—'}
|
||||
<td className="px-3 py-2 text-slate-600">
|
||||
{orDash(d.tvgsName)}
|
||||
{d.tvgsResultAt && <span className="ml-1 text-[11px] text-slate-400">({formatDateTime(d.tvgsResultAt)})</span>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-600">{d.note ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-slate-600">{orDash(d.note)}</td>
|
||||
{isDraft && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
@ -544,7 +582,7 @@ export function KhkkDetailContent({
|
||||
}
|
||||
>
|
||||
{planAttachments.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">Chưa có file đính kèm.</div>
|
||||
<div className="text-sm italic text-slate-400">Chưa có file đính kèm.</div>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{planAttachments.map((a) => (
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
// (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo
|
||||
// "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt.
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { FileCheck, Inbox, Search } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, FileCheck, Inbox, ListTree, Search } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
|
||||
@ -144,8 +144,12 @@ export function KhkkPlanCard({
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
||||
<span className="font-mono">{plan.maKeHoach ?? '(chưa cấp mã)'}</span>
|
||||
{/* [S175 · B3] Chip nhóm: `text-[9px]` là nấc thứ TƯ của thang chữ (13/11/10/9) —
|
||||
gom về 3 nấc chuẩn 13/11/10. Và `inline` + cỡ chữ khác dòng chứa ⇒ hộp chip cao
|
||||
khác chữ bên cạnh nên tụt baseline; `inline-flex items-center leading-none` ghim
|
||||
nó thẳng hàng thay vì dựa vào baseline của 2 cỡ chữ khác nhau. */}
|
||||
{plan.approvalGroup != null && (
|
||||
<span className="rounded border border-violet-200 bg-violet-50 px-1 text-[9px] font-semibold text-violet-700">
|
||||
<span className="inline-flex shrink-0 items-center rounded border border-violet-200 bg-violet-50 px-1 py-0.5 text-[10px] font-semibold leading-none text-violet-700">
|
||||
N{plan.approvalGroup}
|
||||
</span>
|
||||
)}
|
||||
@ -227,7 +231,7 @@ export function KhkkListPanel({
|
||||
const shown = group === null ? plans : plans.filter(k => k.approvalGroup === group)
|
||||
if (shown.length === 0) {
|
||||
return (
|
||||
<p className="px-3 py-2 text-[11px] italic text-slate-400">
|
||||
<p className="px-3 py-2 text-[11px] italic text-slate-500">
|
||||
{group === null ? 'Gói này chưa có kế hoạch ký kết.' : `Gói này chưa có phiếu nhóm N${group}.`}
|
||||
</p>
|
||||
)
|
||||
@ -275,23 +279,42 @@ export function KhkkListPanel({
|
||||
))}
|
||||
</Select>
|
||||
{flatMode && !deletedView && canPe && (
|
||||
<p className="text-[10px] italic text-slate-400">
|
||||
<p className="text-[10px] italic text-slate-500">
|
||||
Đang lọc — danh sách lấy thẳng từ máy chủ (không giới hạn 200 phiếu của cây).
|
||||
</p>
|
||||
)}
|
||||
{!canPe && (
|
||||
<p className="text-[10px] italic text-slate-400">
|
||||
<p className="text-[10px] italic text-slate-500">
|
||||
Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình không dựng được — danh
|
||||
sách kế hoạch ký kết vẫn liệt kê đầy đủ ở dưới.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* [S175 · B3] ĐẦU CỘT — trước đây chế-độ CÂY đi thẳng vào thân, không tiêu-đề cũng
|
||||
không số đếm ⇒ cột "cụt đầu": lệch cả với màn Thao tác (`KhkkWorkListPanel` có
|
||||
`Tier` header) LẪN với chính chế-độ phẳng (có dòng đếm ở chân). Mirror đúng khuôn
|
||||
`Tier` để 2 màn KHKK cùng một hình. */}
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-slate-200 bg-slate-50/80 px-3 py-2">
|
||||
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded bg-brand-100 text-brand-700" aria-hidden>
|
||||
{flatMode ? <FileCheck className="h-3 w-3" /> : <ListTree className="h-3 w-3" />}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-[11px] font-bold uppercase tracking-wide text-slate-600">
|
||||
{deletedView ? 'Phiếu đã xóa' : flatMode ? 'Danh sách kế hoạch' : 'Cây toàn trình'}
|
||||
</span>
|
||||
{flatMode && !list.isLoading && !list.isError && (
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium tabular-nums text-slate-600">
|
||||
{total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{!flatMode && (
|
||||
<PipelineTreePanel
|
||||
currentStage={2}
|
||||
hideHeader
|
||||
hideSearch
|
||||
bodyMaxHClass="max-h-[28rem] lg:max-h-none"
|
||||
stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect }}
|
||||
/>
|
||||
@ -350,22 +373,28 @@ export function KhkkListPanel({
|
||||
<FileCheck className="mr-1 inline h-3 w-3" />
|
||||
{total} kế hoạch — Trang {page} / {totalPages}
|
||||
</span>
|
||||
{/* [S175 · B3] Ký-tự trần `←`/`→` → icon: mũi tên dạng chữ đổi hình theo font hệ
|
||||
điều-hành và KHÔNG có tên cho trình đọc màn hình. Icon + `aria-label` sửa cả hai. */}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPageChange(Math.max(1, page - 1))}
|
||||
disabled={page <= 1}
|
||||
className="rounded border border-slate-300 px-2 py-0.5 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
aria-label="Trang trước"
|
||||
title="Trang trước"
|
||||
className="rounded border border-slate-300 p-1 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
>
|
||||
←
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className="rounded border border-slate-300 px-2 py-0.5 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
aria-label="Trang sau"
|
||||
title="Trang sau"
|
||||
className="rounded border border-slate-300 p-1 text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
||||
>
|
||||
→
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -171,7 +171,7 @@ export function KhkkWorkListPanel({
|
||||
</div>
|
||||
)}
|
||||
{!candidates.isLoading && !candidates.isError && awaiting.length === 0 && (
|
||||
<p className="p-3 text-[11px] italic text-slate-400">
|
||||
<p className="p-3 text-[11px] italic text-slate-500">
|
||||
Mọi phiếu đã duyệt đều đã có kế hoạch ký kết.
|
||||
</p>
|
||||
)}
|
||||
@ -181,7 +181,7 @@ export function KhkkWorkListPanel({
|
||||
<span className="min-w-0 flex-1 truncate text-[10px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
{projectName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-slate-400">{items.length}</span>
|
||||
<span className="shrink-0 text-[10px] text-slate-500">{items.length}</span>
|
||||
</div>
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{items.map((pe) => (
|
||||
@ -241,7 +241,7 @@ export function KhkkWorkListPanel({
|
||||
</div>
|
||||
)}
|
||||
{!plans.isLoading && !plans.isError && planItems.length === 0 && (
|
||||
<p className="p-3 text-[11px] italic text-slate-400">
|
||||
<p className="p-3 text-[11px] italic text-slate-500">
|
||||
Không còn kế hoạch nào đang xử lý. Phiếu đã duyệt không hiện ở đây — xem tại
|
||||
menu Kế hoạch ký kết HĐ.
|
||||
</p>
|
||||
@ -287,7 +287,7 @@ export function KhkkWorkListPanel({
|
||||
sẽ đọc thành "đang cắt bớt" trong khi thật ra là "đang lọc bỏ". Nói đúng việc
|
||||
đang làm, và chỉ nói khi thật sự có cái bị giấu. */}
|
||||
{plans.data && plans.data.items.length > planItems.length && (
|
||||
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-400">
|
||||
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-500">
|
||||
Đã ẩn kế hoạch ở trạng thái “Đã duyệt” — xem đủ ở menu Kế hoạch ký kết HĐ.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -33,7 +33,8 @@
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { Check, History, Paperclip, Send, Trash2, Undo2, Workflow } from 'lucide-react'
|
||||
import { Download, Eye, History, Paperclip, Send, Trash2 } from 'lucide-react'
|
||||
import { AttachmentPreviewDialog, isPreviewable } from '@/components/pe/AttachmentPreviewDialog'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Dialog } from '@/components/ui/Dialog'
|
||||
import { Label } from '@/components/ui/Label'
|
||||
@ -104,6 +105,8 @@ export function KhkkWorkflowPanel({
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
// [S175 · B1] File đang xem-trước inline (khuôn PE `PeWorkflowPanel.tsx:60`).
|
||||
const [previewAtt, setPreviewAtt] = useState<KhkkAttachmentDto | null>(null)
|
||||
|
||||
const phase = plan.phase as KhkkPhaseValue
|
||||
const steps = plan.workflowSteps ?? []
|
||||
@ -174,15 +177,18 @@ export function KhkkWorkflowPanel({
|
||||
const approvalFiles: KhkkAttachmentDto[] =
|
||||
(plan.attachments ?? []).filter(a => a.purpose === KhkkAttachmentPurpose.ApprovalAttachment)
|
||||
|
||||
// [BE-5] Lịch sử thay đổi — chỉ tải khi người dùng mở khối (không tốn 1 request/phiếu).
|
||||
const [showChangelog, setShowChangelog] = useState(false)
|
||||
// [BE-5] Lịch sử thay đổi.
|
||||
// 🔴 [S175 · B1] Khối này TRƯỚC ĐÂY gập lại và query `enabled: showChangelog` (lazy —
|
||||
// tiết-kiệm 1 request/phiếu). Đợt này bỏ gập theo khuôn PE ⇒ **BẮT BUỘC bỏ luôn cờ
|
||||
// `enabled`**: giữ lại thì khối luôn hiện nhưng query KHÔNG BAO GIỜ chạy, và nó hỏng
|
||||
// dưới dạng "Chưa có thay đổi nào được ghi." — tức trông y hệt phiếu chưa từng sửa,
|
||||
// KHÔNG phải trông như lỗi. Đổi lấy: +1 request mỗi lần mở phiếu, đúng giá PE đang trả.
|
||||
const changelogs = useQuery({
|
||||
queryKey: ['khkk-changelogs', plan.id],
|
||||
queryFn: async () =>
|
||||
(await api.get<KhkkChangelogDto[]>(`/contract-signing-plans/${plan.id}/changelogs`, {
|
||||
params: { take: 200 },
|
||||
})).data,
|
||||
enabled: showChangelog,
|
||||
})
|
||||
|
||||
const transition = useMutation({
|
||||
@ -267,41 +273,44 @@ export function KhkkWorkflowPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card-accent" style={{ ['--accent' as string]: 'var(--color-emerald-500)' }}>
|
||||
// [S175 · B1 · X2] Panel TRẦN khuôn PE (`PeWorkflowPanel.tsx:345`). Trước đây bọc
|
||||
// `card-accent` ⇒ thẻ-trong-thẻ, lệch đập vào mắt trước mọi mục owner liệt kê.
|
||||
// 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`/`shadow-none`: `.card-accent` (`index.css:112`)
|
||||
// nằm NGOÀI `@layer` nên thắng utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật).
|
||||
<div className="space-y-4">
|
||||
{/* ── 1. Header + meta {code} v{NN} · {name} ───────────────────────────── */}
|
||||
<header className="flex flex-wrap items-center gap-2 border-b border-slate-100 px-5 py-3">
|
||||
<span
|
||||
className="icon-chip"
|
||||
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: 'var(--color-emerald-50)', ['--chip-fg' as string]: 'var(--color-emerald-600)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<Workflow className="h-4 w-4" />
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold tracking-tight text-emerald-700">Quy trình duyệt</h3>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>
|
||||
{plan.workflowCode && (
|
||||
<span className="font-mono text-[11px] text-slate-500">
|
||||
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
|
||||
{plan.workflowCode}
|
||||
{plan.workflowVersion != null && ` v${plan.workflowVersion}`}
|
||||
{plan.workflowName && <span className="font-sans"> · {plan.workflowName}</span>}
|
||||
</span>
|
||||
{plan.workflowVersion != null && ` v${String(plan.workflowVersion).padStart(2, '0')}`}
|
||||
{plan.workflowName && <> · <span className="font-sans">{plan.workflowName}</span></>}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-5">
|
||||
{/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */}
|
||||
{plan.endedByLevelFinalize && (
|
||||
<div className="rounded border border-violet-300 bg-violet-50 px-3 py-2 text-[11px] text-violet-800">
|
||||
<div className="font-medium">
|
||||
⚑ Kết thúc tại {plan.finalizeStepName ?? 'cấp được cấu hình'}
|
||||
{plan.finalizeLevelName ? ` · ${plan.finalizeLevelName}` : ''}
|
||||
</div>
|
||||
<div className="mt-0.5">
|
||||
{isApproved
|
||||
? 'Kế hoạch đã duyệt xong ngay tại cấp này — không qua CEO / Ban Giám đốc.'
|
||||
: 'Cấp này được cấu hình duyệt là KẾT THÚC — nếu người duyệt tích ô kết thúc thì kế hoạch không qua CEO.'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* ── 2. Banner kết-thúc-sớm ──────────────────────────────────────────── */}
|
||||
{/* [S175 · B1] violet → EMERALD khuôn PE (`:360-377`). PE dành violet RIÊNG cho
|
||||
"⚑ Duyệt thay CEO"; dùng violet cả ở đây là trộn 2 nghĩa vào một màu. */}
|
||||
{plan.endedByLevelFinalize && (
|
||||
<div className="rounded-md border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
|
||||
{isApproved ? (
|
||||
<span>
|
||||
✅ Kế hoạch đã kết thúc tại{' '}
|
||||
<span className="font-semibold">{plan.finalizeStepName ?? 'cấp duyệt'}</span>
|
||||
{plan.finalizeLevelName ? <> · {plan.finalizeLevelName}</> : null} — không qua CEO.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
⚑ Quy trình rút gọn: duyệt đến{' '}
|
||||
<span className="font-semibold">{plan.finalizeStepName ?? 'cấp này'}</span>
|
||||
{plan.finalizeLevelName ? <> · {plan.finalizeLevelName}</> : null} là KẾT THÚC,
|
||||
không trình CEO. Các cấp trước vẫn duyệt như thường.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 3. Sơ đồ Bước → Cấp → NV ───────────────────────────────────────── */}
|
||||
{steps.length > 0 ? (
|
||||
@ -317,9 +326,10 @@ export function KhkkWorkflowPanel({
|
||||
{STATUS_ICON[st]}
|
||||
</span>
|
||||
<span className="text-slate-800">Bước {step.order} — {step.name}</span>
|
||||
{/* Chip PHÒNG của Bước (BE-4 join `Departments`). */}
|
||||
{/* Chip PHÒNG của Bước (BE-4 join `Departments`).
|
||||
[S175 · B1] viền-xám-nền-trắng → EMERALD đặc khuôn PE (`:406-410`). */}
|
||||
{step.departmentName && (
|
||||
<span className="rounded border border-slate-300 bg-white px-1.5 py-0.5 text-[10px] font-medium text-slate-600">
|
||||
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
|
||||
{step.departmentName}
|
||||
</span>
|
||||
)}
|
||||
@ -458,11 +468,11 @@ export function KhkkWorkflowPanel({
|
||||
}}
|
||||
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Duyệt cấp hiện tại'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
'rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
blockedByLevel ? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400' : 'border-emerald-300 text-emerald-700 hover:bg-emerald-50',
|
||||
)}
|
||||
>
|
||||
<Check className="h-3 w-3" /> Duyệt
|
||||
✓ Duyệt
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -470,11 +480,11 @@ export function KhkkWorkflowPanel({
|
||||
onClick={() => { if (!blockedByLevel) { setComment(''); setPendingFiles([]); setAction(KhkkTransitionAction.Return) } }}
|
||||
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Trả lại người soạn sửa'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
'rounded border px-2 py-1 text-[11px] font-bold transition',
|
||||
blockedByLevel ? 'cursor-not-allowed border-slate-200 bg-slate-50 text-slate-400' : 'border-amber-300 text-amber-700 hover:bg-amber-50',
|
||||
)}
|
||||
>
|
||||
<Undo2 className="h-3 w-3" /> Trả lại
|
||||
← Trả lại
|
||||
</button>
|
||||
{/* 🧊 [S174 owner] Nút "TỪ CHỐI" ĐÃ GỠ — anh chốt "vậy thì bỏ luôn từ chối
|
||||
~ tương tự PE". Duyệt NCC có code từ-chối nhưng THỰC TẾ không hiện
|
||||
@ -488,7 +498,8 @@ export function KhkkWorkflowPanel({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
className="inline-flex items-center gap-1 rounded border border-slate-300 px-2 py-1 text-[11px] font-bold text-slate-600 transition hover:border-red-300 hover:bg-red-50 hover:text-red-700"
|
||||
title="Xóa phiếu này (phiếu chuyển sang mục “Đã xóa”)"
|
||||
className="inline-flex items-center gap-1 rounded border border-red-300 bg-white px-2 py-1 text-[11px] font-bold text-red-700 transition hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" /> Xóa phiếu
|
||||
</button>
|
||||
@ -498,32 +509,52 @@ export function KhkkWorkflowPanel({
|
||||
)}
|
||||
|
||||
{/* ── 5. 📎 File đính kèm khi duyệt — TỰ ẨN khi rỗng (khuôn PE :928-967) ── */}
|
||||
{/* [S175 · B1] +`border-t pt-4` +`<h3 text-sm>` +nút `<Eye>` xem trước.
|
||||
Route BE ĐÃ CÓ `ContractSigningPlansController.cs:231` (`/view`, cùng hình với
|
||||
PE `:317`) ⇒ chỉ cần truyền `basePath` cho dialog dùng chung.
|
||||
🔴 Nhãn "Tải xuống" chuyển vào `title=` chứ KHÔNG bỏ: giữ đúng chuỗi đó trong
|
||||
bundle để phép verify prod còn control-dương soi được. */}
|
||||
{approvalFiles.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
|
||||
<Paperclip className="h-3.5 w-3.5 text-teal-500" />
|
||||
File đính kèm khi duyệt
|
||||
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">
|
||||
{approvalFiles.length}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-1.5 space-y-1.5">
|
||||
<div className="border-t border-slate-200 pt-4">
|
||||
<h3 className="text-sm font-semibold text-slate-900">📎 File đính kèm khi duyệt</h3>
|
||||
<p className="mt-0.5 text-[11px] text-slate-500">File do người duyệt tải lên trong quá trình duyệt.</p>
|
||||
<div className="mt-2 space-y-1">
|
||||
{approvalFiles.map(a => (
|
||||
<li key={a.id} className="flex items-center justify-between gap-2 rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-slate-700">{a.fileName}</div>
|
||||
<div className="text-slate-500">{formatSize(a.fileSize)} · {formatDateTime(a.createdAt)}</div>
|
||||
</div>
|
||||
<div key={a.id} className="flex items-center gap-1.5 rounded bg-slate-50 px-1.5 py-1 text-[11px]">
|
||||
<Paperclip className="h-3 w-3 shrink-0 text-slate-400" />
|
||||
<span className="min-w-0 flex-1 truncate text-slate-700" title={a.fileName}>{a.fileName}</span>
|
||||
<span className="shrink-0 text-[10px] text-slate-400">{formatSize(a.fileSize)}</span>
|
||||
{isPreviewable(a.fileName) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewAtt(a)}
|
||||
className="shrink-0 rounded px-1 text-violet-600 hover:bg-violet-50"
|
||||
title="Xem trước"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadAttachment(a)}
|
||||
className="shrink-0 text-[11px] font-medium text-brand-600 hover:underline"
|
||||
className="shrink-0 rounded px-1 text-brand-600 hover:bg-brand-50"
|
||||
title="Tải xuống"
|
||||
>
|
||||
Tải xuống
|
||||
<Download className="h-3 w-3" />
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{previewAtt && (
|
||||
<AttachmentPreviewDialog
|
||||
open
|
||||
basePath="/contract-signing-plans"
|
||||
evaluationId={plan.id}
|
||||
attachmentId={previewAtt.id}
|
||||
fileName={previewAtt.fileName}
|
||||
onClose={() => setPreviewAtt(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -531,61 +562,63 @@ export function KhkkWorkflowPanel({
|
||||
{/* [B4/D-11] BE ĐÃ LỌC hàng `decision = Pending` (hành-vi GỬI duyệt) ⇒ số (N) ở đây
|
||||
CÙNG NGHĨA với (N) của Duyệt NCC. FE KHÔNG dedupe lại; sự kiện "Gửi duyệt" vẫn
|
||||
còn nguyên ở "Lịch sử thay đổi" bên dưới. */}
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
|
||||
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
||||
Lịch sử duyệt
|
||||
<span className="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-slate-500">
|
||||
{approvals.length}
|
||||
</span>
|
||||
</div>
|
||||
{/* [S175 · B1] Bỏ pill đếm → `(n)` trong ngoặc · thẻ `bg-slate-50/60 text-[11px]`
|
||||
→ `bg-white p-3 text-sm` · mốc-giờ ĐẨY PHẢI bằng `justify-between`.
|
||||
🔴 Badge 2-ngả → 3-NGẢ: trước đây MỌI thứ không phải `Reject` đều tô emerald
|
||||
⇒ "Trả lại" (amber ở PE) bị vẽ xanh như đã-duyệt — đọc ngược nghĩa.
|
||||
`toPhase` đã có sẵn (`khkk.ts:307`) nên phân được TraLai ⟂ TuChoi ⟂ Duyệt. */}
|
||||
<div className="border-t border-slate-200 pt-4">
|
||||
<h3 className="text-sm font-semibold text-slate-900">Lịch sử duyệt ({approvals.length})</h3>
|
||||
{approvals.length === 0 ? (
|
||||
<p className="mt-1.5 text-[11px] italic text-slate-400">Chưa có lượt duyệt nào.</p>
|
||||
) : (
|
||||
<ul className="mt-1.5 space-y-1.5">
|
||||
{approvals.map(a => (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]">
|
||||
<div className="flex flex-wrap items-center gap-x-1.5 text-slate-700">
|
||||
<span className={cn(
|
||||
'rounded px-1.5 py-0.5 text-[9px] font-semibold',
|
||||
a.decision === KhkkApprovalDecision.Reject ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800',
|
||||
)}>
|
||||
{DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'}
|
||||
</span>
|
||||
<span className="font-medium">{a.approvedByFullName ?? '(hệ thống)'}</span>
|
||||
<span className="text-slate-400">·</span>
|
||||
<span className="text-slate-500">{formatDateTime(a.approvedAt)}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-slate-600">
|
||||
{a.stepName ? `Bước ${a.stepOrder} — ${a.stepName}` : 'Cấp duyệt'}
|
||||
{a.levelOrder != null && ` · Cấp ${a.levelOrder}`}
|
||||
{a.levelName ? ` (${a.levelName})` : ''}
|
||||
<span className="text-slate-400">
|
||||
{' '}· {KHKK_PHASE_LABELS[a.fromPhase]} → {KHKK_PHASE_LABELS[a.toPhase]}
|
||||
</span>
|
||||
</div>
|
||||
{a.comment?.trim() && (
|
||||
<div className="mt-0.5 whitespace-pre-wrap text-slate-600">{a.comment}</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{approvals.map(a => {
|
||||
const isReject = a.toPhase === KhkkPhase.TuChoi
|
||||
const isReturn = a.toPhase === KhkkPhase.TraLai
|
||||
return (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-slate-700">
|
||||
<div className="flex flex-wrap items-center gap-x-1.5">
|
||||
<span className={cn(
|
||||
'rounded px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
isReject ? 'bg-rose-100 text-rose-800'
|
||||
: isReturn ? 'bg-amber-100 text-amber-800'
|
||||
: 'bg-emerald-100 text-emerald-800',
|
||||
)}>
|
||||
{DECISION_LABEL[a.decision] ?? 'Chuyển trạng thái'}
|
||||
</span>
|
||||
<span className="font-medium">{a.approvedByFullName ?? '(hệ thống)'}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-500">{formatDateTime(a.approvedAt)}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] text-slate-600">
|
||||
{a.stepName ? `Bước ${a.stepOrder} — ${a.stepName}` : 'Cấp duyệt'}
|
||||
{a.levelOrder != null && ` · Cấp ${a.levelOrder}`}
|
||||
{a.levelName ? ` (${a.levelName})` : ''}
|
||||
<span className="text-slate-400">
|
||||
{' '}· {KHKK_PHASE_LABELS[a.fromPhase]} → {KHKK_PHASE_LABELS[a.toPhase]}
|
||||
</span>
|
||||
</div>
|
||||
{a.comment?.trim() && (
|
||||
<div className="mt-1 whitespace-pre-wrap text-[12px] text-slate-600">{a.comment}</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 7. Lịch sử thay đổi ────────────────────────────────────────────── */}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowChangelog(v => !v)}
|
||||
className="flex w-full items-center gap-1.5 text-xs font-semibold text-slate-700 hover:text-brand-700"
|
||||
>
|
||||
{/* [S175 · B1] BỎ GẬP — PE render thẳng (`PeDetailTabs.tsx:787-794`). Nội-dung
|
||||
đã tải sẵn qua query; giấu sau 1 cú bấm chỉ thêm ma-sát, không tiết-kiệm gì. */}
|
||||
<div className="border-t border-slate-200 pt-4">
|
||||
<h3 className="flex items-center gap-1.5 text-sm font-semibold text-slate-900">
|
||||
<History className="h-3.5 w-3.5 text-slate-400" />
|
||||
Lịch sử thay đổi
|
||||
<span className="ml-auto text-[10px] font-normal text-slate-400">{showChangelog ? 'Ẩn' : 'Xem'}</span>
|
||||
</button>
|
||||
{showChangelog && (
|
||||
<div className="mt-1.5">
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
{changelogs.isLoading && <p className="text-[11px] text-slate-400">Đang tải…</p>}
|
||||
{changelogs.isError && <p className="text-[11px] text-red-600">Không tải được lịch sử thay đổi.</p>}
|
||||
{changelogs.data && changelogs.data.length === 0 && (
|
||||
@ -614,10 +647,8 @@ export function KhkkWorkflowPanel({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{action !== null && (
|
||||
<Dialog
|
||||
@ -625,7 +656,7 @@ export function KhkkWorkflowPanel({
|
||||
onClose={() => setAction(null)}
|
||||
title={dialogTitle}
|
||||
footer={<>
|
||||
<Button variant="outline" onClick={() => setAction(null)}>Huỷ</Button>
|
||||
<Button variant="outline" onClick={() => setAction(null)}>Hủy</Button>
|
||||
<Button onClick={() => transition.mutate(action)} disabled={confirmDisabled}>
|
||||
{transition.isPending ? 'Đang gửi...' : 'Xác nhận'}
|
||||
</Button>
|
||||
@ -663,22 +694,26 @@ export function KhkkWorkflowPanel({
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{/* [S175 · B1 micro-copy] Nhãn Ô NHẬP theo PE (`PeWorkflowPanel.tsx:869` "Ghi chú
|
||||
(tùy chọn)"). 🔴 CHỈ đổi tên TRƯỜNG — chuỗi "Ý kiến cấp duyệt" ở dòng trợ-giúp
|
||||
bên dưới là TÊN THẬT của mục đích đến trong phiếu (Section 5, Mig 26), đổi nó
|
||||
là trỏ người dùng tới một mục không tồn tại. */}
|
||||
<Label>
|
||||
Ý kiến{commentRequired ? <span className="text-red-500"> *</span> : ' (tuỳ chọn)'}
|
||||
Ghi chú{commentRequired ? <span className="text-red-500"> *</span> : ' (tùy chọn)'}
|
||||
</Label>
|
||||
<Textarea
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={2000}
|
||||
placeholder={commentRequired ? 'Nêu lý do trả lại / từ chối...' : 'Ý kiến của bạn (ghi vào phiếu)...'}
|
||||
placeholder={commentRequired ? 'Nêu lý do trả lại / từ chối...' : 'Ghi chú của bạn (ghi vào phiếu)...'}
|
||||
/>
|
||||
|
||||
{/* [QĐ-3 / BE-7] File đính kèm khi duyệt — upload THẬT qua đường riêng
|
||||
`POST {id}/approval-attachments` (BE ép `purpose=ApprovalAttachment`). */}
|
||||
{action !== KhkkTransitionAction.Submit && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-xs">File đính kèm (tuỳ chọn)</Label>
|
||||
<Label className="text-xs">File đính kèm (tùy chọn)</Label>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@ -714,7 +749,7 @@ export function KhkkWorkflowPanel({
|
||||
|
||||
{action === KhkkTransitionAction.Approve && (
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
Ý kiến sẽ được ghi vào mục “Ý kiến cấp duyệt” của kế hoạch (1 ô/Cấp — duyệt lại
|
||||
Ghi chú sẽ được ghi vào mục “Ý kiến cấp duyệt” của kế hoạch (1 ô/Cấp — duyệt lại
|
||||
thì ghi đè ô cũ).
|
||||
</p>
|
||||
)}
|
||||
@ -727,7 +762,7 @@ export function KhkkWorkflowPanel({
|
||||
onClose={() => setConfirmDelete(false)}
|
||||
title="🗑 Xóa kế hoạch ký kết"
|
||||
footer={<>
|
||||
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Huỷ</Button>
|
||||
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Hủy</Button>
|
||||
<Button onClick={() => removePlan.mutate()} disabled={removePlan.isPending}>
|
||||
{removePlan.isPending ? 'Đang xóa...' : 'Xóa phiếu'}
|
||||
</Button>
|
||||
@ -739,6 +774,6 @@ export function KhkkWorkflowPanel({
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -15,10 +15,15 @@ export function isPreviewable(fileName: string): boolean {
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
/** Id của bản-ghi CHỦ (phiếu PE, hoặc kế-hoạch KHKK khi truyền `basePath`). */
|
||||
evaluationId: string
|
||||
attachmentId: string
|
||||
fileName: string
|
||||
onClose: () => void
|
||||
/** [S175 B1] Prefix route của module chủ. Mặc-định `/purchase-evaluations` ⇒ **6 call-site
|
||||
* PE hiện có KHÔNG phải sửa dòng nào** (module đã golive — mở trục mới mà bắt sửa hết
|
||||
* call-site cũ là tự đẻ rủi-ro ở chỗ đang chạy tốt). KHKK truyền `/contract-signing-plans`. */
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
/** Preview file inline qua BE endpoint `/view` (Content-Disposition: inline).
|
||||
@ -29,6 +34,7 @@ type Props = {
|
||||
* cỡ viewport (Dialog dùng chung chỉ có sm/md/lg → tự render overlay riêng). */
|
||||
export function AttachmentPreviewDialog({
|
||||
open, evaluationId, attachmentId, fileName, onClose,
|
||||
basePath = '/purchase-evaluations',
|
||||
}: Props) {
|
||||
const [blobUrl, setBlobUrl] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@ -58,7 +64,7 @@ export function AttachmentPreviewDialog({
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
api.get(`/purchase-evaluations/${evaluationId}/attachments/${attachmentId}/view`, {
|
||||
api.get(`${basePath}/${evaluationId}/attachments/${attachmentId}/view`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then(res => {
|
||||
@ -78,7 +84,7 @@ export function AttachmentPreviewDialog({
|
||||
if (currentUrl) window.URL.revokeObjectURL(currentUrl)
|
||||
setBlobUrl(null)
|
||||
}
|
||||
}, [open, evaluationId, attachmentId])
|
||||
}, [open, evaluationId, attachmentId, basePath])
|
||||
|
||||
const ready = blobUrl && !loading && !error
|
||||
|
||||
|
||||
@ -89,6 +89,7 @@ export function PipelineTreePanel({
|
||||
bodyMaxHClass,
|
||||
stage2Options,
|
||||
hideHeader,
|
||||
hideSearch,
|
||||
}: {
|
||||
currentStage: PipelineStageNo
|
||||
className?: string
|
||||
@ -107,6 +108,12 @@ export function PipelineTreePanel({
|
||||
// 🔴 Nút "Hiện/Ẩn cây" (mobile) sống TRONG thanh này và là đường DUY NHẤT mở thân cây khi
|
||||
// `<lg`. Vì vậy ẩn thanh ⇒ PHẢI ép thân cây hiện luôn, nếu không mobile mất trắng cây.
|
||||
hideHeader?: boolean
|
||||
// [S175 · B3] Ẩn ô lọc RIÊNG của cây. CHỈ dùng ở host đã có ô tìm của chính nó ngay trên
|
||||
// (KHKK GĐ2) — ở đó 2 ô tìm xếp chồng nhau mà nghĩa lại khác: ô ngoài đi SERVER-SIDE và
|
||||
// gõ vào là chuyển hẳn sang danh-sách phẳng (`flatMode`), nên ô trong cây KHÔNG bao giờ
|
||||
// dùng tới — người dùng gõ phát nào là cây biến mất phát đó.
|
||||
// 6 host còn lại KHÔNG truyền ⇒ giữ nguyên ô lọc cây như cũ.
|
||||
hideSearch?: boolean
|
||||
}) {
|
||||
const { canPe, buildStages } = usePipelineStages()
|
||||
const [search, setSearch] = useState('')
|
||||
@ -232,16 +239,18 @@ export function PipelineTreePanel({
|
||||
|
||||
{/* [S174] `hideHeader` ⇒ không còn nút toggle ⇒ ép `flex` để `<lg` vẫn thấy cây. */}
|
||||
<div className={cn('min-h-0 flex-col', hideHeader || mobileOpen ? 'flex' : 'hidden', 'lg:flex')}>
|
||||
<div className="relative border-b border-slate-200 p-2">
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Lọc dự án / hạng mục / mã phiếu…"
|
||||
aria-label="Lọc cây toàn trình"
|
||||
className="h-8 w-full rounded-md border border-slate-200 bg-white pl-7 pr-2 text-[12px] text-slate-700 placeholder:text-slate-400 focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
||||
/>
|
||||
</div>
|
||||
{!hideSearch && (
|
||||
<div className="relative border-b border-slate-200 p-2">
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Lọc dự án / hạng mục / mã phiếu…"
|
||||
aria-label="Lọc cây toàn trình"
|
||||
className="h-8 w-full rounded-md border border-slate-200 bg-white pl-7 pr-2 text-[12px] text-slate-700 placeholder:text-slate-400 focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cn('overflow-y-auto', bodyMaxHClass ?? 'max-h-[28rem] lg:max-h-[calc(100vh-16rem)]')}>
|
||||
{!canPe && (
|
||||
|
||||
@ -3,8 +3,14 @@
|
||||
//
|
||||
// [S174 owner] ĐỔI TỪ TRANG-GIẤY-CUỘN SANG HOST 2-PANEL — anh chốt: **"làm giống giao diện
|
||||
// thao tác của Duyệt NCC"** (`pages/pe/PurchaseEvaluationWorkspacePage.tsx`). Khuôn bê về:
|
||||
// · header 1 dòng + grid `lg:grid-cols-[300px_1fr]`;
|
||||
// · Panel 1 = danh sách (`KhkkWorkListPanel`) + nút hành-động dính đáy;
|
||||
// · header 1 dòng + grid `lg:grid-cols-[260px_1fr] xl:grid-cols-[320px_1fr]` — ĐÚNG BYTE
|
||||
// khuôn PE (`PurchaseEvaluationWorkspacePage.tsx:80`). 🧊 Comment cũ ghi `300px` là số
|
||||
// của bản nháp đầu, KHÔNG bao giờ khớp code sau khi đợt A land 260/320 (S175 vá).
|
||||
// · Panel 1 = danh sách (`KhkkWorkListPanel`) — 2 tầng, MỖI tầng có header dính ĐỈNH
|
||||
// (`sticky top-0`) gom theo dự án. 🧊 Comment cũ ghi "nút hành-động dính đáy" là MÔ TẢ
|
||||
// CỦA PE, không phải của đây: `grep -c Button KhkkWorkListPanel` = **0** (control-dương
|
||||
// `PeListPanel` = 8). Bê nguyên câu mô tả từ khuôn nguồn sang là tự khai một thứ chưa
|
||||
// dựng — S175 vá theo L3 H-5.
|
||||
// · Panel 2 = EmptyState | form tạo | phiếu đang XEM.
|
||||
// 🔴 Panel 1 chia 2 TẦNG theo đúng ảnh anh khoanh: trên = "Cột danh sách Duyệt NCC"
|
||||
// (bấm ⇒ *tạo luôn KHKK*), dưới = "Cột KHKK đã tạo" (bấm ⇒ *chỉ xem*).
|
||||
|
||||
Reference in New Issue
Block a user