Files
solution-erp/docs/changelog/sessions/2026-05-08-1945-s18-pe-v2-polish-clone-b.md
pqhuy1987 daad79d282 [CLAUDE] Docs: chốt Session 18 wrap-up — PE V2 polish + Clone B + Mig 25 IsUserSelectable + 4 bug fix UAT
Session 18 (16:56 → 19:45, 7 commit `aaa1c6c` → `32a8d4d`):
- B1 Pe Duyệt filter cứng "Đã gửi duyệt"
- B2 HistoryTab filter Trả lại / Gửi lại
- B3 Clone V2 cho B (DuyetNccPhuongAn) — audit reuse pattern
- B4 Fix silent 403 ApprovalWorkflowsV2Controller
- B5 Fix sidebar highlight queryMatches transient keys
- B6 Mig 25 IsUserSelectable + Designer pin toggle + bỏ "(clone)" + Workspace filter
- B7 Cleanup orphan zip files

Updates:
- STATUS — header 24→25 mig + 43→44 gotcha + 1 row Recently Done top + session log link
- HANDOFF — TL;DR S18 đầy đủ + cảnh báo S19+ (giữ S17 narrative §6.5)
- CLAUDE.md root — count 25 mig + Mig 25 description block
- schema-diagram §14 — heading 22→25 + cột IsUserSelectable + filter logic section + Pending S19+ Mig 26/27
- gotchas — +#44 silent 403 + checklist debug 21
- migration-todos — Phase 9 S18 done section
- session log mới đầy đủ E2E narrative

Stats: 25 mig, 58 tables, ~141 endpoints, 81 test pass (no change), 44 gotcha, 14 memory entries, 6 skill, 7 commit S18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:56:42 +07:00

15 KiB
Raw Blame History

Session 18 — 2026-05-08 (16:56 → 19:45) — PE V2 polish + Clone B + Mig 25 IsUserSelectable + 4 bug fix UAT

Dev: Claude Duration: ~2h49m Base commit: 8680f4c (Session 17 wrap-up) Final HEAD: 32a8d4d Commits: 7

Bối cảnh

Tiếp Session 17 (PE V2 schema end-to-end DONE). User UAT live trên prod eoffice.solutions.com.vn test phiếu A (DuyetNcc), feedback chuỗi polish nhỏ + clone V2 cho type B (DuyetNccPhuongAn). Áp memory feedback_uat_skip_verify (skip dotnet test mỗi chunk, push ngay) + lesson hotfix CI 0ae3fe2 (rename/remove → bắt buộc npm run build).

7 batch deliverable theo thứ tự user feedback:

B1 (aaa1c6c) — Pe Duyệt filter cứng "Đã gửi duyệt"

User: "Duyệt bỏ cái trạng thái đi, chỉ load những trạng thái 'Đã gửi duyệt' là đc."

Screenshot UAT cho thấy leaf "Duyệt" hiển thị 3 phiếu: 1 phiếu Nháp + 2 phiếu Đã gửi duyệt. Phiếu Nháp KHÔNG nên hiện ở leaf "Duyệt".

Root cause: /purchase-evaluations/inbox BE endpoint hiện loose UAT (mọi authenticated user thấy tất cả phiếu, không filter theo actor là approver Cấp hiện tại). Phân quyền strict V2 đã pending Session 19+.

Fix FE-only (workaround):

  • PurchaseEvaluationsListPage.tsx (cả fe-admin + fe-user):
    • Khi pendingMe=true → ẩn dropdown "Tất cả trạng thái", thay bằng hint amber "Lọc cố định: Đã gửi duyệt (phiếu đang chờ duyệt)"
    • Filter cứng client-side: allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet) — loại Nháp/Trả lại/Đã duyệt/Từ chối
    • Header count dùng rows.length khi pendingMe (inbox không paged, không phải list.data?.total)
  • PurchaseEvaluationsListPage.tsx cả 2 app: KHÔNG đụng dropdown ở Danh sách (pendingMe=false) — vẫn cho user filter mọi trạng thái

Verify: npm run build × 2 pass · 0 TS error.

B2 (917446d) — HistoryTab filter Trả lại / Gửi duyệt lại

User: "Lịch sử thay đổi: Chỉ bắt các dòng thay đổi khi trả lại và gửi duyệt lại thôi nhé, không cần bắt trạng thái duyệt và các thay đổi trước khi trả lại."

Approach: FE filter (BE giữ audit data đầy đủ — reversible nếu user đổi ý / cần audit trail compliance / legal).

