[CLAUDE] PurchaseEvaluation: demo seed 4 phieu + MaPhieu atomic sequence + Pe_* perm defaults
Polish session tiep cua PE module skeleton (commit 2c6f0ca..3990066):
3 task A (MISSING in MVP) khac STATUS.md In Progress:
1. Demo PE data seed (SeedDemoPurchaseEvaluationsAsync)
- 4 phieu varied A/B x phase: A-001 DangSoanThao (mo), A-002
ChoCEODuyetNCC (winner+9 quotes), A-003 DaDuyet (chua tao HD,
PaymentTerms JSON), B-001 ChoDuAn (5-step giua chung).
- Idempotent: skip-if-[DEMO]-exists.
- Approval history dung policy A (3-step) hoac B (5-step).
2. MaPhieu atomic sequence — Migration 13
- Format PE/{YYYY}/{TypeLetter}/{Seq:D3} (vd PE/2026/A/001).
- PurchaseEvaluationCodeSequence entity (Prefix PK).
- IPurchaseEvaluationCodeGenerator + impl SERIALIZABLE
transaction (mirror ContractCodeGenerator 1:1).
- Replace Random.Shared trong CreatePurchaseEvaluationCommandHandler.
- Migration AddPurchaseEvaluationCodeSequences (1 bang).
3. Pe_* permission defaults
- SeedPurchaseEvaluationPermissionDefaultsAsync — 7 role business x 9 menu key.
- Drafter/DeptManager/Procurement: R+C+U; CostControl/PM/Director/AuthorizedSigner: R+U.
- DeptManager them Delete (xoa nhap).
- Idempotent per-(roleId x menuKey).
Build: 0 error, 2 warning (pre-existing DocxRenderer).
Files: 4 new + 8 modified (1 migration + entity + generator + DI + 2 ctx + 2 features).
Resolves: STATUS.md In Progress §A — 3 item PE MISSING.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,19 +1,22 @@
|
|||||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
# HANDOFF — Brief 5 phút cho session tiếp theo
|
||||||
|
|
||||||
**Last updated:** 2026-04-23 tối (Phase 6 — PE skeleton + G-084 hardening, **PE còn chỉnh nhiều**)
|
**Last updated:** 2026-04-24 sáng (Phase 6 — PE polish: demo seed + MaPhieu atomic + Pe_* perm defaults)
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
**PE module skeleton E2E** — 2 quy trình A/B config được admin, 10 bảng, 17
|
**PE module skeleton E2E** — 2 quy trình A/B config được admin, 10 bảng, 17
|
||||||
endpoint, 3 page × 2 app + kế thừa HĐ 1-click. BE + FE + migration đã push
|
endpoint, 3 page × 2 app + kế thừa HĐ 1-click. BE + FE + migration đã push
|
||||||
lên Gitea main (`2c6f0ca..3990066`, 6 commit). **CHƯA XONG — nhiều polish
|
lên Gitea main (`2c6f0ca..3990066`, 6+ commit). **Polish session 2026-04-24:**
|
||||||
+ feature thiếu cho session tiếp** (xem STATUS.md §🔥 In Progress đầy đủ).
|
demo seed 4 phiếu (`[DEMO] PE/.../A-001..A-003 + B-001`) + MaPhieu atomic
|
||||||
|
sequence (mirror ContractCodeSequences) + Pe_* permission defaults cho 7
|
||||||
|
role business → admin login thấy data ngay. Còn polish nhỏ (designer UI,
|
||||||
|
attachments, details mapping, export PDF) — xem STATUS.md §🔥 In Progress.
|
||||||
|
|
||||||
**G-084 hardening:** localhost → 127.0.0.1 trong scripts + skill, doc gotcha
|
**G-084 hardening:** localhost → 127.0.0.1 trong scripts + skill, doc gotcha
|
||||||
#33 từ bài học VietReport (IPv4/IPv6 port hijack trên VPS shared).
|
#33 từ bài học VietReport (IPv4/IPv6 port hijack trên VPS shared).
|
||||||
|
|
||||||
**Tổng:** 46 DB tables, ~110 endpoints, 12 migrations, 33 gotchas. Blocker
|
**Tổng:** 47 DB tables (+1 `PurchaseEvaluationCodeSequences`), ~110 endpoints,
|
||||||
user/ops cũ vẫn còn: UAT thật + SMTP + rotate creds + runner apply.
|
13 migrations (+13 `AddPurchaseEvaluationCodeSequences`), 33 gotchas.
|
||||||
|
|
||||||
## ⚠️ CẢNH BÁO session tiếp
|
## ⚠️ CẢNH BÁO session tiếp
|
||||||
|
|
||||||
@ -110,9 +113,9 @@ Xem **STATUS.md §🔥 In Progress** đầy đủ (nhóm A/B/C/D). Tóm tắt nh
|
|||||||
2. **PE Attachments upload** — copy pattern `ContractAttachmentFeatures.cs` + `ContractAttachmentsSection.tsx`. Entity + enum có sẵn.
|
2. **PE Attachments upload** — copy pattern `ContractAttachmentFeatures.cs` + `ContractAttachmentsSection.tsx`. Entity + enum có sẵn.
|
||||||
3. **Ý kiến 4 phòng ban** (Phê duyệt / P.CCM / P.MuaHàng / SM-PM) — Excel form có, entity chưa map. Cần design: 4 text field + signoff date, hoặc dùng Approvals với role-kind.
|
3. **Ý kiến 4 phòng ban** (Phê duyệt / P.CCM / P.MuaHàng / SM-PM) — Excel form có, entity chưa map. Cần design: 4 text field + signoff date, hoặc dùng Approvals với role-kind.
|
||||||
4. **Payment terms tách field** từ JSON blob → 6 field riêng (Tạm ứng / TT tạm / Quyết toán / Bảo hành / Hạn mức / Đánh giá) theo Excel section D.
|
4. **Payment terms tách field** từ JSON blob → 6 field riêng (Tạm ứng / TT tạm / Quyết toán / Bảo hành / Hạn mức / Đánh giá) theo Excel section D.
|
||||||
5. **Seed demo PE data** — 2-3 phiếu cho UAT + screenshot.
|
5. ~~Seed demo PE data~~ ✅ DONE (2026-04-24, 4 phiếu varied A/B × phase).
|
||||||
6. **Permission grant Pe_* cho non-admin role** — `/system/permissions` tick `PurchaseEvaluations.Read` + CRUD cho role Drafter/PRO/CCM/BOD.
|
6. ~~Permission grant Pe_* defaults~~ ✅ DONE (2026-04-24, 7 role × 9 menu key).
|
||||||
7. **MaPhieu format chính thức** — user confirm.
|
7. ~~MaPhieu format chính thức~~ ✅ DONE (2026-04-24, atomic `PE/{YYYY}/{A\|B}/{Seq:D3}`).
|
||||||
8. **Export phiếu PDF/Excel** — tái dùng `IDocumentConverter` + template.
|
8. **Export phiếu PDF/Excel** — tái dùng `IDocumentConverter` + template.
|
||||||
9. **Auto-map PE Details → Contract Details** khi gen HĐ (optional — nâng cấp).
|
9. **Auto-map PE Details → Contract Details** khi gen HĐ (optional — nâng cấp).
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
|
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
|
||||||
|
|
||||||
**Last updated:** 2026-04-23 tối (Phase 6 — PE module **skeleton E2E + G-084 hardening, CÒN CHỈNH NHIỀU**)
|
**Last updated:** 2026-04-24 sáng (Phase 6 — PE module **demo seed + MaPhieu atomic + Pe_* perm defaults**)
|
||||||
|
|
||||||
## 📍 Phase hiện tại: **Module Duyệt NCC (tiền-HĐ) — skeleton deployed, refinement WIP** — 46 DB tables (+10 PE), ~110 endpoints (+17 PE), 12 migrations, 6 commits PE push Gitea (`2c6f0ca..3990066`). Skeleton BE + FE + kế thừa HĐ hoạt động; còn nhiều polish + feature thiếu (designer UI, attachments, details mapping, export PDF).
|
## 📍 Phase hiện tại: **Module Duyệt NCC (tiền-HĐ) — skeleton deployed, refinement WIP** — 47 DB tables (+10 PE +1 MaPhieu seq), ~110 endpoints (+17 PE), 13 migrations (+13 `AddPurchaseEvaluationCodeSequences`), 6+ commits PE push Gitea. Skeleton BE + FE + kế thừa HĐ + 4 [DEMO] PE seed + Pe_* perm defaults hoạt động; còn polish: designer UI, attachments, details mapping, export PDF.
|
||||||
|
|
||||||
### 🌐 Production URLs
|
### 🌐 Production URLs
|
||||||
|
|
||||||
@ -27,8 +27,8 @@
|
|||||||
- Menu leaf `PeWf_*` đã seed rồi, resolver Layout.tsx đã map `/system/pe-workflows/:code`
|
- Menu leaf `PeWf_*` đã seed rồi, resolver Layout.tsx đã map `/system/pe-workflows/:code`
|
||||||
- [ ] **PE Attachments upload** — pattern copy từ `ContractAttachmentFeatures.cs` + FE `ContractAttachmentsSection.tsx`. Entity + `PurchaseEvaluationAttachmentPurpose` enum đã sẵn (QuoteDocument/RequirementSpec/DecisionExport).
|
- [ ] **PE Attachments upload** — pattern copy từ `ContractAttachmentFeatures.cs` + FE `ContractAttachmentsSection.tsx`. Entity + `PurchaseEvaluationAttachmentPurpose` enum đã sẵn (QuoteDocument/RequirementSpec/DecisionExport).
|
||||||
- [ ] **Auto-map PE Details → Contract 7 per-type Details khi gen HĐ** — hiện `CreateContractFromEvaluationCommand` chỉ copy header + GiaTri, KHÔNG copy Details. Cần mapping per ContractType (PE Detail schema flat ≠ 7 Contract Details schemas).
|
- [ ] **Auto-map PE Details → Contract 7 per-type Details khi gen HĐ** — hiện `CreateContractFromEvaluationCommand` chỉ copy header + GiaTri, KHÔNG copy Details. Cần mapping per ContractType (PE Detail schema flat ≠ 7 Contract Details schemas).
|
||||||
- [ ] **Demo PE data seed** — chưa có `[DEMO]` PE nào. Cần 2-3 phiếu seed (1 NccOnly đã DaDuyet chưa tạo HĐ, 1 NccWithPlan ở phase ChoCCM giữa chừng, 1 TuChoi) cho UAT + screenshot.
|
- [x] ✅ **Demo PE data seed** (2026-04-24) — 4 phiếu `[DEMO]` cover full state-space: A-001 DangSoanThao (Drafter mới mở), A-002 ChoCEODuyetNCC (winner đề xuất + 9 quotes), A-003 DaDuyet (chưa tạo HĐ — kế thừa demo), B-001 ChoDuAn (5-step giữa chừng). Idempotent skip-if-exists. PaymentTerms JSON cho A-003.
|
||||||
- [ ] **MaPhieu format chính thức** — hiện `PE-YYYYMM-XXXX` random 4-digit. User said "tính sau". Cần confirm format: `{ProjectCode}/PE/{seq:D3}` hoặc atomic sequence bảng như `ContractCodeSequences`.
|
- [x] ✅ **MaPhieu atomic sequence** (2026-04-24) — Migration 13 `AddPurchaseEvaluationCodeSequences` (1 bảng, Prefix PK). `IPurchaseEvaluationCodeGenerator` + `PurchaseEvaluationCodeGenerator` (SERIALIZABLE transaction, mirror ContractCodeGenerator). Format `PE/{YYYY}/{TypeLetter}/{Seq:D3}` — VD `PE/2026/A/001`. Wired vào CreatePurchaseEvaluationCommandHandler thay Random.Shared. DI registered.
|
||||||
- [ ] **Section "Ý kiến các phòng ban" (Phê duyệt/P.CCM/P.MuaHàng/SM-PM) ở tab Thông tin** — Excel form mẫu có, entity hiện chưa map. Cần thêm 4-8 text field + signoff date (hoặc dùng Approvals row như ContractApprovals).
|
- [ ] **Section "Ý kiến các phòng ban" (Phê duyệt/P.CCM/P.MuaHàng/SM-PM) ở tab Thông tin** — Excel form mẫu có, entity hiện chưa map. Cần thêm 4-8 text field + signoff date (hoặc dùng Approvals row như ContractApprovals).
|
||||||
|
|
||||||
### B. UX / Polish cần chỉnh
|
### B. UX / Polish cần chỉnh
|
||||||
@ -36,7 +36,7 @@
|
|||||||
- [ ] **Payment terms chi tiết** — hiện JSON blob. UX tách field riêng (Tạm ứng / TT tạm / Quyết toán / Bảo hành / Hạn mức / Đánh giá) theo Excel section D.
|
- [ ] **Payment terms chi tiết** — hiện JSON blob. UX tách field riêng (Tạm ứng / TT tạm / Quyết toán / Bảo hành / Hạn mức / Đánh giá) theo Excel section D.
|
||||||
- [ ] **Matrix Quotes bulk paste** — click cell → popup: OK. Nhưng bulk paste column giá từ Excel chưa có (power user feature).
|
- [ ] **Matrix Quotes bulk paste** — click cell → popup: OK. Nhưng bulk paste column giá từ Excel chưa có (power user feature).
|
||||||
- [ ] **Export phiếu PDF/Excel** — user cần bản in ký. Tái dùng `IDocumentConverter` + template `PE-TrinhDuyet.docx`.
|
- [ ] **Export phiếu PDF/Excel** — user cần bản in ký. Tái dùng `IDocumentConverter` + template `PE-TrinhDuyet.docx`.
|
||||||
- [ ] **Permission grant Pe_* cho non-admin role** — admin thấy menu Pe_*, non-admin chưa. Cần vào `/system/permissions` tick `PurchaseEvaluations.Read` cho role Drafter/Procurement/CostControl/Director.
|
- [x] ✅ **Permission grant Pe_* cho non-admin role** (2026-04-24) — `SeedPurchaseEvaluationPermissionDefaultsAsync` grant CRUD per role: Drafter/DeptManager/Procurement (R+C+U) · CostControl/PM/Director/AuthorizedSigner (R+U). DeptManager thêm Delete. Idempotent per-(roleId × menuKey). 9 menu key (PurchaseEvaluations + 2× group + 6× leaf) × 7 role = ~63 row defaults. Sau seed admin bật/tắt finer-grain qua `/system/permissions`.
|
||||||
- [ ] **fe-user Inbox** — hiện chỉ HĐ. Cần thêm section "Phiếu Duyệt NCC chờ tôi" (hoặc route `/pe-inbox` riêng).
|
- [ ] **fe-user Inbox** — hiện chỉ HĐ. Cần thêm section "Phiếu Duyệt NCC chờ tôi" (hoặc route `/pe-inbox` riêng).
|
||||||
- [ ] **Sidebar accordion fe-user** — test `Pe_DuyetNcc` group với `Ct_*` accordion context (Layout.tsx fe-user accordion hiện chỉ track `Ct_<code>`, có thể cần extend cover `Pe_<code>`).
|
- [ ] **Sidebar accordion fe-user** — test `Pe_DuyetNcc` group với `Ct_*` accordion context (Layout.tsx fe-user accordion hiện chỉ track `Ct_<code>`, có thể cần extend cover `Pe_<code>`).
|
||||||
|
|
||||||
@ -58,6 +58,7 @@
|
|||||||
|
|
||||||
| Ngày | Ai | Task | Commit |
|
| Ngày | Ai | Task | Commit |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| 2026-04-24 | Claude | **PE polish — Demo seed + MaPhieu atomic + Pe_* perm defaults** — Migration 13 `AddPurchaseEvaluationCodeSequences` (Prefix PK, mirror ContractCodeSequences). `IPurchaseEvaluationCodeGenerator` + impl SERIALIZABLE — format `PE/{YYYY}/{A\|B}/{Seq:D3}`. Replace Random.Shared trong CreatePEHandler. `SeedDemoPurchaseEvaluationsAsync` 4 phiếu varied (A-001 DangSoanThao, A-002 ChoCEODuyetNCC + 9 quotes, A-003 DaDuyet PaymentTerms JSON, B-001 ChoDuAn 5-step). `SeedPurchaseEvaluationPermissionDefaultsAsync` 7 role × 9 menu key — Drafter/DeptManager/Procurement Create+Update, các role duyệt R+U, DeptManager thêm Delete. | (pending) |
|
||||||
| 2026-04-24 | Claude | **Rebrand 3 domain huypham.vn → solutions.com.vn E2E** — 18 file repo (FE env + scripts + CI/CD + docs + skill + code comments). Viết `scripts/migrate-domains.ps1` (ASCII-only gotcha #30) chạy trên VPS: 3 HTTP binding mới + 3 cert Let's Encrypt (HTTP-01 via SelfHosting) + auto HTTPS 443 + redirect. CI/CD auto-deploy commit `66c1a5c+b93dacf`: appsettings.Production.json CORS mới + FE bundle `VITE_API_BASE_URL=api.solutions.com.vn`. E2E verified: `/api/purchase-evaluations` 401, CORS preflight OK, FE dist có domain mới. URL cũ fallback. Claude SSH tự chạy (không cần user RDP). | `66c1a5c` · `b93dacf` |
|
| 2026-04-24 | Claude | **Rebrand 3 domain huypham.vn → solutions.com.vn E2E** — 18 file repo (FE env + scripts + CI/CD + docs + skill + code comments). Viết `scripts/migrate-domains.ps1` (ASCII-only gotcha #30) chạy trên VPS: 3 HTTP binding mới + 3 cert Let's Encrypt (HTTP-01 via SelfHosting) + auto HTTPS 443 + redirect. CI/CD auto-deploy commit `66c1a5c+b93dacf`: appsettings.Production.json CORS mới + FE bundle `VITE_API_BASE_URL=api.solutions.com.vn`. E2E verified: `/api/purchase-evaluations` 401, CORS preflight OK, FE dist có domain mới. URL cũ fallback. Claude SSH tự chạy (không cần user RDP). | `66c1a5c` · `b93dacf` |
|
||||||
| 2026-04-23 | Claude | **SeedDemoUsersAsync robust reconcile + 16 demo users** — Reconcile pattern (per-user try-catch, fix drift dept/position/role nếu user existing). 13 → 16 demo (+bod.tran NĐUQ thứ 2, +pm.le PM thứ 2, +nv.truong NV CCM). Detailed log created/fixed/failed counts. Resolves prod issue: chỉ thấy admin user vì old skip-if-exists fail silent. | `a667665` |
|
| 2026-04-23 | Claude | **SeedDemoUsersAsync robust reconcile + 16 demo users** — Reconcile pattern (per-user try-catch, fix drift dept/position/role nếu user existing). 13 → 16 demo (+bod.tran NĐUQ thứ 2, +pm.le PM thứ 2, +nv.truong NV CCM). Detailed log created/fixed/failed counts. Resolves prod issue: chỉ thấy admin user vì old skip-if-exists fail silent. | `a667665` |
|
||||||
| 2026-04-23 | Claude | **G-084 hardening** — localhost → 127.0.0.1 trong `deploy-iis.ps1` + skill `iis-deploy-runbook`. Thêm gotcha #33 (IPv4/IPv6 port hijack) ref VietReport incident + 3 rules (reverse-proxy IP literal / backend loopback IPv4 explicit / service dependency). SOLUTION_ERP risk thấp (API in-process IIS, no ARR proxy) nhưng chuẩn hóa cho tương lai. | `3990066` |
|
| 2026-04-23 | Claude | **G-084 hardening** — localhost → 127.0.0.1 trong `deploy-iis.ps1` + skill `iis-deploy-runbook`. Thêm gotcha #33 (IPv4/IPv6 port hijack) ref VietReport incident + 3 rules (reverse-proxy IP literal / backend loopback IPv4 explicit / service dependency). SOLUTION_ERP risk thấp (API in-process IIS, no ARR proxy) nhưng chuẩn hóa cho tương lai. | `3990066` |
|
||||||
|
|||||||
@ -336,19 +336,29 @@
|
|||||||
- [ ] **Auto-map PE Details → Contract Details khi gen HĐ** (optional — nâng cấp)
|
- [ ] **Auto-map PE Details → Contract Details khi gen HĐ** (optional — nâng cấp)
|
||||||
- 7 mapping function per ContractType (khó vì schema khác biệt)
|
- 7 mapping function per ContractType (khó vì schema khác biệt)
|
||||||
- MVP: skip, user nhập manual
|
- MVP: skip, user nhập manual
|
||||||
- [ ] **Demo PE data seed** — `SeedDemoPurchaseEvaluationsAsync`
|
- [x] ✅ **Demo PE data seed** — `SeedDemoPurchaseEvaluationsAsync` (2026-04-24)
|
||||||
- 1 NccOnly DaDuyet chưa tạo HĐ (showcase kế thừa button)
|
- A-001 DangSoanThao (Drafter mới mở, chưa có quotes)
|
||||||
- 1 NccWithPlan phase ChoCCM (showcase workflow middle)
|
- A-002 ChoCEODuyetNCC (winner đề xuất + 9 quotes 3×3 grid)
|
||||||
- 1 TuChoi (showcase reject path)
|
- A-003 DaDuyet (chưa tạo HĐ — showcase kế thừa button) + PaymentTerms JSON
|
||||||
- [ ] **MaPhieu format chính thức** — hiện `PE-YYYYMM-XXXX` random. User confirm format + atomic sequence:
|
- B-001 ChoDuAn (5-step giữa chừng)
|
||||||
- Option A: `{ProjectCode}/PE/{yyyy}/{seq:D3}` — atomic via `PurchaseEvaluationCodeSequences` bảng mới
|
- Idempotent: skip-if-`[DEMO]` exists
|
||||||
- Option B: Tái dùng `ContractCodeSequences` với Prefix = `PE-{yyyy}-{ProjectCode}`
|
- [x] ✅ **MaPhieu format chính thức + atomic sequence** (2026-04-24)
|
||||||
|
- Format: `PE/{YYYY}/{TypeLetter}/{Seq:D3}` (TypeLetter = A | B)
|
||||||
|
- `PurchaseEvaluationCodeSequences` bảng mới (Prefix PK, mirror ContractCodeSequences)
|
||||||
|
- `IPurchaseEvaluationCodeGenerator` + impl SERIALIZABLE transaction
|
||||||
|
- Migration 13 `AddPurchaseEvaluationCodeSequences` (1 bảng)
|
||||||
|
- Wired vào CreatePurchaseEvaluationCommandHandler thay Random.Shared
|
||||||
|
|
||||||
### B. UX / Polish
|
### B. UX / Polish
|
||||||
|
|
||||||
- [ ] Matrix Quotes **bulk paste** từ Excel column giá → paste matrix row
|
- [ ] Matrix Quotes **bulk paste** từ Excel column giá → paste matrix row
|
||||||
- [ ] **Export phiếu PDF/Excel** — tái dùng `IDocumentConverter` + template `PE-TrinhDuyet.docx` upload qua FormsPage
|
- [ ] **Export phiếu PDF/Excel** — tái dùng `IDocumentConverter` + template `PE-TrinhDuyet.docx` upload qua FormsPage
|
||||||
- [ ] **Permission grant Pe_* cho non-admin role** — 1 click trong PermissionsPage matrix (Pe_* inherit từ `PurchaseEvaluations.Read` tương tự Contracts inheritance)
|
- [x] ✅ **Permission grant Pe_* cho non-admin role** (2026-04-24)
|
||||||
|
- `SeedPurchaseEvaluationPermissionDefaultsAsync` — 7 role × 9 menu key
|
||||||
|
- Drafter/DeptManager/Procurement: R+C+U
|
||||||
|
- CostControl/PM/Director/AuthorizedSigner: R+U
|
||||||
|
- DeptManager thêm Delete (xóa nháp)
|
||||||
|
- Idempotent per-(roleId × menuKey), admin tinh chỉnh tiếp qua /system/permissions
|
||||||
- [ ] **fe-user Inbox** thêm section "Phiếu Duyệt NCC chờ tôi" (hoặc route `/pe-inbox`)
|
- [ ] **fe-user Inbox** thêm section "Phiếu Duyệt NCC chờ tôi" (hoặc route `/pe-inbox`)
|
||||||
- [ ] **Sidebar accordion fe-user** extend cover `Pe_<code>` (hiện chỉ track `Ct_<code>`)
|
- [ ] **Sidebar accordion fe-user** extend cover `Pe_<code>` (hiện chỉ track `Ct_<code>`)
|
||||||
- [ ] **Dashboard** — thêm KPI "PE phiếu chờ tôi", "PE đã duyệt tháng này", "PE cần tạo HĐ"
|
- [ ] **Dashboard** — thêm KPI "PE phiếu chờ tôi", "PE đã duyệt tháng này", "PE cần tạo HĐ"
|
||||||
|
|||||||
@ -0,0 +1,127 @@
|
|||||||
|
# Session 2026-04-24 sáng — PE polish: Demo seed + MaPhieu atomic + Pe_* perm defaults
|
||||||
|
|
||||||
|
**Context:** Tiếp nối session "[Roles+Demo+Pending cleanup]" (commit a667665) — user yêu cầu
|
||||||
|
"Chốt lại toàn bộ MD: 'rules, architech, gotcha, skill, daily, hand-off, DB,
|
||||||
|
luồng DB ...' và làm tiếp đi nhé." → continue PE polish theo STATUS.md §🔥 In
|
||||||
|
Progress nhóm A (chức năng MISSING trong MVP).
|
||||||
|
|
||||||
|
## Tasks completed (4)
|
||||||
|
|
||||||
|
### 1. Demo PE data seed — 4 phiếu varied
|
||||||
|
|
||||||
|
**File:** `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs`
|
||||||
|
|
||||||
|
`SeedDemoPurchaseEvaluationsAsync` — pattern mirror `SeedDemoContractsAsync`:
|
||||||
|
|
||||||
|
| Phiếu | Type | Phase | Đặc điểm | Quotes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `[DEMO]-A-001` | DuyetNcc | DangSoanThao | Drafter mới mở, chưa nhập giá | 0 |
|
||||||
|
| `[DEMO]-A-002` | DuyetNcc | ChoCEODuyetNCC | Winner đề xuất + 9 quotes 3×3 grid | 9 |
|
||||||
|
| `[DEMO]-A-003` | DuyetNcc | DaDuyet | Chưa tạo HĐ — showcase kế thừa + PaymentTerms JSON | 4 |
|
||||||
|
| `[DEMO]-B-001` | DuyetNccPhuongAn | ChoDuAn | 5-step giữa chừng (Procurement đã chuyển) | 9 |
|
||||||
|
|
||||||
|
**Idempotent:** skip nếu MaPhieu prefix `[DEMO]` exists. Approval history seed
|
||||||
|
đúng theo policy A (3-step Procurement→CCM→CEO) hoặc B (5-step
|
||||||
|
Procurement→PM→CCM→CEO PA→CEO NCC). ApproverUserId mapping đúng role.
|
||||||
|
|
||||||
|
**Resolves:** STATUS.md §🔥 A — "Demo PE data seed".
|
||||||
|
|
||||||
|
### 2. MaPhieu atomic sequence — Migration 13
|
||||||
|
|
||||||
|
**Files mới:**
|
||||||
|
- `src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluationCodeSequence.cs` — Prefix PK + LastSeq + UpdatedAt
|
||||||
|
- `src/Backend/SolutionErp.Application/PurchaseEvaluations/Services/IPurchaseEvaluationWorkflowService.cs` — thêm `IPurchaseEvaluationCodeGenerator` interface
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationCodeGenerator.cs` — impl SERIALIZABLE transaction
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260424033449_AddPurchaseEvaluationCodeSequences.cs` — 1 bảng (Prefix nvarchar(100) PK)
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `IApplicationDbContext` + `ApplicationDbContext` thêm `PurchaseEvaluationCodeSequences` DbSet
|
||||||
|
- `PurchaseEvaluationConfiguration` thêm `PurchaseEvaluationCodeSequenceConfiguration` (Prefix max 100)
|
||||||
|
- `DependencyInjection` register Scoped `IPurchaseEvaluationCodeGenerator → PurchaseEvaluationCodeGenerator`
|
||||||
|
- `PurchaseEvaluationFeatures` `CreatePurchaseEvaluationCommandHandler` constructor inject codeGen, replace `Random.Shared` với `await codeGen.GenerateAsync(entity, ct)`
|
||||||
|
|
||||||
|
**Format:** `PE/{YYYY}/{TypeLetter}/{Seq:D3}` — VD `PE/2026/A/001`, `PE/2026/B/001`.
|
||||||
|
TypeLetter = `A` (DuyetNcc) | `B` (DuyetNccPhuongAn).
|
||||||
|
|
||||||
|
**Pattern mirror:** `ContractCodeGenerator` 1:1 — SERIALIZABLE transaction +
|
||||||
|
UPDATE-or-INSERT seq row + commit. Race-safe trên multi-instance.
|
||||||
|
|
||||||
|
**Resolves:** STATUS.md §🔥 A — "MaPhieu format chính thức".
|
||||||
|
|
||||||
|
### 3. Pe_* permission defaults — 7 role × 9 menu key
|
||||||
|
|
||||||
|
**File:** `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs`
|
||||||
|
|
||||||
|
`SeedPurchaseEvaluationPermissionDefaultsAsync` — call cuối
|
||||||
|
`SeedAdminPermissionsAsync`. Strategy:
|
||||||
|
|
||||||
|
| Role | Read | Create | Update | Delete |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Drafter, DeptManager, Procurement | ✓ | ✓ | ✓ | (chỉ DeptManager) |
|
||||||
|
| CostControl, ProjectManager, Director, AuthorizedSigner | ✓ | — | ✓ | — |
|
||||||
|
|
||||||
|
9 menu key per role: `PurchaseEvaluations` (root) + 2 group (`Pe_DuyetNcc`,
|
||||||
|
`Pe_DuyetNccPhuongAn`) + 6 leaf (List/Create/Pending × 2). Total ~63 row insert.
|
||||||
|
|
||||||
|
**Idempotent:** skip per-(roleId × menuKey) đã có row. Admin tinh chỉnh thêm
|
||||||
|
qua `/system/permissions`.
|
||||||
|
|
||||||
|
**Resolves:** STATUS.md §🔥 B — "Permission grant Pe_*".
|
||||||
|
|
||||||
|
### 4. Docs updates
|
||||||
|
|
||||||
|
- `docs/STATUS.md` — Phase line 7 "47 DB tables", "13 migrations". Tick `Demo PE data seed`, `MaPhieu format chính thức`, `Permission grant Pe_*`. Add Recently Done row.
|
||||||
|
- `docs/HANDOFF.md` — TL;DR update, gạch các item ✅ DONE, "47 DB tables" "13 migrations".
|
||||||
|
- `docs/changelog/migration-todos.md` — Tick 3 task PE.A.
|
||||||
|
|
||||||
|
## Build status
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet build SolutionErp.slnx --nologo -v minimal
|
||||||
|
→ Build succeeded. 2 Warning(s) (pre-existing DocxRenderer), 0 Error(s).
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files touched (12)
|
||||||
|
|
||||||
|
**New (4):**
|
||||||
|
- `src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluationCodeSequence.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationCodeGenerator.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260424033449_AddPurchaseEvaluationCodeSequences.cs` + `.Designer.cs`
|
||||||
|
|
||||||
|
**Modified (8):**
|
||||||
|
- `src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs`
|
||||||
|
- `src/Backend/SolutionErp.Application/PurchaseEvaluations/Services/IPurchaseEvaluationWorkflowService.cs`
|
||||||
|
- `src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs`
|
||||||
|
- `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs`
|
||||||
|
|
||||||
|
## Pending after this session (xem STATUS.md §🔥 In Progress)
|
||||||
|
|
||||||
|
### A. Còn MISSING
|
||||||
|
- PE Workflow admin designer UI (`/system/pe-workflows/:typeCode`)
|
||||||
|
- PE Attachments upload (entity + enum sẵn)
|
||||||
|
- Auto-map PE Details → Contract 7 per-type Details khi gen HĐ
|
||||||
|
- Section "Ý kiến phòng ban" trong PE form
|
||||||
|
|
||||||
|
### B. UX
|
||||||
|
- Payment terms tách field (JSON → 6 field)
|
||||||
|
- Matrix Quotes bulk paste từ Excel
|
||||||
|
- Export PDF/Excel
|
||||||
|
- fe-user Inbox PE section
|
||||||
|
- Sidebar accordion fe-user PE
|
||||||
|
|
||||||
|
### C. Edge case
|
||||||
|
- Reject path E2E
|
||||||
|
- Delete với linked contract
|
||||||
|
- Workflow v02 invariant pin v01
|
||||||
|
|
||||||
|
### D. Deploy
|
||||||
|
- win-acme task fix
|
||||||
|
- Remove huypham.vn old binding sau verify stable
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
`[CLAUDE] PurchaseEvaluation: demo seed 4 phiếu + MaPhieu atomic sequence + Pe_* perm defaults`
|
||||||
@ -56,6 +56,7 @@ public interface IApplicationDbContext
|
|||||||
DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions { get; }
|
DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions { get; }
|
||||||
DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps { get; }
|
DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps { get; }
|
||||||
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
||||||
|
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { get; }
|
||||||
|
|
||||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,8 @@ public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<Create
|
|||||||
public class CreatePurchaseEvaluationCommandHandler(
|
public class CreatePurchaseEvaluationCommandHandler(
|
||||||
IApplicationDbContext db,
|
IApplicationDbContext db,
|
||||||
ICurrentUser currentUser,
|
ICurrentUser currentUser,
|
||||||
IPurchaseEvaluationWorkflowService workflow) : IRequestHandler<CreatePurchaseEvaluationCommand, Guid>
|
IPurchaseEvaluationWorkflowService workflow,
|
||||||
|
IPurchaseEvaluationCodeGenerator codeGen) : IRequestHandler<CreatePurchaseEvaluationCommand, Guid>
|
||||||
{
|
{
|
||||||
public async Task<Guid> Handle(CreatePurchaseEvaluationCommand request, CancellationToken ct)
|
public async Task<Guid> Handle(CreatePurchaseEvaluationCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@ -67,8 +68,9 @@ public class CreatePurchaseEvaluationCommandHandler(
|
|||||||
workflow.GetPhaseSla(PurchaseEvaluationPhase.DangSoanThao) ?? TimeSpan.FromDays(3)),
|
workflow.GetPhaseSla(PurchaseEvaluationPhase.DangSoanThao) ?? TimeSpan.FromDays(3)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-gen MaPhieu đơn giản PE-YYYYMM-XXXX (4 digit random) — format sau
|
// Atomic MaPhieu sequence — format PE/{YYYY}/{TypeLetter}/{Seq:D3}
|
||||||
entity.MaPhieu = $"PE-{DateTime.UtcNow:yyyyMM}-{Random.Shared.Next(1000, 9999)}";
|
// (mirror IContractCodeGenerator pattern, transaction SERIALIZABLE).
|
||||||
|
entity.MaPhieu = await codeGen.GenerateAsync(entity, ct);
|
||||||
|
|
||||||
db.PurchaseEvaluations.Add(entity);
|
db.PurchaseEvaluations.Add(entity);
|
||||||
|
|
||||||
|
|||||||
@ -18,3 +18,15 @@ public interface IPurchaseEvaluationWorkflowService
|
|||||||
|
|
||||||
TimeSpan? GetPhaseSla(PurchaseEvaluationPhase phase);
|
TimeSpan? GetPhaseSla(PurchaseEvaluationPhase phase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atomic sequence generator cho mã PE (MaPhieu) — mirror IContractCodeGenerator.
|
||||||
|
// Format: PE/{YYYY}/{TypeLetter}/{Seq:D3}
|
||||||
|
// - YYYY = năm hiện tại (UTC)
|
||||||
|
// - TypeLetter = "A" (DuyetNcc) / "B" (DuyetNccPhuongAn)
|
||||||
|
// - Seq = 3 chữ số tăng dần per (year × type)
|
||||||
|
// VD: PE/2026/A/001, PE/2026/A/002, PE/2026/B/001
|
||||||
|
public interface IPurchaseEvaluationCodeGenerator
|
||||||
|
{
|
||||||
|
// Gen mã phiếu atomic. Transaction SERIALIZABLE để tránh race condition.
|
||||||
|
Task<string> GenerateAsync(PurchaseEvaluation evaluation, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
namespace SolutionErp.Domain.PurchaseEvaluations;
|
||||||
|
|
||||||
|
// Sequence generator cho mã PE (MaPhieu) — mirror ContractCodeSequence pattern.
|
||||||
|
// Prefix = phần đầu mã (vd "PE/2026/A" cho A type 2026, "PE/2026/B" cho B type).
|
||||||
|
// LastSeq tăng dần, update atomic qua transaction SERIALIZABLE.
|
||||||
|
public class PurchaseEvaluationCodeSequence
|
||||||
|
{
|
||||||
|
public string Prefix { get; set; } = string.Empty;
|
||||||
|
public int LastSeq { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
@ -34,6 +34,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IContractCodeGenerator, ContractCodeGenerator>();
|
services.AddScoped<IContractCodeGenerator, ContractCodeGenerator>();
|
||||||
services.AddScoped<IContractWorkflowService, ContractWorkflowService>();
|
services.AddScoped<IContractWorkflowService, ContractWorkflowService>();
|
||||||
services.AddScoped<IPurchaseEvaluationWorkflowService, PurchaseEvaluationWorkflowService>();
|
services.AddScoped<IPurchaseEvaluationWorkflowService, PurchaseEvaluationWorkflowService>();
|
||||||
|
services.AddScoped<IPurchaseEvaluationCodeGenerator, PurchaseEvaluationCodeGenerator>();
|
||||||
services.AddScoped<IContractExcelExporter, ContractExcelExporter>();
|
services.AddScoped<IContractExcelExporter, ContractExcelExporter>();
|
||||||
services.AddScoped<INotificationService, NotificationService>();
|
services.AddScoped<INotificationService, NotificationService>();
|
||||||
services.AddScoped<IChangelogService, ChangelogService>();
|
services.AddScoped<IChangelogService, ChangelogService>();
|
||||||
|
|||||||
@ -57,6 +57,7 @@ public class ApplicationDbContext
|
|||||||
public DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions => Set<PurchaseEvaluationWorkflowDefinition>();
|
public DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions => Set<PurchaseEvaluationWorkflowDefinition>();
|
||||||
public DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps => Set<PurchaseEvaluationWorkflowStep>();
|
public DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps => Set<PurchaseEvaluationWorkflowStep>();
|
||||||
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
|
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
|
||||||
|
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -203,3 +203,16 @@ public class PurchaseEvaluationWorkflowStepApproverConfiguration : IEntityTypeCo
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror ContractCodeSequenceConfiguration — Prefix là PK, atomic UPDATE qua
|
||||||
|
// SERIALIZABLE transaction trong PurchaseEvaluationCodeGenerator.
|
||||||
|
public class PurchaseEvaluationCodeSequenceConfiguration
|
||||||
|
: IEntityTypeConfiguration<PurchaseEvaluationCodeSequence>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PurchaseEvaluationCodeSequence> b)
|
||||||
|
{
|
||||||
|
b.ToTable("PurchaseEvaluationCodeSequences");
|
||||||
|
b.HasKey(x => x.Prefix);
|
||||||
|
b.Property(x => x.Prefix).HasMaxLength(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -671,6 +671,323 @@ public static class DbInitializer
|
|||||||
c1.MaHopDong, c2.MaHopDong, c3.MaHopDong, c4.MaHopDong, c5.MaHopDong, c6.MaHopDong, c7.MaHopDong);
|
c1.MaHopDong, c2.MaHopDong, c3.MaHopDong, c4.MaHopDong, c5.MaHopDong, c6.MaHopDong, c7.MaHopDong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed 4 demo PE phiếu — varied Type (A/B) × Phase (DangSoanThao / mid /
|
||||||
|
// ChoCEODuyetNCC / DaDuyet). Idempotent: skip nếu MaPhieu prefix `[DEMO]`
|
||||||
|
// đã tồn tại (KHÔNG backfill — vì Quotes/Approvals nested khó merge).
|
||||||
|
//
|
||||||
|
// Mỗi phiếu có:
|
||||||
|
// - 2-3 NCC tham gia thầu (Suppliers)
|
||||||
|
// - 2-4 hạng mục (Details — group A.I/A.II/A.III)
|
||||||
|
// - N×M Quotes (báo giá per supplier × hạng mục)
|
||||||
|
// - Approval history theo phase đã đi qua
|
||||||
|
// - Phiếu DaDuyet có SelectedSupplierId (winner)
|
||||||
|
//
|
||||||
|
// MaPhieu format tạm hardcode `[DEMO]-A-001` etc. — sau này MaPhieu atomic
|
||||||
|
// sequence (migration 13) sẽ replace.
|
||||||
|
private static async Task SeedDemoPurchaseEvaluationsAsync(
|
||||||
|
ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
|
||||||
|
{
|
||||||
|
// Skip nếu đã có demo PE
|
||||||
|
if (await db.PurchaseEvaluations.AnyAsync(p => p.MaPhieu != null && p.MaPhieu!.StartsWith("[DEMO]")))
|
||||||
|
{
|
||||||
|
logger.LogInformation("SeedDemoPurchaseEvaluationsAsync: skip — đã có [DEMO] PE.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup masters
|
||||||
|
var suppliersByCode = await db.Suppliers.ToDictionaryAsync(s => s.Code, s => s);
|
||||||
|
var projectsByCode = await db.Projects.ToDictionaryAsync(p => p.Code, p => p);
|
||||||
|
if (suppliersByCode.Count < 3 || projectsByCode.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("SeedDemoPurchaseEvaluationsAsync: skip — thiếu Supplier/Project master.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup actor users (per role)
|
||||||
|
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local");
|
||||||
|
var proPham = await userManager.FindByEmailAsync("pro.pham@solutionerp.local");
|
||||||
|
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local");
|
||||||
|
var pmNguyen = await userManager.FindByEmailAsync("pm.nguyen@solutionerp.local");
|
||||||
|
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local");
|
||||||
|
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Helper: build PE phiếu với Suppliers + Details + Quotes + Approvals
|
||||||
|
async Task<PurchaseEvaluation> createDemoPeAsync(
|
||||||
|
string maPhieu,
|
||||||
|
PurchaseEvaluationType type,
|
||||||
|
PurchaseEvaluationPhase finalPhase,
|
||||||
|
string tenGoiThau,
|
||||||
|
string projectCode,
|
||||||
|
string? diaDiem,
|
||||||
|
string? moTa,
|
||||||
|
string[] supplierCodes, // 2-3 supplier codes
|
||||||
|
(string GroupCode, string GroupName, string ItemCode, string NoiDung, string Dvt, decimal KlNs, decimal KlTc, decimal DgNs)[] detailRows,
|
||||||
|
// Quotes: index by [detailIdx][supplierIdx] → (BgVat, ChuaVat, IsSelected)
|
||||||
|
(decimal BgVat, decimal ChuaVat, bool IsSelected)[][] quotesGrid,
|
||||||
|
string? winnerSupplierCode = null,
|
||||||
|
string? paymentTermsJson = null)
|
||||||
|
{
|
||||||
|
var activeWfId = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
|
||||||
|
.Where(w => w.EvaluationType == type && w.IsActive)
|
||||||
|
.Select(w => (Guid?)w.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
var project = projectsByCode.TryGetValue(projectCode, out var p) ? p : projectsByCode.Values.First();
|
||||||
|
|
||||||
|
var pe = new PurchaseEvaluation
|
||||||
|
{
|
||||||
|
MaPhieu = $"[DEMO]-{maPhieu}",
|
||||||
|
Type = type,
|
||||||
|
Phase = PurchaseEvaluationPhase.DangSoanThao, // tạo ở phase 1, transition sau
|
||||||
|
TenGoiThau = $"[DEMO] {tenGoiThau}",
|
||||||
|
ProjectId = project.Id,
|
||||||
|
DepartmentId = qsDeptId,
|
||||||
|
DrafterUserId = qsHoang?.Id,
|
||||||
|
DiaDiem = diaDiem,
|
||||||
|
MoTa = moTa,
|
||||||
|
WorkflowDefinitionId = activeWfId,
|
||||||
|
SlaDeadline = nowUtc.AddDays(7),
|
||||||
|
PaymentTerms = paymentTermsJson,
|
||||||
|
};
|
||||||
|
db.PurchaseEvaluations.Add(pe);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Suppliers
|
||||||
|
var supplierEntities = new List<PurchaseEvaluationSupplier>();
|
||||||
|
for (int i = 0; i < supplierCodes.Length; i++)
|
||||||
|
{
|
||||||
|
var sCode = supplierCodes[i];
|
||||||
|
if (!suppliersByCode.TryGetValue(sCode, out var supplier)) continue;
|
||||||
|
var pes = new PurchaseEvaluationSupplier
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = pe.Id,
|
||||||
|
SupplierId = supplier.Id,
|
||||||
|
DisplayName = supplier.Name,
|
||||||
|
ContactName = supplier.ContactPerson,
|
||||||
|
ContactEmail = supplier.Email,
|
||||||
|
ContactPhone = supplier.Phone,
|
||||||
|
PaymentTermText = i == 0 ? "TGN-30 ngày" : i == 1 ? "TGN-45 ngày" : "Tiến độ",
|
||||||
|
Note = i == 0 ? "ĐÃ CHỐT SO SÁNH LẦN 1" : null,
|
||||||
|
Order = i + 1,
|
||||||
|
};
|
||||||
|
db.PurchaseEvaluationSuppliers.Add(pes);
|
||||||
|
supplierEntities.Add(pes);
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Details
|
||||||
|
var detailEntities = new List<PurchaseEvaluationDetail>();
|
||||||
|
for (int i = 0; i < detailRows.Length; i++)
|
||||||
|
{
|
||||||
|
var (gc, gn, ic, nd, dvt, klNs, klTc, dgNs) = detailRows[i];
|
||||||
|
var det = new PurchaseEvaluationDetail
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = pe.Id,
|
||||||
|
GroupCode = gc, GroupName = gn,
|
||||||
|
ItemCode = ic, NoiDung = nd, DonViTinh = dvt,
|
||||||
|
KhoiLuongNganSach = klNs,
|
||||||
|
KhoiLuongThiCong = klTc,
|
||||||
|
DonGiaNganSach = dgNs,
|
||||||
|
ThanhTienNganSach = klNs * dgNs,
|
||||||
|
Order = i + 1,
|
||||||
|
};
|
||||||
|
db.PurchaseEvaluationDetails.Add(det);
|
||||||
|
detailEntities.Add(det);
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Quotes: detailIdx × supplierIdx
|
||||||
|
for (int di = 0; di < detailEntities.Count; di++)
|
||||||
|
{
|
||||||
|
if (di >= quotesGrid.Length) break;
|
||||||
|
var row = quotesGrid[di];
|
||||||
|
for (int si = 0; si < supplierEntities.Count; si++)
|
||||||
|
{
|
||||||
|
if (si >= row.Length) break;
|
||||||
|
var (bgVat, chuaVat, isSelected) = row[si];
|
||||||
|
db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
|
||||||
|
{
|
||||||
|
PurchaseEvaluationDetailId = detailEntities[di].Id,
|
||||||
|
PurchaseEvaluationSupplierId = supplierEntities[si].Id,
|
||||||
|
BgVat = bgVat,
|
||||||
|
ChuaVat = chuaVat,
|
||||||
|
ThanhTien = detailEntities[di].KhoiLuongNganSach * chuaVat,
|
||||||
|
IsSelected = isSelected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Approval history theo flow của type
|
||||||
|
PurchaseEvaluationPhase[] flow = type == PurchaseEvaluationType.DuyetNcc
|
||||||
|
? [PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM,
|
||||||
|
PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet]
|
||||||
|
: [PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoDuAn,
|
||||||
|
PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA,
|
||||||
|
PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet];
|
||||||
|
|
||||||
|
var current = PurchaseEvaluationPhase.DangSoanThao;
|
||||||
|
foreach (var next in flow)
|
||||||
|
{
|
||||||
|
if (next > finalPhase && finalPhase != PurchaseEvaluationPhase.DaDuyet) break;
|
||||||
|
if (next > finalPhase) break;
|
||||||
|
Guid? actorId = next switch
|
||||||
|
{
|
||||||
|
PurchaseEvaluationPhase.ChoPurchasing => qsHoang?.Id, // Drafter submit
|
||||||
|
PurchaseEvaluationPhase.ChoDuAn => proPham?.Id, // Procurement chuyển sang
|
||||||
|
PurchaseEvaluationPhase.ChoCCM => type == PurchaseEvaluationType.DuyetNcc
|
||||||
|
? proPham?.Id // A: Procurement
|
||||||
|
: pmNguyen?.Id, // B: PM
|
||||||
|
PurchaseEvaluationPhase.ChoCEODuyetPA => ccmTran?.Id, // CCM
|
||||||
|
PurchaseEvaluationPhase.ChoCEODuyetNCC => type == PurchaseEvaluationType.DuyetNcc
|
||||||
|
? ccmTran?.Id // A: CCM
|
||||||
|
: bodHuynh?.Id, // B: CEO duyệt PA xong
|
||||||
|
PurchaseEvaluationPhase.DaDuyet => bodHuynh?.Id, // CEO chốt
|
||||||
|
_ => qsHoang?.Id,
|
||||||
|
};
|
||||||
|
db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = pe.Id,
|
||||||
|
FromPhase = current,
|
||||||
|
ToPhase = next,
|
||||||
|
ApproverUserId = actorId,
|
||||||
|
Decision = ApprovalDecision.Approve,
|
||||||
|
Comment = next switch
|
||||||
|
{
|
||||||
|
PurchaseEvaluationPhase.ChoPurchasing => "Demo seed — Drafter submit phiếu",
|
||||||
|
PurchaseEvaluationPhase.ChoCCM => "Demo seed — chuyển CCM kiểm tra ngân sách",
|
||||||
|
PurchaseEvaluationPhase.DaDuyet => "Demo seed — CEO duyệt chọn NCC",
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
ApprovedAt = nowUtc.AddMinutes(-(flow.Length - Array.IndexOf(flow, next)) * 30),
|
||||||
|
});
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
pe.Phase = current;
|
||||||
|
|
||||||
|
// Set winner nếu DaDuyet
|
||||||
|
if (current == PurchaseEvaluationPhase.DaDuyet
|
||||||
|
&& winnerSupplierCode is not null
|
||||||
|
&& suppliersByCode.TryGetValue(winnerSupplierCode, out var winner))
|
||||||
|
{
|
||||||
|
pe.SelectedSupplierId = winner.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// 1. Phiếu A — DangSoanThao (Drafter còn đang soạn)
|
||||||
|
// ===========================================================
|
||||||
|
var pe1 = await createDemoPeAsync(
|
||||||
|
maPhieu: "A-001",
|
||||||
|
type: PurchaseEvaluationType.DuyetNcc,
|
||||||
|
finalPhase: PurchaseEvaluationPhase.DangSoanThao,
|
||||||
|
tenGoiThau: "Cung cấp xi măng + sắt thép Q3/2026 — FLOCK 01",
|
||||||
|
projectCode: "FLOCK01",
|
||||||
|
diaDiem: "Lô K, KCN Lộc An",
|
||||||
|
moTa: "Đợt 1: 200 tấn xi măng + 30 tấn thép cho phần thân.",
|
||||||
|
supplierCodes: ["NCC-XIMANG", "NCC-THEP", "NCC-DIEN"],
|
||||||
|
detailRows:
|
||||||
|
[
|
||||||
|
("A.I", "Xi măng", "XM-001", "Xi măng PCB40 50kg", "tan", 200, 200, 1_800_000),
|
||||||
|
("A.I", "Xi măng", "XM-002", "Xi măng PCB30 50kg", "tan", 50, 50, 1_650_000),
|
||||||
|
("A.II", "Sắt thép", "TH-014", "Thép cây phi 14", "kg", 15000, 15000, 18_500),
|
||||||
|
("A.II", "Sắt thép", "TH-018", "Thép cây phi 18", "kg", 15000, 15000, 18_800),
|
||||||
|
],
|
||||||
|
quotesGrid:
|
||||||
|
[
|
||||||
|
// Hạng 1 (xi măng PCB40) — chưa nhập (Drafter mới mở phiếu)
|
||||||
|
[(0, 0, false), (0, 0, false), (0, 0, false)],
|
||||||
|
[(0, 0, false), (0, 0, false), (0, 0, false)],
|
||||||
|
[(0, 0, false), (0, 0, false), (0, 0, false)],
|
||||||
|
[(0, 0, false), (0, 0, false), (0, 0, false)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// 2. Phiếu A — ChoCEODuyetNCC (sắp duyệt — đã có quotes + winner đề xuất)
|
||||||
|
// ===========================================================
|
||||||
|
var pe2 = await createDemoPeAsync(
|
||||||
|
maPhieu: "A-002",
|
||||||
|
type: PurchaseEvaluationType.DuyetNcc,
|
||||||
|
finalPhase: PurchaseEvaluationPhase.ChoCEODuyetNCC,
|
||||||
|
tenGoiThau: "Cung cấp gạch xây + cát đá Q2/2026 — Vinhomes Ocean Park",
|
||||||
|
projectCode: "VHOMES-OP",
|
||||||
|
diaDiem: "Vinhomes Ocean Park, Gia Lâm, HN",
|
||||||
|
moTa: "Vật tư xây dựng đợt 2 cho block A1-A5.",
|
||||||
|
supplierCodes: ["NCC-XIMANG", "NCC-THEP", "NCC-DIEN"],
|
||||||
|
detailRows:
|
||||||
|
[
|
||||||
|
("A.I", "Gạch", "GACH-001", "Gạch xây 4 lỗ 8x8x18", "vien", 50000, 50000, 1_500),
|
||||||
|
("A.II", "Cát đá", "CAT-001", "Cát vàng đổ bê tông", "m3", 200, 200, 350_000),
|
||||||
|
("A.II", "Cát đá", "DA-001", "Đá 1x2", "m3", 150, 150, 280_000),
|
||||||
|
],
|
||||||
|
quotesGrid:
|
||||||
|
[
|
||||||
|
// Hạng 1: Gạch — supplier 1 thắng (báo giá thấp + cam kết)
|
||||||
|
[(1_700, 1_550, true), (1_750, 1_590, false), (1_780, 1_618, false)],
|
||||||
|
// Hạng 2: Cát vàng — supplier 1 thắng
|
||||||
|
[(385_000, 350_000, true), (390_000, 354_000, false), (395_000, 359_000, false)],
|
||||||
|
// Hạng 3: Đá 1x2 — supplier 1 thắng
|
||||||
|
[(308_000, 280_000, true), (315_000, 286_000, false), (320_000, 290_000, false)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// 3. Phiếu A — DaDuyet (đã duyệt — chưa tạo HĐ — demo "Tạo HĐ từ phiếu")
|
||||||
|
// ===========================================================
|
||||||
|
var pe3 = await createDemoPeAsync(
|
||||||
|
maPhieu: "A-003",
|
||||||
|
type: PurchaseEvaluationType.DuyetNcc,
|
||||||
|
finalPhase: PurchaseEvaluationPhase.DaDuyet,
|
||||||
|
tenGoiThau: "Cung cấp ống PVC + phụ kiện cấp thoát nước — Resort Phú Quốc",
|
||||||
|
projectCode: "RESORT-PQ",
|
||||||
|
diaDiem: "Bãi Trường, Phú Quốc, Kiên Giang",
|
||||||
|
moTa: "Hệ thống cấp thoát nước villa + biệt thự ven biển.",
|
||||||
|
supplierCodes: ["NCC-XIMANG", "NCC-THEP"],
|
||||||
|
detailRows:
|
||||||
|
[
|
||||||
|
("A.I", "Ống nhựa", "ONG-090", "Ống PVC phi 90", "m", 2000, 2000, 65_000),
|
||||||
|
("A.I", "Ống nhựa", "ONG-110", "Ống PVC phi 110", "m", 1500, 1500, 95_000),
|
||||||
|
],
|
||||||
|
quotesGrid:
|
||||||
|
[
|
||||||
|
[(71_500, 65_000, true), (72_600, 66_000, false)],
|
||||||
|
[(104_500, 95_000, true), (106_700, 97_000, false)],
|
||||||
|
],
|
||||||
|
winnerSupplierCode: "NCC-XIMANG",
|
||||||
|
paymentTermsJson: """{"tamUng":20,"thanhToanTam":70,"quyetToan":10,"baoHanh":12,"hanMucCongNo":500000000,"danhGia":"Đối tác chiến lược 3 năm — uy tín cao."}""");
|
||||||
|
|
||||||
|
// ===========================================================
|
||||||
|
// 4. Phiếu B — ChoDuAn (5-step giữa chừng — Procurement đã chuyển)
|
||||||
|
// ===========================================================
|
||||||
|
var pe4 = await createDemoPeAsync(
|
||||||
|
maPhieu: "B-001",
|
||||||
|
type: PurchaseEvaluationType.DuyetNccPhuongAn,
|
||||||
|
finalPhase: PurchaseEvaluationPhase.ChoDuAn,
|
||||||
|
tenGoiThau: "Cung cấp + lắp đặt cẩu tháp 6 tháng — FLOCK 01",
|
||||||
|
projectCode: "FLOCK01",
|
||||||
|
diaDiem: "Lô K, KCN Lộc An",
|
||||||
|
moTa: "Phương án A: thuê cẩu Liebherr 320 EC-H. Phương án B: thuê cẩu Potain MD 365.",
|
||||||
|
supplierCodes: ["DV-VANCHUYEN", "DV-CLEAN", "NTP-XD"],
|
||||||
|
detailRows:
|
||||||
|
[
|
||||||
|
("A.I", "Cẩu tháp", "CT-001", "Cẩu tháp Liebherr 320 EC-H — phương án A", "thang", 6, 6, 90_000_000),
|
||||||
|
("A.II", "Vận chuyển", "VC-001", "Vận chuyển + lắp đặt cẩu tháp", "lan", 1, 1, 80_000_000),
|
||||||
|
("A.III", "Bảo trì", "BT-001", "Bảo trì cẩu tháp định kỳ", "lan", 6, 6, 5_000_000),
|
||||||
|
],
|
||||||
|
quotesGrid:
|
||||||
|
[
|
||||||
|
[(99_000_000, 90_000_000, false), (104_500_000, 95_000_000, false), (96_800_000, 88_000_000, false)],
|
||||||
|
[(88_000_000, 80_000_000, false), (93_500_000, 85_000_000, false), (82_500_000, 75_000_000, false)],
|
||||||
|
[(5_500_000, 5_000_000, false), (5_775_000, 5_250_000, false), (5_280_000, 4_800_000, false)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
logger.LogInformation("Seed 4 demo PE phiếu: {P1} {P2} {P3} {P4}",
|
||||||
|
pe1.MaPhieu, pe2.MaPhieu, pe3.MaPhieu, pe4.MaPhieu);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 5.1 security: log warning nếu admin vẫn dùng password mặc định sau deploy production.
|
// Phase 5.1 security: log warning nếu admin vẫn dùng password mặc định sau deploy production.
|
||||||
private static async Task WarnDefaultAdminPasswordAsync(UserManager<User> userManager, ILogger logger)
|
private static async Task WarnDefaultAdminPasswordAsync(UserManager<User> userManager, ILogger logger)
|
||||||
{
|
{
|
||||||
@ -1024,6 +1341,79 @@ public static class DbInitializer
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
logger.LogInformation("Seeded {Count} admin permissions", added);
|
logger.LogInformation("Seeded {Count} admin permissions", added);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defaults cho Pe_* permissions per role — mỗi role business liên quan
|
||||||
|
// PE module được grant CRUD level phù hợp lên 2 type (DuyetNcc /
|
||||||
|
// DuyetNccPhuongAn).
|
||||||
|
// Idempotent: skip per-(role,menuKey) đã có, chỉ add row mới.
|
||||||
|
await SeedPurchaseEvaluationPermissionDefaultsAsync(db, roleManager, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission defaults cho module Duyệt NCC (Pe_*). Strategy:
|
||||||
|
// - Drafter / DeptManager / Procurement / CostControl / Director / AuthorizedSigner / ProjectManager
|
||||||
|
// đều có Read + Create + Update trên CẢ 2 type (xem được, soạn / chỉnh).
|
||||||
|
// - Workflow admin (PeWf_*) chỉ Admin (đã grant qua MenuKeys.All) — không cấp non-admin.
|
||||||
|
// - Idempotent: skip per-(roleId × menuKey) đã có row.
|
||||||
|
private static async Task SeedPurchaseEvaluationPermissionDefaultsAsync(
|
||||||
|
ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
|
||||||
|
{
|
||||||
|
// Roles cần access PE module
|
||||||
|
var roleNames = new[]
|
||||||
|
{
|
||||||
|
AppRoles.Drafter, AppRoles.DeptManager,
|
||||||
|
AppRoles.Procurement, AppRoles.CostControl,
|
||||||
|
AppRoles.ProjectManager,
|
||||||
|
AppRoles.Director, AppRoles.AuthorizedSigner,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Menu keys cần grant: PurchaseEvaluations root + per-type group + 3 leaf
|
||||||
|
var menuKeys = new List<string> { MenuKeys.PurchaseEvaluations };
|
||||||
|
foreach (var typeCode in MenuKeys.PurchaseEvaluationTypeCodes)
|
||||||
|
{
|
||||||
|
menuKeys.Add(MenuKeys.PurchaseEvaluationGroup(typeCode));
|
||||||
|
menuKeys.Add(MenuKeys.PurchaseEvaluationList(typeCode));
|
||||||
|
menuKeys.Add(MenuKeys.PurchaseEvaluationCreate(typeCode));
|
||||||
|
menuKeys.Add(MenuKeys.PurchaseEvaluationPending(typeCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
foreach (var roleName in roleNames)
|
||||||
|
{
|
||||||
|
var role = await roleManager.FindByNameAsync(roleName);
|
||||||
|
if (role is null) continue;
|
||||||
|
|
||||||
|
var existing = await db.Permissions
|
||||||
|
.Where(p => p.RoleId == role.Id && menuKeys.Contains(p.MenuKey))
|
||||||
|
.Select(p => p.MenuKey)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Drafter có quyền Create + Update (soạn phiếu); Director chỉ Read +
|
||||||
|
// Update (duyệt — xem + nhấn nút). Map default rõ ràng:
|
||||||
|
bool canCreate = roleName is AppRoles.Drafter or AppRoles.DeptManager
|
||||||
|
or AppRoles.Procurement;
|
||||||
|
bool canUpdate = true; // mọi role được transition (workflow check tách)
|
||||||
|
bool canDelete = roleName == AppRoles.DeptManager; // chỉ TPB xóa nháp
|
||||||
|
|
||||||
|
foreach (var menuKey in menuKeys)
|
||||||
|
{
|
||||||
|
if (existing.Contains(menuKey)) continue;
|
||||||
|
db.Permissions.Add(new Permission
|
||||||
|
{
|
||||||
|
RoleId = role.Id,
|
||||||
|
MenuKey = menuKey,
|
||||||
|
CanRead = true,
|
||||||
|
CanCreate = canCreate,
|
||||||
|
CanUpdate = canUpdate,
|
||||||
|
CanDelete = canDelete,
|
||||||
|
});
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (added > 0)
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
logger.LogInformation("Seeded {Count} PE permission defaults across {Roles} roles", added, roleNames.Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9 departments từ QT-TP-NCC.docx — reference data, không phải demo.
|
// 9 departments từ QT-TP-NCC.docx — reference data, không phải demo.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPurchaseEvaluationCodeSequences : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PurchaseEvaluationCodeSequences",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Prefix = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
LastSeq = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PurchaseEvaluationCodeSequences", x => x.Prefix);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PurchaseEvaluationCodeSequences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2165,6 +2165,23 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("PurchaseEvaluationChangelogs", (string)null);
|
b.ToTable("PurchaseEvaluationChangelogs", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationCodeSequence", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Prefix")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("LastSeq")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Prefix");
|
||||||
|
|
||||||
|
b.ToTable("PurchaseEvaluationCodeSequences", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b =>
|
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
|
using SolutionErp.Application.PurchaseEvaluations.Services;
|
||||||
|
using SolutionErp.Domain.PurchaseEvaluations;
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Services;
|
||||||
|
|
||||||
|
// Mirror ContractCodeGenerator pattern — atomic sequence per (year × type).
|
||||||
|
// Prefix = "PE/{YYYY}/{TypeLetter}".
|
||||||
|
public class PurchaseEvaluationCodeGenerator(IApplicationDbContext db, IDateTime dateTime)
|
||||||
|
: IPurchaseEvaluationCodeGenerator
|
||||||
|
{
|
||||||
|
public async Task<string> GenerateAsync(PurchaseEvaluation evaluation, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var typeLetter = evaluation.Type switch
|
||||||
|
{
|
||||||
|
PurchaseEvaluationType.DuyetNcc => "A",
|
||||||
|
PurchaseEvaluationType.DuyetNccPhuongAn => "B",
|
||||||
|
_ => "X",
|
||||||
|
};
|
||||||
|
var year = dateTime.UtcNow.Year;
|
||||||
|
var prefix = $"PE/{year}/{typeLetter}";
|
||||||
|
|
||||||
|
var context = (DbContext)db;
|
||||||
|
await using var tx = await context.Database
|
||||||
|
.BeginTransactionAsync(IsolationLevel.Serializable, ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var seq = await db.PurchaseEvaluationCodeSequences
|
||||||
|
.FirstOrDefaultAsync(s => s.Prefix == prefix, ct);
|
||||||
|
if (seq is null)
|
||||||
|
{
|
||||||
|
seq = new PurchaseEvaluationCodeSequence
|
||||||
|
{
|
||||||
|
Prefix = prefix, LastSeq = 1, UpdatedAt = dateTime.UtcNow,
|
||||||
|
};
|
||||||
|
db.PurchaseEvaluationCodeSequences.Add(seq);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
seq.LastSeq += 1;
|
||||||
|
seq.UpdatedAt = dateTime.UtcNow;
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
await tx.CommitAsync(ct);
|
||||||
|
return $"{prefix}/{seq.LastSeq:D3}";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await tx.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user