From 82d7fcff4de0226cf0eaeeb1b5444292cd702fc6 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sat, 30 May 2026 11:10:44 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Workflow:=20LeaveBalance=20business?= =?UTF-8?q?=20logic=20=E2=80=94=20tr=E1=BB=AB=20ph=C3=A9p=20khi=20duy?= =?UTF-8?q?=E1=BB=87t=20+=20s=E1=BB=91=20d=C6=B0=20(Phase=2011=20P11-B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Số dư phép theo (User × LeaveType × Year) + trừ tự động khi đơn nghỉ duyệt cuối. Policy: cho phép vượt số dư (âm) + cảnh báo (anh main chốt), tích hợp vào trang đơn nghỉ. Schema (Mig 42 AddLeaveBalances — pure additive, 1 bảng): - LeaveBalance: UserId + LeaveTypeId + Year + EntitledDays + UsedDays + AdjustmentDays. UNIQUE (UserId,LeaveTypeId,Year), FK LeaveType Restrict, decimal(5,2). Remaining = Entitled + Adjustment − Used (computed, không store). Deduction hook (ApproveLeaveRequestHandler nhánh terminal DaDuyet — exactly-once): - Upsert LeaveBalance(RequesterUserId, LeaveTypeId, StartDate.Year), auto-create từ LeaveType.DaysPerYear, UsedDays += NumDays. Guard Status!=DaGuiDuyet chặn re-approve. FK invariant guard (em main thêm sau test reveal FK risk): - Create + UpdateDraft validate LeaveTypeId tồn tại (AnyAsync) → ConflictException. Đóng cửa vào — bogus type không thể tới deduction FK insert (tránh 500 kẹt đơn). CQRS LeaveBalanceFeatures.cs: GetMy (self, lazy merge active LeaveType) + GetUser (admin) + AdjustLeaveBalance (admin upsert carry-over). Controller [Authorize] + admin Roles=Admin. Embed: GetLeaveRequestByIdHandler trả balance NGƯỜI TẠO (approver xem thấy đúng). FE: WorkflowAppDetailPage ×2 — block "Số dư phép" + cảnh báo vượt khi kind=leave (SHA256 identical). Tests (+11, 130→154 PASS): deduction single/multi-level/accumulate/negative-allowed/ reject-return-no-deduct + lazy-merge + adjust upsert + Create guard bogus→Conflict. Cũng repair 2 test S42 terminal FK-fail (template BuildLeave +seed LeaveType). Verify: build 0 error · 154 test · FE ×2 · reviewer Max PASS (deduction exactly-once + FK invariant fully closed, 2 minor concurrency/comment defer). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../implementer-backend/MEMORY.md | 3 +- .../implementer-frontend/MEMORY.md | 3 +- .claude/agent-memory/reviewer/MEMORY.md | 1 + .../agent-memory/test-specialist/MEMORY.md | 7 +- .../pages/office/WorkflowAppDetailPage.tsx | 27 + fe-admin/src/types/workflowApps.ts | 4 + .../pages/office/WorkflowAppDetailPage.tsx | 27 + fe-user/src/types/workflowApps.ts | 4 + .../Controllers/LeaveBalancesController.cs | 42 + .../Interfaces/IApplicationDbContext.cs | 3 + .../Hrm/LeaveBalanceFeatures.cs | 150 + .../Office/LeaveOtApprovalFeatures.cs | 65 +- .../Office/WorkflowAppsFeatures.cs | 4 + .../SolutionErp.Domain/Hrm/LeaveBalance.cs | 27 + .../Persistence/ApplicationDbContext.cs | 3 + .../LeaveBalanceConfiguration.cs | 29 + ...0260530034336_AddLeaveBalances.Designer.cs | 6373 +++++++++++++++++ .../20260530034336_AddLeaveBalances.cs | 68 + .../ApplicationDbContextModelSnapshot.cs | 71 + .../Application/LeaveBalanceTests.cs | 424 ++ .../Application/WorkflowAppApproveV2Tests.cs | 31 +- 21 files changed, 7356 insertions(+), 10 deletions(-) create mode 100644 src/Backend/SolutionErp.Api/Controllers/LeaveBalancesController.cs create mode 100644 src/Backend/SolutionErp.Application/Hrm/LeaveBalanceFeatures.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/LeaveBalance.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/LeaveBalanceConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/LeaveBalanceTests.cs diff --git a/.claude/agent-memory/implementer-backend/MEMORY.md b/.claude/agent-memory/implementer-backend/MEMORY.md index 89ce86a..9f0cb71 100644 --- a/.claude/agent-memory/implementer-backend/MEMORY.md +++ b/.claude/agent-memory/implementer-backend/MEMORY.md @@ -66,7 +66,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i ## 🧠 SOLUTION_ERP BE conventions (S40) - **BE .NET 10:** PascalCase entities + DTO records + command names. CQRS+MediatR+FluentValidation+AutoMapper. Repository qua `IApplicationDbContext`. `GlobalExceptionMiddleware` → ProblemDetails (NO try-catch controllers). -- **State S41:** 41 mig (last `WireWorkflowAppsApprovalV2`) · 89 SQL tables · ~211 endpoints · 130 test baseline (test-specialist owns). Phase 9 UAT skip per chunk (`feedback_uat_skip_verify`). +- **State S43:** 42 mig (last `AddLeaveBalances`) · 90 SQL tables · ~214 endpoints · 130 test baseline (test-specialist owns). Phase 9 UAT skip per chunk (`feedback_uat_skip_verify`). - **Build:** `dotnet build SolutionErp.slnx` clean 0 err. Commit `[CLAUDE] : ` + Co-Authored-By Claude Opus 4.8 (1M context). - **Pin (KHÔNG `*`/latest):** MediatR `12.4.1` (14 fail DI) · Swashbuckle `6.9.0` · Node CI `20.x` · LibreOffice `25.8.6` · @microsoft/signalr `8.0.7`. @@ -74,6 +74,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i ## 📅 Recent activity (FIFO — older → archive/git) +- **S43 P11-B Wave 1 — LeaveBalance business logic (Mig 42 `AddLeaveBalances`, 7 file: 1 entity + 1 config + 2 DbSet edit + Mig 3-file + 1 hook edit + 1 Features + 1 Controller):** Case 1/3 deterministic ~98% em main spec. Pattern 12-ter-adjacent single-entity: entity `LeaveBalance:AuditableEntity` (UserId/LeaveTypeId/Year + EntitledDays/UsedDays/AdjustmentDays decimal(5,2), nav LeaveType). Config FK LeaveType WithMany() **Restrict** (catalog no cascade) + UNIQUE composite (UserId,LeaveTypeId,Year) + IX UserId. Mig diff CLEAN: 1 CreateTable + 3 IX, no drift. Applied BOTH DB (Dev `SolutionErp_Dev` + Design default). **Deduction hook:** insert in `ApproveLeaveRequestHandler` terminal else (DaDuyet branch) ONLY — UPSERT LeaveBalance, `bal.UsedDays += p.NumDays`, exactly-once guaranteed by early guard `Status != DaGuiDuyet throw`. OtRequest/Travel/Vehicle UNTOUCHED (only Leave has balance). CQRS `Application.Hrm`: DTO RemainingDays=Entitled+Adjustment−Used COMPUTED (not stored) + GetMy/GetUser lazy-merge (load active LeaveTypes + balances → in-memory merge, synth default when no row — KHÔNG EF LEFT JOIN translate) + AdjustLeaveBalanceCommand admin upsert (HasValue-gated). **Policy resolved:** HRM admin convention = `[Authorize(Roles="Admin")]` NOT menu policy (verified HrmConfigsController write endpoints) — used on GET-by-user + PUT /adjust; /my = `[Authorize]`. Controller injects IDateTime for year default `clock.Now.Year` (thin, no DateTime.Now hardcode). HRM no HasQueryFilter → `.Where(!IsDeleted)` manual everywhere. KHÔNG touch FE/test/commit. Build 0 err (2 pre-existing DocxRenderer warn). Tag `[s43, p11-b-w1, mig42, leave-balance, single-entity]`. - **S42 P11-A SEED — 4 sample ApprovalWorkflow V2 for WorkflowApps (DbInitializer.cs only, +4 method ~210 LOC + 4 call):** Case 1 mechanical mirror `SeedSampleProposalWorkflowV2Async` EXACT × 4 (Leave5/Ot6/Travel9/Vehicle7). Each: idempotent `AnyAsync(ApplicableType==X)` guard → resolve approver `binh.le@solutions.com.vn` (SAME user as Proposal/Contract seed, null→LogWarning+return) → 1 ApprovalWorkflow (Version=1, IsActive+IsUserSelectable=true, ActivatedAt=UtcNow) + 1 Step (Order=1, Name="Cấp duyệt", DepartmentId=CCM?.Id) + 1 Level (Order=1, ApproverUserId). Codes QT-NP/OT/CT/XE-V2-001. Wired 4 calls after SeedSampleProposalWorkflowV2Async (NOT gated DemoSeed, gotcha #51 infra seed). Enum verified Grep. Build 0 err 0 warn. Bash tool = bash NOT PowerShell despite env hint (use `cd && cmd | grep`). Spec deterministic 100% → ACCEPT Case 1. NOT touched App/Controller/FE/test/Mig. Tag `[s42, p11-a, seed, mirror-proposal-exact]`. - **S42 P11-A Wave 2b APP — wire ApproveV2 CQRS Travel+Vehicle (`TravelVehicleApprovalFeatures.cs` ~830 LOC + 2 controller edit):** Cookie-cutter mirror Wave 2a / ProposalFeatures Region 2. 1 new file ns `Application.Office`: 2 module × (DetailDto + LevelOpinionDto + GetById JOIN Step/Level metadata + UpdateDraft + Submit + Approve UPSERT+advance + Reject TuChoi + Return TraLai+RejectedFromStatus) + 1 shared `internal static TravelVehicleCodeGen.GenerateMaDonTuAsync` (Serializable tx, `WorkflowAppCodeSequences` Prefix-keyed, prefix `DT/CT/{year}` Travel & `DX/XE/{year}` Vehicle, format `{prefix}/{seq:D3}` — D3 no year segment per spec). KEY gotcha: WorkflowAppStatus enum DIFFERS from ProposalStatus int values (DaGuiDuyet=2 not 1, TraLai=3) → mirror by SEMANTIC enum member not literal. Owner = `RequesterUserId` (not DrafterUserId). Submit verify wf.ApplicableType==Travel9/Vehicle7 else Conflict. 2 controller +6 route each (GET{id}/PUT/submit/approve/reject/return) nested body records, CreatedAtAction. KHÔNG sửa WorkflowAppsFeatures.cs/Leave/Ot/FE/test/seed. Build 0 err. Spec deterministic ~98% em main → ACCEPT Case 1/2. Tag `[s42, p11-a, wave-2b, mirror-proposal-region2]`. - **S42 P11-A Wave 2a APP — wire ApproveV2 CQRS Leave+Ot (`LeaveOtApprovalFeatures.cs` ~770 LOC + 2 controller edit):** Pattern 4 (UPSERT in Approve, 0 opinion endpoint) + cookie-cutter mirror ProposalFeatures Region 2. 1 new file ns `Application.Office`: 2 module × (DetailDto + LevelOpinionDto + GetById JOIN Step/Level metadata + UpdateDraft + Submit + Approve UPSERT+advance + Reject TuChoi + Return TraLai+RejectedFromStatus) + 1 shared `internal static WorkflowAppCodeGen.GenerateMaDonTuAsync` (Serializable tx + `WorkflowAppCodeSequences` Prefix-keyed, prefix DT/LR & DT/OT, format `{prefix}/{year}/{seq:D3}`). Approve: flatten allLevels OrderBy Step→Level, currentSlot=allLevels[order-1], actor==ApproverUserId OR Admin, comment empty→placeholder, advance OR terminal DaDuyet. Submit verify wf.ApplicableType==Leave5/Ot6 else Conflict + gen MaDonTu nếu null. 2 controller +6 route each (GET{id}/PUT/submit/approve/reject/return) mirror ProposalsController nested body records (`WorkflowActionBody`). KHÔNG sửa WorkflowAppsFeatures.cs (Region 1 Create/List ở đó). Build 0 err (2 warn DocxRenderer pre-existing). Spec deterministic 100% em main → ACCEPT Case 1. Travel/Vehicle (Wave 2b) + test (Wave 4) deferred. Tag `[s42, p11-a, wave-2a, mirror-proposal-region2]`. diff --git a/.claude/agent-memory/implementer-frontend/MEMORY.md b/.claude/agent-memory/implementer-frontend/MEMORY.md index 4f06549..d92995e 100644 --- a/.claude/agent-memory/implementer-frontend/MEMORY.md +++ b/.claude/agent-memory/implementer-frontend/MEMORY.md @@ -42,7 +42,8 @@ Dynamic class purged. PALETTE array full literal `as const` cycle `index % lengt ## 📅 Recent activity (last 10 FIFO) -- **2026-05-29 (S39 agent split setup):** NEW agent từ split implementer. Seeded FE patterns (16-bis 9× + SHA256 mirror + KIND_CONFIG + Tailwind palette + PageHeader S37). Prior FE work absorbed: S33 EmployeesListPage + S34 Directory + S35 HrmConfigs declarative + S36 MeetingCalendar + S37 Proposal + S38 WorkflowApps generic. First dedicated spawn pending em main S39+ FE task. +- **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 {...})()` cho conditional block có derived vars — sạch hơn tách helper. +- **2026-05-29 (S39 agent split setup):** NEW agent từ split implementer. Seeded FE patterns (16-bis 9× + SHA256 mirror + KIND_CONFIG + Tailwind palette + PageHeader S37). Prior FE work absorbed: S33 EmployeesListPage + S34 Directory + S35 HrmConfigs declarative + S36 MeetingCalendar + S37 Proposal + S38 WorkflowApps generic. --- diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md index 76f1aa9..c301cbd 100644 --- a/.claude/agent-memory/reviewer/MEMORY.md +++ b/.claude/agent-memory/reviewer/MEMORY.md @@ -57,6 +57,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod ## 📅 Recent activity (FIFO — older → archive/git) +- **2026-05-30 (S43 P11-B LeaveBalance pre-commit — PASS, Max no-truncate):** 14 file (LeaveBalance entity+config+Mig42 + Features + Controller + deduction hook + Create/Update LeaveType guard + embed balance + FE×4 + tests). 154 PASS (130→154). **Deduction exactly-once VERIFIED** (terminal else only, guard Status!=DaGuiDuyet chặn re-approve; advance/reject/return no-deduct). **FK invariant fully closed** — grep 2 write site LeaveTypeId (Create + UpdateDraft) cả 2 guard AnyAsync→Conflict, bogus type không thể tới terminal FK insert. Embed balance = RequesterUserId (approver thấy đúng người tạo). admin `[Authorize(Roles=Admin)]`. **2 MINOR defer:** concurrency lost-update UsedDays (no RowVersion — human-sequential accept) · stale line-num comment. Verdict PASS. Tag `[s43, p11b-leavebalance, max-clean]`. - **2026-05-28 (S35 G-H2 BE CRUD 16 endpoint pre-commit — PASS, Smart Friend 8× CLEAN):** 2 NEW file `HrmConfigFeatures.cs` 439 + Controller 137. build clean, 130/130 PASS. Cat1: 0 mock, 8 ConflictException (Holiday Update composite `(Year,Date)` BOTH fields). Cat3: class `[Authorize]` + 12 per-action `[Authorize(Roles="Admin")]`. Cat5: 8 Validator MaxLength MATCH EF source (Code=50 not spec 20). **2 MINOR defer:** ListHolidays no IsActive filter (inconsistent sibling) · OtPolicy "1 active unique" NOT enforced handler (G-P1 ambiguous nếu 2+ active). Verdict PASS. Tag `[s35, smart-friend-8x-clean]`. - **2026-05-26 (S33 Plan B G-H1 Phase 2 pre-commit — PASS, Smart Friend 6× CLEAN):** 17 file (3 BE + 6 FE new + 6 mod + 2). SHA256 mirror 3 file IDENTICAL admin==user. 5 endpoint real mediator.Send 0 mock. Mig 34 `AddEmployeeProfiles` 7 table UNIQUE indexes + FK Cascade. SeedDemoEmployeeProfiles NOT gated DemoSeed (gotcha #51 ✓). gotcha #50 Layout staticMap mirror ✓. **3 MINOR defer:** EmployeeCode race SERIALIZABLE low-risk · Update 3 bool not nullable (partial reset) · Delete DateTime.UtcNow direct. Verdict PASS. Tag `[s33, hrm-mig34, smart-friend-6x]`. - **Smart Friend cumulative 8× CLEAN:** (1) S22 #44 silent-403 · (2) S25 #48 SQLite tie-break · (3) S29 password ≥12 · (4) S29 ApplicableType cross-module · (5) S33 BW test · (6) S33 Plan B Phase 2 · (7) S35 FE forms · (8) S35 G-H2. Plus 9× G-O2 (S36, em không track ở đây). 2 MAJOR catches total (S29 password + S29 ApplicableType); rest clean với MINOR defer. diff --git a/.claude/agent-memory/test-specialist/MEMORY.md b/.claude/agent-memory/test-specialist/MEMORY.md index d1c6590..97abf76 100644 --- a/.claude/agent-memory/test-specialist/MEMORY.md +++ b/.claude/agent-memory/test-specialist/MEMORY.md @@ -15,9 +15,12 @@ WRITE specialist độc quyền `tests/**`. xUnit + FluentAssertions 7.2 + EF SQ - ❌ NOT: production code `src/Backend/**` + `fe-*/**` → test reveal bug → REPORT em main, KHÔNG fix - ❌ NOT: decide WHAT to test (test plan) → em main + reviewer chốt priority -## 📊 Baseline 141 PASS (58 Domain + 83 Infra) ← S42 +11 WorkflowApp ApproveV2 +## 📊 Baseline 152 PASS (58 Domain + 94 Infra) ← S43 +8 LeaveBalance + repaired 2 template terminal FK-fail Run: `dotnet test SolutionErp.slnx --nologo --verbosity minimal` +### ⚠️ Pattern: deduction hook FK → seed LeaveType cho terminal test (S43) +LeaveBalance → LeaveType `Restrict` FK. ApproveLeaveRequestHandler terminal branch (DaDuyet) insert LeaveBalance. Test đi tới DaDuyet PHẢI seed 1 LeaveType row + LeaveRequest.LeaveTypeId = type.Id (KHÔNG random Guid → FK fail SQLite Error 19). Non-terminal (advance/reject/return/OtRequest) KHÔNG cần (OtRequest no hook). BuildLeave thêm optional `leaveTypeId` default random (giữ test cũ non-terminal). Year = StartDate.Year. Negative allowed (no quota guard → Remaining<0 OK). Query lazy synth Entitled=DaysPerYear khi 0 row. + ## ⏱️ Timing rules (docs/rules.md §7) - Feature mới = test-after (UAT ổn → viết, Phase 9 skip per `feedback_uat_skip_verify`) - Bug fix = test-before BẮT BUỘC (reproduce → fix) @@ -49,9 +52,9 @@ Test theo CODE (single source truth), document mismatch header comment + report. ## 📅 Recent activity (last 10 FIFO) -- **2026-05-29 (S39 agent split setup):** NEW dedicated agent. Seeded test patterns (10 reflection authz + 11 infra helper + 12 InternalsVisibleTo + #48 SQLite tie-break + spec drift S34). Inherited coverage gap backlog 4 priority items từ S36 Reviewer audit (130 PASS baseline). First spawn pending em main S39+ test bundle task (recommend Gap 1 Holiday composite UNIQUE first). - **2026-05-29 (S40 baseline audit smoke):** CONFIRMED 130 PASS (Domain 58 + Infra 72), 0 fail/skip, ~15s. Runner count authoritative; raw `[Fact]/[Theory]` attr = 48+70 (Theory→InlineData expand). Infra spread 15 files. Gap re-verified vs prod: EmployeesController+HrmConfigsController EXIST, authz regression chỉ ApprovalWorkflowsV2Controller (gotcha #44 gap real). Proposal = Domain entity + EF config only, CHƯA có ApproveV2Async service (S37 skeleton, defer đúng). Agent load OK. AUDIT-only, no write. - **2026-05-30 (S42 P11-A Wave4):** +11 test `tests/.../Application/WorkflowAppApproveV2Tests.cs` → **141 PASS** (Infra 72→83). LeaveRequest 8 case full (Submit happy/guard×2, Approve advance/terminal/UPSERT-invariant/forbidden/empty-comment-placeholder, Reject→TuChoi, Return→TraLai+RejectedFromStatus) + OtRequest smoke (submit→approve single-level→DaDuyet). **No prod bug** — LeaveOt ApproveV2 wire correct, all PASS first run. **NEW Pattern:** WorkflowApps handlers = CQRS MediatR (KHÔNG service) → instantiate handler trực tiếp `new ApproveLeaveRequestHandler(db, AsUser(u), clock).Handle(cmd,ct)`, chỉ 3 dep (IApplicationDbContext + TestCurrentUser + FixedDateTime) — nhẹ hơn 6-dep Contract service. MaDonTu format "DT/LR/2026/001". Gap #4 (Workflow Apps) PARTIAL done — Travel/Vehicle mirror pending. ⚠️ Lesson: CWD drift (fe-user) → ghi MEMORY nhầm path, em main relocate. Verify CWD root trước Write memory. +- **2026-05-30 (S43 P11-B Wave3 LeaveBalance):** +8 test `tests/.../Application/LeaveBalanceTests.cs` → **152 PASS** (Infra 86→94). Deduction hook (ApproveLeaveRequestHandler terminal) full: deduct single-level (create row from DaysPerYear), only-at-terminal multi-level (advance no-deduct + 1× terminal), accumulate UPSERT (5+2=7 no new row), negative allowed (Used20>Entitled12 → Remaining−8 no throw), Reject+Return no-deduct (split 5a/5b), GetMyLeaveBalances lazy synth (2 active type filter inactive), AdjustLeaveBalance upsert. **⚠️ FOUND + FIXED 2 pre-existing RED** in S42 template (`Approve_LastLevel_TransitionsToDaDuyet` + `Approve_EmptyComment_StoresPlaceholder`): Wave 1 deduction hook (uncommitted, prod) làm terminal insert LeaveBalance FK→LeaveTypes Restrict FAIL vì BuildLeave dùng `LeaveTypeId=Guid.NewGuid()`. **NOT prod bug** (prod đơn luôn pin LeaveType thật) — fix tại test: BuildLeave +optional leaveTypeId, seed LeaveType ở 2 test đó. Baseline thật trước S43 = 142-pass/2-RED (KHÔNG phải 144-green). REPORTED em main. --- diff --git a/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx b/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx index 451a035..e4db1fa 100644 --- a/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx +++ b/fe-admin/src/pages/office/WorkflowAppDetailPage.tsx @@ -345,6 +345,33 @@ export function WorkflowAppDetailPage() { + {/* Số dư phép (chỉ kind=leave) — Wave 2 hiển thị balance đã embed trong detail */} + {kind === 'leave' && d.leaveBalanceRemaining != null && (() => { + const remaining = d.leaveBalanceRemaining + const numDays = d.numDays ?? 0 + const year = d.startDate ? new Date(d.startDate).getFullYear() : new Date().getFullYear() + const isApproved = d.status === WorkflowAppStatus.DaDuyet + const overBudget = remaining < 0 || (!isApproved && remaining < numDays) + return ( +
+

Số dư phép

+
+ Số dư phép năm {year}:{' '} + Được hưởng {d.leaveBalanceEntitled ?? '—'} ·{' '} + Đã dùng {d.leaveBalanceUsed ?? '—'} ·{' '} + Còn {remaining} ngày +
+ {overBudget && ( +
+ {remaining < 0 + ? '⚠️ Đã âm số dư phép' + : `⚠️ Đơn ${numDays} ngày vượt số dư còn lại (${remaining} ngày)`} +
+ )} +
+ ) + })()} + {/* Section 2: Quy trình duyệt */}