Logic giữ rows match một trong:

  • Workflow transition về TraLai: entityType === 5 (Workflow) && phaseAtChange === 98 (TraLai)
  • Workflow transition từ TraLai: entityType === 5 && summary?.includes('TraLai →')
  • Mọi thay đổi nội dung (Header/Detail/Supplier/Quote/Attachment) khi phaseAtChange === 98

Logic loại:

  • Workflow Approve cùng cấp/phase (Cấp 1→2→DaDuyet): entityType === 5 && phaseAtChange ≠ 98 && summary KHÔNG chứa 'TraLai →'
  • Sửa nội dung khi phase ≠ TraLai (lần soạn thảo gốc, ChoDuyet đầu)

Empty state: "Chưa có lịch sử trả lại / gửi duyệt lại."

Mirror PeDetailTabs.tsx HistoryTab fe-admin + fe-user.

B3 (937eb24) — Clone V2 cho B (DuyetNccPhuongAn)

User: "OK đồng ý cứ giữ đúng thiết kế như thế này, hiện tại, tao chốt: Quy trình chọn thầu phụ - NCC → Duyệt NCC → Đã đúng chính xác với yêu cầu của tao. Plan cho tao kế hoạch clone toàn bộ các cập nhật từ lúc đổi lại quy trình → Đến thời điểm hiện tại và clone nó sang: Quy trình chọn thầu phụ - NCC → Duyệt NCC và Giải pháp."

Audit reuse trước khi clone — phát hiện 80% đã chung qua ApplicableType discriminator:

Layer Đã chung cho B?
Schema V2 (Mig 22-24) ApplicableType enum 1=A / 2=B / 3=Contract
BE Service ApproveV2Async không hardcode type
App CQRS GetAwAdminOverview / CreateAwDefinition / Validator chung
API /api/approval-workflows-v2?applicableType=N dynamic
FE Designer ApprovalWorkflowsV2Page TYPE_CODE_TO_INT cả 3 type
FE Layout regex ^AwV2_(.+)$ match dynamic typeCode
FE App.tsx route :typeCode dynamic
FE Workspace + List + Pending chung qua ?type=N
Menu Pe_DuyetNccPhuongAn_* (List/Create/Pending) đã seed Mig 12

Chỉ thiếu (3 file ~60 LOC):

  • Domain/Identity/MenuKeys.cs +const ApprovalWorkflowDuyetNccPhuongAnV2 = "AwV2_DuyetNccPhuongAn" + add vào All[]
  • Infrastructure/Persistence/DbInitializer.cs:
    • SeedMenusAsync +leaf "Duyệt NCC và Giải pháp (Mới)" (Order=2 cạnh leaf A Order=1) dưới root ApprovalWorkflowsV2
    • +method SeedSampleApprovalWorkflowsV2Async (idempotent — skip nếu admin đã tạo workflow B nào hoặc thiếu test user nv.test@solutions.com.vn / Phòng CCM):
      • Seed QT-DN-PA-V2-001 v01 IsActive=true
      • 1 Bước "Phòng CCM" (DepartmentId resolve từ Code="CCM")
      • 1 Cấp 1 NV: test user nv.test@solutions.com.vn
  • fe-admin/src/lib/menuKeys.ts +AwV2_DuyetNccPhuongAn

KHÔNG cần migration / Service code mới / Designer page mới. Memory mới feedback_audit_reuse_before_clone.md capture pattern.

User feedback: "OK khá tốt, 1 phát chạy luôn :))" → confirm approach worked.

B4 (f77ea38) — Fix silent 403 ApprovalWorkflowsV2Controller

Triệu chứng: UAT user nv.test (Drafter) login Workspace tạo phiếu B → dropdown "Quy trình duyệt" empty mặc dù Admin Designer cùng URL endpoint thấy v01 sample + v02 admin clone đầy đủ. KHÔNG có toast error / network panel hint.

Root cause: ApprovalWorkflowsV2Controller class-level [Authorize(Policy = "Workflows.Read")] → non-admin role (Drafter chỉ có PurchaseEvaluations.Read) bị 403 Forbidden khi GET /api/approval-workflows-v2. TanStack Query catch HTTP error trả data=undefined, FE component render dropdown empty không có "loading" / "error" state visible → silent fail.

Fix:

[ApiController]
[Route("api/approval-workflows-v2")]
[Authorize]                                   // ← any authenticated, không hardcode policy
public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
{
    [HttpGet]
    public async Task<...> Overview(...) { ... }   // ← inherit class policy = any authenticated

    [HttpPost]
    [Authorize(Policy = "Workflows.Create")]  // ← admin Designer
    public async Task<...> Create(...) { ... }

    [HttpPatch("{id:guid}/user-selectable")]
    [Authorize(Policy = "Workflows.Create")]
    public async Task<...> SetUserSelectable(...) { ... }   // (added at B6)

    [HttpDelete("{id:guid}")]
    [Authorize(Policy = "Workflows.Create")]
    public async Task<...> Delete(...) { ... }
}

