[CLAUDE] Docs+Skill: Session 19 consolidate — schema-diagram §15 + skill refresh
User chốt Session 19 toàn bộ: update các MD chưa làm ở Chunk D Docs trước (theo §6.5 không cố sửa khi không cần — nhưng schema mới fundamental + skill drift quá lệch → đáng update ngay). - docs/database/schema-diagram.md — thêm §15 PE Level Opinions V2 (Mig 26): bối cảnh decision (5 câu Q&A user chốt), schema bảng + constraint design rationale (UNIQUE composite, FK Cascade Pe + Restrict Level, denorm SignedByFullName), Service hook pattern (ApproveV2Async UPSERT), so sánh anti-pattern Mig 15 cũ (endpoint POST/opinions rời) vs Mig 26 (Service hook auto sync khi Duyệt). Pattern reusable cho derived state khác. §16 cũ "Liên quan" đổi thành §16 (renumber). - .claude/skills/ef-core-migration/SKILL.md — frontmatter "21 migration" → "26 migration" + table history thêm Mig 22 ApprovalWorkflowsV2 / Mig 23 PE.ApprovalWorkflowId / Mig 24 CurrentApprovalLevelOrder / Mig 25 IsUserSelectable / Mig 26 PeLevelOpinionsForV2. Total bảng 55→59. Code pointers + Related cross-ref §15 mới. - .claude/skills/README.md — count "16 migration" → "26 migration" + "41 bẫy" → "44 bẫy" (drift cumulative S16-S19 patch). Path filter `.claude/skills/**` + `docs/**` → CI skip deploy (gotcha #41). Verify: dotnet test 81 pass (no regression). Memory home dir KHÔNG commit (ở C:\Users\pqhuy\.claude\): add entry mới `feedback_service_hook_vs_endpoint.md` + MEMORY.md index +1 row (15 entries total).
This commit is contained in:
@ -805,12 +805,84 @@ Default behavior (sau Mig 25 backfill): active workflows tự động `IsUserSel
|
||||
|
||||
Workspace `PeWorkspaceCreateView` query `.filter(w => w.isUserSelectable)` — chỉ hiện workflows admin đã ghim.
|
||||
|
||||
### Pending Session 19+:
|
||||
### Pending Session 20+:
|
||||
|
||||
- Contract V2 wire (Mig 26): mirror PE pattern — thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` + `ContractWorkflowService.ApproveV2Async`
|
||||
- Drop legacy V1: sau khi không còn phiếu pin `WorkflowDefinitionId` → drop `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + drop deprecated columns `RejectedAtStepIndex` / `RejectedFromPhase` (Mig 27 cleanup)
|
||||
- Contract V2 wire (Mig 27/28): mirror PE pattern — thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` + `ContractWorkflowService.ApproveV2Async` + `ContractLevelOpinions` (mirror Mig 26)
|
||||
- Drop legacy V1: sau khi không còn phiếu pin `WorkflowDefinitionId` → drop `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + drop deprecated columns `RejectedAtStepIndex` / `RejectedFromPhase` (Mig cleanup)
|
||||
- Drop Mig 15 `PurchaseEvaluationDepartmentOpinions` cho V2 phiếu (sau khi tất cả phiếu V2 chỉ dùng Mig 26 LevelOpinions). Phiếu V1 legacy giữ Mig 15 backward compat.
|
||||
|
||||
## 15. Liên quan
|
||||
## 15. PE Level Opinions V2 (Migration 26 — 1 bảng mới, Session 19)
|
||||
|
||||
**Mục đích:** Thay thế Section 5 "Ý kiến 4 phòng ban" CỨNG (Mig 15 — PheDuyet/CCM/MuaHàng/SmPm) → động theo `ApprovalWorkflowsV2` (Mig 22-25). Mỗi (PE × Level) = 1 row sign-off với ý kiến + chữ ký NV.
|
||||
|
||||
**Bối cảnh decision (đáng giữ nguyên văn cho session sau):** User UAT Section 5 cũ "có 4 box CỨNG" — không scale theo workflow V2 cấu hình động (N Bước × N Cấp × N NV). Q&A 5 câu chốt spec trước code (memory `feedback_drastic_refactor_scope` áp dụng):
|
||||
|
||||
- **Q1=1B (gắn — KHÔNG rời):** Comment khi NV nhấn Duyệt qua Workflow Panel → auto sync sang OpinionBox của NV đó. Section 5 read-only summary, KHÔNG có form input rời (anti-pattern Mig 15 cũ — user phải double action: duyệt + ký).
|
||||
- **Q2=2A+Admin override:** NV chính chủ duyệt (match `ApproverUserId == actor.Id`). Admin override → fallback first level group, lưu `SignedByUserId = admin.Id`. FE banner amber "⚠ Admin <name> duyệt thay" khi `SignedByUserId !== ApproverUserId` → minh bạch ai thực sự duyệt.
|
||||
- **Q3=V2 hết:** Phiếu V2 (pin `ApprovalWorkflowId`) → render dynamic. Phiếu V1 legacy (chỉ `WorkflowDefinitionId`) → fallback Mig 15 readOnly (giữ data history, KHÔNG drop). Mig 15 deprecated cho V2 phiếu, drop sau UAT confirm.
|
||||
- **Q4=4C+placeholder:** Phase=DaDuyet/TuChoi → khoá hoàn toàn. Comment empty/whitespace → ghi `"(duyệt — không ý kiến)"` placeholder (vẫn tính là sign-off).
|
||||
- **Q5=5A grid layout:** Group theo Step (header "Bước N — Phòng X" badge emerald + hint "(N người duyệt)") → grid-cols-2 cho N approvers. "Bước 1 phòng A có 2 NV → 2 box ngang hàng" — wrap row khi N>2.
|
||||
|
||||
**Bảng `PurchaseEvaluationLevelOpinions`:**
|
||||
|
||||
```
|
||||
Id uniqueidentifier PK
|
||||
PurchaseEvaluationId FK Cascade → PurchaseEvaluations
|
||||
ApprovalWorkflowLevelId FK Restrict → ApprovalWorkflowLevels (Mig 22)
|
||||
Comment nvarchar(2000) -- ý kiến hoặc placeholder
|
||||
SignedAt datetime2 NOT NULL -- luôn có khi UPSERT
|
||||
SignedByUserId uniqueidentifier NOT NULL -- NV chính chủ HOẶC Admin override
|
||||
SignedByFullName nvarchar(200) NOT NULL -- denorm: tránh user xóa/đổi tên
|
||||
+ AuditableEntity audit fields (CreatedAt/UpdatedAt/IsDeleted/...)
|
||||
|
||||
UNIQUE (PurchaseEvaluationId, ApprovalWorkflowLevelId)
|
||||
INDEX (ApprovalWorkflowLevelId)
|
||||
```
|
||||
|
||||
**Constraint design rationale (KHÔNG paraphrase):**
|
||||
|
||||
- **UNIQUE composite (PEId, LevelId):** 1 row per (phiếu × Cấp). Latest write wins khi Service ApproveV2Async UPSERT — phiếu loop qua TraLai → resubmit → cùng NV duyệt lại = OVERWRITE row cũ (không tạo duplicate). Multi-NV cùng Cấp (OR-of-N) = nhiều LevelId distinct → mỗi NV có row riêng nếu duyệt.
|
||||
- **FK Cascade Pe:** Xoá phiếu (soft-delete actually) → opinions cascade.
|
||||
- **FK Restrict Level:** Admin xoá Level (qua Designer V2) bị chặn nếu opinion tồn tại — protect history sign-off.
|
||||
- **`SignedByUserId` KHÔNG nav (denorm `SignedByFullName`):** User có thể bị soft-delete / đổi tên → opinion vẫn render lịch sử đúng tên tại thời điểm ký. Tránh cascade phức tạp khi xoá user.
|
||||
|
||||
**Service hook pattern (ApproveV2Async):**
|
||||
|
||||
```csharp
|
||||
// Sau khi log approval row → UPSERT opinion cho Cấp hiện tại
|
||||
var matchingLevel = pendingLevelGroup
|
||||
.FirstOrDefault(l => l.ApproverUserId == actorUserId) // NV chính chủ
|
||||
?? pendingLevelGroup.First(); // Admin fallback first
|
||||
|
||||
var existing = await db.PurchaseEvaluationLevelOpinions
|
||||
.FirstOrDefaultAsync(o => o.PEId == pe.Id && o.LevelId == matchingLevel.Id, ct);
|
||||
|
||||
var normalizedComment = string.IsNullOrWhiteSpace(comment)
|
||||
? "(duyệt — không ý kiến)" // Q4 bonus placeholder
|
||||
: comment.Trim();
|
||||
|
||||
// Upsert: existing → update; null → Add new
|
||||
```
|
||||
|
||||
**Reject KHÔNG sync** (Trả lại / Từ chối) — giữ snapshot opinion cũ nếu có.
|
||||
|
||||
**Lưu ý anti-pattern (so với Mig 15 cũ):**
|
||||
|
||||
- ❌ Mig 15 dùng endpoint POST `/pe/{id}/opinions` cho user nhập rời + 4 box CỨNG → user phải nhấn 2 action (Duyệt workflow + Ký opinion box) → UX kém + risk inconsistency
|
||||
- ✅ Mig 26 dùng Service hook UPSERT khi nhấn Duyệt → 1 action user, sync auto
|
||||
|
||||
→ Pattern **"derived state qua Service hook, KHÔNG endpoint CRUD riêng"** áp dụng được cho:
|
||||
- Audit trail tự động khi save entity
|
||||
- Notification interceptor SaveChangesInterceptor
|
||||
- Code generation (gen mã HĐ khi DangDongDau) — đã có
|
||||
|
||||
### Pending Session 20+:
|
||||
|
||||
- Contract Section 5 V2 dynamic — mirror PE Mig 26 pattern. Tạo `ContractLevelOpinions` (Mig sau Contract V2 wire). Audit-reuse pattern memory `feedback_audit_reuse_before_clone` áp dụng.
|
||||
- Drop Mig 15 cho V2 phiếu — cleanup sau UAT confirm.
|
||||
- Test V2 Service hook — Domain test ApproveV2 + UPSERT opinion match logic + Admin override match firstLevel + comment empty placeholder.
|
||||
|
||||
## 16. Liên quan
|
||||
|
||||
- [`database-guide.md`](database-guide.md) — conventions + migration workflow + cheatsheet đầy đủ
|
||||
- [`../architecture.md`](../architecture.md) — layered architecture + data flow
|
||||
|
||||
Reference in New Issue
Block a user