Files
solution-erp/docs/changelog/sessions/2026-05-09-0400-pe-section-5-v2-dynamic-mig26.md
pqhuy1987 17f697aa94 [CLAUDE] Docs: chốt Session 19 — PE Section 5 V2 dynamic + Mig 26
Chunk D Docs cho Session 19 (PE Section 5 dynamic theo
ApprovalWorkflowLevel):

- docs/STATUS.md — Recently Done row Session 19 chi tiết + header
  narrative 25→26 mig + 58→59 tables + 5 spec Q&A + 4 chunk per-commit
- docs/HANDOFF.md — TL;DR Session 19 đầy đủ (polish 3 button +
  Chunk A/B/C summary + Stats Δ) + 7 cảnh báo Session 20+. Giữ TL;DR
  S18 nguyên văn theo §6.5 (KHÔNG cắt narrative)
- docs/changelog/migration-todos.md — Phase 9 Session 19 done block
  với 5 commit + Stats final + Defer Session 20+ (8 task pending)
- docs/changelog/sessions/2026-05-09-0400-pe-section-5-v2-dynamic-mig26.md
  — Session log mới, full Q&A + Chunks + Bug fix + Stats cumulative
- CLAUDE.md (root) — count 25→26 mig + 58→59 tables + Mig 26 description

KHÔNG đụng (per §6.5 không cố sửa khi không cần):
- rules.md / architecture.md / PROJECT-MAP.md / workflow-contract.md /
  forms-spec.md / database-guide.md
- Skills (6) — drift defer cron audit 2026-06-01
- schema-diagram.md §16 PE Level Opinions V2 — defer cùng §17-21 cron
  audit 2026-06-01

