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>
15 KiB
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.lengthkhipendingMe(inbox không paged, không phảilist.data?.total)
- Khi
PurchaseEvaluationsListPage.tsxcả 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+constApprovalWorkflowDuyetNccPhuongAnV2 = "AwV2_DuyetNccPhuongAn"+ add vàoAll[]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 rootApprovalWorkflowsV2- +method
SeedSampleApprovalWorkflowsV2Async(idempotent — skip nếu admin đã tạo workflow B nào hoặc thiếu test usernv.test@solutions.com.vn/ Phòng CCM):- Seed
QT-DN-PA-V2-001 v01IsActive=true - 1 Bước "Phòng CCM" (DepartmentId resolve từ
Code="CCM") - 1 Cấp 1 NV: test user
nv.test@solutions.com.vn
- Seed
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.tsxDesigner 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:
IsActivesemantic = "đ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=1giữ 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.IsUserSelectableproperty — independent vớiIsActive - DTO
AwDefinitionDto+field CreateAwDefinitionCommandHandler set defaultIsUserSelectable = truecho 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(policyWorkflows.Createadmin only) DbInitializer.SeedSampleApprovalWorkflowsV2AsyncsetIsUserSelectable = truecho sample
FE Designer (fe-admin/ApprovalWorkflowsV2Page.tsx):
DefinitionDto+fieldisUserSelectable: booleanTypePanel+mutationtoggleSelectablecall PATCH endpoint, invalidate queryDefinitionCardprops +onToggleSelectable: () => void- Badge amber "📌 Cho user chọn" (lucide
Pinicon) cạnh Đang áp dụng/Archived khidef.isUserSelectable === true - Button "📌 Ghim cho user / 🚫 Bỏ ghim" (lucide
Pin/PinOff) trong action group
FE Workspace (cả fe-admin + fe-user PeWorkspaceCreateView.tsx):
approvalWorkflowsuseQuery 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 --cached2 filerm -fxóa local- Add
*.ziprule vào.gitignore - Commit cleanup
E2E verified
- ✅
dotnet build SolutionErp.slnx0 error - ✅
dotnet test SolutionErp.slnx81 pass (58 Domain + 23 Infra) — no change S18 (feature mới UAT defer test §7) - ✅
dotnet ef migrations add AddIsUserSelectableToApprovalWorkflows3-file rule OK - ✅ Migration 25 apply LocalDB OK qua
DbInitializer.MigrateAsyncstartup - ✅
npm run build× fe-admin + fe-user pass mỗi commit có FE changes - ✅ CI deploy commit B6
2a53107success (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 blockdocs/database/schema-diagram.md §14— heading "Migration 22-24" → "22-25" + +cộtIsUserSelectable+ section "IsUserSelectable filter logic" + Pending Session 19+ update Mig 26/27docs/gotchas.md— +#44 silent 403 from over-restrictive Authorize policy + checklist debug item 21docs/changelog/migration-todos.md— Phase 9 Session 18 done section đầy đủ + Defer Session 19+ checklistdocs/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ớidocs/architecture.md— không có changes structuraldocs/PROJECT-MAP.md— không có structural changesdocs/workflow-contract.md— Contract V2 chưa wire (S19+)docs/forms-spec.md— không liên quandocs/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(commit937eb24block 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:
- Contract V2 wire (Mig 26) — apply audit-reuse pattern, mirror PE
- Phân quyền strict V2 — list/inbox/detail filter theo actor role + approver scope
- 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 | aaa1c6c → 32a8d4d |
| BE LOC | ~16100 | ~16200 | +~100 (Mig 25 + Set command + endpoint + sample seed) |
| FE LOC | ~17500 | ~17600 | +~100 (Designer pin toggle + Workspace filter + filter helpers) |