2. Quy trình duyệt

diff --git a/fe-admin/src/types/workflowApps.ts b/fe-admin/src/types/workflowApps.ts index e8aad54..cdd78a1 100644 --- a/fe-admin/src/types/workflowApps.ts +++ b/fe-admin/src/types/workflowApps.ts @@ -78,6 +78,10 @@ export interface WorkflowAppDetail { endDate?: string numDays?: number reason?: string + // leave balance (P11-B Wave 1: embed số dư phép NGƯỜI TẠO cho loại phép + năm đơn; null nếu loại phép không tồn tại) + leaveBalanceEntitled?: number | null + leaveBalanceUsed?: number | null + leaveBalanceRemaining?: number | null // ot otDate?: string startTime?: string diff --git a/fe-user/src/pages/office/WorkflowAppDetailPage.tsx b/fe-user/src/pages/office/WorkflowAppDetailPage.tsx index 451a035..e4db1fa 100644 --- a/fe-user/src/pages/office/WorkflowAppDetailPage.tsx +++ b/fe-user/src/pages/office/WorkflowAppDetailPage.tsx @@ -345,6 +345,33 @@ export function WorkflowAppDetailPage() {
+ {/* Số dư phép (chỉ kind=leave) — Wave 2 hiển thị balance đã embed trong detail */} + {kind === 'leave' && d.leaveBalanceRemaining != null && (() => { + const remaining = d.leaveBalanceRemaining + const numDays = d.numDays ?? 0 + const year = d.startDate ? new Date(d.startDate).getFullYear() : new Date().getFullYear() + const isApproved = d.status === WorkflowAppStatus.DaDuyet + const overBudget = remaining < 0 || (!isApproved && remaining < numDays) + return ( +
+

Số dư phép

+
+ Số dư phép năm {year}:{' '} + Được hưởng {d.leaveBalanceEntitled ?? '—'} ·{' '} + Đã dùng {d.leaveBalanceUsed ?? '—'} ·{' '} + Còn {remaining} ngày +
+ {overBudget && ( +
+ {remaining < 0 + ? '⚠️ Đã âm số dư phép' + : `⚠️ Đơn ${numDays} ngày vượt số dư còn lại (${remaining} ngày)`} +
+ )} +
+ ) + })()} + {/* Section 2: Quy trình duyệt */}

2. Quy trình duyệt

diff --git a/fe-user/src/types/workflowApps.ts b/fe-user/src/types/workflowApps.ts index e8aad54..cdd78a1 100644 --- a/fe-user/src/types/workflowApps.ts +++ b/fe-user/src/types/workflowApps.ts @@ -78,6 +78,10 @@ export interface WorkflowAppDetail { endDate?: string numDays?: number reason?: string + // leave balance (P11-B Wave 1: embed số dư phép NGƯỜI TẠO cho loại phép + năm đơn; null nếu loại phép không tồn tại) + leaveBalanceEntitled?: number | null + leaveBalanceUsed?: number | null + leaveBalanceRemaining?: number | null // ot otDate?: string startTime?: string diff --git a/src/Backend/SolutionErp.Api/Controllers/LeaveBalancesController.cs b/src/Backend/SolutionErp.Api/Controllers/LeaveBalancesController.cs new file mode 100644 index 0000000..2758693 --- /dev/null +++ b/src/Backend/SolutionErp.Api/Controllers/LeaveBalancesController.cs @@ -0,0 +1,42 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Application.Hrm; + +namespace SolutionErp.Api.Controllers; + +// Phase 11 P11-B Wave 1 (Mig 42 — S43) — Số dư phép theo năm. +// /my = mọi user đăng nhập (xem phép của chính mình). Admin endpoint (xem user khác + +// điều chỉnh) dùng [Authorize(Roles = "Admin")] — mirror HrmConfigsController convention +// (HRM write/admin = Roles "Admin", KHÔNG menu policy). +[ApiController] +[Route("api/leave-balances")] +[Authorize] +public class LeaveBalancesController(IMediator mediator, IDateTime clock) : ControllerBase +{ + [HttpGet("my")] + public async Task GetMy([FromQuery] int? year) + => Ok(await mediator.Send(new GetMyLeaveBalancesQuery(year ?? clock.Now.Year))); + + [HttpGet] + [Authorize(Roles = "Admin")] + public async Task GetForUser([FromQuery] Guid userId, [FromQuery] int? year) + => Ok(await mediator.Send(new GetUserLeaveBalancesQuery(userId, year ?? clock.Now.Year))); + + [HttpPut("adjust")] + [Authorize(Roles = "Admin")] + public async Task Adjust([FromBody] AdjustLeaveBalanceBody body) + { + await mediator.Send(new AdjustLeaveBalanceCommand( + body.UserId, body.LeaveTypeId, body.Year, body.EntitledDays, body.AdjustmentDays)); + return NoContent(); + } + + public record AdjustLeaveBalanceBody( + Guid UserId, + Guid LeaveTypeId, + int Year, + decimal? EntitledDays, + decimal? AdjustmentDays); +} diff --git a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs index a48f775..3552da0 100644 --- a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs @@ -139,5 +139,8 @@ public interface IApplicationDbContext // Phase 10.4 G-P1 (Mig 40 — S38) — Chấm công web GPS. DbSet Attendances { get; } + // Phase 11 P11-B Wave 1 (Mig 42) — Số dư phép theo năm (NV × LoạiPhép × Năm). + DbSet LeaveBalances { get; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/Backend/SolutionErp.Application/Hrm/LeaveBalanceFeatures.cs b/src/Backend/SolutionErp.Application/Hrm/LeaveBalanceFeatures.cs new file mode 100644 index 0000000..853a6bf --- /dev/null +++ b/src/Backend/SolutionErp.Application/Hrm/LeaveBalanceFeatures.cs @@ -0,0 +1,150 @@ +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using SolutionErp.Application.Common.Exceptions; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Application.Hrm; + +// Phase 11 P11-B Wave 1 (Mig 42 — S43 2026-05-30) — Số dư phép theo năm. +// Query lazy: list mọi LeaveType IsActive LEFT JOIN LeaveBalance — chưa có row thì +// synthesize default (Entitled=DaysPerYear, Used=0, Adjustment=0). Admin upsert qua +// AdjustLeaveBalanceCommand. Trừ phép tự động ở ApproveLeaveRequestHandler (terminal). +// +// RemainingDays = EntitledDays + AdjustmentDays − UsedDays (COMPUTED, KHÔNG store). +// HRM entities KHÔNG có global HasQueryFilter → query phải Where(!IsDeleted) thủ công. + +// ===== DTO ===== + +public record LeaveBalanceDto( + Guid LeaveTypeId, + string Code, + string Name, + int Year, + decimal EntitledDays, + decimal UsedDays, + decimal AdjustmentDays, + decimal RemainingDays, + decimal DaysPerYear, + bool IsPaid); + +// ===== Shared builder: merge active LeaveTypes + balances cho 1 user/năm ===== + +internal static class LeaveBalanceProjection +{ + public static async Task> BuildAsync( + IApplicationDbContext db, Guid userId, int year, CancellationToken ct) + { + var types = await db.LeaveTypes.AsNoTracking() + .Where(t => !t.IsDeleted && t.IsActive) + .OrderBy(t => t.Code) + .Select(t => new { t.Id, t.Code, t.Name, t.DaysPerYear, t.IsPaid }) + .ToListAsync(ct); + + var balances = await db.LeaveBalances.AsNoTracking() + .Where(b => !b.IsDeleted && b.UserId == userId && b.Year == year) + .Select(b => new { b.LeaveTypeId, b.EntitledDays, b.UsedDays, b.AdjustmentDays }) + .ToListAsync(ct); + var byType = balances.ToDictionary(b => b.LeaveTypeId); + + return types.Select(t => + { + byType.TryGetValue(t.Id, out var b); + var entitled = b?.EntitledDays ?? t.DaysPerYear; + var used = b?.UsedDays ?? 0m; + var adjustment = b?.AdjustmentDays ?? 0m; + return new LeaveBalanceDto( + t.Id, t.Code, t.Name, year, + entitled, used, adjustment, + entitled + adjustment - used, + t.DaysPerYear, t.IsPaid); + }).ToList(); + } +} + +// ===== Query: số dư phép của chính mình ===== + +public record GetMyLeaveBalancesQuery(int Year) : IRequest>; + +public class GetMyLeaveBalancesHandler(IApplicationDbContext db, ICurrentUser cu) + : IRequestHandler> +{ + public async Task> Handle(GetMyLeaveBalancesQuery req, CancellationToken ct) + { + if (cu.UserId is null) throw new UnauthorizedException(); + return await LeaveBalanceProjection.BuildAsync(db, cu.UserId.Value, req.Year, ct); + } +} + +// ===== Query: số dư phép của 1 user (admin) ===== + +public record GetUserLeaveBalancesQuery(Guid UserId, int Year) : IRequest>; + +public class GetUserLeaveBalancesHandler(IApplicationDbContext db) + : IRequestHandler> +{ + public async Task> Handle(GetUserLeaveBalancesQuery req, CancellationToken ct) + => await LeaveBalanceProjection.BuildAsync(db, req.UserId, req.Year, ct); +} + +// ===== Command: admin upsert (carry-over / điều chỉnh) ===== + +public record AdjustLeaveBalanceCommand( + Guid UserId, + Guid LeaveTypeId, + int Year, + decimal? EntitledDays, + decimal? AdjustmentDays) : IRequest; + +public class AdjustLeaveBalanceValidator : AbstractValidator +{ + public AdjustLeaveBalanceValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.LeaveTypeId).NotEmpty(); + RuleFor(x => x.Year).InclusiveBetween(2000, 2100); + RuleFor(x => x.EntitledDays).GreaterThanOrEqualTo(0).When(x => x.EntitledDays.HasValue); + } +} + +public class AdjustLeaveBalanceHandler(IApplicationDbContext db, ICurrentUser cu, IDateTime clock) + : IRequestHandler +{ + public async Task Handle(AdjustLeaveBalanceCommand req, CancellationToken ct) + { + var typeExists = await db.LeaveTypes.AsNoTracking() + .AnyAsync(t => t.Id == req.LeaveTypeId && !t.IsDeleted, ct); + if (!typeExists) throw new NotFoundException("LeaveType", req.LeaveTypeId); + + var bal = await db.LeaveBalances + .FirstOrDefaultAsync(b => b.UserId == req.UserId && b.LeaveTypeId == req.LeaveTypeId + && b.Year == req.Year && !b.IsDeleted, ct); + + if (bal is null) + { + var daysPerYear = await db.LeaveTypes.AsNoTracking() + .Where(t => t.Id == req.LeaveTypeId).Select(t => t.DaysPerYear).FirstOrDefaultAsync(ct); + bal = new LeaveBalance + { + UserId = req.UserId, + LeaveTypeId = req.LeaveTypeId, + Year = req.Year, + EntitledDays = req.EntitledDays ?? daysPerYear, + UsedDays = 0, + AdjustmentDays = req.AdjustmentDays ?? 0, + CreatedAt = clock.UtcNow, + CreatedBy = cu.UserId, + }; + db.LeaveBalances.Add(bal); + } + else + { + if (req.EntitledDays.HasValue) bal.EntitledDays = req.EntitledDays.Value; + if (req.AdjustmentDays.HasValue) bal.AdjustmentDays = req.AdjustmentDays.Value; + bal.UpdatedAt = clock.UtcNow; + bal.UpdatedBy = cu.UserId; + } + await db.SaveChangesAsync(ct); + } +} diff --git a/src/Backend/SolutionErp.Application/Office/LeaveOtApprovalFeatures.cs b/src/Backend/SolutionErp.Application/Office/LeaveOtApprovalFeatures.cs index 75e5401..56b0b3a 100644 --- a/src/Backend/SolutionErp.Application/Office/LeaveOtApprovalFeatures.cs +++ b/src/Backend/SolutionErp.Application/Office/LeaveOtApprovalFeatures.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using SolutionErp.Application.Common.Exceptions; using SolutionErp.Application.Common.Interfaces; using SolutionErp.Domain.ApprovalWorkflowsV2; +using SolutionErp.Domain.Hrm; using SolutionErp.Domain.Office; namespace SolutionErp.Application.Office; @@ -78,7 +79,12 @@ public record LeaveRequestDetailDto( int? CurrentApprovalLevelOrder, int? RejectedFromStatus, DateTime CreatedAt, - List LevelOpinions); + List LevelOpinions, + // P11-B: số dư phép của NGƯỜI TẠO cho (LeaveType, năm của đơn) — embed để approver + // cũng thấy (khác /my = balance người xem). null nếu loại phép không tồn tại. + decimal? LeaveBalanceEntitled, + decimal? LeaveBalanceUsed, + decimal? LeaveBalanceRemaining); public record GetLeaveRequestByIdQuery(Guid Id) : IRequest; @@ -140,12 +146,36 @@ public class GetLeaveRequestByIdHandler(IApplicationDbContext db) .OrderBy(o => o.StepOrder).ThenBy(o => o.LevelOrder) .ToList(); + // P11-B: số dư phép người tạo cho (LeaveType, năm của StartDate) — hiển thị + cảnh báo vượt. + // Lazy: chưa có row → từ LeaveType.DaysPerYear (Used=0). Remaining = Entitled + Adjustment − Used. + var balYear = p.StartDate.Year; + var balRow = await db.LeaveBalances.AsNoTracking() + .Where(b => b.UserId == p.RequesterUserId && b.LeaveTypeId == p.LeaveTypeId + && b.Year == balYear && !b.IsDeleted) + .Select(b => new { b.EntitledDays, b.UsedDays, b.AdjustmentDays }) + .FirstOrDefaultAsync(ct); + decimal? balEntitled, balUsed, balRemaining; + if (balRow is not null) + { + balEntitled = balRow.EntitledDays + balRow.AdjustmentDays; + balUsed = balRow.UsedDays; + balRemaining = balRow.EntitledDays + balRow.AdjustmentDays - balRow.UsedDays; + } + else + { + var dpy = await db.LeaveTypes.AsNoTracking() + .Where(t => t.Id == p.LeaveTypeId).Select(t => (decimal?)t.DaysPerYear).FirstOrDefaultAsync(ct); + balEntitled = dpy; + balUsed = dpy.HasValue ? 0m : (decimal?)null; + balRemaining = dpy; + } + return new LeaveRequestDetailDto( p.Id, p.MaDonTu, p.RequesterUserId, p.RequesterFullName, p.LeaveTypeId, p.StartDate, p.EndDate, p.NumDays, p.Reason, (int)p.Status, p.ApprovalWorkflowId, wfCode, wfName, p.CurrentApprovalLevelOrder, p.RejectedFromStatus.HasValue ? (int)p.RejectedFromStatus.Value : (int?)null, - p.CreatedAt, opinions); + p.CreatedAt, opinions, balEntitled, balUsed, balRemaining); } } @@ -195,6 +225,11 @@ public class UpdateLeaveRequestDraftHandler(IApplicationDbContext db, ICurrentUs throw new ConflictException("Quy trình duyệt không thuộc loại Đơn nghỉ phép."); } + // P11-B: enforce LeaveTypeId tồn tại nếu đổi (deduction FK→LeaveTypes Restrict). + if (req.LeaveTypeId != p.LeaveTypeId + && !await db.LeaveTypes.AsNoTracking().AnyAsync(t => t.Id == req.LeaveTypeId, ct)) + throw new ConflictException("Loại phép không tồn tại."); + p.LeaveTypeId = req.LeaveTypeId; p.StartDate = req.StartDate; p.EndDate = req.EndDate; @@ -319,6 +354,32 @@ public class ApproveLeaveRequestHandler(IApplicationDbContext db, ICurrentUser c { p.Status = WorkflowAppStatus.DaDuyet; p.CurrentApprovalLevelOrder = null; + + // P11-B: trừ phép khi duyệt cuối — chạy đúng 1 lần (DaDuyet không approve lại, + // early guard Status != DaGuiDuyet chặn re-approve). UPSERT LeaveBalance theo năm. + var year = p.StartDate.Year; + var bal = await db.LeaveBalances.FirstOrDefaultAsync( + b => b.UserId == p.RequesterUserId && b.LeaveTypeId == p.LeaveTypeId && b.Year == year, ct); + if (bal is null) + { + var daysPerYear = await db.LeaveTypes.AsNoTracking() + .Where(t => t.Id == p.LeaveTypeId).Select(t => t.DaysPerYear).FirstOrDefaultAsync(ct); + bal = new LeaveBalance + { + UserId = p.RequesterUserId, + LeaveTypeId = p.LeaveTypeId, + Year = year, + EntitledDays = daysPerYear, + UsedDays = 0, + AdjustmentDays = 0, + CreatedAt = clock.UtcNow, + CreatedBy = cu.UserId, + }; + db.LeaveBalances.Add(bal); + } + bal.UsedDays += p.NumDays; + bal.UpdatedAt = clock.UtcNow; + bal.UpdatedBy = cu.UserId; } p.UpdatedAt = clock.UtcNow; p.UpdatedBy = cu.UserId; diff --git a/src/Backend/SolutionErp.Application/Office/WorkflowAppsFeatures.cs b/src/Backend/SolutionErp.Application/Office/WorkflowAppsFeatures.cs index 6660dad..f9a1f5c 100644 --- a/src/Backend/SolutionErp.Application/Office/WorkflowAppsFeatures.cs +++ b/src/Backend/SolutionErp.Application/Office/WorkflowAppsFeatures.cs @@ -41,6 +41,10 @@ public class CreateLeaveRequestHandler(IApplicationDbContext db, ICurrentUser cu public async Task Handle(CreateLeaveRequestCommand req, CancellationToken ct) { if (cu.UserId is null) throw new UnauthorizedException(); + // P11-B: enforce LeaveTypeId tồn tại (deduction lúc duyệt cuối insert LeaveBalance + // FK→LeaveTypes Restrict — bogus type → 500 kẹt đơn). Guard tại cửa Create. + if (!await db.LeaveTypes.AsNoTracking().AnyAsync(t => t.Id == req.LeaveTypeId, ct)) + throw new ConflictException("Loại phép không tồn tại."); var e = new LeaveRequest { RequesterUserId = cu.UserId.Value, diff --git a/src/Backend/SolutionErp.Domain/Hrm/LeaveBalance.cs b/src/Backend/SolutionErp.Domain/Hrm/LeaveBalance.cs new file mode 100644 index 0000000..037497d --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/LeaveBalance.cs @@ -0,0 +1,27 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Hrm; + +// Phase 11 P11-B Wave 1 (Mig 42 — S43 2026-05-30) — Số dư phép theo năm. +// Track quota phép từng NV × LoạiPhép × Năm. Trừ phép tự động khi đơn nghỉ +// phép duyệt cuối (ApproveLeaveRequestHandler terminal branch UPSERT UsedDays). +// +// Remaining = EntitledDays + AdjustmentDays − UsedDays (COMPUTED ở DTO, KHÔNG store). +// UNIQUE (UserId, LeaveTypeId, Year) — 1 row mỗi NV mỗi loại mỗi năm. +public class LeaveBalance : AuditableEntity +{ + public Guid UserId { get; set; } + public Guid LeaveTypeId { get; set; } + public int Year { get; set; } + + // Phân bổ năm — mặc định lấy từ LeaveType.DaysPerYear lúc tạo row. + public decimal EntitledDays { get; set; } + + // Đã dùng — cộng dồn NumDays mỗi đơn nghỉ phép duyệt cuối. + public decimal UsedDays { get; set; } + + // Admin carry-over / điều chỉnh (dồn phép năm trước, thưởng phép...). Default 0. + public decimal AdjustmentDays { get; set; } + + public LeaveType? LeaveType { get; set; } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs index 880266f..2ae6098 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs @@ -125,6 +125,9 @@ public class ApplicationDbContext // Phase 10.4 G-P1 (Mig 40 — S38) — Chấm công web GPS. public DbSet Attendances => Set(); + // Phase 11 P11-B Wave 1 (Mig 42) — Số dư phép theo năm. + public DbSet LeaveBalances => Set(); + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/LeaveBalanceConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/LeaveBalanceConfiguration.cs new file mode 100644 index 0000000..fc55d06 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/LeaveBalanceConfiguration.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 42 P11-B Wave 1 (Phase 11) — Số dư phép theo năm. +// FK LeaveType WithMany() Restrict (catalog không cascade). UNIQUE composite +// (UserId, LeaveTypeId, Year). HRM no global HasQueryFilter — list query +// MUST .Where(!IsDeleted) thủ công ở handler. +public class LeaveBalanceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("LeaveBalances"); + + e.Property(x => x.EntitledDays).HasPrecision(5, 2); + e.Property(x => x.UsedDays).HasPrecision(5, 2); + e.Property(x => x.AdjustmentDays).HasPrecision(5, 2); + + e.HasOne(x => x.LeaveType) + .WithMany() + .HasForeignKey(x => x.LeaveTypeId) + .OnDelete(DeleteBehavior.Restrict); + + e.HasIndex(x => new { x.UserId, x.LeaveTypeId, x.Year }).IsUnique(); + e.HasIndex(x => x.UserId); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.Designer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.Designer.cs new file mode 100644 index 0000000..a231bc1 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.Designer.cs @@ -0,0 +1,6373 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SolutionErp.Infrastructure.Persistence; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260530034336_AddLeaveBalances")] + partial class AddLeaveBalances + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("ApplicableType") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsUserSelectable") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApplicableType", "IsActive"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.ToTable("ApprovalWorkflows", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AllowApproverEditBudget") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverEditDetails") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverSkipToFinal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnOneLevel") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnOneStep") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnToAssignee") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnToDrafter") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ApprovalWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("ApprovalWorkflowStepId", "Order"); + + b.ToTable("ApprovalWorkflowLevels", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("ApprovalWorkflowId", "Order"); + + b.ToTable("ApprovalWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaNganSach") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NamNganSach") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenNganSach") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TongNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaNganSach") + .IsUnique() + .HasFilter("[MaNganSach] IS NOT NULL"); + + b.HasIndex("NamNganSach"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "ApprovedAt"); + + b.ToTable("BudgetApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "CreatedAt"); + + b.HasIndex("BudgetId", "EntityType"); + + b.ToTable("BudgetChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("BudgetId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("BudgetId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_BudgetDeptApprovals_Budget_Phase_Dept_Stage"); + + b.ToTable("BudgetDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "Order"); + + b.ToTable("BudgetDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetManualAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("BudgetManualName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BypassProcurementAndCCM") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("CurrentWorkflowStepIndex") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DraftData") + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("GiaTri") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaHopDong") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NoiDung") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedAtStepIndex") + .HasColumnType("int"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenHopDong") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("BudgetId"); + + b.HasIndex("MaHopDong") + .IsUnique() + .HasFilter("[MaHopDong] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("SupplierId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "ApprovedAt"); + + b.ToTable("ContractApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId"); + + b.ToTable("ContractAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.HasIndex("ContractId", "EntityType"); + + b.ToTable("ContractChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ContractCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.ToTable("ContractComments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("ContractId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("ContractId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_Dept_Stage"); + + b.ToTable("ContractDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("ContractId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("ContractLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DenNgay") + .HasColumnType("datetime2"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaDichVu") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGian") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TuNgay") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("DichVuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MaCongViec") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenCongViec") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("YeuCauKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("GiaoKhoanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThueVAT") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("MuaBanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LoaiDichVu") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PhamViDichVu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("SLA") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacDvDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DieuKienGiaoHang") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DieuKienThanhToan") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NhomSP") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacNccDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianGiao") + .HasColumnType("datetime2"); + + b.Property("ThongSoKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NhaCungCapDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("HangMuc") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("ThauPhuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("ContractType", "IsActive"); + + b.ToTable("WorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("WorkflowDefinitionId", "Order"); + + b.ToTable("WorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkflowStepId"); + + b.ToTable("WorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType") + .IsUnique(); + + b.ToTable("WorkflowTypeAssignments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractClause", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ContractClauses", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FieldSpec") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FormCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Format") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType"); + + b.HasIndex("FormCode") + .IsUnique(); + + b.ToTable("ContractTemplates", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("EmployeeCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DocumentType") + .HasColumnType("int"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiryDate") + .HasColumnType("date"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("date"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DocumentType"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeDocuments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeEducation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CertificateIssueDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DegreeLevel") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EducationMode") + .HasColumnType("int"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FromDate") + .HasColumnType("date"); + + b.Property("GradeLevel") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Major") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SchoolName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ToDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeEducations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeFamilyRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BirthYear") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Occupation") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Relationship") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeFamilyRelations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AcademicTitle") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AnnualLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("BankAccount") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("BankBranch") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BankName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BaseSalary") + .HasColumnType("decimal(18,2)"); + + b.Property("BirthPlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BloodType") + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("CommunistPartyJoinDate") + .HasColumnType("date"); + + b.Property("CompensatoryLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmergencyContactAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EmergencyContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("EmployeeCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("EmployeeStatus") + .HasColumnType("int"); + + b.Property("EmployeeType") + .HasColumnType("int"); + + b.Property("Ethnicity") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HeightCm") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("date"); + + b.Property("Hometown") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IdCardIssueDate") + .HasColumnType("date"); + + b.Property("IdCardIssuePlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IdCardNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("InternalPhone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsCommunistParty") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsTradeUnion") + .HasColumnType("bit"); + + b.Property("IsYouthUnion") + .HasColumnType("bit"); + + b.Property("MaritalStatus") + .HasColumnType("int"); + + b.Property("MedicalRegistrationPlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Nationality") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PassportNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PermanentAddressText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PermanentDistrictId") + .HasColumnType("uniqueidentifier"); + + b.Property("PermanentProvinceId") + .HasColumnType("uniqueidentifier"); + + b.Property("PermanentWardId") + .HasColumnType("uniqueidentifier"); + + b.Property("PersonalEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Qualification") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Religion") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RemainingLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("ResignDate") + .HasColumnType("date"); + + b.Property("SeniorityLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("SocialInsuranceNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SocialInsuranceStartDate") + .HasColumnType("date"); + + b.Property("StreetAddressPermanent") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StreetAddressTemporary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TemporaryAddressText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TemporaryDistrictId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemporaryProvinceId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemporaryWardId") + .HasColumnType("uniqueidentifier"); + + b.Property("TimekeepingCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TotalSalary") + .HasColumnType("decimal(18,2)"); + + b.Property("TradeUnionJoinDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("WeightKg") + .HasColumnType("int"); + + b.Property("WorkLocation") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("YouthUnionJoinDate") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeCode") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.HasIndex("Phone"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("EmployeeProfiles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeSkill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LanguageId") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Level") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.HasIndex("Kind"); + + b.ToTable("EmployeeSkills", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeWorkHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompanyAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FromDate") + .HasColumnType("date"); + + b.Property("Industry") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobDescription") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("JobTitle") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResignReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ToDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeWorkHistories", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Holiday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .HasColumnType("bit"); + + b.Property("IsRecurring") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Date") + .IsUnique(); + + b.ToTable("Holidays", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AdjustmentDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntitledDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UsedDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("LeaveBalances", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DaysPerYear") + .HasColumnType("decimal(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequiresAttachment") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("LeaveTypes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.OtPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaxHoursPerDay") + .HasColumnType("int"); + + b.Property("MaxHoursPerMonth") + .HasColumnType("int"); + + b.Property("MaxHoursPerYear") + .HasColumnType("int"); + + b.Property("MultiplierHoliday") + .HasColumnType("decimal(4,2)"); + + b.Property("MultiplierWeekday") + .HasColumnType("decimal(4,2)"); + + b.Property("MultiplierWeekend") + .HasColumnType("decimal(4,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("OtPolicies", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.ShiftPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BreakMinutes") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkDays") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ShiftPatterns", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Property("Key") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DisplayLabel") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ParentKey") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Key"); + + b.HasIndex("ParentKey"); + + b.ToTable("MenuItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CanCreate") + .HasColumnType("bit"); + + b.Property("CanDelete") + .HasColumnType("bit"); + + b.Property("CanRead") + .HasColumnType("bit"); + + b.Property("CanUpdate") + .HasColumnType("bit"); + + b.Property("MenuKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MenuKey"); + + b.HasIndex("RoleId", "MenuKey") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ShortName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("CanBypassReview") + .HasColumnType("bit"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Position") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("RefreshToken") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.MaterialItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginCountry") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Specification") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("MaterialItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.ServiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("ServiceItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.UnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("UnitsOfMeasure", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("WorkItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Departments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ContactPerson") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("Suppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Href") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("RefId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId", "ReadAt"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AttendanceDate") + .HasColumnType("datetime2"); + + b.Property("CheckInAccuracy") + .HasColumnType("decimal(8,2)"); + + b.Property("CheckInAt") + .HasColumnType("datetime2"); + + b.Property("CheckInLatitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckInLongitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckOutAccuracy") + .HasColumnType("decimal(8,2)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime2"); + + b.Property("CheckOutLatitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckOutLongitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IpAddressIn") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IpAddressOut") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OtHours") + .HasColumnType("decimal(5,2)"); + + b.Property("SourceIn") + .HasColumnType("int"); + + b.Property("SourceOut") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkHours") + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "AttendanceDate") + .IsUnique(); + + b.ToTable("Attendances", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ItTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignedToFullName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AssignedToUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaTicket") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Resolution") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToUserId"); + + b.HasIndex("Category"); + + b.HasIndex("MaTicket") + .IsUnique() + .HasFilter("[MaTicket] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("ItTickets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NumDays") + .HasColumnType("decimal(5,2)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("LeaveRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("LeaveRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("LeaveRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BookedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BookedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RoomId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BookedByUserId"); + + b.HasIndex("RoomId", "StartAt"); + + b.ToTable("MeetingBookings", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BookingId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("BookingId", "UserId") + .IsUnique(); + + b.ToTable("MeetingBookingAttendees", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingRoom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Equipment") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Location") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("MeetingRooms", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("Hours") + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("OtDate") + .HasColumnType("datetime2"); + + b.Property("OtPolicyId") + .HasColumnType("uniqueidentifier"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("OtRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("OtRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("OtRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("OtRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Proposal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AmountEstimate") + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDeXuat") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("DrafterUserId"); + + b.HasIndex("MaDeXuat") + .IsUnique() + .HasFilter("[MaDeXuat] IS NOT NULL"); + + b.HasIndex("Status"); + + b.ToTable("Proposals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MimeType") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ProposalId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UploadedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UploadedByUserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ProposalId"); + + b.ToTable("ProposalAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ProposalCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProposalId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("ProposalId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("ProposalLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EstimatedCost") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NumDays") + .HasColumnType("int"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("TravelRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("TravelRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("TravelRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("TravelRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("DriverName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EndAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("VehicleLicense") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VehicleName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.HasIndex("VehicleLicense", "StartAt"); + + b.ToTable("VehicleBookings", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBookingLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("VehicleBookingId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("VehicleBookingId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("VehicleBookingLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.WorkflowAppCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("WorkflowAppCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetManualAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("BudgetManualName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("CurrentWorkflowStepIndex") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DiaDiem") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaPhieu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedAtStepIndex") + .HasColumnType("int"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SelectedSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenGoiThau") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("BudgetId"); + + b.HasIndex("ContractId"); + + b.HasIndex("MaPhieu") + .IsUnique() + .HasFilter("[MaPhieu] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("WorkflowDefinitionId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("PurchaseEvaluations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "ApprovedAt"); + + b.ToTable("PurchaseEvaluationApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.ToTable("PurchaseEvaluationAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "CreatedAt"); + + b.HasIndex("PurchaseEvaluationId", "EntityType"); + + b.ToTable("PurchaseEvaluationChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("PurchaseEvaluationCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_PEDeptApprovals_PE_Phase_Dept_Stage"); + + b.ToTable("PurchaseEvaluationDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Opinion") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Kind") + .IsUnique(); + + b.ToTable("PurchaseEvaluationDepartmentOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuongNganSach") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("KhoiLuongThiCong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTienNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Order"); + + b.ToTable("PurchaseEvaluationDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("PurchaseEvaluationId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BgVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChuaVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsSelected") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationDetailId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.HasIndex("PurchaseEvaluationDetailId", "PurchaseEvaluationSupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationQuotes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentTermText") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.HasIndex("PurchaseEvaluationId", "SupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationSuppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EvaluationType") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("EvaluationType", "IsActive"); + + b.ToTable("PurchaseEvaluationWorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("PurchaseEvaluationWorkflowDefinitionId", "Order"); + + b.ToTable("PurchaseEvaluationWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationWorkflowStepId"); + + b.ToTable("PurchaseEvaluationWorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", "Step") + .WithMany("Levels") + .HasForeignKey("ApprovalWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ApproverUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", "ApprovalWorkflow") + .WithMany("Steps") + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ApprovalWorkflow"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Approvals") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Changelogs") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("DepartmentApprovals") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Details") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null) + .WithMany() + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Approvals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Attachments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Changelogs") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Comments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DepartmentApprovals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("LevelOpinions") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DichVuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("GiaoKhoanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("MuaBanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacDvDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacNccDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NhaCungCapDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("ThauPhuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SolutionErp.Domain.Contracts.WorkflowDefinition", "WorkflowDefinition") + .WithMany("Steps") + .HasForeignKey("WorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkflowDefinition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.Contracts.WorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("WorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeDocument", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Documents") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeEducation", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Educations") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeFamilyRelation", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("FamilyRelations") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", "User") + .WithOne() + .HasForeignKey("SolutionErp.Domain.Hrm.EmployeeProfile", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeSkill", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Skills") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeWorkHistory", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("WorkHistories") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.HasOne("SolutionErp.Domain.Hrm.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent") + .WithMany("Children") + .HasForeignKey("ParentKey") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Menu") + .WithMany("Permissions") + .HasForeignKey("MenuKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Menu"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.LeaveRequest", "LeaveRequest") + .WithMany("LevelOpinions") + .HasForeignKey("LeaveRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LeaveRequest"); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.HasOne("SolutionErp.Domain.Office.MeetingRoom", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b => + { + b.HasOne("SolutionErp.Domain.Office.MeetingBooking", "Booking") + .WithMany("Attendees") + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.OtRequest", "OtRequest") + .WithMany("LevelOpinions") + .HasForeignKey("OtRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("OtRequest"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalAttachment", b => + { + b.HasOne("SolutionErp.Domain.Office.Proposal", "Proposal") + .WithMany("Attachments") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.Proposal", "Proposal") + .WithMany("LevelOpinions") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.TravelRequest", "TravelRequest") + .WithMany("LevelOpinions") + .HasForeignKey("TravelRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("TravelRequest"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBookingLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.VehicleBooking", "VehicleBooking") + .WithMany("LevelOpinions") + .HasForeignKey("VehicleBookingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("VehicleBooking"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null) + .WithMany() + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Approvals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Attachments") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Changelogs") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("DepartmentApprovals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentOpinion", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("DepartmentOpinions") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Details") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("LevelOpinions") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail") + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", null) + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationId"); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", "Supplier") + .WithMany() + .HasForeignKey("PurchaseEvaluationSupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Detail"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Suppliers") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", "Definition") + .WithMany("Steps") + .HasForeignKey("PurchaseEvaluationWorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("PurchaseEvaluationWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.Navigation("Levels"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b => + { + b.Navigation("Approvals"); + + b.Navigation("Changelogs"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("Details"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("Comments"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("DichVuDetails"); + + b.Navigation("GiaoKhoanDetails"); + + b.Navigation("LevelOpinions"); + + b.Navigation("MuaBanDetails"); + + b.Navigation("NguyenTacDvDetails"); + + b.Navigation("NguyenTacNccDetails"); + + b.Navigation("NhaCungCapDetails"); + + b.Navigation("ThauPhuDetails"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Navigation("Approvers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.Navigation("Documents"); + + b.Navigation("Educations"); + + b.Navigation("FamilyRelations"); + + b.Navigation("Skills"); + + b.Navigation("WorkHistories"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Navigation("Children"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.Navigation("Attendees"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Proposal", b => + { + b.Navigation("Attachments"); + + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBooking", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("DepartmentOpinions"); + + b.Navigation("Details"); + + b.Navigation("LevelOpinions"); + + b.Navigation("Quotes"); + + b.Navigation("Suppliers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Navigation("Approvers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.cs new file mode 100644 index 0000000..84744cb --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260530034336_AddLeaveBalances.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddLeaveBalances : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LeaveBalances", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: false), + LeaveTypeId = table.Column(type: "uniqueidentifier", nullable: false), + Year = table.Column(type: "int", nullable: false), + EntitledDays = table.Column(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false), + UsedDays = table.Column(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false), + AdjustmentDays = table.Column(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: true), + UpdatedBy = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false), + DeletedAt = table.Column(type: "datetime2", nullable: true), + DeletedBy = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LeaveBalances", x => x.Id); + table.ForeignKey( + name: "FK_LeaveBalances_LeaveTypes_LeaveTypeId", + column: x => x.LeaveTypeId, + principalTable: "LeaveTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_LeaveBalances_LeaveTypeId", + table: "LeaveBalances", + column: "LeaveTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_LeaveBalances_UserId", + table: "LeaveBalances", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_LeaveBalances_UserId_LeaveTypeId_Year", + table: "LeaveBalances", + columns: new[] { "UserId", "LeaveTypeId", "Year" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LeaveBalances"); + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 28abe82..75ef19e 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -2553,6 +2553,66 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.ToTable("Holidays", (string)null); }); + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AdjustmentDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntitledDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UsedDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("LeaveBalances", (string)null); + }); + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveType", b => { b.Property("Id") @@ -5827,6 +5887,17 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.Navigation("EmployeeProfile"); }); + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.HasOne("SolutionErp.Domain.Hrm.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("LeaveType"); + }); + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => { b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent") diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/LeaveBalanceTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/LeaveBalanceTests.cs new file mode 100644 index 0000000..0af8b1a --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Application/LeaveBalanceTests.cs @@ -0,0 +1,424 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SolutionErp.Application.Common.Exceptions; +using SolutionErp.Application.Hrm; +using SolutionErp.Application.Office; +using SolutionErp.Domain.ApprovalWorkflowsV2; +using SolutionErp.Domain.Hrm; +using SolutionErp.Domain.Identity; +using SolutionErp.Domain.Office; +using SolutionErp.Infrastructure.Tests.Common; + +namespace SolutionErp.Infrastructure.Tests.Application; + +// Phase 11 P11-B Wave 3 (S43 2026-05-30) — test-after, critical-algo (financial-ish trừ phép). +// Cover deduction hook trong ApproveLeaveRequestHandler terminal branch (LeaveOtApprovalFeatures.cs:344) +// + LeaveBalanceFeatures.cs query lazy-merge + AdjustLeaveBalance upsert. +// +// CHỐT theo CODE (single source of truth): +// - Deduct CHỈ ở nhánh terminal DaDuyet (CurrentApprovalLevelOrder == allLevels.Count). +// Advance level KHÔNG trừ. Reject/Return KHÔNG trừ. +// - UPSERT theo UNIQUE (UserId, LeaveTypeId, Year). Auto-create EntitledDays=LeaveType.DaysPerYear, +// UsedDays=0, AdjustmentDays=0 nếu chưa có row. UsedDays += NumDays. +// - Year = StartDate.Year (KHÔNG phải EndDate / clock năm). +// - KHÔNG validate âm → Used > Entitled cho phép (Remaining < 0, không throw). +// - Query Remaining = EntitledDays + AdjustmentDays − UsedDays (COMPUTED ở DTO). +// Lazy: chưa có row → synthesize Entitled=DaysPerYear, Used=0, Adjustment=0. +// +// ⚠️ Pre-existing failure REPORT (S42 template WorkflowAppApproveV2Tests.cs): hook Wave 1 mới +// làm 2 test terminal cũ FK-fail (BuildLeave LeaveTypeId=Guid.NewGuid() → LeaveBalance insert +// FK→LeaveTypes fail). Fix tại file đó: seed LeaveType + truyền Id. KHÔNG phải prod bug — +// prod đơn nghỉ luôn pin LeaveType thật. +// +// FK note: LeaveBalance → LeaveType Restrict (LeaveBalanceConfiguration). MỌI đơn nghỉ test +// terminal PHẢI seed 1 LeaveType row + LeaveRequest.LeaveTypeId = type.Id đó. +public class LeaveBalanceTests +{ + private static readonly DateTime FixedNow = new(2026, 5, 30, 8, 0, 0, DateTimeKind.Utc); + + private static (IdentityFixture fix, TestApplicationDbContext db, FixedDateTime clock) NewCtx() + { + var fix = new IdentityFixture(); + var db = fix.Services.GetRequiredService(); + var clock = new FixedDateTime(FixedNow); + return (fix, db, clock); + } + + private static TestCurrentUser AsUser(User u, params string[] roles) + => new() { UserId = u.Id, FullName = u.FullName, Roles = roles ?? Array.Empty() }; + + // Seed 1 LeaveType row (Restrict FK target). Trả entity để dùng Id cho LeaveRequest.LeaveTypeId. + private static async Task SeedLeaveTypeAsync( + TestApplicationDbContext db, string code, decimal daysPerYear, bool isActive = true, bool isPaid = true) + { + var type = new LeaveType + { + Id = Guid.NewGuid(), + Code = code, + Name = $"Loại {code}", + DaysPerYear = daysPerYear, + IsPaid = isPaid, + IsActive = isActive, + }; + db.LeaveTypes.Add(type); + await db.SaveChangesAsync(CancellationToken.None); + return type; + } + + // Seed 1 Bước × N Cấp LeaveRequest workflow (mirror WorkflowAppApproveV2Tests). Levels theo Order asc. + private static async Task<(ApprovalWorkflow wf, List levels)> SeedLeaveWorkflowAsync( + TestApplicationDbContext db, params Guid[] approverUserIds) + { + var wf = new ApprovalWorkflow + { + Id = Guid.NewGuid(), + Code = "QT-LR-BAL", + Version = 1, + Name = "Quy trình nghỉ phép test balance", + ApplicableType = ApprovalWorkflowApplicableType.LeaveRequest, + IsActive = true, + IsUserSelectable = true, + }; + var step = new ApprovalWorkflowStep + { + Id = Guid.NewGuid(), + ApprovalWorkflowId = wf.Id, + Order = 1, + DepartmentId = null, + Name = "Bước 1", + }; + var levels = new List(); + for (var i = 0; i < approverUserIds.Length; i++) + { + levels.Add(new ApprovalWorkflowLevel + { + Id = Guid.NewGuid(), + ApprovalWorkflowStepId = step.Id, + Order = i + 1, + ApproverUserId = approverUserIds[i], + }); + } + db.ApprovalWorkflows.Add(wf); + db.ApprovalWorkflowSteps.Add(step); + db.ApprovalWorkflowLevels.AddRange(levels); + await db.SaveChangesAsync(CancellationToken.None); + return (wf, levels); + } + + // BuildLeave với LeaveTypeId explicit (KHÁC template — bắt buộc seeded type cho FK trừ phép). + private static LeaveRequest BuildLeave( + Guid requesterId, + Guid leaveTypeId, + Guid? workflowId, + WorkflowAppStatus status, + int? currentLevel, + decimal numDays = 3, + DateTime? startDate = null) + { + var start = startDate ?? FixedNow.Date; + return new LeaveRequest + { + Id = Guid.NewGuid(), + RequesterUserId = requesterId, + RequesterFullName = "Người tạo", + LeaveTypeId = leaveTypeId, + StartDate = start, + EndDate = start.AddDays((double)numDays - 1), + NumDays = numDays, + Reason = "Nghỉ việc riêng", + Status = status, + ApprovalWorkflowId = workflowId, + CurrentApprovalLevelOrder = currentLevel, + }; + } + + // ============ Case 1: Deduct single-level — tạo balance row mới ============ + + [Fact] + public async Task Approve_LastLevel_DeductsLeave_CreatesNewBalanceRow_FromDaysPerYear() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-b1@test.local", "Requester", null, Array.Empty()); + var approver = await fix.CreateUserAsync("ap-b1@test.local", "Approver", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id); + + // single-level, pin tại cấp cuối (=1), NumDays=3 + var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 3); + db.LeaveRequests.Add(leave); + await db.SaveChangesAsync(CancellationToken.None); + + await new ApproveLeaveRequestHandler(db, AsUser(approver), clock) + .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt"), CancellationToken.None); + + leave.Status.Should().Be(WorkflowAppStatus.DaDuyet); + leave.CurrentApprovalLevelOrder.Should().BeNull(); + + var bal = await db.LeaveBalances + .SingleAsync(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id); + bal.Year.Should().Be(2026, "Year = StartDate.Year"); + bal.EntitledDays.Should().Be(12m, "auto-create từ LeaveType.DaysPerYear"); + bal.UsedDays.Should().Be(3m, "UsedDays += NumDays"); + bal.AdjustmentDays.Should().Be(0m); + (bal.EntitledDays + bal.AdjustmentDays - bal.UsedDays).Should().Be(9m, "Remaining = 12 + 0 − 3"); + } + } + + // ============ Case 2: Deduct only at terminal (multi-level) — chỉ trừ 1 lần ============ + + [Fact] + public async Task Approve_MultiLevel_NoDeductAtIntermediate_DeductsOnceAtTerminal() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-b2@test.local", "Requester", null, Array.Empty()); + var ap1 = await fix.CreateUserAsync("ap1-b2@test.local", "Approver 1", null, Array.Empty()); + var ap2 = await fix.CreateUserAsync("ap2-b2@test.local", "Approver 2", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id, ap2.Id); + + var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 4); + db.LeaveRequests.Add(leave); + await db.SaveChangesAsync(CancellationToken.None); + + // Cấp 1 duyệt → advance, CHƯA terminal → KHÔNG có balance row + await new ApproveLeaveRequestHandler(db, AsUser(ap1), clock) + .Handle(new ApproveLeaveRequestCommand(leave.Id, "ok cấp 1"), CancellationToken.None); + leave.CurrentApprovalLevelOrder.Should().Be(2); + (await db.LeaveBalances.CountAsync(b => b.UserId == requester.Id)) + .Should().Be(0, "advance level KHÔNG trừ phép"); + + // Cấp 2 (cuối) duyệt → terminal → trừ 1 lần + await new ApproveLeaveRequestHandler(db, AsUser(ap2), clock) + .Handle(new ApproveLeaveRequestCommand(leave.Id, "ok cấp cuối"), CancellationToken.None); + leave.Status.Should().Be(WorkflowAppStatus.DaDuyet); + + var balances = await db.LeaveBalances.Where(b => b.UserId == requester.Id).ToListAsync(); + balances.Should().HaveCount(1, "chỉ 1 row, trừ đúng 1 lần ở terminal"); + balances[0].UsedDays.Should().Be(4m); + } + } + + // ============ Case 3: Accumulate existing balance — KHÔNG tạo row mới ============ + + [Fact] + public async Task Approve_LastLevel_AccumulatesExistingBalance_SameRow() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-b3@test.local", "Requester", null, Array.Empty()); + var approver = await fix.CreateUserAsync("ap-b3@test.local", "Approver", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id); + + // Pre-seed balance đã dùng 5 ngày cùng (User, Type, Year=2026) + db.LeaveBalances.Add(new LeaveBalance + { + Id = Guid.NewGuid(), + UserId = requester.Id, + LeaveTypeId = type.Id, + Year = 2026, + EntitledDays = 12m, + UsedDays = 5m, + AdjustmentDays = 0m, + CreatedAt = FixedNow, + }); + var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 2); + db.LeaveRequests.Add(leave); + await db.SaveChangesAsync(CancellationToken.None); + + await new ApproveLeaveRequestHandler(db, AsUser(approver), clock) + .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt"), CancellationToken.None); + + var balances = await db.LeaveBalances + .Where(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id && b.Year == 2026).ToListAsync(); + balances.Should().HaveCount(1, "UNIQUE (User,Type,Year) — accumulate KHÔNG tạo row mới"); + balances[0].UsedDays.Should().Be(7m, "5 + 2 cộng dồn"); + } + } + + // ============ Case 4: Negative allowed (allow+warn policy) — KHÔNG throw ============ + + [Fact] + public async Task Approve_LastLevel_OverEntitled_AllowsNegativeRemaining_NoThrow() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-b4@test.local", "Requester", null, Array.Empty()); + var approver = await fix.CreateUserAsync("ap-b4@test.local", "Approver", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id); + + // NumDays=20 > Entitled 12 → Remaining âm, KHÔNG được throw + var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 20); + db.LeaveRequests.Add(leave); + await db.SaveChangesAsync(CancellationToken.None); + + var act = async () => await new ApproveLeaveRequestHandler(db, AsUser(approver), clock) + .Handle(new ApproveLeaveRequestCommand(leave.Id, "duyệt vượt"), CancellationToken.None); + + await act.Should().NotThrowAsync("policy allow+warn — KHÔNG chặn vượt quota"); + leave.Status.Should().Be(WorkflowAppStatus.DaDuyet); + + var bal = await db.LeaveBalances.SingleAsync(b => b.UserId == requester.Id && b.LeaveTypeId == type.Id); + bal.UsedDays.Should().Be(20m); + (bal.EntitledDays + bal.AdjustmentDays - bal.UsedDays).Should().Be(-8m, "Remaining = 12 − 20 = −8"); + } + } + + // ============ Case 5a: Reject KHÔNG trừ ============ + + [Fact] + public async Task Reject_DoesNotDeductLeave() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-b5a@test.local", "Requester", null, Array.Empty()); + var approver = await fix.CreateUserAsync("ap-b5a@test.local", "Approver", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id); + + var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 3); + db.LeaveRequests.Add(leave); + await db.SaveChangesAsync(CancellationToken.None); + + await new RejectLeaveRequestHandler(db, AsUser(approver), clock) + .Handle(new RejectLeaveRequestCommand(leave.Id, "từ chối"), CancellationToken.None); + + leave.Status.Should().Be(WorkflowAppStatus.TuChoi); + (await db.LeaveBalances.CountAsync(b => b.UserId == requester.Id)) + .Should().Be(0, "chỉ terminal DaDuyet mới trừ — TuChoi KHÔNG"); + } + } + + // ============ Case 5b: Return KHÔNG trừ ============ + + [Fact] + public async Task Return_DoesNotDeductLeave() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-b5b@test.local", "Requester", null, Array.Empty()); + var approver = await fix.CreateUserAsync("ap-b5b@test.local", "Approver", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + var (wf, _) = await SeedLeaveWorkflowAsync(db, approver.Id); + + var leave = BuildLeave(requester.Id, type.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, numDays: 3); + db.LeaveRequests.Add(leave); + await db.SaveChangesAsync(CancellationToken.None); + + await new ReturnLeaveRequestHandler(db, AsUser(approver), clock) + .Handle(new ReturnLeaveRequestCommand(leave.Id, "trả lại sửa"), CancellationToken.None); + + leave.Status.Should().Be(WorkflowAppStatus.TraLai); + (await db.LeaveBalances.CountAsync(b => b.UserId == requester.Id)) + .Should().Be(0, "TraLai KHÔNG trừ phép"); + } + } + + // ============ Case 6: GetMyLeaveBalances lazy — synthesize default cho mọi active type ============ + + [Fact] + public async Task GetMyLeaveBalances_NoBalanceRows_SynthesizesDefaultsForActiveTypes() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var user = await fix.CreateUserAsync("req-b6@test.local", "User", null, Array.Empty()); + await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + await SeedLeaveTypeAsync(db, "SICK", daysPerYear: 30m); + // 1 type inactive — KHÔNG xuất hiện trong kết quả + await SeedLeaveTypeAsync(db, "RETIRED", daysPerYear: 5m, isActive: false); + + var result = await new GetMyLeaveBalancesHandler(db, AsUser(user)) + .Handle(new GetMyLeaveBalancesQuery(2026), CancellationToken.None); + + result.Should().HaveCount(2, "chỉ 2 type active, inactive bị loại"); + result.Should().OnlyContain(d => d.UsedDays == 0m, "chưa có row → Used=0"); + // ordered by Code: ANNUAL trước SICK + var annual = result.Single(d => d.Code == "ANNUAL"); + annual.EntitledDays.Should().Be(12m, "synthesize từ DaysPerYear"); + annual.RemainingDays.Should().Be(12m, "Remaining = 12 + 0 − 0"); + var sick = result.Single(d => d.Code == "SICK"); + sick.EntitledDays.Should().Be(30m); + sick.RemainingDays.Should().Be(30m); + } + } + + // ============ Case 7: AdjustLeaveBalance upsert — tạo row khi chưa có + query phản ánh ============ + + [Fact] + public async Task AdjustLeaveBalance_NoRow_CreatesRow_QueryReflectsRemaining() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var admin = await fix.CreateUserAsync("admin-b7@test.local", "Quản trị", null, new[] { "Admin" }); + var target = await fix.CreateUserAsync("tgt-b7@test.local", "Nhân viên", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + + // Admin upsert: EntitledDays=15 (override DaysPerYear), AdjustmentDays=2 (carry-over) + await new AdjustLeaveBalanceHandler(db, AsUser(admin, "Admin"), clock) + .Handle(new AdjustLeaveBalanceCommand(target.Id, type.Id, 2026, EntitledDays: 15m, AdjustmentDays: 2m), + CancellationToken.None); + + var bal = await db.LeaveBalances + .SingleAsync(b => b.UserId == target.Id && b.LeaveTypeId == type.Id && b.Year == 2026); + bal.EntitledDays.Should().Be(15m, "override DaysPerYear bằng giá trị admin nhập"); + bal.AdjustmentDays.Should().Be(2m); + bal.UsedDays.Should().Be(0m); + + // Query lại — Remaining phản ánh: 15 + 2 − 0 = 17 + var result = await new GetUserLeaveBalancesHandler(db) + .Handle(new GetUserLeaveBalancesQuery(target.Id, 2026), CancellationToken.None); + var annual = result.Single(d => d.Code == "ANNUAL"); + annual.EntitledDays.Should().Be(15m); + annual.AdjustmentDays.Should().Be(2m); + annual.RemainingDays.Should().Be(17m, "Remaining = Entitled 15 + Adjustment 2 − Used 0"); + } + } + + // ============ Guard P11-B: LeaveTypeId phải tồn tại (chặn 500 FK-fail lúc duyệt cuối) ============ + + [Fact] + public async Task CreateLeaveRequest_BogusLeaveTypeId_ThrowsConflict_NoRowAdded() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-guard@test.local", "Requester", null, Array.Empty()); + // KHÔNG seed LeaveType → LeaveTypeId random là bogus. + var act = async () => await new CreateLeaveRequestHandler(db, AsUser(requester), clock) + .Handle(new CreateLeaveRequestCommand(Guid.NewGuid(), FixedNow.Date, FixedNow.Date.AddDays(1), + 1m, "nghỉ", ApprovalWorkflowId: null), CancellationToken.None); + + await act.Should().ThrowAsync().WithMessage("*Loại phép không tồn tại*"); + (await db.LeaveRequests.CountAsync()).Should().Be(0, "guard chặn trước khi Add"); + } + } + + [Fact] + public async Task CreateLeaveRequest_ValidLeaveType_Succeeds() + { + var (fix, db, clock) = NewCtx(); + using (fix) + { + var requester = await fix.CreateUserAsync("req-guard2@test.local", "Requester", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", daysPerYear: 12m); + + var id = await new CreateLeaveRequestHandler(db, AsUser(requester), clock) + .Handle(new CreateLeaveRequestCommand(type.Id, FixedNow.Date, FixedNow.Date.AddDays(1), + 1m, "nghỉ", ApprovalWorkflowId: null), CancellationToken.None); + + id.Should().NotBeEmpty(); + (await db.LeaveRequests.CountAsync(x => x.Id == id)).Should().Be(1); + } + } +} diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/WorkflowAppApproveV2Tests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/WorkflowAppApproveV2Tests.cs index 3c38559..92f66e6 100644 --- a/tests/SolutionErp.Infrastructure.Tests/Application/WorkflowAppApproveV2Tests.cs +++ b/tests/SolutionErp.Infrastructure.Tests/Application/WorkflowAppApproveV2Tests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using SolutionErp.Application.Common.Exceptions; using SolutionErp.Application.Office; using SolutionErp.Domain.ApprovalWorkflowsV2; +using SolutionErp.Domain.Hrm; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Office; using SolutionErp.Infrastructure.Tests.Common; @@ -76,17 +77,20 @@ public class WorkflowAppApproveV2Tests return (wf, levels); } + // leaveTypeId: chỉ cần seeded type thật khi đơn đi tới TERMINAL (DaDuyet) — nhánh đó + // trừ phép insert LeaveBalance FK→LeaveTypes (Restrict). Test non-terminal để null (random). private static LeaveRequest BuildLeave( Guid requesterId, Guid? workflowId, WorkflowAppStatus status, - int? currentLevel) + int? currentLevel, + Guid? leaveTypeId = null) => new() { Id = Guid.NewGuid(), RequesterUserId = requesterId, RequesterFullName = "Người tạo", - LeaveTypeId = Guid.NewGuid(), + LeaveTypeId = leaveTypeId ?? Guid.NewGuid(), StartDate = FixedNow.Date, EndDate = FixedNow.Date.AddDays(2), NumDays = 3, @@ -96,6 +100,23 @@ public class WorkflowAppApproveV2Tests CurrentApprovalLevelOrder = currentLevel, }; + // Seed 1 LeaveType (FK target cho trừ phép terminal). Trả entity dùng Id. + private static async Task SeedLeaveTypeAsync(TestApplicationDbContext db, string code, decimal daysPerYear) + { + var type = new LeaveType + { + Id = Guid.NewGuid(), + Code = code, + Name = $"Loại {code}", + DaysPerYear = daysPerYear, + IsPaid = true, + IsActive = true, + }; + db.LeaveTypes.Add(type); + await db.SaveChangesAsync(CancellationToken.None); + return type; + } + // ============ Case 1: Submit happy path ============ [Fact] @@ -210,10 +231,11 @@ public class WorkflowAppApproveV2Tests var requester = await fix.CreateUserAsync("req-c4@test.local", "Requester", null, Array.Empty()); var ap1 = await fix.CreateUserAsync("ap1-c4@test.local", "Approver 1", null, Array.Empty()); var ap2 = await fix.CreateUserAsync("ap2-c4@test.local", "Approver 2", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", 12m); // terminal trừ phép → cần LeaveType thật var (wf, _) = await SeedLeaveWorkflowAsync(db, ap1.Id, ap2.Id); // pin tại Cấp cuối (2) - var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 2); + var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 2, leaveTypeId: type.Id); db.LeaveRequests.Add(leave); await db.SaveChangesAsync(CancellationToken.None); @@ -304,9 +326,10 @@ public class WorkflowAppApproveV2Tests { var requester = await fix.CreateUserAsync("req-c7@test.local", "Requester", null, Array.Empty()); var ap1 = await fix.CreateUserAsync("ap1-c7@test.local", "Approver 1", null, Array.Empty()); + var type = await SeedLeaveTypeAsync(db, "ANNUAL", 12m); // single-level → terminal → trừ phép cần LeaveType var (wf, levels) = await SeedLeaveWorkflowAsync(db, ap1.Id); - var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1); + var leave = BuildLeave(requester.Id, wf.Id, WorkflowAppStatus.DaGuiDuyet, currentLevel: 1, leaveTypeId: type.Id); db.LeaveRequests.Add(leave); await db.SaveChangesAsync(CancellationToken.None);