# 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<PeSupplier | null>(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 | `<table>` 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:
        <LevelOpinionBox /> (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:
  <StepOpinionsBox stepOrder stepName departmentName totalApprovers opinions={stepOpinions} />
    - 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 <StepOpinionEntry opinion /> 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)
