[CLAUDE] Docs: chốt Session 20 — PE Detail UI restructure 3 yêu cầu UX (Chunk D)

Wrap-up docs cho 3 chunk code đã push:
- 9dee00d Chunk A — BE auto-seed Hạng mục mặc định + FE reorder section
- 2bba851 Chunk B — Nested grid HangMucCard, NCC expand inline + drop SuppliersTab
- f2f01f4 Chunk C — Section Ý kiến gộp đồng cấp cùng Phòng (1 box/Step, chỉ hiện signed)

Files updated:
- docs/STATUS.md — Last updated + Recently Done row S20 trên cùng (giữ S19 nguyên văn §6.5)
- docs/HANDOFF.md — Last updated + TL;DR Session 20 section ở đầu + pending S21+ + hard blocker ops (giữ S19 nguyên văn §6.5)
- docs/changelog/migration-todos.md — Phase 9 Session 20 done section + 9 defer item S21+ (giữ S19 nguyên văn §6.5)
- docs/changelog/sessions/2026-05-11-1100-pe-ui-restructure-s20.md (NEW) — session log

KHÔNG đụng rules/architecture/PROJECT-MAP/workflow-contract/forms-spec/database-guide/
schema-diagram/CLAUDE.md per §6.5 (S20 không thêm migration / gotcha mới, drift unchanged từ S19 → defer cron audit 2026-06-01).

Path filter CI sẽ skip (docs-only commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-11 10:12:13 +07:00
parent f2f01f4765
commit f8e5675edf
4 changed files with 404 additions and 2 deletions

View File

@ -0,0 +1,206 @@
# 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)