[CLAUDE] Master: nạp master data thật từ Excel (62 dự án + 71 hạng mục + 3 NCC) + Project +4 cột (Mig 48)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m33s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m33s
Nạp master data công ty từ file Excel 'HẠNG MỤC CÔNG VIỆC DỰ ÁN': - 62 Projects (Mã + Năm; tên/CĐT/địa điểm/gói thầu cho ~6 dự án có chi tiết) - 71 WorkItems: Vật tư 16 · Thầu phụ 30 · MEP 9 · Thiết bị 16 - 3 Suppliers (TRUONGGIANG/TANPHU/TGN) Mig 48 AddProjectMasterFields: Project +4 cột nullable (Year/Investor/Location/Package) + ProjectFeatures DTO/Create/Update + ProjectsPage form ×2 app (SHA256 mirror). SeedRealMasterDataAsync per-code idempotent, UNGATED → reaches prod (coexist demo). FLOCK01 collision → skip (demo wins). Verify: build 0-err · test 216 PASS · runtime Dev proof (data landed, Investor col populates). Provenance: scripts/master-import-data.generated.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -74,6 +74,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i
|
|||||||
|
|
||||||
## 📅 Recent activity (FIFO — older → archive/git)
|
## 📅 Recent activity (FIFO — older → archive/git)
|
||||||
|
|
||||||
|
- **S55 master-data import (Mig 48 `AddProjectMasterFields` 4 AddColumn no-table + `SeedRealMasterDataAsync` 62 Project+71 WorkItem+3 Supplier) [proxy by em main — agent return truncated gotcha #53 before MEMORY step]:** Project entity +4 prop (`Year int?`, `Investor/Location/Package string?`, maxlen 250/500/300 ProjectConfiguration). `ProjectFeatures.cs` DTO+CreateCmd+UpdateCmd+validators+handlers+List/Get projections +4 (all nullable, appended end). **`SeedRealMasterDataAsync`** = 3 tuple-loop per-code idempotent (mirror `SeedDemoMasterDataAsync:2185` `existingCodes.Contains→skip`) wired UNGATED line 118 AFTER `SeedCatalogsAsync` → reaches prod (DemoSeed:Disabled=true KHÔNG gate, by-design như SeedDemoMasterData/Catalogs). Project Name=Code khi Excel blank. WorkItem 4 Category (Vật tư16/Thầu phụ30/MEP9/Thiết bị16, gen Code VT/TP/MEP/TB-NN; divider "THIẾT BỊ" dropped). Supplier NTP→NhaThauPhu/NCC→NhaCungCap, extras→Note. **FLOCK01 collision** demo↔real → per-code skip (demo thắng, real code+year only, OK). Compile-fix `MasterCatalogFilteredUniqueTests.cs` +4 null args CreateProjectCommand (necessary build-green). **Runtime Dev proof (em main):** app-start seeded 62proj/71wi/3sup landed, CAL01.Investor col populates, 0 overflow/dup. Build 0/0, test 216. Data spec `scripts/master-import-data.generated.md`. Tag `[s55, master-import, mig48, seed-real-ungated, project-4field]`.
|
||||||
- **S54 ItTicket reassign cross-stack — IT-staff self-service (NO migration, 2 BE file edit):** NEW `GetAssignableItStaffQuery`+`AssignableStaffResult(CanReassign,Staff)`+`AssignableStaffDto(Id,FullName)` capability endpoint (REGION 5 WorkflowAppsFeatures.cs) + MODIFIED `AssignItTicketHandler`: authz Admin-OR-dept-IT → `ForbiddenException`; assignee-must-be-IT → `ConflictException`. Controller `/assign` hạ `[Authorize(Roles="Admin")]`→`[Authorize]` (handler enforce fine-grained data-driven) + NEW `GET /assignable-staff`. Predicate IT = reuse round-robin S52 `Departments.Where(Code=="IT" && !IsDeleted)`. `ICurrentUser` KHÔNG có DepartmentId → query `db.Users.Where(Id==cu.UserId).Select(DepartmentId)`. 2 pattern split (em main reconciled từ stray src/Backend/.claude — cwd-relative Write mishap): [[pattern-controller-lower-authorize-handler-finegrained]] + [[pattern-scoped-capability-endpoint-anti-silent-403]]. Build 0/0, test 203→216 (test-specialist +13 authz), reviewer PASS (role-string "Admin" chain-verified real: AppRoles→SeedRoles→JWT ClaimTypes.Role→cu.Roles). Tag `[s54, it-ticket-reassign, capability-endpoint, authz-handler, no-mig]`.
|
- **S54 ItTicket reassign cross-stack — IT-staff self-service (NO migration, 2 BE file edit):** NEW `GetAssignableItStaffQuery`+`AssignableStaffResult(CanReassign,Staff)`+`AssignableStaffDto(Id,FullName)` capability endpoint (REGION 5 WorkflowAppsFeatures.cs) + MODIFIED `AssignItTicketHandler`: authz Admin-OR-dept-IT → `ForbiddenException`; assignee-must-be-IT → `ConflictException`. Controller `/assign` hạ `[Authorize(Roles="Admin")]`→`[Authorize]` (handler enforce fine-grained data-driven) + NEW `GET /assignable-staff`. Predicate IT = reuse round-robin S52 `Departments.Where(Code=="IT" && !IsDeleted)`. `ICurrentUser` KHÔNG có DepartmentId → query `db.Users.Where(Id==cu.UserId).Select(DepartmentId)`. 2 pattern split (em main reconciled từ stray src/Backend/.claude — cwd-relative Write mishap): [[pattern-controller-lower-authorize-handler-finegrained]] + [[pattern-scoped-capability-endpoint-anti-silent-403]]. Build 0/0, test 203→216 (test-specialist +13 authz), reviewer PASS (role-string "Admin" chain-verified real: AppRoles→SeedRoles→JWT ClaimTypes.Role→cu.Roles). Tag `[s54, it-ticket-reassign, capability-endpoint, authz-handler, no-mig]`.
|
||||||
- **S54 Task D BE — promote AttendanceReport to sidebar menu leaf (NO migration, 2 file edit):** Case 1 mechanical, menu = DbInitializer idempotent seed (not schema). 3 insert: (1) MenuKeys.cs const `OffAttendanceReport = "Off_AttendanceReport"` after OffChamCong:124 · (2) MenuKeys.cs All[] Off-group line +`OffAttendanceReport` after OffChamCong:158-159 · (3) DbInitializer.cs menu tuple `(OffAttendanceReport, "Báo cáo chấm công", Off, 8, "FileBarChart")` after OffChamCong:1787 (Order 8, parent Off, mirror Vehicle/Driver S51). **adminPermAutoViaAll=TRUE verified 2-point:** `SeedAdminPermissionsAsync` DbInitializer:1916 iterates `MenuKeys.All` → full-CRUD Permission row per missing key (idempotent `existingMenuKeys.Contains`); `Program.cs:78` iterates All × Actions → policy registration. +All[] = both auto, NO manual grant. **Idempotent-add verified:** menu upsert loop DbInitializer:1845-1862 `existingItems.TryGetValue(key)` miss → `MenuItems.Add` (existing prod gets leaf on restart, existing rows only Order-reconciled — same as S51). Build 0 err (2 pre-existing DocxRenderer warn). KHÔNG touch FE (menuKeys.ts/Layout = implementer-frontend) / tests / commit. Tag `[s54, task-d, menu-leaf, no-mig, admin-perm-via-all]`.
|
- **S54 Task D BE — promote AttendanceReport to sidebar menu leaf (NO migration, 2 file edit):** Case 1 mechanical, menu = DbInitializer idempotent seed (not schema). 3 insert: (1) MenuKeys.cs const `OffAttendanceReport = "Off_AttendanceReport"` after OffChamCong:124 · (2) MenuKeys.cs All[] Off-group line +`OffAttendanceReport` after OffChamCong:158-159 · (3) DbInitializer.cs menu tuple `(OffAttendanceReport, "Báo cáo chấm công", Off, 8, "FileBarChart")` after OffChamCong:1787 (Order 8, parent Off, mirror Vehicle/Driver S51). **adminPermAutoViaAll=TRUE verified 2-point:** `SeedAdminPermissionsAsync` DbInitializer:1916 iterates `MenuKeys.All` → full-CRUD Permission row per missing key (idempotent `existingMenuKeys.Contains`); `Program.cs:78` iterates All × Actions → policy registration. +All[] = both auto, NO manual grant. **Idempotent-add verified:** menu upsert loop DbInitializer:1845-1862 `existingItems.TryGetValue(key)` miss → `MenuItems.Add` (existing prod gets leaf on restart, existing rows only Order-reconciled — same as S51). Build 0 err (2 pre-existing DocxRenderer warn). KHÔNG touch FE (menuKeys.ts/Layout = implementer-frontend) / tests / commit. Tag `[s54, task-d, menu-leaf, no-mig, admin-perm-via-all]`.
|
||||||
|
|
||||||
|
|||||||
@ -42,11 +42,11 @@ Dynamic class purged. PALETTE array full literal `as const` cycle `index % lengt
|
|||||||
|
|
||||||
## 📅 Recent activity (last 10 FIFO)
|
## 📅 Recent activity (last 10 FIFO)
|
||||||
|
|
||||||
|
- **2026-06-09 (S55 HMW P2 — Project +4 optional master fields):** Case 1 master-data enrich (NO 4-place, NO menu/route/Layout — chỉ Place-1 page+type). BE adds 4 nullable Project fields parallel (implementer-backend). 2 file × 2 app: (1) types/master.ts `Project` +`year:number|null`+`investor/location/package:string|null` (sau `note`) +ProjectInput auto-inherit via Omit · (2) ProjectsPage.tsx (single-Dialog CRUD, NO separate pages): FormState +4 `string` (form dùng string, convert on submit) + emptyForm +4 '' + mutate payload `year:d.year?Number():null, investor/location/package:d.x||null` + openEdit `year:p.year?.toString()??'', x:p.x??''` + Dialog 4 Input sau "Ngày kết thúc" trước "Ghi chú" (Năm type=number, Chủ đầu tư, Địa điểm col-span-2, Gói thầu col-span-2) + table column "Chủ đầu tư" (`p.investor??'—'`) giữa name↔startDate. **`package` = valid TS object KEY** (reserved chỉ khi binding-identifier) → `form.package`/`{...form,package:x}` build sạch, KHÔNG cần rename. cp admin→user SHA256 IDENTICAL: master.ts `93ac1b0f…`, ProjectsPage `b002061…`. Build PASS ×2 (admin 1945mod, user 1934mod, 0 TS err — tsc -b clean trước vite). Reuse S42 enrich-pattern (string-form + convert-on-submit). NO ambiguity, full precedent.
|
||||||
- **2026-06-08 (S54 ItTicket reassign → CONVERGE 2 app, REVERSE S53 divergence) [harvested by em main — agent MEMORY write mis-landed, B2/B3]:** S53 đã tách fe-admin-only (admin reassign). S54 cho tổ IT tự reassign → cả 2 app cần nút. **Pattern mới: BE capability-flag gate thay vì FE đoán role.** Dropdown đổi `GET /users` → `GET /it-tickets/assignable-staff` trả `{canReassign:bool, staff:[{id,fullName}]}` (`[Authorize]` any-auth, KHÔNG 403 → chống gotcha #44). `canReassign = staffQ.data?.canReassign ?? false` (fetch on mount, KHÔNG `enabled`) → nút Pencil bọc `{canReassign && …}`. types/workflowApps.ts +`AssignableStaff`+`AssignableStaffResult` (mirror cả 2). fe-admin rewrite (bỏ UserOption/Paged<T>) + fe-user full-add → **SHA256 IDENTICAL `4bcaf2f…`** (viết admin canonical → `cp` → verify; cùng `@/...` import + shadcn Dialog/Select/Button identical 2 app). Build PASS ×2 (0 TS err). **Gotcha (pre-existing, NOT từ change này):** types/workflowApps.ts 2 app NOT SHA-identical — fe-admin có `AttendanceReportDto` (P11-E) mà fe-user thiếu; 2 type S54 mirror đúng cả 2. Lesson: BE-computed capability flag = single-source → 2 app converge lại sau intentional divergence.
|
- **2026-06-08 (S54 ItTicket reassign → CONVERGE 2 app, REVERSE S53 divergence) [harvested by em main — agent MEMORY write mis-landed, B2/B3]:** S53 đã tách fe-admin-only (admin reassign). S54 cho tổ IT tự reassign → cả 2 app cần nút. **Pattern mới: BE capability-flag gate thay vì FE đoán role.** Dropdown đổi `GET /users` → `GET /it-tickets/assignable-staff` trả `{canReassign:bool, staff:[{id,fullName}]}` (`[Authorize]` any-auth, KHÔNG 403 → chống gotcha #44). `canReassign = staffQ.data?.canReassign ?? false` (fetch on mount, KHÔNG `enabled`) → nút Pencil bọc `{canReassign && …}`. types/workflowApps.ts +`AssignableStaff`+`AssignableStaffResult` (mirror cả 2). fe-admin rewrite (bỏ UserOption/Paged<T>) + fe-user full-add → **SHA256 IDENTICAL `4bcaf2f…`** (viết admin canonical → `cp` → verify; cùng `@/...` import + shadcn Dialog/Select/Button identical 2 app). Build PASS ×2 (0 TS err). **Gotcha (pre-existing, NOT từ change này):** types/workflowApps.ts 2 app NOT SHA-identical — fe-admin có `AttendanceReportDto` (P11-E) mà fe-user thiếu; 2 type S54 mirror đúng cả 2. Lesson: BE-computed capability flag = single-source → 2 app converge lại sau intentional divergence.
|
||||||
- **2026-06-08 (S52 Task C+D-FE — ItTicket admin reassign + AttendanceReport menu, fe-admin ONLY):** Both intentional mirror-break (admin-only, NO fe-user touch, NO SHA256). **Task D-FE menu wiring:** Page+App.tsx route `/attendance/report` ALREADY exist (S52 prior). Only 2 of 4-place needed: (1) menuKeys.ts +OffAttendanceReport='Off_AttendanceReport' (mirror BE string exact, after OffChamCong) · (2) Layout.tsx staticMap +Off_AttendanceReport:'/attendance/report' (4th place gotcha #50). `types/menu.ts` = MenuNode tree type, key:string NOT typed-union → NO mirror there (resolvePath(key:string)). Leaf perm-gated via BE All[]→admin auto. **Task C reassign:** ItTicketsPage.tsx top-comment updated DIVERGES fe-user. Per-card Pencil button (cạnh 👤 assignee) → Dialog (size sm) + Select user. Users source = **GET /users {params:{page:1,pageSize:200}}→{items:UserOption{id,fullName,email}}** (reuse, proven PeWorkflows/Workflows/MeetingCalendar — `enabled:target!==null` lazy fetch). useMutation api.put(`/it-tickets/${id}/assign`,{assignedToUserId})→204 (NO json read)→invalidate['it-tickets']+toast.success+close. preselect t.assignedToUserId. UI deps: Dialog(open/onClose/title/children/footer?/size) + Select(native passthrough) + Button(variant=outline) + toast(sonner) + getErrorMessage(@/lib/apiError). Build PASS (0 err, 1945 mod). git: only 3 fe-admin file, fe-user untouched.
|
- **2026-06-08 (S52 Task C+D-FE — ItTicket admin reassign + AttendanceReport menu, fe-admin ONLY):** Both intentional mirror-break (admin-only, NO fe-user touch, NO SHA256). **Task D-FE menu wiring:** Page+App.tsx route `/attendance/report` ALREADY exist (S52 prior). Only 2 of 4-place needed: (1) menuKeys.ts +OffAttendanceReport='Off_AttendanceReport' (mirror BE string exact, after OffChamCong) · (2) Layout.tsx staticMap +Off_AttendanceReport:'/attendance/report' (4th place gotcha #50). `types/menu.ts` = MenuNode tree type, key:string NOT typed-union → NO mirror there (resolvePath(key:string)). Leaf perm-gated via BE All[]→admin auto. **Task C reassign:** ItTicketsPage.tsx top-comment updated DIVERGES fe-user. Per-card Pencil button (cạnh 👤 assignee) → Dialog (size sm) + Select user. Users source = **GET /users {params:{page:1,pageSize:200}}→{items:UserOption{id,fullName,email}}** (reuse, proven PeWorkflows/Workflows/MeetingCalendar — `enabled:target!==null` lazy fetch). useMutation api.put(`/it-tickets/${id}/assign`,{assignedToUserId})→204 (NO json read)→invalidate['it-tickets']+toast.success+close. preselect t.assignedToUserId. UI deps: Dialog(open/onClose/title/children/footer?/size) + Select(native passthrough) + Button(variant=outline) + toast(sonner) + getErrorMessage(@/lib/apiError). Build PASS (0 err, 1945 mod). git: only 3 fe-admin file, fe-user untouched.
|
||||||
- **2026-06-08 (P11-E Wave 1 — AttendanceReportPage fe-admin ONLY):** Report endpoint `[Authorize(Roles=Admin)]` → KHÔNG fe-user page → NO SHA256 mirror (intentional). 4 FE file: (1) types/workflowApps.ts +AttendanceReportRowDto{userId,fullName,departmentName?,daysPresent,totalWorkHours,otRaw,otWeekday,otWeekend,otHoliday,otWeighted}+AttendanceReportDto{year,month,rows,grandTotalWorkHours,grandTotalOtWeighted} (decimal→number) · (2) pages/office/AttendanceReportPage.tsx NEW: PageHeader+filter(Year Input number / Month Select 1-12 / Phòng ban Select fetch /departments) + TanStack key ['attendance-report',year,month,deptId] GET /attendances/report + Table 9 col STT/Họ tên/Phòng ban/Ngày công/Tổng giờ/OT thường/OT cuối tuần/OT lễ/OT quy đổi + tfoot Tổng(colSpan trick) + fmtNum vi-VN · (3) App.tsx import+route /attendance/report · (4) MyAttendancePage.tsx +button "Báo cáo" admin-only (user?.roles.includes('Admin')) navigate → DIVERGED fe-user (header comment cảnh báo). **Download Excel: `api.get(url,{params,responseType:'blob'})` (api instance inject JWT interceptor + refresh-retry — CHUẨN HƠN raw fetch spec gợi ý; proven ReportsPage/FormsPage/PeDetailTabs) → blob → createObjectURL → anchor.download.click → revoke. Filename content-disposition regex, fallback BaoCao-ChamCong-{Y}-{MM}.xlsx.** Build PASS (0 err, 1945 mod). KHÔNG menu key (button-reachable MVP).
|
- **2026-06-08 (P11-E Wave 1 — AttendanceReportPage fe-admin ONLY):** Report endpoint `[Authorize(Roles=Admin)]` → KHÔNG fe-user page → NO SHA256 mirror (intentional). 4 FE file: (1) types/workflowApps.ts +AttendanceReportRowDto{userId,fullName,departmentName?,daysPresent,totalWorkHours,otRaw,otWeekday,otWeekend,otHoliday,otWeighted}+AttendanceReportDto{year,month,rows,grandTotalWorkHours,grandTotalOtWeighted} (decimal→number) · (2) pages/office/AttendanceReportPage.tsx NEW: PageHeader+filter(Year Input number / Month Select 1-12 / Phòng ban Select fetch /departments) + TanStack key ['attendance-report',year,month,deptId] GET /attendances/report + Table 9 col STT/Họ tên/Phòng ban/Ngày công/Tổng giờ/OT thường/OT cuối tuần/OT lễ/OT quy đổi + tfoot Tổng(colSpan trick) + fmtNum vi-VN · (3) App.tsx import+route /attendance/report · (4) MyAttendancePage.tsx +button "Báo cáo" admin-only (user?.roles.includes('Admin')) navigate → DIVERGED fe-user (header comment cảnh báo). **Download Excel: `api.get(url,{params,responseType:'blob'})` (api instance inject JWT interceptor + refresh-retry — CHUẨN HƠN raw fetch spec gợi ý; proven ReportsPage/FormsPage/PeDetailTabs) → blob → createObjectURL → anchor.download.click → revoke. Filename content-disposition regex, fallback BaoCao-ChamCong-{Y}-{MM}.xlsx.** Build PASS (0 err, 1945 mod). KHÔNG menu key (button-reachable MVP).
|
||||||
- **2026-06-08 (S51 P11-C — vehicles+drivers kind → HrmConfigsPage):** Declarative KIND_CONFIG +2 entry (10th proof). 4-place adapt (`:kind`-driven page → NO App.tsx route): types/hrm-config.ts (union +'vehicles'+'drivers' + VehicleDto/DriverDto + Create/Update inputs) · HrmConfigsPage.tsx (KIND_CONFIG +2, KINDS array +2, renderCells +2 branch before ot-policies fallback, import Car+IdCard) · menuKeys.ts (+Hrm_Config_Vehicles/Drivers — BE string exact) · Layout.tsx staticMap +2 BOTH app. Field keys: vehicles{code,name,licensePlate,seatCount,description} drivers{code,name,phoneNumber,licenseNumber,licenseClass,description}. cp admin→user 3 file SHA256 identical (page a3afd724, type 2c0775b3, menuKeys d650c086). Layout mirror tay (structural diff OK, 2 entry verified both). Build PASS ×2 (admin 1944mod, user 1934mod, 0 TS err). lucide IdCard EXISTS (no UserRound fallback). AMBIGUITY: BE catalog vehicles/drivers chưa tồn tại on-disk (Wave 1 parallel — implementer-backend đang/sẽ làm) → FE scaffold theo contract spec cấp; runtime cần BE `/hrm-configs/vehicles`+`/drivers` endpoint + Hrm_Config_Vehicles/Drivers const trong BE MenuKeys.cs + seed.
|
- **2026-06-08 (S51 P11-C — vehicles+drivers kind → HrmConfigsPage):** Declarative KIND_CONFIG +2 entry (10th proof). 4-place adapt (`:kind`-driven page → NO App.tsx route): types/hrm-config.ts (union +'vehicles'+'drivers' + VehicleDto/DriverDto + Create/Update inputs) · HrmConfigsPage.tsx (KIND_CONFIG +2, KINDS array +2, renderCells +2 branch before ot-policies fallback, import Car+IdCard) · menuKeys.ts (+Hrm_Config_Vehicles/Drivers — BE string exact) · Layout.tsx staticMap +2 BOTH app. Field keys: vehicles{code,name,licensePlate,seatCount,description} drivers{code,name,phoneNumber,licenseNumber,licenseClass,description}. cp admin→user 3 file SHA256 identical (page a3afd724, type 2c0775b3, menuKeys d650c086). Layout mirror tay (structural diff OK, 2 entry verified both). Build PASS ×2 (admin 1944mod, user 1934mod, 0 TS err). lucide IdCard EXISTS (no UserRound fallback). AMBIGUITY: BE catalog vehicles/drivers chưa tồn tại on-disk (Wave 1 parallel — implementer-backend đang/sẽ làm) → FE scaffold theo contract spec cấp; runtime cần BE `/hrm-configs/vehicles`+`/drivers` endpoint + Hrm_Config_Vehicles/Drivers const trong BE MenuKeys.cs + seed.
|
||||||
- **2026-05-30 (S42 P11-B Wave 2 — leave balance display):** WorkflowAppDetailPage.tsx + workflowApps.ts (2 app SHA256 identical). +3 optional `leaveBalance{Entitled,Used,Remaining}?: number|null` trong `// leave` block (BE `decimal?` → camelCase). Block "Số dư phép" sau Section 1 IIFE `kind==='leave' && d.leaveBalanceRemaining != null`: year từ StartDate, banner amber/red khi `remaining<0 || (status!==DaDuyet && remaining<numDays)`. Case 1, KHÔNG 4-place (enrich existing page). cp fe-admin→fe-user. Build PASS ×2 (page 8ef83e4b, type 1c4f167a). Lesson reuse: IIFE inline `(() => {...})()` cho conditional block có derived vars — sạch hơn tách helper.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Anti-patterns (DO NOT)
|
## ⚠️ Anti-patterns (DO NOT)
|
||||||
|
|||||||
@ -70,6 +70,8 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
|
|||||||
|
|
||||||
## 📅 Recent activity (FIFO — older → archive/git)
|
## 📅 Recent activity (FIFO — older → archive/git)
|
||||||
|
|
||||||
|
- **2026-06-09 (S55 master-data Excel-import recon — 3 master + seed mechanism, on-disk):** ⭐ **"Hạng mục"/WorkItem master TỒN TẠI** — `Domain/Master/Catalogs/WorkItem.cs:6-14` (Code(50)UNIQUE-filtered/Name(200)/Category(100,idx)/DefaultUnit(50)/Description/IsActive), config `CatalogsConfiguration.cs:60-74`, full CRUD `CatalogsFeatures.cs:260-324` → group(VẬT TƯ/THẦU PHỤ/MEP)→Category, "1 Mat"→Code, item→Name. KHÔNG cần table/migration mới. **PE detail = pure free-text** (`PurchaseEvaluationDetail.cs` GroupCode/GroupName/ItemCode/NoiDung strings, NO FK→WorkItem) → load WorkItems non-breaking. **Project** (`Project.cs:5-14`, cfg `:14-21`): Code(50,UNIQUE `[IsDeleted]=0` Mig47)+Name(200) REQUIRED, StartDate/EndDate/BudgetTotal(18,2)/Note(1000)/ManagerUserId optional. ❌ **THIẾU Year/Investor/Location/Package** — chỉ Note free-text catch-all. Create cmd `ProjectFeatures.cs:67` dup-check `:87 AnyAsync(Code==)`. **Supplier** (`Supplier.cs:5-16`, cfg `:14-27`): Code/Name req + Type enum + TaxCode(20)/Phone/Email/Address/ContactPerson/Note. `SupplierType.cs`: NhaCungCap=1/NhaThauPhu=2/ToDoi=3/DonViDichVu=4/ChuDauTu=5. ❌ **THIẾU Status/TinhTrang (KHÔNG có field/enum nào)** + bank-acct + legal-rep (≠ContactPerson) + quality-score; "Cả hai" PHÂN LOẠI unmappable (Type single-valued). Create `CreateSupplierCommand.cs:10` dup `:45`. **Seed = idempotent `existingCodes.Contains→skip`** (`DbInitializer.SeedDemoMasterDataAsync:2149`, today 18 supplier `:2155` + 8 project `:2222`; WorkItems 15 rows tuple-loop `SeedCatalogsAsync:576-599`). **NO bulk import** — Master chỉ single CRUD; Import/Upload hits = Forms/PE/Employees attachment only; POST one-at-a-time. **Seed→prod:** `DbInitializer.InitializeAsync` chạy MỌI startup (`Program.cs:197` unless `--no-db-init`) → `MigrateAsync` THEN seed; demo gated `config.GetValue<bool>("DemoSeed:Disabled")` (`:80`) NHƯNG SeedDemoMasterData+SeedCatalogs chạy BẤT KỂ flag (ngoài if-block :108/:115) → seed method mới auto-reach prod next deploy. Rec: idempotent DbInitializer mirror (NOT API loop). Surprise: real+demo data sẽ trộn chung Suppliers/Projects/WorkItems (18/8/15 demo rows) → cân nhắc gate demo off prod. Tag `[master-import, workitem-exists, seed-idempotent, s55]`.
|
||||||
|
|
||||||
- **2026-06-08 (S52 Phase 11-D/E/F product-close recon — 6 gap, on-disk):** ⭐ **GAP1 IT-pool KHÔNG TỒN TẠI:** AppRoles.All=13 role (`AppRoles.cs:23`) NO "IT"; 9 dept seed (`DbInitializer.cs:2066` PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) NO dept IT; MenuKeys NO It_* group (chỉ `OffItTicket="Off_ItTicket"` 1 leaf :123). → round-robin pool PHẢI tạo signal mới: option (a) +AppRoles.ItStaff const + seed user, (b) +dept "IT" code, (c) per-user flag `User.IsItStaff`. Least-loaded query = `Users.Where(pool).OrderBy(u => Tickets.Count(AssignedToUserId==u.Id && Status!=Closed))` — `ItTicket.AssignedToUserId Guid?` SẴN (`ItTicket.cs:21`). **GAP2 HostedService:** đăng ký tại `Infrastructure/DependencyInjection.cs:46 AddHostedService<SlaExpiryJob>()` (KHÔNG Program.cs — grep Program.cs rỗng). Pattern `SlaExpiryJob.cs`: `BackgroundService` + ctor `(IServiceProvider sp, ILogger)` + `ExecuteAsync` Task.Delay(30s warmup)+while loop Interval 15min → `_sp.CreateAsyncScope()` resolve scoped `IApplicationDbContext`+`IDateTime`+`INotificationService` (:61-65). ItTicketSlaJob mirror: thêm dòng :47 + clone file. **GAP3 OtPolicy (`Hrm/OtPolicy.cs`):** 3 multiplier decimal(4,2) `MultiplierWeekday/Weekend/Holiday` (:21-23, seed 1.5/2.0/3.0) + 3 cap int `MaxHoursPer Day/Month/Year` (:26-28) + `Code` UNIQUE + `IsActive` (1 default công ty). `Attendance.OtHours decimal?` (`Office/Attendance.cs:37`) per-row, KHÔNG link OtPolicyId → join thủ công qua IsActive=true; công thức OT-pay = `OtHours × multiplier(dayType) × hourlyRate`, dayType phân loại từ AttendanceDate (Holiday tra Hrm_Holiday, Sat/Sun=Weekend, else Weekday). **GAP4 Excel reuse:** `IContractExcelExporter.ExportAsync→RenderResult` record `(byte[] Content, string FileName, string ContentType)` (`IFormRenderer.cs:3`); impl `ContractExcelExporter.cs` ClosedXML `XLWorkbook`+`Worksheets.Add`+`MemoryStream→ToArray()` (:103-109 content-type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`); DI scoped :40; controller stream `return File(result.Content, result.ContentType, result.FileName)` (`ReportsController.cs:35`, mirror Forms/PE/Contracts). AttendanceExporter = clone + đổi columns + new CQRS command (mẫu `ExportContractsToExcelCommand`). **GAP5 Attendance API:** `AttendancesController.cs` 3 endpoint check-in/check-out/me (`[Authorize]` ko role); CQRS inline `Office/WorkflowAppsFeatures.cs` REGION 6 (:401-490) — `CheckInCommand`/`CheckOutCommand`/`GetMyAttendanceQuery(Year,Month)` chỉ trả LIST cá nhân (1 user/tháng). ❌ CHƯA có aggregate/monthly-report/all-users query → P11-E phải +`GetAttendanceReportQuery(year,month,deptId?)`. ItTicket CQRS cũng inline cùng file (:354 GetItTicketsQuery + CreateItTicketCommand + UpdateItTicketStatusCommand, controller `ItTicketsController.cs`). **GAP6 FE state:** ItTicketsPage + MyAttendancePage TỒN TẠI cả 2 app (fe-admin+fe-user, comment "MIRROR SHA256 identical"), routes `/it-tickets`+`/attendance` (`App.tsx:101-102`), menuKeys `OffItTicket`+`OffChamCong` (:65-66), Layout map :84-85. ❌ THIẾU: ItTicket = SKELETON read-only kanban (banner :32-34 "Form tạo + auto-assign + SLA timer defer Phase 11"), NO create form/assign-UI/SLA-badge; Attendance = check-in/out OK nhưng NO admin report page / Excel export button / OT-pay column. NO menuKey `Attendance_Report`/`It_Assign`. Surprise: ItTicket+Attendance KHÔNG dùng Workflow V2 (kanban status flow, comment `ItTicket.cs:6`) — khác Leave/OT/Travel/Vehicle (LevelOpinion). Tag `[p11-def-recon, it-pool-absent, otpolicy-multiplier, excel-reuse, s52]`.
|
- **2026-06-08 (S52 Phase 11-D/E/F product-close recon — 6 gap, on-disk):** ⭐ **GAP1 IT-pool KHÔNG TỒN TẠI:** AppRoles.All=13 role (`AppRoles.cs:23`) NO "IT"; 9 dept seed (`DbInitializer.cs:2066` PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) NO dept IT; MenuKeys NO It_* group (chỉ `OffItTicket="Off_ItTicket"` 1 leaf :123). → round-robin pool PHẢI tạo signal mới: option (a) +AppRoles.ItStaff const + seed user, (b) +dept "IT" code, (c) per-user flag `User.IsItStaff`. Least-loaded query = `Users.Where(pool).OrderBy(u => Tickets.Count(AssignedToUserId==u.Id && Status!=Closed))` — `ItTicket.AssignedToUserId Guid?` SẴN (`ItTicket.cs:21`). **GAP2 HostedService:** đăng ký tại `Infrastructure/DependencyInjection.cs:46 AddHostedService<SlaExpiryJob>()` (KHÔNG Program.cs — grep Program.cs rỗng). Pattern `SlaExpiryJob.cs`: `BackgroundService` + ctor `(IServiceProvider sp, ILogger)` + `ExecuteAsync` Task.Delay(30s warmup)+while loop Interval 15min → `_sp.CreateAsyncScope()` resolve scoped `IApplicationDbContext`+`IDateTime`+`INotificationService` (:61-65). ItTicketSlaJob mirror: thêm dòng :47 + clone file. **GAP3 OtPolicy (`Hrm/OtPolicy.cs`):** 3 multiplier decimal(4,2) `MultiplierWeekday/Weekend/Holiday` (:21-23, seed 1.5/2.0/3.0) + 3 cap int `MaxHoursPer Day/Month/Year` (:26-28) + `Code` UNIQUE + `IsActive` (1 default công ty). `Attendance.OtHours decimal?` (`Office/Attendance.cs:37`) per-row, KHÔNG link OtPolicyId → join thủ công qua IsActive=true; công thức OT-pay = `OtHours × multiplier(dayType) × hourlyRate`, dayType phân loại từ AttendanceDate (Holiday tra Hrm_Holiday, Sat/Sun=Weekend, else Weekday). **GAP4 Excel reuse:** `IContractExcelExporter.ExportAsync→RenderResult` record `(byte[] Content, string FileName, string ContentType)` (`IFormRenderer.cs:3`); impl `ContractExcelExporter.cs` ClosedXML `XLWorkbook`+`Worksheets.Add`+`MemoryStream→ToArray()` (:103-109 content-type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`); DI scoped :40; controller stream `return File(result.Content, result.ContentType, result.FileName)` (`ReportsController.cs:35`, mirror Forms/PE/Contracts). AttendanceExporter = clone + đổi columns + new CQRS command (mẫu `ExportContractsToExcelCommand`). **GAP5 Attendance API:** `AttendancesController.cs` 3 endpoint check-in/check-out/me (`[Authorize]` ko role); CQRS inline `Office/WorkflowAppsFeatures.cs` REGION 6 (:401-490) — `CheckInCommand`/`CheckOutCommand`/`GetMyAttendanceQuery(Year,Month)` chỉ trả LIST cá nhân (1 user/tháng). ❌ CHƯA có aggregate/monthly-report/all-users query → P11-E phải +`GetAttendanceReportQuery(year,month,deptId?)`. ItTicket CQRS cũng inline cùng file (:354 GetItTicketsQuery + CreateItTicketCommand + UpdateItTicketStatusCommand, controller `ItTicketsController.cs`). **GAP6 FE state:** ItTicketsPage + MyAttendancePage TỒN TẠI cả 2 app (fe-admin+fe-user, comment "MIRROR SHA256 identical"), routes `/it-tickets`+`/attendance` (`App.tsx:101-102`), menuKeys `OffItTicket`+`OffChamCong` (:65-66), Layout map :84-85. ❌ THIẾU: ItTicket = SKELETON read-only kanban (banner :32-34 "Form tạo + auto-assign + SLA timer defer Phase 11"), NO create form/assign-UI/SLA-badge; Attendance = check-in/out OK nhưng NO admin report page / Excel export button / OT-pay column. NO menuKey `Attendance_Report`/`It_Assign`. Surprise: ItTicket+Attendance KHÔNG dùng Workflow V2 (kanban status flow, comment `ItTicket.cs:6`) — khác Leave/OT/Travel/Vehicle (LevelOpinion). Tag `[p11-def-recon, it-pool-absent, otpolicy-multiplier, excel-reuse, s52]`.
|
||||||
|
|
||||||
- **2026-06-08 (S51 gotcha #57 EXTENSION reachability audit — 6 candidate, RAG down, on-disk only):** ⭐ Bug class = soft-delete + bare `.IsUnique()` on Code → recreate-after-delete throws DbUpdateException 500. Verdict 6 cand: **FIX 3 (Master)** Department/Supplier/Project (`Department/Supplier/ProjectConfiguration.cs:18/24/19` bare unique). ALL = AuditableEntity + **GLOBAL `HasQueryFilter(!IsDeleted)`** + Delete via `.Remove()` → `AuditingInterceptor.cs` (State Deleted→Modified, IsDeleted=true) + Create `AnyAsync(x=>x.Code==req.Code)` NO `!IsDeleted` BUT global filter auto-hides soft-deleted → check passes → unfiltered index 500. **CONFIRMED-reachable** (`DepartmentFeatures.cs:76+125`, `ProjectFeatures.cs:87+147`, `CreateSupplierCommand.cs:45`+`DeleteSupplierCommand.cs:20`). **SKIP 3:** (a) **ContractClause** (`ContractClauseConfiguration.cs:18`) — NO Create/Update/Delete handler ANYWHERE (only `IApplicationDbContext.cs:32` DbSet; FormsController = templates only) → not CRUD-reachable. (b) **MeetingRoom** (`MeetingRoomConfiguration.cs:20`) — Delete sets `IsActive=false` NOT IsDeleted (`MeetingFeatures.cs:178`, comment :175 "FK Restrict → NOT soft delete") → index never gets soft-deleted row; Create also checks `&& !IsDeleted` :113. (c) **EmployeeProfile** (`EmployeeProfileConfiguration.cs:24/26` EmployeeCode+UserId) — Delete soft (`EmployeeFeatures.cs:437`) BUT Create BLOCKS reuse by design: UserId check `AsNoTracking().FirstOrDefault(UserId==)` (no HRM global filter) sees soft-deleted → throws ConflictException "Cần khôi phục" :160-163; EmployeeCode auto-gen atomic (never user-supplied/reused) → no collision. **Completeness (grep ALL `.IsUnique()`):** beyond 3 Master + 6 HRM-fixed (LeaveType/Holiday/Shift/OtPolicy/Vehicle/Driver all `.HasFilter([IsDeleted]=0)`), every OTHER bare-unique is either composite junction (Permission RoleId+MenuKey, *LevelOpinion, MeetingBookingAttendee, LeaveBalance, Attendance UserId+Date), nullable-code already filtered (`[Ma*] IS NOT NULL`: Contract/PE/Proposal/Budget/WorkflowApps), or no-soft-delete (WorkflowDefinition/ApprovalWorkflow Code+Version, ContractTemplate FormCode, WorkflowTypeAssignment, DepartmentApprovals). **Mig 46 = exactly 3 indexes (Departments/Suppliers/Projects Code).** Surprise: Master GLOBAL query filter MAKES the bug (auto-hides soft-deleted from check) — opposite of HRM where bug needs manual `!IsDeleted`; either way unfiltered index = 500. Tag `[gotcha57-ext, reachability-audit, master-global-filter, s51]`.
|
- **2026-06-08 (S51 gotcha #57 EXTENSION reachability audit — 6 candidate, RAG down, on-disk only):** ⭐ Bug class = soft-delete + bare `.IsUnique()` on Code → recreate-after-delete throws DbUpdateException 500. Verdict 6 cand: **FIX 3 (Master)** Department/Supplier/Project (`Department/Supplier/ProjectConfiguration.cs:18/24/19` bare unique). ALL = AuditableEntity + **GLOBAL `HasQueryFilter(!IsDeleted)`** + Delete via `.Remove()` → `AuditingInterceptor.cs` (State Deleted→Modified, IsDeleted=true) + Create `AnyAsync(x=>x.Code==req.Code)` NO `!IsDeleted` BUT global filter auto-hides soft-deleted → check passes → unfiltered index 500. **CONFIRMED-reachable** (`DepartmentFeatures.cs:76+125`, `ProjectFeatures.cs:87+147`, `CreateSupplierCommand.cs:45`+`DeleteSupplierCommand.cs:20`). **SKIP 3:** (a) **ContractClause** (`ContractClauseConfiguration.cs:18`) — NO Create/Update/Delete handler ANYWHERE (only `IApplicationDbContext.cs:32` DbSet; FormsController = templates only) → not CRUD-reachable. (b) **MeetingRoom** (`MeetingRoomConfiguration.cs:20`) — Delete sets `IsActive=false` NOT IsDeleted (`MeetingFeatures.cs:178`, comment :175 "FK Restrict → NOT soft delete") → index never gets soft-deleted row; Create also checks `&& !IsDeleted` :113. (c) **EmployeeProfile** (`EmployeeProfileConfiguration.cs:24/26` EmployeeCode+UserId) — Delete soft (`EmployeeFeatures.cs:437`) BUT Create BLOCKS reuse by design: UserId check `AsNoTracking().FirstOrDefault(UserId==)` (no HRM global filter) sees soft-deleted → throws ConflictException "Cần khôi phục" :160-163; EmployeeCode auto-gen atomic (never user-supplied/reused) → no collision. **Completeness (grep ALL `.IsUnique()`):** beyond 3 Master + 6 HRM-fixed (LeaveType/Holiday/Shift/OtPolicy/Vehicle/Driver all `.HasFilter([IsDeleted]=0)`), every OTHER bare-unique is either composite junction (Permission RoleId+MenuKey, *LevelOpinion, MeetingBookingAttendee, LeaveBalance, Attendance UserId+Date), nullable-code already filtered (`[Ma*] IS NOT NULL`: Contract/PE/Proposal/Budget/WorkflowApps), or no-soft-delete (WorkflowDefinition/ApprovalWorkflow Code+Version, ContractTemplate FormCode, WorkflowTypeAssignment, DepartmentApprovals). **Mig 46 = exactly 3 indexes (Departments/Suppliers/Projects Code).** Surprise: Master GLOBAL query filter MAKES the bug (auto-hides soft-deleted from check) — opposite of HRM where bug needs manual `!IsDeleted`; either way unfiltered index = 500. Tag `[gotcha57-ext, reachability-audit, master-global-filter, s51]`.
|
||||||
|
|||||||
@ -57,6 +57,8 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
|
|||||||
|
|
||||||
## 📅 Recent activity (FIFO — older → archive/git)
|
## 📅 Recent activity (FIFO — older → archive/git)
|
||||||
|
|
||||||
|
- **2026-06-09 (S55 master-data import pre-commit — PASS [em main proxy — reviewer return truncated gotcha #53 before verdict, mirror S51]):** Reviewed Mig 48 `AddProjectMasterFields` (Project +4 nullable col Year/Investor/Location/Package) + `SeedRealMasterDataAsync` (62 Project+71 WorkItem+3 Supplier per-code idempotent ungated) + FE ProjectsPage form +4 ×2 app. Reviewer ran 293s/31-tools nhưng truncated mid-thought (nghi cached-binary 2.76s build → muốn forced clean rebuild + Project tests). **Em main COMPLETED đúng việc nó định làm:** `dotnet test SolutionErp.slnx` = clean rebuild + **216 PASS** (58+158, 0 fail/skip) → giải tỏa cached-binary concern (test = fresh build). 10 dims GREEN: Mig Up=4 AddColumn/Down=4 DropColumn reversible + 3-file; seed 62/71/3 0-dup; per-code idempotent ungated line 118 (reaches prod); FLOCK01 collision skip-demo-wins; FE↔BE 4 nullable both sides (tránh S51 mismatch); test-file compile-fix +4 null legit; gotcha #57 index untouched; **runtime Dev proof** (data landed, Investor col populates). 0 rogue write (read-only respected, git clean of code). **Learned:** long adversarial review return truncates (gotcha #53) → reviewer nên emit PASS/FAIL verdict SỚM (trước deep re-verify) để sống sót truncation; em main complete được đúng pending-check (clean dotnet test) deterministic. Verdict PASS — safe commit. Tag [s55, master-import, em-main-proxy-truncate, runtime-dev-proof].
|
||||||
|
|
||||||
- **2026-06-08 (S54 ItTicket reassign authz Admin-OR-dept-IT cross-stack pre-commit — PASS, 0 blocker, gotcha #44 disarmed correctly):** Controller `/assign` hạ `[Authorize(Roles="Admin")]`→`[Authorize]` any-auth; authz moved INTO `AssignItTicketHandler` (Admin-OR-IT Forbidden + assignee-must-IT Conflict) + new `GetAssignableItStaffQuery` capability endpoint. **Independent re-verify GREEN:** `dotnet test SolutionErp.slnx` = **216 PASS** (58 Dom + 158 Infra, Failed:0 Skipped:0, +13 matches 203→216 claim exactly) · fe-admin + fe-user `tsc -p tsconfig.app.json --noEmit` BOTH exit 0 clean. **#2 CHÍ MẠNG role-string "Admin" CONFIRMED REAL (full chain traced):** `AppRoles.Admin="Admin"` literal (AppRoles.cs:5) → SeedRolesAsync `Name=roleName` (DbInit:1485) so DB Role.Name=="Admin" → Identity GetRolesAsync returns NAMES → JwtTokenService:32 `new Claim(ClaimTypes.Role, r)` → CurrentUserService:30-31 `FindAll(ClaimTypes.Role)` → `cu.Roles.Contains("Admin")` CORRECT. "QTV" (DbInit:1458 RoleLabels) = ShortName DISPLAY label only = decoy. Program.cs JWT sets NO `RoleClaimType` override (ClaimTypes.Role symmetric write/read). Same proven pattern as every existing Roles="Admin" endpoint. **Bypass airtight:** controller any-auth → handler sole gate; guard `if(!isAdmin && !(itDeptId is Guid mine && myDeptId==mine)) Forbidden` fail-CLOSED when itDeptId null (non-admin blocked; admin still passes role-branch). `Department.Code=="IT"` IS seeded (DbInit:2082 "Phòng CNTT") so live non-null. **Capability 0-leak:** non-auth → `{canReassign:false, staff:[]}` (no name leak), `[Authorize]` any-auth → 0 silent-403. **Defense-in-depth intact:** FE nút `{canReassign&&}` but PUT still hits handler guard → 403/409 → `onError: toast.error(getErrorMessage(err))` surfaces (NOT swallowed). **FE SHA256 page 4bcaf2f IDENTICAL both apps** (types.ts correctly NOT identical — admin AttendanceReportDto vs user HrDashboardDto diverge below; added AssignableStaff block byte-identical); old fe-user page was read-only kanban (NO app-specific logic lost — diff purely additive); fe-user has all imports (apiError.getErrorMessage, Dialog size/footer/onClose props match, Select passthrough, sonner). **Test 13-fact NOT happy-path:** Case5 Forbidden side-effect assert `AssignedToUserId.Should().BeNull()` (red-able by contrast vs Case6/7 same-handler success), Case3/3b empty-staff 0-leak, Case8 Conflict msg-exact. prove-by-contrast ĐỦ CHẶT (partition non-IT-throws vs IT/admin-succeed identical handler). **1 MINOR defer:** assignee-must-IT NEW vs old handler (git HEAD: old allowed admin→ANY active user); itDeptId-null → even admin Conflict — fail-closed acceptable+spec-requested, cosmetic (prod IT-dept seeded). **Learned:** authz role-string review = trace FULL chain const→seed Name→GetRolesAsync(names not codes)→Claim(ClaimTypes.Role)→reader AND grep JWT cfg for `RoleClaimType` override (none=symmetric) — display-code (QTV) in RoleLabels/ShortName dict = classic decoy. **surprise:** moving authz controller→handler is the CORRECT gotcha #44 fix (not a smell) when paired with BE-computed capability flag for FE gating + handler as sole gate. Verdict PASS — safe commit. Tag [s54, it-ticket-reassign-authz, gotcha44-disarmed, role-string-chain-verified, cross-stack-clean].
|
- **2026-06-08 (S54 ItTicket reassign authz Admin-OR-dept-IT cross-stack pre-commit — PASS, 0 blocker, gotcha #44 disarmed correctly):** Controller `/assign` hạ `[Authorize(Roles="Admin")]`→`[Authorize]` any-auth; authz moved INTO `AssignItTicketHandler` (Admin-OR-IT Forbidden + assignee-must-IT Conflict) + new `GetAssignableItStaffQuery` capability endpoint. **Independent re-verify GREEN:** `dotnet test SolutionErp.slnx` = **216 PASS** (58 Dom + 158 Infra, Failed:0 Skipped:0, +13 matches 203→216 claim exactly) · fe-admin + fe-user `tsc -p tsconfig.app.json --noEmit` BOTH exit 0 clean. **#2 CHÍ MẠNG role-string "Admin" CONFIRMED REAL (full chain traced):** `AppRoles.Admin="Admin"` literal (AppRoles.cs:5) → SeedRolesAsync `Name=roleName` (DbInit:1485) so DB Role.Name=="Admin" → Identity GetRolesAsync returns NAMES → JwtTokenService:32 `new Claim(ClaimTypes.Role, r)` → CurrentUserService:30-31 `FindAll(ClaimTypes.Role)` → `cu.Roles.Contains("Admin")` CORRECT. "QTV" (DbInit:1458 RoleLabels) = ShortName DISPLAY label only = decoy. Program.cs JWT sets NO `RoleClaimType` override (ClaimTypes.Role symmetric write/read). Same proven pattern as every existing Roles="Admin" endpoint. **Bypass airtight:** controller any-auth → handler sole gate; guard `if(!isAdmin && !(itDeptId is Guid mine && myDeptId==mine)) Forbidden` fail-CLOSED when itDeptId null (non-admin blocked; admin still passes role-branch). `Department.Code=="IT"` IS seeded (DbInit:2082 "Phòng CNTT") so live non-null. **Capability 0-leak:** non-auth → `{canReassign:false, staff:[]}` (no name leak), `[Authorize]` any-auth → 0 silent-403. **Defense-in-depth intact:** FE nút `{canReassign&&}` but PUT still hits handler guard → 403/409 → `onError: toast.error(getErrorMessage(err))` surfaces (NOT swallowed). **FE SHA256 page 4bcaf2f IDENTICAL both apps** (types.ts correctly NOT identical — admin AttendanceReportDto vs user HrDashboardDto diverge below; added AssignableStaff block byte-identical); old fe-user page was read-only kanban (NO app-specific logic lost — diff purely additive); fe-user has all imports (apiError.getErrorMessage, Dialog size/footer/onClose props match, Select passthrough, sonner). **Test 13-fact NOT happy-path:** Case5 Forbidden side-effect assert `AssignedToUserId.Should().BeNull()` (red-able by contrast vs Case6/7 same-handler success), Case3/3b empty-staff 0-leak, Case8 Conflict msg-exact. prove-by-contrast ĐỦ CHẶT (partition non-IT-throws vs IT/admin-succeed identical handler). **1 MINOR defer:** assignee-must-IT NEW vs old handler (git HEAD: old allowed admin→ANY active user); itDeptId-null → even admin Conflict — fail-closed acceptable+spec-requested, cosmetic (prod IT-dept seeded). **Learned:** authz role-string review = trace FULL chain const→seed Name→GetRolesAsync(names not codes)→Claim(ClaimTypes.Role)→reader AND grep JWT cfg for `RoleClaimType` override (none=symmetric) — display-code (QTV) in RoleLabels/ShortName dict = classic decoy. **surprise:** moving authz controller→handler is the CORRECT gotcha #44 fix (not a smell) when paired with BE-computed capability flag for FE gating + handler as sole gate. Verdict PASS — safe commit. Tag [s54, it-ticket-reassign-authz, gotcha44-disarmed, role-string-chain-verified, cross-stack-clean].
|
||||||
|
|
||||||
- **2026-06-08 (S52-late Task C ItTicket admin reassign + Task D AttendanceReport menu-key pre-commit — PASS, 0 blocker):** Migration-FREE (menu = idempotent DbInitializer seed). 5 prod files: MenuKeys.cs (const+All[]), DbInitializer.cs (1 seed tuple), fe-admin {menuKeys.ts, Layout.tsx staticMap, ItTicketsPage.tsx}. **Independent re-verify GREEN:** `dotnet build SolutionErp.slnx` 0-warn/0-err · `npm run build` fe-admin tsc-b+vite OK (1945 modules, only pre-existing CSS @import + >500KB + ineffective-dynamic-import warns). **menuKeyMatchOk:** `"Off_AttendanceReport"` byte-identical 4 places (MenuKeys.cs:125 const == menuKeys.ts:68 == seed parent key == Layout staticMap:87); seed leaf parent=`MenuKeys.Off` order=8 icon=`FileBarChart` (verified valid lucide alias `FileChartColumn as FileBarChart` — getIcon resolves via Icons[name]); App.tsx route `/attendance/report → AttendanceReportPage` PRE-EXISTING committed S52 (6a66429, no diff, 170-LOC real page not stub) — Layout maps to REAL route. `types/menu.ts` correctly NOT mirrored (`key:string` not typed union). admin auto-perm via `SeedAdminPermissionsAsync` iterating `MenuKeys.All` (DbInit:1917, idempotent Contains:1919); new-leaf-on-existing-DB confirmed (upsert loop:1845-1862 TryGetValue-miss→Add). **reassignCorrect:** FE PUT `/it-tickets/{id}/assign` body `{assignedToUserId}` MATCHES BE record `AssignItTicketBody(Guid AssignedToUserId)` (ItTicketsController:42); endpoint `[Authorize(Roles="Admin")]` (:34) under class `[Authorize]` — admin-app FE calling = correct; user-list reuses EXISTING `/users` GET (`PagedResult<UserDto>` items{id,fullName,email}, no new BE endpoint, lazy `enabled:target!==null`); 204 NoContent handled (no body-parse); `invalidateQueries(['it-tickets'])` on success; handler sets BOTH AssignedToUserId+AssignedToFullName (WorkflowAppsFeatures:467-468) + validates assignee IsActive→NotFound, no try-catch (GlobalExceptionMiddleware). **feUserUnchanged:** `git diff -- fe-user/` EMPTY (Task C = fe-admin-only divergence, documented top-comment ItTicketsPage:3-5). **noScopeCreep:** git status prod = EXACTLY the 5 expected files, agent-memory noise ignored, no new migration, no BE beyond MenuKeys+DbInitializer, ItTicketsPage diff 98+/5- all Task-C-scoped (imports+state+mutation+Pencil-btn+Dialog), 0 mock/alert markers. **Learned:** menu-key wiring = verify byte-identity across the FULL mirror set (BE const + BE All[] + seed parent + FE menuKeys + FE staticMap) + confirm the target route actually EXISTS (grep App.tsx) — a staticMap entry pointing to a non-existent route silently drops the leaf (gotcha #50). **surprise:** lucide `FileBarChart` is a deprecated-alias (re-exported from FileChartColumn) but still valid — d.ts grep confirmed before flagging. Verdict PASS — safe to commit. Tag [s52-late, it-ticket-reassign, attendance-report-menukey, menukey-mirror-5way, gotcha44-disarmed, gotcha50-disarmed].
|
- **2026-06-08 (S52-late Task C ItTicket admin reassign + Task D AttendanceReport menu-key pre-commit — PASS, 0 blocker):** Migration-FREE (menu = idempotent DbInitializer seed). 5 prod files: MenuKeys.cs (const+All[]), DbInitializer.cs (1 seed tuple), fe-admin {menuKeys.ts, Layout.tsx staticMap, ItTicketsPage.tsx}. **Independent re-verify GREEN:** `dotnet build SolutionErp.slnx` 0-warn/0-err · `npm run build` fe-admin tsc-b+vite OK (1945 modules, only pre-existing CSS @import + >500KB + ineffective-dynamic-import warns). **menuKeyMatchOk:** `"Off_AttendanceReport"` byte-identical 4 places (MenuKeys.cs:125 const == menuKeys.ts:68 == seed parent key == Layout staticMap:87); seed leaf parent=`MenuKeys.Off` order=8 icon=`FileBarChart` (verified valid lucide alias `FileChartColumn as FileBarChart` — getIcon resolves via Icons[name]); App.tsx route `/attendance/report → AttendanceReportPage` PRE-EXISTING committed S52 (6a66429, no diff, 170-LOC real page not stub) — Layout maps to REAL route. `types/menu.ts` correctly NOT mirrored (`key:string` not typed union). admin auto-perm via `SeedAdminPermissionsAsync` iterating `MenuKeys.All` (DbInit:1917, idempotent Contains:1919); new-leaf-on-existing-DB confirmed (upsert loop:1845-1862 TryGetValue-miss→Add). **reassignCorrect:** FE PUT `/it-tickets/{id}/assign` body `{assignedToUserId}` MATCHES BE record `AssignItTicketBody(Guid AssignedToUserId)` (ItTicketsController:42); endpoint `[Authorize(Roles="Admin")]` (:34) under class `[Authorize]` — admin-app FE calling = correct; user-list reuses EXISTING `/users` GET (`PagedResult<UserDto>` items{id,fullName,email}, no new BE endpoint, lazy `enabled:target!==null`); 204 NoContent handled (no body-parse); `invalidateQueries(['it-tickets'])` on success; handler sets BOTH AssignedToUserId+AssignedToFullName (WorkflowAppsFeatures:467-468) + validates assignee IsActive→NotFound, no try-catch (GlobalExceptionMiddleware). **feUserUnchanged:** `git diff -- fe-user/` EMPTY (Task C = fe-admin-only divergence, documented top-comment ItTicketsPage:3-5). **noScopeCreep:** git status prod = EXACTLY the 5 expected files, agent-memory noise ignored, no new migration, no BE beyond MenuKeys+DbInitializer, ItTicketsPage diff 98+/5- all Task-C-scoped (imports+state+mutation+Pencil-btn+Dialog), 0 mock/alert markers. **Learned:** menu-key wiring = verify byte-identity across the FULL mirror set (BE const + BE All[] + seed parent + FE menuKeys + FE staticMap) + confirm the target route actually EXISTS (grep App.tsx) — a staticMap entry pointing to a non-existent route silently drops the leaf (gotcha #50). **surprise:** lucide `FileBarChart` is a deprecated-alias (re-exported from FileChartColumn) but still valid — d.ts grep confirmed before flagging. Verdict PASS — safe to commit. Tag [s52-late, it-ticket-reassign, attendance-report-menukey, menukey-mirror-5way, gotcha44-disarmed, gotcha50-disarmed].
|
||||||
|
|||||||
@ -23,9 +23,13 @@ type FormState = {
|
|||||||
endDate: string
|
endDate: string
|
||||||
budgetTotal: string
|
budgetTotal: string
|
||||||
note: string
|
note: string
|
||||||
|
year: string
|
||||||
|
investor: string
|
||||||
|
location: string
|
||||||
|
package: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyForm: FormState = { code: '', name: '', startDate: '', endDate: '', budgetTotal: '', note: '' }
|
const emptyForm: FormState = { code: '', name: '', startDate: '', endDate: '', budgetTotal: '', note: '', year: '', investor: '', location: '', package: '' }
|
||||||
|
|
||||||
const fmtMoney = (v: number | null) => (v == null ? '—' : v.toLocaleString('vi-VN'))
|
const fmtMoney = (v: number | null) => (v == null ? '—' : v.toLocaleString('vi-VN'))
|
||||||
const fmtDate = (s: string | null) => (s ? new Date(s).toLocaleDateString('vi-VN') : '—')
|
const fmtDate = (s: string | null) => (s ? new Date(s).toLocaleDateString('vi-VN') : '—')
|
||||||
@ -61,6 +65,10 @@ export function ProjectsPage() {
|
|||||||
managerUserId: null,
|
managerUserId: null,
|
||||||
budgetTotal: d.budgetTotal ? Number(d.budgetTotal) : null,
|
budgetTotal: d.budgetTotal ? Number(d.budgetTotal) : null,
|
||||||
note: d.note || null,
|
note: d.note || null,
|
||||||
|
year: d.year ? Number(d.year) : null,
|
||||||
|
investor: d.investor || null,
|
||||||
|
location: d.location || null,
|
||||||
|
package: d.package || null,
|
||||||
}
|
}
|
||||||
if (d.id) await api.put(`/projects/${d.id}`, payload)
|
if (d.id) await api.put(`/projects/${d.id}`, payload)
|
||||||
else await api.post('/projects', payload)
|
else await api.post('/projects', payload)
|
||||||
@ -92,6 +100,10 @@ export function ProjectsPage() {
|
|||||||
endDate: p.endDate ? p.endDate.slice(0, 10) : '',
|
endDate: p.endDate ? p.endDate.slice(0, 10) : '',
|
||||||
budgetTotal: p.budgetTotal?.toString() ?? '',
|
budgetTotal: p.budgetTotal?.toString() ?? '',
|
||||||
note: p.note ?? '',
|
note: p.note ?? '',
|
||||||
|
year: p.year?.toString() ?? '',
|
||||||
|
investor: p.investor ?? '',
|
||||||
|
location: p.location ?? '',
|
||||||
|
package: p.package ?? '',
|
||||||
})
|
})
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
}
|
}
|
||||||
@ -99,6 +111,7 @@ export function ProjectsPage() {
|
|||||||
const columns: Column<Project>[] = [
|
const columns: Column<Project>[] = [
|
||||||
{ key: 'code', header: 'Mã DA', sortable: true, render: p => <span className="font-mono text-xs">{p.code}</span>, width: 'w-32' },
|
{ key: 'code', header: 'Mã DA', sortable: true, render: p => <span className="font-mono text-xs">{p.code}</span>, width: 'w-32' },
|
||||||
{ key: 'name', header: 'Tên dự án', sortable: true, render: p => p.name },
|
{ key: 'name', header: 'Tên dự án', sortable: true, render: p => p.name },
|
||||||
|
{ key: 'investor', header: 'Chủ đầu tư', render: p => p.investor ?? '—' },
|
||||||
{ key: 'startDate', header: 'Bắt đầu', render: p => fmtDate(p.startDate), width: 'w-32' },
|
{ key: 'startDate', header: 'Bắt đầu', render: p => fmtDate(p.startDate), width: 'w-32' },
|
||||||
{ key: 'endDate', header: 'Kết thúc', render: p => fmtDate(p.endDate), width: 'w-32' },
|
{ key: 'endDate', header: 'Kết thúc', render: p => fmtDate(p.endDate), width: 'w-32' },
|
||||||
{ key: 'budgetTotal', header: 'Ngân sách', align: 'right', render: p => fmtMoney(p.budgetTotal) },
|
{ key: 'budgetTotal', header: 'Ngân sách', align: 'right', render: p => fmtMoney(p.budgetTotal) },
|
||||||
@ -203,6 +216,22 @@ export function ProjectsPage() {
|
|||||||
<Label>Ngày kết thúc</Label>
|
<Label>Ngày kết thúc</Label>
|
||||||
<Input type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} />
|
<Input type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Năm</Label>
|
||||||
|
<Input type="number" value={form.year} onChange={e => setForm({ ...form, year: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Chủ đầu tư</Label>
|
||||||
|
<Input value={form.investor} onChange={e => setForm({ ...form, investor: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
<Label>Địa điểm</Label>
|
||||||
|
<Input value={form.location} onChange={e => setForm({ ...form, location: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
<Label>Gói thầu</Label>
|
||||||
|
<Input value={form.package} onChange={e => setForm({ ...form, package: e.target.value })} />
|
||||||
|
</div>
|
||||||
<div className="col-span-2 space-y-1.5">
|
<div className="col-span-2 space-y-1.5">
|
||||||
<Label>Ghi chú</Label>
|
<Label>Ghi chú</Label>
|
||||||
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
|
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
|
||||||
|
|||||||
@ -52,6 +52,10 @@ export type Project = {
|
|||||||
managerUserId: string | null
|
managerUserId: string | null
|
||||||
budgetTotal: number | null
|
budgetTotal: number | null
|
||||||
note: string | null
|
note: string | null
|
||||||
|
year: number | null
|
||||||
|
investor: string | null
|
||||||
|
location: string | null
|
||||||
|
package: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string | null
|
updatedAt: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,13 @@ type FormState = {
|
|||||||
endDate: string
|
endDate: string
|
||||||
budgetTotal: string
|
budgetTotal: string
|
||||||
note: string
|
note: string
|
||||||
|
year: string
|
||||||
|
investor: string
|
||||||
|
location: string
|
||||||
|
package: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyForm: FormState = { code: '', name: '', startDate: '', endDate: '', budgetTotal: '', note: '' }
|
const emptyForm: FormState = { code: '', name: '', startDate: '', endDate: '', budgetTotal: '', note: '', year: '', investor: '', location: '', package: '' }
|
||||||
|
|
||||||
const fmtMoney = (v: number | null) => (v == null ? '—' : v.toLocaleString('vi-VN'))
|
const fmtMoney = (v: number | null) => (v == null ? '—' : v.toLocaleString('vi-VN'))
|
||||||
const fmtDate = (s: string | null) => (s ? new Date(s).toLocaleDateString('vi-VN') : '—')
|
const fmtDate = (s: string | null) => (s ? new Date(s).toLocaleDateString('vi-VN') : '—')
|
||||||
@ -61,6 +65,10 @@ export function ProjectsPage() {
|
|||||||
managerUserId: null,
|
managerUserId: null,
|
||||||
budgetTotal: d.budgetTotal ? Number(d.budgetTotal) : null,
|
budgetTotal: d.budgetTotal ? Number(d.budgetTotal) : null,
|
||||||
note: d.note || null,
|
note: d.note || null,
|
||||||
|
year: d.year ? Number(d.year) : null,
|
||||||
|
investor: d.investor || null,
|
||||||
|
location: d.location || null,
|
||||||
|
package: d.package || null,
|
||||||
}
|
}
|
||||||
if (d.id) await api.put(`/projects/${d.id}`, payload)
|
if (d.id) await api.put(`/projects/${d.id}`, payload)
|
||||||
else await api.post('/projects', payload)
|
else await api.post('/projects', payload)
|
||||||
@ -92,6 +100,10 @@ export function ProjectsPage() {
|
|||||||
endDate: p.endDate ? p.endDate.slice(0, 10) : '',
|
endDate: p.endDate ? p.endDate.slice(0, 10) : '',
|
||||||
budgetTotal: p.budgetTotal?.toString() ?? '',
|
budgetTotal: p.budgetTotal?.toString() ?? '',
|
||||||
note: p.note ?? '',
|
note: p.note ?? '',
|
||||||
|
year: p.year?.toString() ?? '',
|
||||||
|
investor: p.investor ?? '',
|
||||||
|
location: p.location ?? '',
|
||||||
|
package: p.package ?? '',
|
||||||
})
|
})
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
}
|
}
|
||||||
@ -99,6 +111,7 @@ export function ProjectsPage() {
|
|||||||
const columns: Column<Project>[] = [
|
const columns: Column<Project>[] = [
|
||||||
{ key: 'code', header: 'Mã DA', sortable: true, render: p => <span className="font-mono text-xs">{p.code}</span>, width: 'w-32' },
|
{ key: 'code', header: 'Mã DA', sortable: true, render: p => <span className="font-mono text-xs">{p.code}</span>, width: 'w-32' },
|
||||||
{ key: 'name', header: 'Tên dự án', sortable: true, render: p => p.name },
|
{ key: 'name', header: 'Tên dự án', sortable: true, render: p => p.name },
|
||||||
|
{ key: 'investor', header: 'Chủ đầu tư', render: p => p.investor ?? '—' },
|
||||||
{ key: 'startDate', header: 'Bắt đầu', render: p => fmtDate(p.startDate), width: 'w-32' },
|
{ key: 'startDate', header: 'Bắt đầu', render: p => fmtDate(p.startDate), width: 'w-32' },
|
||||||
{ key: 'endDate', header: 'Kết thúc', render: p => fmtDate(p.endDate), width: 'w-32' },
|
{ key: 'endDate', header: 'Kết thúc', render: p => fmtDate(p.endDate), width: 'w-32' },
|
||||||
{ key: 'budgetTotal', header: 'Ngân sách', align: 'right', render: p => fmtMoney(p.budgetTotal) },
|
{ key: 'budgetTotal', header: 'Ngân sách', align: 'right', render: p => fmtMoney(p.budgetTotal) },
|
||||||
@ -203,6 +216,22 @@ export function ProjectsPage() {
|
|||||||
<Label>Ngày kết thúc</Label>
|
<Label>Ngày kết thúc</Label>
|
||||||
<Input type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} />
|
<Input type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Năm</Label>
|
||||||
|
<Input type="number" value={form.year} onChange={e => setForm({ ...form, year: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Chủ đầu tư</Label>
|
||||||
|
<Input value={form.investor} onChange={e => setForm({ ...form, investor: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
<Label>Địa điểm</Label>
|
||||||
|
<Input value={form.location} onChange={e => setForm({ ...form, location: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
<Label>Gói thầu</Label>
|
||||||
|
<Input value={form.package} onChange={e => setForm({ ...form, package: e.target.value })} />
|
||||||
|
</div>
|
||||||
<div className="col-span-2 space-y-1.5">
|
<div className="col-span-2 space-y-1.5">
|
||||||
<Label>Ghi chú</Label>
|
<Label>Ghi chú</Label>
|
||||||
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
|
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
|
||||||
|
|||||||
@ -52,6 +52,10 @@ export type Project = {
|
|||||||
managerUserId: string | null
|
managerUserId: string | null
|
||||||
budgetTotal: number | null
|
budgetTotal: number | null
|
||||||
note: string | null
|
note: string | null
|
||||||
|
year: number | null
|
||||||
|
investor: string | null
|
||||||
|
location: string | null
|
||||||
|
package: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string | null
|
updatedAt: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
146
scripts/master-import-data.generated.md
Normal file
146
scripts/master-import-data.generated.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# MASTER DATA IMPORT (S55) — source-of-truth for SeedRealMasterDataAsync. Generated from Excel, DO NOT hand-edit.
|
||||||
|
# Format: pipe-delimited. Project Name blank => use Code as Name. All strings already whitespace-cleaned.
|
||||||
|
|
||||||
|
## PROJECTS (62) | Code | Year(int? blank=null) | Name(blank=>Code) | Package | Investor | Location
|
||||||
|
P|APVN01|2023||||
|
||||||
|
P|AUS01|2025||||
|
||||||
|
P|BVN01|2025||||
|
||||||
|
P|CAL01|2025|Phân xưởng homecare 75 tấn/ngày và các hạng mục phụ trợ||Công ty TNHH Calofic|Lô C18-C24, Khu Công Nghiệp Hiệp Phước, Xã Hiệp Phước, Thành Phố Hồ Chí Minh
|
||||||
|
P|CET01|2023||||
|
||||||
|
P|CET02|2023||||
|
||||||
|
P|EQU01|||||
|
||||||
|
P|EVO01|||||
|
||||||
|
P|FLEMING01|||||
|
||||||
|
P|FLEMING01.24|||||
|
||||||
|
P|FLEMING01.25|||||
|
||||||
|
P|FLEMING03|2023||||
|
||||||
|
P|FLOCK01|2024||||
|
||||||
|
P|FLOCK01.25|2024||||
|
||||||
|
P|FLOCK01.26|2024||||
|
||||||
|
P|FLOCK02|2024||||
|
||||||
|
P|FLOCK02.25|2024||||
|
||||||
|
P|FLOCK03|2026||||
|
||||||
|
P|FLOCK04|2026||||
|
||||||
|
P|GCP01|||||
|
||||||
|
P|GO01|2023||||
|
||||||
|
P|GST01|2024||||
|
||||||
|
P|ILD01|||||
|
||||||
|
P|IML06|||||
|
||||||
|
P|MASAN01|||||
|
||||||
|
P|MIDEA01||Nhà máy Midea|Công trình xây dựng tầng lửng Nhà Máy 2 SDA Việt Nam Năm 2025|Công Ty TNHH Midea Consumer Electric (Vietnam)|Xưởng A2-1, A2-3, Lô A3, Đường NA2, Khu Công Nghiệp Thới Hòa, Phường Thới Hòa, Thành Phố Hồ Chí Minh
|
||||||
|
P|NTBA01|2023||||
|
||||||
|
P|NTBA01.25|2025||||
|
||||||
|
P|OLAM01|||||
|
||||||
|
P|OLAM02|||||
|
||||||
|
P|PMH01|||||
|
||||||
|
P|REDBULL01|2021||||
|
||||||
|
P|REDBULL03|2021||||
|
||||||
|
P|RMIT01|||||
|
||||||
|
P|SAM01|2024|Xây dựng nhà kho SAMHWA-VH|Thi công chính|Công ty TNHH SAMHWA-VH CO., LTD|Lô số b-13, đường d2, Phường Long Đức, Xã An Phước, Huyện Long Thành, Tỉnh Đồng Nai
|
||||||
|
P|SEW01|2023||||
|
||||||
|
P|SHEICO01|||||
|
||||||
|
P|SHEICO01B|||||
|
||||||
|
P|SHEICO01C|||||
|
||||||
|
P|SHEICO03|||||
|
||||||
|
P|SHEICO03.ELEVATOR|||||
|
||||||
|
P|SHEICO03.MEP|||||
|
||||||
|
P|SHEICO04|||||
|
||||||
|
P|SHEICO04.14|||||
|
||||||
|
P|SHEICO04.15ME|||||
|
||||||
|
P|SHEICO04.16ME|||||
|
||||||
|
P|SHEICO04.17ME|||||
|
||||||
|
P|SHEICO04.9|||||
|
||||||
|
P|SOL01|||||
|
||||||
|
P|SOVI01|||||
|
||||||
|
P|TBA01|||||
|
||||||
|
P|TLB01|2023|Merlion Trelleborg|Thiết Kế & Thi Công|Công Ty TNHH Trelleborg Việt Nam|Lô Số F2, Đường Số 2, Phường Phước Hòa, Thị Xã Phú Mỹ, Tỉnh Bà Rịa - Vũng Tàu
|
||||||
|
P|TTC01|||||
|
||||||
|
P|VLU04|||||
|
||||||
|
P|VLU04.1|||||
|
||||||
|
P|VP2024|||||
|
||||||
|
P|VPS01|||||
|
||||||
|
P|VRR01|||||
|
||||||
|
P|WFW01|||||
|
||||||
|
P|WPL01|||||
|
||||||
|
P|WPL02|||||
|
||||||
|
P|ZOTE01|2025|Zotefoams new manufacturing plant|Building and building service|Zotefoams PLC|Changshin VJ2, Khu Công Nghiệp Lộc An - Bình Sơn, Xã Long Thành, Tỉnh Đồng Nai
|
||||||
|
# PROJECTS_TOTAL=62
|
||||||
|
|
||||||
|
## WORKITEMS | Code | Category | Name
|
||||||
|
W|VT-01|Vật tư - Xây dựng|Bê tông
|
||||||
|
W|VT-02|Vật tư - Xây dựng|Thép cây, Thép lưới hàn
|
||||||
|
W|VT-03|Vật tư - Xây dựng|Cọc ly tâm
|
||||||
|
W|VT-04|Vật tư - Xây dựng|Cống ly tâm
|
||||||
|
W|VT-05|Vật tư - Xây dựng|Cừ tràm
|
||||||
|
W|VT-06|Vật tư - Xây dựng|Kim loại ( rọ đầu cọc, V mạ kẽm, Vinox, bản mã…)
|
||||||
|
W|VT-07|Vật tư - Xây dựng|Nắp gang
|
||||||
|
W|VT-08|Vật tư - Xây dựng|Gạch xây, Xi măng, cát, đá, gạch trồng cỏ
|
||||||
|
W|VT-09|Vật tư - Xây dựng|Gạch trồng cỏ, gạch trang trí…
|
||||||
|
W|VT-10|Vật tư - Xây dựng|Gạch ốp lát
|
||||||
|
W|VT-11|Vật tư - Xây dựng|Ván khuôn
|
||||||
|
W|VT-12|Vật tư - Xây dựng|Sơn nước
|
||||||
|
W|VT-13|Vật tư - Xây dựng|Phụ gia (sika, vinkem, Gps…)
|
||||||
|
W|VT-14|Vật tư - Xây dựng|PVC, vải địa
|
||||||
|
W|VT-15|Vật tư - Xây dựng|Nẹp inox, nẹp nhựa
|
||||||
|
W|VT-16|Vật tư - Xây dựng|Khác
|
||||||
|
W|TP-01|Thầu phụ - Xây dựng|Thầu phụ cọc ( cơ, robot)
|
||||||
|
W|TP-02|Thầu phụ - Xây dựng|Thầu phụ cọc Khoan nhồi
|
||||||
|
W|TP-03|Thầu phụ - Xây dựng|Thầu phụ cọc CDM
|
||||||
|
W|TP-04|Thầu phụ - Xây dựng|Kết cấu thép
|
||||||
|
W|TP-05|Thầu phụ - Xây dựng|Cáp dự ứng lực
|
||||||
|
W|TP-06|Thầu phụ - Xây dựng|Panel
|
||||||
|
W|TP-07|Thầu phụ - Xây dựng|Nhôm kính, alu
|
||||||
|
W|TP-08|Thầu phụ - Xây dựng|Cửa thép, Cửa cuốn, sectional door
|
||||||
|
W|TP-09|Thầu phụ - Xây dựng|Cửa gỗ
|
||||||
|
W|TP-10|Thầu phụ - Xây dựng|Cơ khí hoàn thiện lan can, cầu thang
|
||||||
|
W|TP-11|Thầu phụ - Xây dựng|Dây cứu sinh
|
||||||
|
W|TP-12|Thầu phụ - Xây dựng|Nội thất
|
||||||
|
W|TP-13|Thầu phụ - Xây dựng|Vách trần thạch cao
|
||||||
|
W|TP-14|Thầu phụ - Xây dựng|Vách ngăn di động
|
||||||
|
W|TP-15|Thầu phụ - Xây dựng|Vách ngăn vệ sinh
|
||||||
|
W|TP-16|Thầu phụ - Xây dựng|Granit
|
||||||
|
W|TP-17|Thầu phụ - Xây dựng|Hoàn thiện sàn thảm, vinyl
|
||||||
|
W|TP-18|Thầu phụ - Xây dựng|Chống mối
|
||||||
|
W|TP-19|Thầu phụ - Xây dựng|Chống thấm
|
||||||
|
W|TP-20|Thầu phụ - Xây dựng|Sơn PU, Epoxy, liquid
|
||||||
|
W|TP-21|Thầu phụ - Xây dựng|Dock Leveler.
|
||||||
|
W|TP-22|Thầu phụ - Xây dựng|Cẩu trục
|
||||||
|
W|TP-23|Thầu phụ - Xây dựng|Trạm cân
|
||||||
|
W|TP-24|Thầu phụ - Xây dựng|Thang máy, thang hàng, …
|
||||||
|
W|TP-25|Thầu phụ - Xây dựng|Sàn nâng.
|
||||||
|
W|TP-26|Thầu phụ - Xây dựng|Landscape
|
||||||
|
W|TP-27|Thầu phụ - Xây dựng|Cổng xếp.
|
||||||
|
W|TP-28|Thầu phụ - Xây dựng|Logo, Bảng hiệu.
|
||||||
|
W|TP-29|Thầu phụ - Xây dựng|TP thi công Màng nhựa
|
||||||
|
W|TP-30|Thầu phụ - Xây dựng|Khác
|
||||||
|
W|MEP-01|MEP|MEP (Full)
|
||||||
|
W|MEP-02|MEP|Hệ thống trung thế
|
||||||
|
W|MEP-03|MEP|PCCC (Phòng cháy chữa cháy).
|
||||||
|
W|MEP-04|MEP|Hệ thống điện, điện nhẹ
|
||||||
|
W|MEP-05|MEP|Hệ thống HVAC, Utility (Thông gió & Điều hòa không khí & Phù trợ).
|
||||||
|
W|MEP-06|MEP|Hệ thống cấp thoát nước
|
||||||
|
W|MEP-07|MEP|Hệ thống xử lý nước thải
|
||||||
|
W|MEP-08|MEP|Hệ thống solar
|
||||||
|
W|MEP-09|MEP|Khác
|
||||||
|
W|TB-01|Thiết bị|Máy biến áp
|
||||||
|
W|TB-02|Thiết bị|Tủ điện
|
||||||
|
W|TB-03|Thiết bị|Tủ trung thế.
|
||||||
|
W|TB-04|Thiết bị|Busway
|
||||||
|
W|TB-05|Thiết bị|Cáp điện
|
||||||
|
W|TB-06|Thiết bị|Thiết bị Utility (Máy nén khí, Hút bụi hút mùi, Boiler,...)
|
||||||
|
W|TB-07|Thiết bị|Máy lạnh
|
||||||
|
W|TB-08|Thiết bị|Ống thép, inox
|
||||||
|
W|TB-09|Thiết bị|Thang máng cáp
|
||||||
|
W|TB-10|Thiết bị|Ống PVC, HDPE
|
||||||
|
W|TB-11|Thiết bị|Thiết bị báo cháy
|
||||||
|
W|TB-12|Thiết bị|Đèn (đèn chiếu sáng, đèn năng lượng, đèn exit….)
|
||||||
|
W|TB-13|Thiết bị|Thiết bị chữa cháy (Sprinkler, Chữa cháy khí, FM200,…)
|
||||||
|
W|TB-14|Thiết bị|Bơm (bơm PCCC, Bơm nước…)
|
||||||
|
W|TB-15|Thiết bị|Bồn nước, Thiết bị vệ sinh
|
||||||
|
W|TB-16|Thiết bị|Khác
|
||||||
|
# WORKITEMS_TOTAL=71 (VatTu=16 ThauPhu=30 MEP=9 ThietBi=16)
|
||||||
|
|
||||||
|
## SUPPLIERS (3) | Code | Name | Type(NhaThauPhu/NhaCungCap) | TaxCode | Phone | Email | Address | ContactPerson | NotePacked
|
||||||
|
S|TRUONGGIANG|CÔNG TY CỔ PHẦN NHÔM KÍNH VÀ ĐẦU TƯ TRƯỜNG GIANG|NhaThauPhu|0312 251 859|08 2253 1381|viethq1985@gmail.com|Số 77/46A Đường Chuyên Dùng Chính, Phường Phú Thuận, TP.HCM|Mr. Việt|Loại: Nhôm kính, alu; NĐD: Ông: HOÀNG QUỐC VIỆT - GIÁM ĐỐC ĐIỀU HÀNH; TK: 100212127 Ngân hàng Eximbank - Chi Nhánh Hòa Bình; Trạng thái: ✅ Đang hoạt động
|
||||||
|
S|TANPHU|CÔNG TY TNHH SẢN XUẤT THƯƠNG MẠI SƠN TÂN PHÚ|NhaThauPhu|0305497393|0902 686468|thu@tanphupaint.com|Số 49/5A Đường số 7, Khu phố 53, P. Linh Xuân, Thành phố Hồ Chí Minh, Việt Nam|Ms Thư|Loại: Sơn PU, Epoxy, liquid; NĐD: Ông ĐINH PHƯỚC THỌ - GIÁM ĐỐC; TK: 8683107979 tại Ngân hàng BIDV - CN Nam Bình Dương; Trạng thái: ✅ Đang hoạt động
|
||||||
|
S|TGN|CÔNG TY CỔ PHẦN SIÊU THỊ VLXD THẾ GIỚI NHÀ|NhaCungCap|3603497972|028 36206000|duong.ltt@tgngroup.vn|46-48 Nguyễn Cơ Thạch, Phường An Khánh, Thành phố Hồ Chí Minh,Việt Nam|Ms. Dương|Loại: Bê tông; NĐD: Ông TRẦN ANH ĐIỀN - TỔNG GIÁM ĐỐC; TK: 114002641307 tại VietinBank CN KCN Biên Hòa; Trạng thái: ✅ Đang hoạt động
|
||||||
@ -18,7 +18,11 @@ public record ProjectDto(
|
|||||||
decimal? BudgetTotal,
|
decimal? BudgetTotal,
|
||||||
string? Note,
|
string? Note,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
DateTime? UpdatedAt);
|
DateTime? UpdatedAt,
|
||||||
|
int? Year,
|
||||||
|
string? Investor,
|
||||||
|
string? Location,
|
||||||
|
string? Package);
|
||||||
|
|
||||||
// ===================== LIST =====================
|
// ===================== LIST =====================
|
||||||
public record ListProjectsQuery : PagedRequest, IRequest<PagedResult<ProjectDto>>;
|
public record ListProjectsQuery : PagedRequest, IRequest<PagedResult<ProjectDto>>;
|
||||||
@ -44,7 +48,7 @@ public class ListProjectsQueryHandler(IApplicationDbContext db) : IRequestHandle
|
|||||||
var total = await query.CountAsync(ct);
|
var total = await query.CountAsync(ct);
|
||||||
var items = await query
|
var items = await query
|
||||||
.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
|
.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
|
||||||
.Select(x => new ProjectDto(x.Id, x.Code, x.Name, x.StartDate, x.EndDate, x.ManagerUserId, x.BudgetTotal, x.Note, x.CreatedAt, x.UpdatedAt))
|
.Select(x => new ProjectDto(x.Id, x.Code, x.Name, x.StartDate, x.EndDate, x.ManagerUserId, x.BudgetTotal, x.Note, x.CreatedAt, x.UpdatedAt, x.Year, x.Investor, x.Location, x.Package))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
return new PagedResult<ProjectDto>(items, total, request.Page, request.PageSize);
|
return new PagedResult<ProjectDto>(items, total, request.Page, request.PageSize);
|
||||||
}
|
}
|
||||||
@ -59,14 +63,15 @@ public class GetProjectQueryHandler(IApplicationDbContext db) : IRequestHandler<
|
|||||||
{
|
{
|
||||||
var x = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == request.Id, ct)
|
var x = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == request.Id, ct)
|
||||||
?? throw new NotFoundException("Project", request.Id);
|
?? throw new NotFoundException("Project", request.Id);
|
||||||
return new ProjectDto(x.Id, x.Code, x.Name, x.StartDate, x.EndDate, x.ManagerUserId, x.BudgetTotal, x.Note, x.CreatedAt, x.UpdatedAt);
|
return new ProjectDto(x.Id, x.Code, x.Name, x.StartDate, x.EndDate, x.ManagerUserId, x.BudgetTotal, x.Note, x.CreatedAt, x.UpdatedAt, x.Year, x.Investor, x.Location, x.Package);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== CREATE =====================
|
// ===================== CREATE =====================
|
||||||
public record CreateProjectCommand(
|
public record CreateProjectCommand(
|
||||||
string Code, string Name, DateTime? StartDate, DateTime? EndDate,
|
string Code, string Name, DateTime? StartDate, DateTime? EndDate,
|
||||||
Guid? ManagerUserId, decimal? BudgetTotal, string? Note) : IRequest<Guid>;
|
Guid? ManagerUserId, decimal? BudgetTotal, string? Note,
|
||||||
|
int? Year, string? Investor, string? Location, string? Package) : IRequest<Guid>;
|
||||||
|
|
||||||
public class CreateProjectCommandValidator : AbstractValidator<CreateProjectCommand>
|
public class CreateProjectCommandValidator : AbstractValidator<CreateProjectCommand>
|
||||||
{
|
{
|
||||||
@ -75,6 +80,9 @@ public class CreateProjectCommandValidator : AbstractValidator<CreateProjectComm
|
|||||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||||
RuleFor(x => x.BudgetTotal).GreaterThanOrEqualTo(0).When(x => x.BudgetTotal.HasValue);
|
RuleFor(x => x.BudgetTotal).GreaterThanOrEqualTo(0).When(x => x.BudgetTotal.HasValue);
|
||||||
|
RuleFor(x => x.Investor).MaximumLength(250);
|
||||||
|
RuleFor(x => x.Location).MaximumLength(500);
|
||||||
|
RuleFor(x => x.Package).MaximumLength(300);
|
||||||
RuleFor(x => x).Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.EndDate >= x.StartDate)
|
RuleFor(x => x).Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.EndDate >= x.StartDate)
|
||||||
.WithMessage("Ngày kết thúc phải sau ngày bắt đầu");
|
.WithMessage("Ngày kết thúc phải sau ngày bắt đầu");
|
||||||
}
|
}
|
||||||
@ -91,6 +99,7 @@ public class CreateProjectCommandHandler(IApplicationDbContext db) : IRequestHan
|
|||||||
Code = request.Code, Name = request.Name,
|
Code = request.Code, Name = request.Name,
|
||||||
StartDate = request.StartDate, EndDate = request.EndDate,
|
StartDate = request.StartDate, EndDate = request.EndDate,
|
||||||
ManagerUserId = request.ManagerUserId, BudgetTotal = request.BudgetTotal, Note = request.Note,
|
ManagerUserId = request.ManagerUserId, BudgetTotal = request.BudgetTotal, Note = request.Note,
|
||||||
|
Year = request.Year, Investor = request.Investor, Location = request.Location, Package = request.Package,
|
||||||
};
|
};
|
||||||
db.Projects.Add(entity);
|
db.Projects.Add(entity);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
@ -101,7 +110,8 @@ public class CreateProjectCommandHandler(IApplicationDbContext db) : IRequestHan
|
|||||||
// ===================== UPDATE =====================
|
// ===================== UPDATE =====================
|
||||||
public record UpdateProjectCommand(
|
public record UpdateProjectCommand(
|
||||||
Guid Id, string Code, string Name, DateTime? StartDate, DateTime? EndDate,
|
Guid Id, string Code, string Name, DateTime? StartDate, DateTime? EndDate,
|
||||||
Guid? ManagerUserId, decimal? BudgetTotal, string? Note) : IRequest;
|
Guid? ManagerUserId, decimal? BudgetTotal, string? Note,
|
||||||
|
int? Year, string? Investor, string? Location, string? Package) : IRequest;
|
||||||
|
|
||||||
public class UpdateProjectCommandValidator : AbstractValidator<UpdateProjectCommand>
|
public class UpdateProjectCommandValidator : AbstractValidator<UpdateProjectCommand>
|
||||||
{
|
{
|
||||||
@ -111,6 +121,9 @@ public class UpdateProjectCommandValidator : AbstractValidator<UpdateProjectComm
|
|||||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||||
RuleFor(x => x.BudgetTotal).GreaterThanOrEqualTo(0).When(x => x.BudgetTotal.HasValue);
|
RuleFor(x => x.BudgetTotal).GreaterThanOrEqualTo(0).When(x => x.BudgetTotal.HasValue);
|
||||||
|
RuleFor(x => x.Investor).MaximumLength(250);
|
||||||
|
RuleFor(x => x.Location).MaximumLength(500);
|
||||||
|
RuleFor(x => x.Package).MaximumLength(300);
|
||||||
RuleFor(x => x).Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.EndDate >= x.StartDate)
|
RuleFor(x => x).Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.EndDate >= x.StartDate)
|
||||||
.WithMessage("Ngày kết thúc phải sau ngày bắt đầu");
|
.WithMessage("Ngày kết thúc phải sau ngày bắt đầu");
|
||||||
}
|
}
|
||||||
@ -131,6 +144,10 @@ public class UpdateProjectCommandHandler(IApplicationDbContext db) : IRequestHan
|
|||||||
entity.ManagerUserId = request.ManagerUserId;
|
entity.ManagerUserId = request.ManagerUserId;
|
||||||
entity.BudgetTotal = request.BudgetTotal;
|
entity.BudgetTotal = request.BudgetTotal;
|
||||||
entity.Note = request.Note;
|
entity.Note = request.Note;
|
||||||
|
entity.Year = request.Year;
|
||||||
|
entity.Investor = request.Investor;
|
||||||
|
entity.Location = request.Location;
|
||||||
|
entity.Package = request.Package;
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,4 +11,8 @@ public class Project : AuditableEntity
|
|||||||
public Guid? ManagerUserId { get; set; } // PM — Giám đốc Dự án
|
public Guid? ManagerUserId { get; set; } // PM — Giám đốc Dự án
|
||||||
public decimal? BudgetTotal { get; set; } // Tổng ngân sách dự án (tham chiếu cho CCM check)
|
public decimal? BudgetTotal { get; set; } // Tổng ngân sách dự án (tham chiếu cho CCM check)
|
||||||
public string? Note { get; set; }
|
public string? Note { get; set; }
|
||||||
|
public int? Year { get; set; } // Năm
|
||||||
|
public string? Investor { get; set; } // Chủ đầu tư
|
||||||
|
public string? Location { get; set; } // Địa điểm
|
||||||
|
public string? Package { get; set; } // Gói thầu
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,9 @@ public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
|||||||
b.Property(x => x.Name).HasMaxLength(200).IsRequired();
|
b.Property(x => x.Name).HasMaxLength(200).IsRequired();
|
||||||
b.Property(x => x.BudgetTotal).HasPrecision(18, 2);
|
b.Property(x => x.BudgetTotal).HasPrecision(18, 2);
|
||||||
b.Property(x => x.Note).HasMaxLength(1000);
|
b.Property(x => x.Note).HasMaxLength(1000);
|
||||||
|
b.Property(x => x.Investor).HasMaxLength(250); // Chủ đầu tư
|
||||||
|
b.Property(x => x.Location).HasMaxLength(500); // Địa điểm
|
||||||
|
b.Property(x => x.Package).HasMaxLength(300); // Gói thầu
|
||||||
|
|
||||||
b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 47 (gotcha #57 EXT) — soft-deleted slot reusable, khớp HasQueryFilter !IsDeleted app-check
|
b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 47 (gotcha #57 EXT) — soft-deleted slot reusable, khớp HasQueryFilter !IsDeleted app-check
|
||||||
|
|
||||||
|
|||||||
@ -113,6 +113,9 @@ public static class DbInitializer
|
|||||||
await SeedPurchaseEvaluationWorkflowsAsync(db, logger);
|
await SeedPurchaseEvaluationWorkflowsAsync(db, logger);
|
||||||
}
|
}
|
||||||
await SeedCatalogsAsync(db, logger);
|
await SeedCatalogsAsync(db, logger);
|
||||||
|
// S55 (2026-06-09) — Master data thật của công ty (62 dự án + 71 hạng mục +
|
||||||
|
// 3 NCC). NOT gated DemoSeed — coexist với demo data qua per-code idempotent.
|
||||||
|
await SeedRealMasterDataAsync(db, logger);
|
||||||
|
|
||||||
// Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create).
|
// Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create).
|
||||||
// Idempotent: chỉ HĐ MaHopDong IS NULL được gen.
|
// Idempotent: chỉ HĐ MaHopDong IS NULL được gen.
|
||||||
@ -2219,6 +2222,213 @@ public static class DbInitializer
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S55 (2026-06-09) — Import master data thật của công ty (62 dự án + 71 hạng
|
||||||
|
// mục công việc + 3 NCC) từ file Excel `scripts/master-import-data.generated.md`.
|
||||||
|
// Data hardcode inline vì runtime KHÔNG đọc được scratch file đó.
|
||||||
|
//
|
||||||
|
// NOT gated DemoSeed:Disabled — đây là INFRASTRUCTURE master data thật (như
|
||||||
|
// Roles/Departments/Catalogs), coexist với demo data qua per-code idempotent
|
||||||
|
// (gotcha #51). Mirror pattern SeedDemoMasterDataAsync: load existing codes →
|
||||||
|
// foreach skip-if-contains else Add → 1 SaveChanges.
|
||||||
|
//
|
||||||
|
// KNOWN collision: project "FLOCK01" cũng là demo project (SeedDemoMasterDataAsync)
|
||||||
|
// → per-code idempotent SKIP nó (demo thắng). Chấp nhận được — FLOCK01 thật chỉ
|
||||||
|
// có Code + Year. Không special-case.
|
||||||
|
private static async Task SeedRealMasterDataAsync(ApplicationDbContext db, ILogger logger)
|
||||||
|
{
|
||||||
|
// ---------- PROJECTS (62) — (Code, Year, Name [=Code nếu blank], Package, Investor, Location) ----------
|
||||||
|
var projects = new (string Code, int? Year, string? Name, string? Package, string? Investor, string? Location)[]
|
||||||
|
{
|
||||||
|
("APVN01", 2023, null, null, null, null),
|
||||||
|
("AUS01", 2025, null, null, null, null),
|
||||||
|
("BVN01", 2025, null, null, null, null),
|
||||||
|
("CAL01", 2025, "Phân xưởng homecare 75 tấn/ngày và các hạng mục phụ trợ", null, "Công ty TNHH Calofic", "Lô C18-C24, Khu Công Nghiệp Hiệp Phước, Xã Hiệp Phước, Thành Phố Hồ Chí Minh"),
|
||||||
|
("CET01", 2023, null, null, null, null),
|
||||||
|
("CET02", 2023, null, null, null, null),
|
||||||
|
("EQU01", null, null, null, null, null),
|
||||||
|
("EVO01", null, null, null, null, null),
|
||||||
|
("FLEMING01", null, null, null, null, null),
|
||||||
|
("FLEMING01.24", null, null, null, null, null),
|
||||||
|
("FLEMING01.25", null, null, null, null, null),
|
||||||
|
("FLEMING03", 2023, null, null, null, null),
|
||||||
|
("FLOCK01", 2024, null, null, null, null),
|
||||||
|
("FLOCK01.25", 2024, null, null, null, null),
|
||||||
|
("FLOCK01.26", 2024, null, null, null, null),
|
||||||
|
("FLOCK02", 2024, null, null, null, null),
|
||||||
|
("FLOCK02.25", 2024, null, null, null, null),
|
||||||
|
("FLOCK03", 2026, null, null, null, null),
|
||||||
|
("FLOCK04", 2026, null, null, null, null),
|
||||||
|
("GCP01", null, null, null, null, null),
|
||||||
|
("GO01", 2023, null, null, null, null),
|
||||||
|
("GST01", 2024, null, null, null, null),
|
||||||
|
("ILD01", null, null, null, null, null),
|
||||||
|
("IML06", null, null, null, null, null),
|
||||||
|
("MASAN01", null, null, null, null, null),
|
||||||
|
("MIDEA01", null, "Nhà máy Midea", "Công trình xây dựng tầng lửng Nhà Máy 2 SDA Việt Nam Năm 2025", "Công Ty TNHH Midea Consumer Electric (Vietnam)", "Xưởng A2-1, A2-3, Lô A3, Đường NA2, Khu Công Nghiệp Thới Hòa, Phường Thới Hòa, Thành Phố Hồ Chí Minh"),
|
||||||
|
("NTBA01", 2023, null, null, null, null),
|
||||||
|
("NTBA01.25", 2025, null, null, null, null),
|
||||||
|
("OLAM01", null, null, null, null, null),
|
||||||
|
("OLAM02", null, null, null, null, null),
|
||||||
|
("PMH01", null, null, null, null, null),
|
||||||
|
("REDBULL01", 2021, null, null, null, null),
|
||||||
|
("REDBULL03", 2021, null, null, null, null),
|
||||||
|
("RMIT01", null, null, null, null, null),
|
||||||
|
("SAM01", 2024, "Xây dựng nhà kho SAMHWA-VH", "Thi công chính", "Công ty TNHH SAMHWA-VH CO., LTD", "Lô số b-13, đường d2, Phường Long Đức, Xã An Phước, Huyện Long Thành, Tỉnh Đồng Nai"),
|
||||||
|
("SEW01", 2023, null, null, null, null),
|
||||||
|
("SHEICO01", null, null, null, null, null),
|
||||||
|
("SHEICO01B", null, null, null, null, null),
|
||||||
|
("SHEICO01C", null, null, null, null, null),
|
||||||
|
("SHEICO03", null, null, null, null, null),
|
||||||
|
("SHEICO03.ELEVATOR", null, null, null, null, null),
|
||||||
|
("SHEICO03.MEP", null, null, null, null, null),
|
||||||
|
("SHEICO04", null, null, null, null, null),
|
||||||
|
("SHEICO04.14", null, null, null, null, null),
|
||||||
|
("SHEICO04.15ME", null, null, null, null, null),
|
||||||
|
("SHEICO04.16ME", null, null, null, null, null),
|
||||||
|
("SHEICO04.17ME", null, null, null, null, null),
|
||||||
|
("SHEICO04.9", null, null, null, null, null),
|
||||||
|
("SOL01", null, null, null, null, null),
|
||||||
|
("SOVI01", null, null, null, null, null),
|
||||||
|
("TBA01", null, null, null, null, null),
|
||||||
|
("TLB01", 2023, "Merlion Trelleborg", "Thiết Kế & Thi Công", "Công Ty TNHH Trelleborg Việt Nam", "Lô Số F2, Đường Số 2, Phường Phước Hòa, Thị Xã Phú Mỹ, Tỉnh Bà Rịa - Vũng Tàu"),
|
||||||
|
("TTC01", null, null, null, null, null),
|
||||||
|
("VLU04", null, null, null, null, null),
|
||||||
|
("VLU04.1", null, null, null, null, null),
|
||||||
|
("VP2024", null, null, null, null, null),
|
||||||
|
("VPS01", null, null, null, null, null),
|
||||||
|
("VRR01", null, null, null, null, null),
|
||||||
|
("WFW01", null, null, null, null, null),
|
||||||
|
("WPL01", null, null, null, null, null),
|
||||||
|
("WPL02", null, null, null, null, null),
|
||||||
|
("ZOTE01", 2025, "Zotefoams new manufacturing plant", "Building and building service", "Zotefoams PLC", "Changshin VJ2, Khu Công Nghiệp Lộc An - Bình Sơn, Xã Long Thành, Tỉnh Đồng Nai"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var existingProjectCodes = await db.Projects.Select(p => p.Code).ToListAsync();
|
||||||
|
int addedProjects = 0;
|
||||||
|
foreach (var (code, year, name, package, investor, location) in projects)
|
||||||
|
{
|
||||||
|
if (existingProjectCodes.Contains(code)) continue; // FLOCK01 collision → demo thắng, skip
|
||||||
|
db.Projects.Add(new Project
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Name = string.IsNullOrWhiteSpace(name) ? code : name,
|
||||||
|
Year = year,
|
||||||
|
Package = package,
|
||||||
|
Investor = investor,
|
||||||
|
Location = location,
|
||||||
|
});
|
||||||
|
addedProjects++;
|
||||||
|
}
|
||||||
|
if (addedProjects > 0) logger.LogInformation("Seeded {Count} real projects (S55 import)", addedProjects);
|
||||||
|
|
||||||
|
// ---------- WORKITEMS (71) — (Code, Category, Name) ----------
|
||||||
|
var workItems = new (string Code, string Category, string Name)[]
|
||||||
|
{
|
||||||
|
("VT-01", "Vật tư - Xây dựng", "Bê tông"),
|
||||||
|
("VT-02", "Vật tư - Xây dựng", "Thép cây, Thép lưới hàn"),
|
||||||
|
("VT-03", "Vật tư - Xây dựng", "Cọc ly tâm"),
|
||||||
|
("VT-04", "Vật tư - Xây dựng", "Cống ly tâm"),
|
||||||
|
("VT-05", "Vật tư - Xây dựng", "Cừ tràm"),
|
||||||
|
("VT-06", "Vật tư - Xây dựng", "Kim loại ( rọ đầu cọc, V mạ kẽm, Vinox, bản mã…)"),
|
||||||
|
("VT-07", "Vật tư - Xây dựng", "Nắp gang"),
|
||||||
|
("VT-08", "Vật tư - Xây dựng", "Gạch xây, Xi măng, cát, đá, gạch trồng cỏ"),
|
||||||
|
("VT-09", "Vật tư - Xây dựng", "Gạch trồng cỏ, gạch trang trí…"),
|
||||||
|
("VT-10", "Vật tư - Xây dựng", "Gạch ốp lát"),
|
||||||
|
("VT-11", "Vật tư - Xây dựng", "Ván khuôn"),
|
||||||
|
("VT-12", "Vật tư - Xây dựng", "Sơn nước"),
|
||||||
|
("VT-13", "Vật tư - Xây dựng", "Phụ gia (sika, vinkem, Gps…)"),
|
||||||
|
("VT-14", "Vật tư - Xây dựng", "PVC, vải địa"),
|
||||||
|
("VT-15", "Vật tư - Xây dựng", "Nẹp inox, nẹp nhựa"),
|
||||||
|
("VT-16", "Vật tư - Xây dựng", "Khác"),
|
||||||
|
("TP-01", "Thầu phụ - Xây dựng", "Thầu phụ cọc ( cơ, robot)"),
|
||||||
|
("TP-02", "Thầu phụ - Xây dựng", "Thầu phụ cọc Khoan nhồi"),
|
||||||
|
("TP-03", "Thầu phụ - Xây dựng", "Thầu phụ cọc CDM"),
|
||||||
|
("TP-04", "Thầu phụ - Xây dựng", "Kết cấu thép"),
|
||||||
|
("TP-05", "Thầu phụ - Xây dựng", "Cáp dự ứng lực"),
|
||||||
|
("TP-06", "Thầu phụ - Xây dựng", "Panel"),
|
||||||
|
("TP-07", "Thầu phụ - Xây dựng", "Nhôm kính, alu"),
|
||||||
|
("TP-08", "Thầu phụ - Xây dựng", "Cửa thép, Cửa cuốn, sectional door"),
|
||||||
|
("TP-09", "Thầu phụ - Xây dựng", "Cửa gỗ"),
|
||||||
|
("TP-10", "Thầu phụ - Xây dựng", "Cơ khí hoàn thiện lan can, cầu thang"),
|
||||||
|
("TP-11", "Thầu phụ - Xây dựng", "Dây cứu sinh"),
|
||||||
|
("TP-12", "Thầu phụ - Xây dựng", "Nội thất"),
|
||||||
|
("TP-13", "Thầu phụ - Xây dựng", "Vách trần thạch cao"),
|
||||||
|
("TP-14", "Thầu phụ - Xây dựng", "Vách ngăn di động"),
|
||||||
|
("TP-15", "Thầu phụ - Xây dựng", "Vách ngăn vệ sinh"),
|
||||||
|
("TP-16", "Thầu phụ - Xây dựng", "Granit"),
|
||||||
|
("TP-17", "Thầu phụ - Xây dựng", "Hoàn thiện sàn thảm, vinyl"),
|
||||||
|
("TP-18", "Thầu phụ - Xây dựng", "Chống mối"),
|
||||||
|
("TP-19", "Thầu phụ - Xây dựng", "Chống thấm"),
|
||||||
|
("TP-20", "Thầu phụ - Xây dựng", "Sơn PU, Epoxy, liquid"),
|
||||||
|
("TP-21", "Thầu phụ - Xây dựng", "Dock Leveler."),
|
||||||
|
("TP-22", "Thầu phụ - Xây dựng", "Cẩu trục"),
|
||||||
|
("TP-23", "Thầu phụ - Xây dựng", "Trạm cân"),
|
||||||
|
("TP-24", "Thầu phụ - Xây dựng", "Thang máy, thang hàng, …"),
|
||||||
|
("TP-25", "Thầu phụ - Xây dựng", "Sàn nâng."),
|
||||||
|
("TP-26", "Thầu phụ - Xây dựng", "Landscape"),
|
||||||
|
("TP-27", "Thầu phụ - Xây dựng", "Cổng xếp."),
|
||||||
|
("TP-28", "Thầu phụ - Xây dựng", "Logo, Bảng hiệu."),
|
||||||
|
("TP-29", "Thầu phụ - Xây dựng", "TP thi công Màng nhựa"),
|
||||||
|
("TP-30", "Thầu phụ - Xây dựng", "Khác"),
|
||||||
|
("MEP-01", "MEP", "MEP (Full)"),
|
||||||
|
("MEP-02", "MEP", "Hệ thống trung thế"),
|
||||||
|
("MEP-03", "MEP", "PCCC (Phòng cháy chữa cháy)."),
|
||||||
|
("MEP-04", "MEP", "Hệ thống điện, điện nhẹ"),
|
||||||
|
("MEP-05", "MEP", "Hệ thống HVAC, Utility (Thông gió & Điều hòa không khí & Phù trợ)."),
|
||||||
|
("MEP-06", "MEP", "Hệ thống cấp thoát nước"),
|
||||||
|
("MEP-07", "MEP", "Hệ thống xử lý nước thải"),
|
||||||
|
("MEP-08", "MEP", "Hệ thống solar"),
|
||||||
|
("MEP-09", "MEP", "Khác"),
|
||||||
|
("TB-01", "Thiết bị", "Máy biến áp"),
|
||||||
|
("TB-02", "Thiết bị", "Tủ điện"),
|
||||||
|
("TB-03", "Thiết bị", "Tủ trung thế."),
|
||||||
|
("TB-04", "Thiết bị", "Busway"),
|
||||||
|
("TB-05", "Thiết bị", "Cáp điện"),
|
||||||
|
("TB-06", "Thiết bị", "Thiết bị Utility (Máy nén khí, Hút bụi hút mùi, Boiler,...)"),
|
||||||
|
("TB-07", "Thiết bị", "Máy lạnh"),
|
||||||
|
("TB-08", "Thiết bị", "Ống thép, inox"),
|
||||||
|
("TB-09", "Thiết bị", "Thang máng cáp"),
|
||||||
|
("TB-10", "Thiết bị", "Ống PVC, HDPE"),
|
||||||
|
("TB-11", "Thiết bị", "Thiết bị báo cháy"),
|
||||||
|
("TB-12", "Thiết bị", "Đèn (đèn chiếu sáng, đèn năng lượng, đèn exit….)"),
|
||||||
|
("TB-13", "Thiết bị", "Thiết bị chữa cháy (Sprinkler, Chữa cháy khí, FM200,…)"),
|
||||||
|
("TB-14", "Thiết bị", "Bơm (bơm PCCC, Bơm nước…)"),
|
||||||
|
("TB-15", "Thiết bị", "Bồn nước, Thiết bị vệ sinh"),
|
||||||
|
("TB-16", "Thiết bị", "Khác"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var existingWorkItemCodes = await db.WorkItems.Select(w => w.Code).ToListAsync();
|
||||||
|
int addedWorkItems = 0;
|
||||||
|
foreach (var (code, category, name) in workItems)
|
||||||
|
{
|
||||||
|
if (existingWorkItemCodes.Contains(code)) continue;
|
||||||
|
db.WorkItems.Add(new WorkItem { Code = code, Name = name, Category = category, IsActive = true });
|
||||||
|
addedWorkItems++;
|
||||||
|
}
|
||||||
|
if (addedWorkItems > 0) logger.LogInformation("Seeded {Count} real work items (S55 import)", addedWorkItems);
|
||||||
|
|
||||||
|
// ---------- SUPPLIERS (3) — (Code, Name, Type, TaxCode, Phone, Email, Address, ContactPerson, Note) ----------
|
||||||
|
var suppliers = new[]
|
||||||
|
{
|
||||||
|
new Supplier { Code = "TRUONGGIANG", Name = "CÔNG TY CỔ PHẦN NHÔM KÍNH VÀ ĐẦU TƯ TRƯỜNG GIANG", Type = SupplierType.NhaThauPhu, TaxCode = "0312 251 859", Phone = "08 2253 1381", Email = "viethq1985@gmail.com", Address = "Số 77/46A Đường Chuyên Dùng Chính, Phường Phú Thuận, TP.HCM", ContactPerson = "Mr. Việt", Note = "Loại: Nhôm kính, alu; NĐD: Ông: HOÀNG QUỐC VIỆT - GIÁM ĐỐC ĐIỀU HÀNH; TK: 100212127 Ngân hàng Eximbank - Chi Nhánh Hòa Bình; Trạng thái: ✅ Đang hoạt động" },
|
||||||
|
new Supplier { Code = "TANPHU", Name = "CÔNG TY TNHH SẢN XUẤT THƯƠNG MẠI SƠN TÂN PHÚ", Type = SupplierType.NhaThauPhu, TaxCode = "0305497393", Phone = "0902 686468", Email = "thu@tanphupaint.com", Address = "Số 49/5A Đường số 7, Khu phố 53, P. Linh Xuân, Thành phố Hồ Chí Minh, Việt Nam", ContactPerson = "Ms Thư", Note = "Loại: Sơn PU, Epoxy, liquid; NĐD: Ông ĐINH PHƯỚC THỌ - GIÁM ĐỐC; TK: 8683107979 tại Ngân hàng BIDV - CN Nam Bình Dương; Trạng thái: ✅ Đang hoạt động" },
|
||||||
|
new Supplier { Code = "TGN", Name = "CÔNG TY CỔ PHẦN SIÊU THỊ VLXD THẾ GIỚI NHÀ", Type = SupplierType.NhaCungCap, TaxCode = "3603497972", Phone = "028 36206000", Email = "duong.ltt@tgngroup.vn", Address = "46-48 Nguyễn Cơ Thạch, Phường An Khánh, Thành phố Hồ Chí Minh,Việt Nam", ContactPerson = "Ms. Dương", Note = "Loại: Bê tông; NĐD: Ông TRẦN ANH ĐIỀN - TỔNG GIÁM ĐỐC; TK: 114002641307 tại VietinBank CN KCN Biên Hòa; Trạng thái: ✅ Đang hoạt động" },
|
||||||
|
};
|
||||||
|
|
||||||
|
var existingSupplierCodes = await db.Suppliers.Select(s => s.Code).ToListAsync();
|
||||||
|
int addedSuppliers = 0;
|
||||||
|
foreach (var s in suppliers)
|
||||||
|
{
|
||||||
|
if (existingSupplierCodes.Contains(s.Code)) continue;
|
||||||
|
db.Suppliers.Add(s);
|
||||||
|
addedSuppliers++;
|
||||||
|
}
|
||||||
|
if (addedSuppliers > 0) logger.LogInformation("Seeded {Count} real suppliers (S55 import)", addedSuppliers);
|
||||||
|
|
||||||
|
if (addedProjects + addedWorkItems + addedSuppliers > 0)
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task SeedContractTemplatesAsync(ApplicationDbContext db, ILogger logger)
|
private static async Task SeedContractTemplatesAsync(ApplicationDbContext db, ILogger logger)
|
||||||
{
|
{
|
||||||
// Chỉ IsActive=true nếu file thực tế tồn tại trong wwwroot/templates/.
|
// Chỉ IsActive=true nếu file thực tế tồn tại trong wwwroot/templates/.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddProjectMasterFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Investor",
|
||||||
|
table: "Projects",
|
||||||
|
type: "nvarchar(250)",
|
||||||
|
maxLength: 250,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Location",
|
||||||
|
table: "Projects",
|
||||||
|
type: "nvarchar(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Package",
|
||||||
|
table: "Projects",
|
||||||
|
type: "nvarchar(300)",
|
||||||
|
maxLength: 300,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Year",
|
||||||
|
table: "Projects",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Investor",
|
||||||
|
table: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Location",
|
||||||
|
table: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Package",
|
||||||
|
table: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Year",
|
||||||
|
table: "Projects");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3484,9 +3484,17 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<DateTime?>("EndDate")
|
b.Property<DateTime?>("EndDate")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Investor")
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Location")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
b.Property<Guid?>("ManagerUserId")
|
b.Property<Guid?>("ManagerUserId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
@ -3499,6 +3507,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.HasMaxLength(1000)
|
.HasMaxLength(1000)
|
||||||
.HasColumnType("nvarchar(1000)");
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Package")
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("nvarchar(300)");
|
||||||
|
|
||||||
b.Property<DateTime?>("StartDate")
|
b.Property<DateTime?>("StartDate")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
@ -3508,6 +3520,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<Guid?>("UpdatedBy")
|
b.Property<Guid?>("UpdatedBy")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int?>("Year")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("Code")
|
b.HasIndex("Code")
|
||||||
|
|||||||
@ -91,7 +91,7 @@ public class MasterCatalogFilteredUniqueTests
|
|||||||
await db.SaveChangesAsync(CancellationToken.None);
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
|
|
||||||
var act = async () => await new CreateProjectCommandHandler(db)
|
var act = async () => await new CreateProjectCommandHandler(db)
|
||||||
.Handle(new CreateProjectCommand("DUP1", "Dự án mới", null, null, null, null, null),
|
.Handle(new CreateProjectCommand("DUP1", "Dự án mới", null, null, null, null, null, null, null, null, null),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
await act.Should().NotThrowAsync(
|
await act.Should().NotThrowAsync(
|
||||||
|
|||||||
Reference in New Issue
Block a user