Path filter docs-only → CI sẽ skip deploy (gotcha #41).
2026-05-09 11:11:09 +07:00

230 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Session 19 — 2026-05-09 (00:00 → ~04:00) — PE Section 5 V2 dynamic theo ApprovalWorkflowLevel + Mig 26
**Dev:** Claude
**Duration:** ~4h
**Base commit:** `daad79d` (Session 18 wrap-up docs)
**Final HEAD:** `<Chunk D>`
**Commits:** 5 (1 polish + 3 chunk per-commit + 1 docs)
## Bối cảnh
Session bắt đầu với template start: đọc 5 MD core + memory + cron audit status + CI status. Sau đó user UAT live tiếp Session 18:
1. Polish 3 button "Hành động" Workflow Panel (rút gọn label + 3 màu + bold) — `873e7a1`
2. **Feature mới:** Section 5 "Ý kiến 4 phòng ban" hiện CỨNG 4 box (Mig 15 PheDuyet/CCM/MuaHàng/SmPm từ Phase 8) → **động theo Workflow V2 đã pin**: forEach Step → forEach Level → forEach NV → 1 OpinionBox với ý kiến + tên người. "Bước 1 phòng A có 2 NV → 2 box ngang hàng".
## Polish 3 button (`873e7a1`) — Hành động Workflow Panel
User screenshot turn cuối Session 18: "Hành động: Trả lại / Hủy/Từ chối / Duyệt → Chờ CCM" → yêu cầu rút gọn + 3 màu khác nhau + bold.
**PeWorkflowPanel.tsx (mirror fe-admin + fe-user):**
| Trước | Sau |
|---|---|
| `← Trả lại (về Drafter sửa)` (red) | **`← Trả lại`** (amber) |
| `✗ Hủy / Từ chối` (red) | **`✗ Từ chối`** (red) |
| `✓ Duyệt → Chờ CCM` (brand) | **`✓ Duyệt`** (emerald) |
- Phase đích vẫn hiện qua tooltip title khi hover button Duyệt (KHÔNG mất thông tin)
- font-medium → font-bold
- Logic disabled (V2 actor không trong cấp) giữ nguyên
- Verify: `npm run build` × 2 pass · 0 TS error
## Feature lớn — Section 5 V2 dynamic + Mig 26
### Spec chốt 5 câu Q&A trước code
Theo memory `feedback_drastic_refactor_scope` — feature lớn cần spec rõ trước. User trả lời:
| Q | Chốt | Implement |
|---|---|---|
| Q1 | **1B** | Service V2 sau approve sync auto UPSERT vào LevelOpinion (Section 5 read-only summary) |
| Q2 | **2A + Admin** | Chỉ NV chính chủ duyệt được. Admin override → lưu `SignedByUserId = admin.Id` (FE banner "duyệt thay") |
| Q3 | **chuyển V2 hết** | Phiếu V1 fallback render Mig 15 4 box CỨNG readOnly. Mig 15 KHÔNG drop (giữ data legacy) |
| Q4 | **4C + bonus** | Phase=DaDuyet/TuChoi → khoá hoàn toàn. Admin có quyền duyệt thay. Comment empty → ghi `"(duyệt — không ý kiến)"` |
| Q5 | **5A** | Group "Bước N — Phòng X", grid-cols-2 cho N Cấp |
### Approach pick
Sau phân tích 3 approach (A=bảng mới, B=extend Mig 15, C=VIEW từ Approvals), pick **Approach A** — bảng mới riêng cho V2:
- Clean V1/V2 separation (giống pattern V2 schema Mig 22-24)
- Backward compat 100% V1 phiếu cũ
- 1 row UNIQUE per (PE × Level) — latest write wins khi resubmit qua TraLai
### Chunk A (`77a3058`) — Domain + Mig 26 + EF
**Entity `PurchaseEvaluationLevelOpinion : AuditableEntity`:**
```csharp
public Guid PurchaseEvaluationId { get; set; }
public Guid ApprovalWorkflowLevelId { get; set; }
public string? Comment { get; set; } // max 2000
public DateTime SignedAt { get; set; } // luôn có khi UPSERT
public Guid SignedByUserId { get; set; } // NV chính chủ HOẶC Admin
public string SignedByFullName { get; set; } // denorm — tránh user xóa/đổi tên
```
**EF Configuration:**
- UNIQUE composite (PEId, LevelId)
- FK Cascade Pe (xoá phiếu → xoá opinions)
- FK Restrict Level (admin xoá Level chặn nếu opinion tồn tại — bảo vệ data)
- SignedByUserId KHÔNG nav (denorm only, tránh cascade khi xoá user)
**Migration 26 `AddPeLevelOpinionsForV2`:**
- 1 CREATE TABLE `PurchaseEvaluationLevelOpinions`
- 2 FK (Cascade Pe + Restrict Level)
- 2 index (UNIQUE composite + IX LevelId)
- 3-file rule commit đủ (.cs + Designer + Snapshot)
**Apply LocalDB SolutionErp_Dev OK** qua `dotnet ef database update --connection ...`. Mig 25 + 26 catchup (Mig 25 từ S18 trên `_Design`, _Dev catch up turn này).
**Verify:** dotnet build pass + dotnet test 81 pass (no regression).
### Chunk B (`90baa8e`) — Service V2 hook + DTO + GET include
**Service `PurchaseEvaluationWorkflowService.ApproveV2Async`** sau line `db.PurchaseEvaluationApprovals.Add(...)` chèn block UPSERT opinion:
```csharp
// Mig 26 — UPSERT opinion vào row Level chính chủ. Section 5 FE render
// dynamic theo flow.steps[].levels[]. Q1=1B chốt: comment khi duyệt auto
// sync sang Section 5 (read-only summary).
var matchingLevel = pendingLevelGroup
.FirstOrDefault(l => actorUserId.HasValue && l.ApproverUserId == actorUserId.Value)
?? pendingLevelGroup.First(); // Admin override fallback first
var actorFullName = await ResolveActorFullNameAsync(actorUserId, isSystem, ct);
var existingOpinion = await db.PurchaseEvaluationLevelOpinions
.FirstOrDefaultAsync(o => o.PurchaseEvaluationId == evaluation.Id
&& o.ApprovalWorkflowLevelId == matchingLevel.Id, ct);
var normalizedComment = string.IsNullOrWhiteSpace(comment)
? "(duyệt — không ý kiến)"
: comment.Trim();
// UPSERT: existing → update; null → Add new
```
Reject (Trả lại / Từ chối) KHÔNG sync — giữ snapshot cũ nếu có.
Multi-NV cùng Cấp OR-of-N: match level theo `ApproverUserId == actorUserId`. Admin override → fallback first level group. FE detect SignedByUserId !== Level.ApproverUserId → banner "Admin duyệt thay".
**Helper `ResolveActorFullNameAsync(actorUserId, isSystem, ct)`:**
- isSystem || null → "(System)"
- User exists → FullName ?? UserName
- User deleted → "(unknown)"
**DTO `PurchaseEvaluationLevelOpinionDto` (15 fields)** — denorm StepOrder/StepName/StepDepartmentId/StepDepartmentName/LevelOrder/LevelName/ApproverUserId/ApproverFullName + Comment/SignedAt/SignedByUserId/SignedByFullName.
**GET handler `GetPurchaseEvaluationQueryHandler`:**
- Include LevelOpinions
- Helper `BuildLevelOpinionsAsync(e, ct)` JOIN `ApprovalWorkflows.Steps.Levels` + Departments + Users → denorm DTO list. Empty list khi:
- `e.LevelOpinions.Count == 0`, hoặc
- `e.ApprovalWorkflowId == null` (V1 legacy)
**Verify:** dotnet build pass + dotnet test 81 pass.
### Chunk C (`6e913b3`) — FE Section 5 V2 dynamic mirror 2 app
**Type:** `PeLevelOpinion` (15 field) + `PeDetailBundle.levelOpinions[]`.
**Section 5 PeDetailTabs conditional render:**
```tsx
<Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)">
{mode === 'workspace' && (
<div className="...amber...">
Ý kiến + chữ auto đồng bộ khi NV duyệt phiếu vào menu "Duyệt" để .
</div>
)}
{evaluation.approvalWorkflowId
? <LevelOpinionsSectionV2 ev={evaluation} />
: <DepartmentOpinionsSection ev={evaluation} readOnly={opinionsReadOnly} />}
</Section>
```
**`LevelOpinionsSectionV2`:**
- Empty state khi `flow null` / `0 steps` → message "Workflow chưa cấu hình hoặc chưa có cấp duyệt nào"
- forEach `step` → header "Bước N — <step.name>" + dept badge emerald + hint "(N người duyệt)" nếu totalApprovers > 1
- Body grid-cols-2 cho `step.levels.flatMap(level => level.approvers.map(approver => <LevelOpinionBox/>))`
- Lookup opinion theo (stepOrder, levelOrder, approverUserId) match levelOpinions[]
**`LevelOpinionBox` (read-only — Q1=1B):**
- Title "Cấp N — <ApproverFullName>"
- Badge amber "⚠ Admin <SignedByFullName> duyệt thay" khi `signedByUserId !== approverUserId`
- Badge emerald "✓ Đã duyệt" khi opinion tồn tại
- Empty: "— chưa duyệt" italic gray
- Footer: timestamp signedAt format vi-VN
Workspace mode hint amber giữ — Drafter ở workspace tạo phiếu chưa duyệt nên Section 5 luôn empty.
Mirror fe-admin + fe-user (rule §3.9).
**Verify:** `npm run build` × 2 pass · 0 TS error.
### Chunk D — Docs (current)
5 file MD update:
- `docs/STATUS.md` Recently Done top + DB tables 58→59 + migrations 25→26 + header narrative
- `docs/HANDOFF.md` TL;DR Session 19 + 7 cảnh báo Session 20+ (giữ TL;DR S18 nguyên văn theo §6.5)
- `CLAUDE.md` (root) count 25→26 mig + 58→59 tables + Mig 26 description block
- `docs/changelog/migration-todos.md` Phase 9 Session 19 done section + Defer Session 20+ checklist
- `docs/changelog/sessions/2026-05-09-0400-pe-section-5-v2-dynamic-mig26.md` (file này)
**KHÔNG đụng (per §6.5 không cố sửa khi không cần):**
- `docs/rules.md` — không có rule mới
- `docs/architecture.md` — không có changes structural
- `docs/PROJECT-MAP.md` — không có structural changes
- `docs/workflow-contract.md` — Contract V2 chưa wire (S20+)
- `docs/forms-spec.md` — không liên quan
- `docs/database/schema-diagram.md` — defer cron audit 2026-06-01 (cùng §17-21 đã defer trước)
- `docs/database/database-guide.md` — count migration không hardcode trong header
- Skills (6) — drift "21 migration" / "44 gotcha" defer cron audit 2026-06-01
## E2E verified
-`dotnet build SolutionErp.slnx` 0 error · 2 warning (CS8602 DocxRenderer cũ, không liên quan)
-`dotnet test SolutionErp.slnx` **81 pass** (58 Domain + 23 Infra) — no regression
-`dotnet ef migrations add AddPeLevelOpinionsForV2` 3-file rule OK
- ✅ Mig 26 apply LocalDB SolutionErp_Dev OK (Mig 25 + 26 catchup)
-`npm run build` × fe-admin + fe-user pass mỗi commit có FE changes
- ✅ CI deploy commit `873e7a1` (polish 3 button) success run #168 lúc 02:03:57+07:00
- ⏳ CI deploy commit `77a3058` + `90baa8e` + `6e913b3` (chờ verify run #169-171)
- ⏸️ User UAT live test (defer cho user xác nhận)
## Bug gặp + fix
| Bug | Fix |
|---|---|
| Powershell working dir lệch lên 1 cấp khi `Push-Location fe-admin` | Dùng absolute path `D:\Dropbox\...\fe-admin` |
| Bash `Select-Object` không tồn tại | Chuyển sang PowerShell tool đúng cú pháp |
| Edit fe-user files fail "File has not been read yet" | Read trước Edit (rule harness) |
## Docs updates
(Liệt kê ở section Chunk D trên — 5 file MD).
## Memory updates
KHÔNG add memory mới turn này. Pattern Q&A spec trước code đã có memory `feedback_drastic_refactor_scope` đề cập (dedicated session conservative scope) — không tạo memory mới.
## Handoff to Session 20+
Đọc `docs/HANDOFF.md` "## ⚠️ Điều quan trọng cho Session 20+" cho 7 cảnh báo đầy đủ.
Top priorities:
1. **Test V2 Service wire mới** (Chunk B Service hook) — Domain test ApproveV2 + UPSERT opinion match logic + Admin override
2. **Contract V2 wire (Mig 27/28)** — mirror PE pattern: Contract.ApprovalWorkflowId + ContractLevelOpinions Mig 28 + Service ApproveV2Async + ContractDetailContent Section 5 V2
3. **Phân quyền strict V2** — list/inbox/detail filter actor scope (giải bug /inbox loose)
## Thông số cumulative
| | Trước S19 | Sau S19 | Δ |
|---|---:|---:|---:|
| Migrations | 25 | **26** | +1 |
| DB tables | 58 | **59** | +1 |
| API endpoints | ~141 | ~141 | 0 (UPSERT auto qua Service hook không endpoint) |
| FE pages | 33 | 33 | 0 (modify existing only) |
| Test pass | 81 | 81 | 0 (UAT defer test §7) |
| Gotchas | 44 | 44 | 0 |
| Memory entries | 14 | 14 | 0 |
| Skills | 6 | 6 | 0 |
| Commits | (after S18) | **+4** | 873e7a1 (polish) + 77a3058 (Chunk A) + 90baa8e (Chunk B) + 6e913b3 (Chunk C) + Chunk D Docs (current) |
| BE LOC | ~16200 | ~16400 | +~200 (Mig 26 + Service hook + DTO + GET helper) |
| FE LOC | ~17600 | ~17900 | +~300 (Section 5 V2 dynamic + LevelOpinionBox + types mirror 2 app) |