Pattern reusable cho Contract V2 Mig 26 sau (cùng controller). Add gotcha #44.

B5 (a9c0857) — Fix sidebar highlight queryMatches transient keys

Triệu chứng: Ở leaf "Danh sách" /purchase-evaluations?type=1, click chọn 1 phiếu → URL thành ?type=1&id=abc → leaf bị mất highlight box (gotcha #34 cũ tái phát theo cách khác).

Root cause: queryMatches exact-set equality:

if (aKeys.length !== bKeys.length) return false

target {type} (1 key) vs current {type, id} (2 keys) length mismatch → no match.

Fix:

const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', 'awId'])

function queryMatches(current, target) {
  const a = new URLSearchParams(current)
  const b = new URLSearchParams(target)
  const aKeys = [...a.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort()
  const bKeys = [...b.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort()
  if (aKeys.length !== bKeys.length) return false
  return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
}

Strip transient state keys (selection / search / filter / pagination) → giữ navigation identity (type, pendingMe, mode) check exact-set như cũ.

Edge cases verified:

URL hiện tại Target leaf Match
?type=1&id=abc Danh sách ?type=1 ✓ giữ highlight
?type=1&pendingMe=1 Danh sách ?type=1 ✗ distinct (không cross-highlight Pending)
?type=1&phase=10 Danh sách ?type=1 ✓ giữ highlight (filter dropdown)
?type=1&pendingMe=1&awId=xyz Duyệt ?type=1&pendingMe=1 ✓ giữ highlight

Mirror fe-admin + fe-user Layout.tsx.

B6 (2a53107) — Mig 25 IsUserSelectable + Designer pin toggle + bỏ "(clone)" + Workspace filter

User feedback Admin Designer (screenshot v01 sample + v02 + v03 admin clone): "Bỏ chữ Clone đi nhé, ghi v02, v03... là đủ rồi, khi tao kế thừa tạo quy trình mới. Thêm cho tao nút stick để chọn các quy trình nào mà User đc select bên ngoài khi tạo phiếu."

Bỏ "(clone)" auto-suffix:

  • fe-admin/ApprovalWorkflowsV2Page.tsx Designer line 403: name = cloneFrom.name (giữ nguyên), không append " (clone)". Version số (v02, v03, ...) đã đủ phân biệt.

Pin toggle "Cho user chọn":

Decision approach: Schema field riêng IsUserSelectable thay vì reuse IsActive. Lý do:

  • IsActive semantic = "đang áp dụng (default new version)" — max 1 per ApplicableType (atomic deactivate ở CreateAwDefinitionCommand)
  • User yêu cầu "chọn các quy trình nào" — implies multiple → cần multi-select field độc lập
  • Default behavior không break: backfill IsUserSelectable=1 WHERE IsActive=1 giữ active workflows visible cho user

Migration 25 AddIsUserSelectableToApprovalWorkflows:

ALTER TABLE ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0;
UPDATE ApprovalWorkflows SET IsUserSelectable = 1 WHERE IsActive = 1;  -- backfill

3-file rule commit đủ (.cs + Designer + Snapshot).

BE:

  • Domain ApprovalWorkflow.IsUserSelectable property — independent với IsActive
  • DTO AwDefinitionDto +field
  • CreateAwDefinitionCommand Handler set default IsUserSelectable = true cho version mới (mirror IsActive default)
  • New SetAwUserSelectableCommand(Guid Id, bool IsUserSelectable) + Handler — toggle stick/unstick
  • API endpoint PATCH /api/approval-workflows-v2/{id}/user-selectable (policy Workflows.Create admin only)
  • DbInitializer.SeedSampleApprovalWorkflowsV2Async set IsUserSelectable = true cho sample

FE Designer (fe-admin/ApprovalWorkflowsV2Page.tsx):

  • DefinitionDto +field isUserSelectable: boolean
  • TypePanel +mutation toggleSelectable call PATCH endpoint, invalidate query
  • DefinitionCard props +onToggleSelectable: () => void
  • Badge amber "📌 Cho user chọn" (lucide Pin icon) cạnh Đang áp dụng/Archived khi def.isUserSelectable === true
  • Button "📌 Ghim cho user / 🚫 Bỏ ghim" (lucide Pin / PinOff) trong action group

FE Workspace (cả fe-admin + fe-user PeWorkspaceCreateView.tsx):

  • approvalWorkflows useQuery filter (typeBucket?.history ?? []).filter(w => w.isUserSelectable) — chỉ workflows admin đã ghim hiển thị dropdown user

B7 (32a8d4d) — Cleanup orphan zip files

.claude.zip + docs.zip từ Claude harness session start (orphan dump untracked). B6 commit git add -A lỡ tay cuốn cả 2 file.

Fix:

  • git rm --cached 2 file
  • rm -f xóa local
  • Add *.zip rule vào .gitignore
  • Commit cleanup

E2E verified

  • dotnet build SolutionErp.slnx 0 error
  • dotnet test SolutionErp.slnx 81 pass (58 Domain + 23 Infra) — no change S18 (feature mới UAT defer test §7)
  • dotnet ef migrations add AddIsUserSelectableToApprovalWorkflows 3-file rule OK
  • Migration 25 apply LocalDB OK qua DbInitializer.MigrateAsync startup
  • npm run build × fe-admin + fe-user pass mỗi commit có FE changes
  • CI deploy commit B6 2a53107 success (Gitea Actions)
  • User UAT confirm B chạy 1 phát đúng (commit 937eb24)
  • User UAT confirm Designer toggle pin work (manual test sau commit B6)

Bug gặp + fix

Bug Fix
Drafter Workspace dropdown V2 empty silent (Workflows.Read 403) Class-level [Authorize] only, GET cho any authenticated (B4)
Sidebar leaf mất highlight khi click row (URL có id transient) Strip TRANSIENT_QUERY_KEYS trước queryMatches compare (B5)
Phiếu Nháp lọt vào leaf "Duyệt" (BE /inbox loose UAT) FE filter cứng getPeDisplayStatus === DaGuiDuyet (B1, workaround)
Workspace dropdown hiện cả archived versions (UX không rõ admin pick gì cho user) Mig 25 IsUserSelectable + Designer pin toggle + Workspace filter (B6)
Designer auto-fill "(clone)" suffix khi clone version (xấu, version đã đủ phân biệt) name = cloneFrom.name (B6)
2 file .zip orphan vào commit B6 (git add -A) git rm --cached + *.zip .gitignore (B7)

Docs updates

  • docs/STATUS.md — header 24→25 mig + 43→44 gotcha + 1 row Recently Done Session 18 (top)
  • docs/HANDOFF.md — TL;DR Session 18 mới + cảnh báo Session 19+ (giữ TL;DR S17 nguyên văn theo §6.5 không cắt narrative)
  • CLAUDE.md (root) — count 24→25 mig + Mig 25 description block
  • docs/database/schema-diagram.md §14 — heading "Migration 22-24" → "22-25" + +cột IsUserSelectable + section "IsUserSelectable filter logic" + Pending Session 19+ update Mig 26/27
  • docs/gotchas.md — +#44 silent 403 from over-restrictive Authorize policy + checklist debug item 21
  • docs/changelog/migration-todos.md — Phase 9 Session 18 done section đầy đủ + Defer Session 19+ checklist
  • docs/changelog/sessions/2026-05-08-1945-s18-pe-v2-polish-clone-b.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 (S19+)
  • docs/forms-spec.md — không liên quan
  • docs/database/database-guide.md — count migration không hardcode trong header
  • Skills (6) — không tạo mới, không refresh; drift ef-core-migration / dependency-audit-erp defer cron audit 2026-06-01

Memory updates

  • Add feedback_audit_reuse_before_clone.md (commit 937eb24 block context, B3 confirm pattern). MEMORY.md index +1 row.

Handoff to Session 19+

Đọc docs/HANDOFF.md "## ⚠️ Điều quan trọng cho Session 19+" cho 6 cảnh báo đầy đủ.

Top priorities:

  1. Contract V2 wire (Mig 26) — apply audit-reuse pattern, mirror PE
  2. Phân quyền strict V2 — list/inbox/detail filter theo actor role + approver scope
  3. Drop legacy V1 cleanup (Mig 27) sau khi không còn phiếu pin V1

Thông số cumulative

Trước S18 Sau S18 Δ
Migrations 24 25 +1
DB tables 58 58 0 (Mig 25 chỉ ALTER cột)
API endpoints ~140 ~141 +1 PATCH user-selectable
FE pages 33 33 0 (modify existing)
Test pass 81 81 0 (UAT defer test §7)
Gotchas 43 44 +1 #44 silent 403
Memory entries 13 14 +1 audit reuse pattern
Skills 6 6 0
Commits (after S17) +7 aaa1c6c32a8d4d
BE LOC ~16100 ~16200 +~100 (Mig 25 + Set command + endpoint + sample seed)
FE LOC ~17500 ~17600 +~100 (Designer pin toggle + Workspace filter + filter helpers)