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

11 KiB
Raw Blame History

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:

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:

// 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:

<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 — "
  • Badge amber "⚠ Admin 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)