[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

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:
pqhuy1987
2026-06-09 09:27:04 +07:00
parent f8640d6f18
commit 69cb3937bb
17 changed files with 7069 additions and 9 deletions

View File

@ -74,6 +74,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i
## 📅 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 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]`.

View File

@ -42,11 +42,11 @@ Dynamic class purged. PALETTE array full literal `as const` cycle `index % lengt
## 📅 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 (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 (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)

View File

@ -70,6 +70,8 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 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 (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]`.

View File

@ -57,6 +57,8 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
## 📅 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 (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].

View File

@ -23,9 +23,13 @@ type FormState = {
endDate: string
budgetTotal: 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 fmtDate = (s: string | null) => (s ? new Date(s).toLocaleDateString('vi-VN') : '—')
@ -61,6 +65,10 @@ export function ProjectsPage() {
managerUserId: null,
budgetTotal: d.budgetTotal ? Number(d.budgetTotal) : 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)
else await api.post('/projects', payload)
@ -92,6 +100,10 @@ export function ProjectsPage() {
endDate: p.endDate ? p.endDate.slice(0, 10) : '',
budgetTotal: p.budgetTotal?.toString() ?? '',
note: p.note ?? '',
year: p.year?.toString() ?? '',
investor: p.investor ?? '',
location: p.location ?? '',
package: p.package ?? '',
})
setOpen(true)
}
@ -99,6 +111,7 @@ export function ProjectsPage() {
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: '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: '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) },
@ -203,6 +216,22 @@ export function ProjectsPage() {
<Label>Ngày kết thúc</Label>
<Input type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} />
</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 </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">
<Label>Ghi chú</Label>
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />

View File

@ -52,6 +52,10 @@ export type Project = {
managerUserId: string | null
budgetTotal: number | null
note: string | null
year: number | null
investor: string | null
location: string | null
package: string | null
createdAt: string
updatedAt: string | null
}

View File

@ -23,9 +23,13 @@ type FormState = {
endDate: string
budgetTotal: 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 fmtDate = (s: string | null) => (s ? new Date(s).toLocaleDateString('vi-VN') : '—')
@ -61,6 +65,10 @@ export function ProjectsPage() {
managerUserId: null,
budgetTotal: d.budgetTotal ? Number(d.budgetTotal) : 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)
else await api.post('/projects', payload)
@ -92,6 +100,10 @@ export function ProjectsPage() {
endDate: p.endDate ? p.endDate.slice(0, 10) : '',
budgetTotal: p.budgetTotal?.toString() ?? '',
note: p.note ?? '',
year: p.year?.toString() ?? '',
investor: p.investor ?? '',
location: p.location ?? '',
package: p.package ?? '',
})
setOpen(true)
}
@ -99,6 +111,7 @@ export function ProjectsPage() {
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: '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: '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) },
@ -203,6 +216,22 @@ export function ProjectsPage() {
<Label>Ngày kết thúc</Label>
<Input type="date" value={form.endDate} onChange={e => setForm({ ...form, endDate: e.target.value })} />
</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 </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">
<Label>Ghi chú</Label>
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />

View File

@ -52,6 +52,10 @@ export type Project = {
managerUserId: string | null
budgetTotal: number | null
note: string | null
year: number | null
investor: string | null
location: string | null
package: string | null
createdAt: string
updatedAt: string | null
}

View 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

View File

@ -18,7 +18,11 @@ public record ProjectDto(
decimal? BudgetTotal,
string? Note,
DateTime CreatedAt,
DateTime? UpdatedAt);
DateTime? UpdatedAt,
int? Year,
string? Investor,
string? Location,
string? Package);
// ===================== LIST =====================
public record ListProjectsQuery : PagedRequest, IRequest<PagedResult<ProjectDto>>;
@ -44,7 +48,7 @@ public class ListProjectsQueryHandler(IApplicationDbContext db) : IRequestHandle
var total = await query.CountAsync(ct);
var items = await query
.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);
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)
?? 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 =====================
public record CreateProjectCommand(
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>
{
@ -75,6 +80,9 @@ public class CreateProjectCommandValidator : AbstractValidator<CreateProjectComm
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
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)
.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,
StartDate = request.StartDate, EndDate = request.EndDate,
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);
await db.SaveChangesAsync(ct);
@ -101,7 +110,8 @@ public class CreateProjectCommandHandler(IApplicationDbContext db) : IRequestHan
// ===================== UPDATE =====================
public record UpdateProjectCommand(
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>
{
@ -111,6 +121,9 @@ public class UpdateProjectCommandValidator : AbstractValidator<UpdateProjectComm
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
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)
.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.BudgetTotal = request.BudgetTotal;
entity.Note = request.Note;
entity.Year = request.Year;
entity.Investor = request.Investor;
entity.Location = request.Location;
entity.Package = request.Package;
await db.SaveChangesAsync(ct);
}
}

View File

@ -11,4 +11,8 @@ public class Project : AuditableEntity
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 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
}

View File

@ -15,6 +15,9 @@ public class ProjectConfiguration : IEntityTypeConfiguration<Project>
b.Property(x => x.Name).HasMaxLength(200).IsRequired();
b.Property(x => x.BudgetTotal).HasPrecision(18, 2);
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

View File

@ -113,6 +113,9 @@ public static class DbInitializer
await SeedPurchaseEvaluationWorkflowsAsync(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).
// Idempotent: chỉ HĐ MaHopDong IS NULL được gen.
@ -2219,6 +2222,213 @@ public static class DbInitializer
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)
{
// Chỉ IsActive=true nếu file thực tế tồn tại trong wwwroot/templates/.

View File

@ -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");
}
}
}

View File

@ -3484,9 +3484,17 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime2");
b.Property<string>("Investor")
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Location")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
@ -3499,6 +3507,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Package")
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime2");
@ -3508,6 +3520,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int?>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Code")

View File

@ -91,7 +91,7 @@ public class MasterCatalogFilteredUniqueTests
await db.SaveChangesAsync(CancellationToken.None);
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);
await act.Should().NotThrowAsync(