# Session 20 — PE Detail UI restructure (3 yêu cầu UX user) **Date:** 2026-05-11 **Status:** ✅ All 4 chunks pushed (`9dee00d` → `2bba851` → `f2f01f4` → this Docs commit) **Scope:** FE-only restructure UI PE Detail (Duyệt NCC) + 1 nhánh BE nhẹ auto-seed. ## Bối cảnh User UAT live phản hồi: "Logic khá OK rồi, điều chỉnh giao diện chỗ Duyệt NCC 1 tý nhé." 3 yêu cầu cụ thể: 1. Hạng mục đưa lên phía trên + auto-tạo 1 hạng mục từ gói thầu (tên = TenGoiThau, giá trị = ngân sách) 2. Thêm NCC = expand dưới hạng mục (tầng 1 = hạng mục, tầng 2 = NCC, thông tin nhập trên grid) 3. Section Ý kiến: gộp comment đồng cấp cùng Phòng → 1 ô / bước (chỉ hiển thị comment NV đã duyệt) ## Q&A trước khi code (chốt scope) - **Q1 (giữ Section "Chọn NCC TP thắng thầu" riêng?):** = a (giữ riêng, rõ UX) - **Q2 (NCC shared cross-hạng mục?):** = a (shared) — "**nhưng hiện chỉ cần 1 hạng mục trước tiên**" → đơn giản scope Chunk B - **Q3 (chỉ hiển thị NV đã ký?):** = a (KHÔNG show NV chưa duyệt với placeholder) - **Q4 (verify mode?):** "Public luôn đang demo thôi" → Phase 9 UAT iteration skip `dotnet test`, vẫn `npm run build` mỗi chunk ## Chunk A — Hạng mục lên #2 + BE auto-seed 1 row **Commit:** `9dee00d` **BE — `PurchaseEvaluationFeatures.cs` `CreatePurchaseEvaluationCommandHandler`:** Khi tạo phiếu mới, sau khi save PE entity, INSERT thêm 1 `PurchaseEvaluationDetail` mặc định kèm Changelog entry: ```csharp var defaultBudgetValue = linkedBudgetTotal ?? request.BudgetManualAmount ?? 0m; var defaultDetail = new PurchaseEvaluationDetail { PurchaseEvaluationId = entity.Id, GroupCode = "01", GroupName = "Hạng mục chính", NoiDung = request.TenGoiThau, // ← tên hạng mục = tên gói thầu DonViTinh = "gói", KhoiLuongNganSach = 1m, KhoiLuongThiCong = 1m, DonGiaNganSach = defaultBudgetValue, // ← giá trị từ ngân sách link / manual ThanhTienNganSach = defaultBudgetValue, Order = 1, }; ``` `linkedBudgetTotal` mới: nếu PE link Budget, fetch `Budget.TongNganSach` (computed sum BudgetDetails). Nếu không link, fall back `BudgetManualAmount`. Nếu cả 2 null → 0 (user sẽ sửa sau). **FE — `PeDetailTabs.tsx` (mirror fe-admin + fe-user):** Đổi thứ tự 5 section: - Cũ: 1.Thông tin / 2.Chọn NCC / 3.NCC tham gia / 4.Hạng mục / 5.Ý kiến - Mới Chunk A: 1.Thông tin / **2.Hạng mục** ← / 3.Chọn NCC / 4.NCC tham gia / 5.Ý kiến - (Chunk B sẽ gộp Section 4 vào Section 2 — final 4 section) ## Chunk B — NCC nested expand dưới Hạng mục **Commit:** `2bba851` **FE-only mirror fe-admin + fe-user** (~280 LOC mỗi file thay đổi): Restructure `ItemsTab`: - Trước: 1 bảng matrix grid (hạng mục × NCC) — sticky left column hạng mục + repeating col / NCC - Sau: list `HangMucCard` (1 card / 1 hạng mục) `HangMucCard` mới (sub-component nội bộ file, không export): ```tsx function HangMucCard({ detail, ev, readOnly, budgetRowMap, showBudgetCol, onEditDetail }) { const [expanded, setExpanded] = useState(true) // mặc định mở (1 hạng mục demo) const [addNccOpen, setAddNccOpen] = useState(false) const [editNccRow, setEditNccRow] = useState(null) const [quoteEdit, setQuoteEdit] = useState<{ supplier: PeSupplier; existing: PeQuote | null } | null>(null) // 3 mutations: removeDetail, removeNcc, setWinner (move từ SuppliersTab cũ) // Header row: GroupCode + NoiDung + GroupName + KL/ĐG/TT + NS link Δ + Pencil/Trash // Expand panel: NCC inline table } ``` **Layout nested grid:** | Tầng | Component | Hành vi | |---|---|---| | 1 Header | `HangMucCard` div header | GroupCode + NoiDung + 3 stat (KL/ĐG/TT) + NS link Δ (nếu có) + Pencil/Trash actions, click ▼/▶ toggle expand | | 2 Expand body | `` NCC inline | columns: NCC \| Liên hệ \| Điều khoản TT \| **File báo giá** \| ĐG chưa VAT \| ĐG có VAT \| Thành tiền \| Action | **Quote inline:** click cell (chưa VAT / có VAT / Thành tiền) → mở `QuoteDialog` cũ (reuse). **Add NCC:** button `+ Thêm NCC` trong expand panel → `AddSupplierDialog` cũ (reuse). **Sửa NCC info:** ✏ icon mỗi NCC row → `EditSupplierDialog` cũ. **Winner:** ✓ icon click → `setWinner` mutation, row + cell ăn theo màu emerald. **File báo giá:** giữ nguyên `SupplierAttachmentsCell` component, nhúng vào cell mới (TS6133 catch khi quên thêm cột → đã fix). **Drop dead code:** - Function `SuppliersTab` xóa hoàn toàn (~134 LOC) — replace bằng `HangMucCard` expand panel - Giữ `AddSupplierDialog` + `EditSupplierDialog` + `SupplierAttachmentsCell` (HangMucCard call lại) **Section layout cuối Chunk B (4 section):** 1. Thông tin gói thầu 2. Hạng mục + Báo giá NCC (nested expand) 3. Chọn NCC / TP thắng thầu 4. Ý kiến cấp duyệt (NCC tham gia riêng cũ bỏ — gộp vào Section 2.) ## Chunk C — Section Ý kiến gộp đồng cấp cùng Phòng **Commit:** `f2f01f4` **FE-only mirror 2 app** (~134 LOC thay đổi). **Schema Mig 26 không đụng** — vẫn UPSERT 1 row / Level trong `PurchaseEvaluationLevelOpinions` qua `ApproveV2Async`. Chỉ thay đổi render layer. **Trước (S19 LevelOpinionsSectionV2):** ``` forEach step: div.grid-cols-2: forEach level: forEach approver: (1 box / NV) - Cấp N — Tên NV - "Đã duyệt" badge or "— chưa duyệt" placeholder italic - comment text - admin override badge nếu signedBy !== approver ``` **Sau (Chunk C):** ``` forEach step: - Header: "Bước N — Tên" + dept badge emerald + "X/Y đã duyệt" counter - Body: - empty → "— Chưa có ý kiến duyệt." italic gray - else → list per signed opinion (sort levelOrder asc, signedAt asc) ``` `StepOpinionEntry`: - Header: ApproverFullName + "Cấp N" badge slate + admin override badge (amber) nếu có - Right: emerald rounded-full timestamp "✓ DD/MM/YYYY HH:mm" - Body: comment text NV chưa duyệt KHÔNG hiển thị (Q3=a) — chỉ 1 box / Step thay vì N box / NV. **Drop dead code:** - Function `LevelOpinionBox` xóa (~50 LOC) — replace bằng `StepOpinionsBox` + `StepOpinionEntry` ## Chunk D — Docs (file này + STATUS + HANDOFF) Đang commit. ## Tổng metrics Session 20 | Metric | Trước S20 | Sau S20 | Delta | |---|---|---|---| | DB tables | 59 | 59 | 0 (FE-only + BE seed hook) | | Migrations | 26 | 26 | 0 | | Endpoints | ~141 | ~141 | 0 (reuse + 1 BE hook trong existing CreatePE handler) | | Unit tests | 81 pass | 81 pass | 0 (UAT skip — Q4 user public luôn) | | Gotchas | 44 | 44 | 0 (TS6133 unused function declaration caught early bởi `npm run build` — đã biết, không record gotcha mới) | | Commits S20 | — | 4 (3 code + 1 docs) | — | | Files changed | — | 3 files (1 BE + 2 FE mirror) | — | | LOC delta | — | ~+50 BE / ~+700 FE / ~−725 FE = net ~+25 FE | — | ## Verify chain mỗi chunk | Chunk | BE build | FE build admin | FE build user | dotnet test | Push | |---|---|---|---|---|---| | A | ✅ 0 warn / 0 err | (no change FE structure) | (no change) | skip (Q4) | `9dee00d` | | B | (no BE change) | ✅ pass | ✅ pass | skip | `2bba851` | | C | (no BE change) | ✅ pass | ✅ pass | skip | `f2f01f4` | CI gate trên Gitea Actions sẽ run `dotnet test SolutionErp.slnx` cho mỗi commit code (path filter docs-only skip cho Chunk D). Verify 3 run pass trước UAT confirm. ## Pending (defer Session 21+) Vẫn carry over từ HANDOFF S19: - Test regression B4 S18 silent 403 fix (HIGH priority — vi phạm rule §7 "test-before bug fix") - Test V2 Service wire `ApproveV2Async` UPSERT opinion (Mig 26 S19) + Section gộp render (Chunk C) - Test Mig 25 PATCH `/user-selectable` endpoint - **Contract V2 wire (Mig 27/28 mirror PE pattern)** — biggest pending Plan - Phân quyền strict V2 (list/inbox/detail filter actor scope) - Drop legacy V1 + Mig 15 4-box deprecated sau UAT confirm Hard blockers ops (S19 carry): - UAT thật 1 tuần - SMTP config (chờ user cấp host/user/pass) - Rotate creds + SQL backup schedule + win-acme fix + remove `.huypham.vn` binding Audit định kỳ: - 2026-06-01 combined audit (skill + doc drift). Drift hiện tại cho audit kế: - `ef-core-migration` skill mention "21 migration" stale (thực 26) - `dependency-audit-erp` count gotcha 41 stale (thực 44) - `schema-diagram` §16 PE Level Opinions V2 + §17-21 Mig 18-21 pending - (S20 không thêm migration / gotcha mới — drift không thay đổi từ S19) ## Cross-ref - Memory `feedback_uat_skip_verify.md` — Q4 áp dụng đúng pattern (verify build mỗi chunk vì có rename/remove function) - Memory `feedback_per_chunk_commit.md` — A/B/C/D chunk pattern - Memory `feedback_audit_reuse_before_clone.md` — Chunk B reuse 3 dialog (AddSupplier/EditSupplier/QuoteDialog) thay vì rewrite - Session 19 changelog `2026-05-09-0400-pe-section-5-v2-dynamic-mig26.md` — context Mig 26 schema (vẫn giữ nguyên Chunk C)