From 48a99e14e7a5cceb0cbb54624f5d1a7e1c928d23 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 26 May 2026 18:18:57 +0700 Subject: [PATCH] [CLAUDE] Domain+App+Infra: Plan B G-H1 Mig 34 EmployeeProfile + seed 30 demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 10.1 G-H1 Hồ sơ Nhân sự Foundation — port từ NamGroup CT_NHANSU (1675 NV, 10 bảng TblNhanVien*) sau anh main chốt S32 4 quyết định (scope FULL 11 module + DB single schema dbo + reuse Workflow V2 + chunk per-module Plan riêng). G-H1 CRITICAL FIRST vì depend by 8/11 module Phase 10 sau (Đề xuất/Đơn từ/OT/Đặt xe/Ticket/Dashboard NS/ Chấm công đều cần EmployeeProfile data). Investigator pre-flight (a103d20) audit NamGroup confirm: - Main TblNhanVien 105 cols (drop 35 cols duplicate User/UX legacy) - 5 satellite Phase 10.1 (defer 3 HĐLĐ Plan H2): WorkHistory + Education + FamilyRelation + Skill polymorphic Kind + Document - 6 enum thay catalog FK (Gender/MaritalStatus/EmployeeStatus/...) - DiaChi dual-write FK + freetext lesson Plan C NamGroup 1675 NV drift Em main 4 decision chốt: 1. 5 satellite Phase 10.1 (defer 3 HĐLĐ Plan H2) 2. Skill polymorphic Kind enum (gộp 3 NamGroup table) 3. DiaChi 6 FK Province/District/Ward declare nullable + freetext dual-write ngày đầu (FK constraint defer G-H2 khi catalog scaffold — Implementer smart decision documented EmployeeProfile.cs comment line 14-17) 4. MaNhanVien format NV/{YYYY}/{Seq:D4} atomic Serializable reset/year Implementer Case 2 (a8f4567) Pattern 12-bis cross-module mirror PE → Hrm cookie-cutter scaffold 17 file mới + 4 modified + 3-file mig rule: Domain (8 file SolutionErp.Domain.Hrm): - EmployeeProfile.cs (main ~70 cols inherit AuditableEntity, 1-1 UNIQUE User) - EmployeeWorkHistory.cs + EmployeeEducation.cs + EmployeeFamilyRelation.cs - EmployeeSkill.cs (polymorphic Kind=Computer/Language/Other) - EmployeeDocument.cs (IdCard/Passport/Degree/Certificate/LaborContract/Other) - EmployeeCodeSequence.cs (PK string Prefix, NOT BaseEntity Id Guid) - Enums.cs (10 enum gọn 1 file) Application (1 file): - IEmployeeCodeGenerator.cs interface (mirror IContractCodeGenerator) Infrastructure (8 file): - EmployeeCodeGenerator.cs impl IsolationLevel.Serializable transaction - 7 EF Configuration file (HasIndex UNIQUE UserId/EmployeeCode/Phone + HasMaxLength + HasColumnType decimal(18,2) + FK Cascade satellite) - DependencyInjection.cs (M): register IEmployeeCodeGenerator → impl Persistence (3 file modified + 2 new mig + 1 snapshot): - IApplicationDbContext.cs (M): +7 DbSet - ApplicationDbContext.cs (M): +7 DbSet impl - ApplicationDbContextModelSnapshot.cs (M): EF auto-update - 20260526110207_AddEmployeeProfiles.cs (NEW, EF auto-gen) - 20260526110207_AddEmployeeProfiles.Designer.cs (NEW, EF auto-gen) DbInitializer.cs (M, em main solo Task 3b ~90 LOC): - using SolutionErp.Domain.Hrm import added - SeedDemoEmployeeProfilesAsync method appended (end of class) - Register call after SeedDemoUsersAsync line 88 (depend user exist first) - NOT gated DemoSeed:Disabled flag (infrastructure data per gotcha #51 lesson) - 30 demo profile mirror 30 user @solutions.com.vn + sequential code NV/{YYYY}/0001..0030 + placeholder masked CMND/BHXH/Bank (bro UAT update qua FE Page Task 5) + EmployeeCodeSequence row LastSeq=30 → production gen tiếp 0031+ Verify: - dotnet build: 0 err 2 unrelated warn DocxRenderer (2.49s + 7.86s rebuild) - dotnet ef database update _Dev: Mig 34 applied (top of __EFMigrationsHistory) - dotnet test: **120/120 PASS** baseline preserved (no test add Phase 10 test-after per §7 UAT mode — test bundle defer Task 4+5+6 done) Stats target Phase 10 end: 33→42 mig (+9 Mig 34-42), 60→85 tables (+25), ~148→250 endpoint (+100), 38→60 FE pages (+22). Current after this commit: 33→34 mig + 60→66 tables + endpoint/FE unchanged (G-H1 Task 4+5 next). Pattern reusable cross-project: - Pattern 12-bis cross-module entity cookie-cutter mirror reinforced 3× (S29 Plan B Contract V2 + S33 Plan B G-H1 EmployeeProfile) - Infrastructure seed OUT of DemoSeed gate (gotcha #51 lesson, mirror Mig 32 SeedSampleContractWorkflowV2) - DiaChi dual-write FK + freetext từ ngày đầu (NamGroup 1675 NV drift lesson) Pending Plan B G-H1 Phase 2 (chờ anh main signal kick off): - Task 4 — Implementer BE CQRS handler + 6 endpoint controller - Task 5 — Implementer FE 2 app EmployeesPage 3-panel + 6 section tabs - Task 6 — Em main Permission menu Hrm_HoSo* seed - Task 7 — Reviewer pre-commit + CICD post-deploy verify Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Interfaces/IApplicationDbContext.cs | 12 + .../Hrm/Services/IEmployeeCodeGenerator.cs | 16 + .../Hrm/EmployeeCodeSequence.cs | 15 + .../Hrm/EmployeeDocument.cs | 26 + .../Hrm/EmployeeEducation.cs | 25 + .../Hrm/EmployeeFamilyRelation.cs | 20 + .../SolutionErp.Domain/Hrm/EmployeeProfile.cs | 137 + .../SolutionErp.Domain/Hrm/EmployeeSkill.cs | 30 + .../Hrm/EmployeeWorkHistory.cs | 23 + src/Backend/SolutionErp.Domain/Hrm/Enums.cs | 84 + .../DependencyInjection.cs | 2 + .../Persistence/ApplicationDbContext.cs | 10 + .../EmployeeCodeSequenceConfiguration.cs | 18 + .../EmployeeDocumentConfiguration.cs | 28 + .../EmployeeEducationConfiguration.cs | 25 + .../EmployeeFamilyRelationConfiguration.cs | 26 + .../EmployeeProfileConfiguration.cs | 101 + .../EmployeeSkillConfiguration.cs | 27 + .../EmployeeWorkHistoryConfiguration.cs | 29 + .../Persistence/DbInitializer.cs | 101 + ...0526110207_AddEmployeeProfiles.Designer.cs | 4712 +++++++++++++++++ .../20260526110207_AddEmployeeProfiles.cs | 356 ++ .../ApplicationDbContextModelSnapshot.cs | 679 +++ .../Services/EmployeeCodeGenerator.cs | 53 + 24 files changed, 6555 insertions(+) create mode 100644 src/Backend/SolutionErp.Application/Hrm/Services/IEmployeeCodeGenerator.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeCodeSequence.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeDocument.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeEducation.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeFamilyRelation.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeProfile.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeSkill.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/EmployeeWorkHistory.cs create mode 100644 src/Backend/SolutionErp.Domain/Hrm/Enums.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeCodeSequenceConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeDocumentConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeEducationConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeFamilyRelationConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeProfileConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeSkillConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeWorkHistoryConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Services/EmployeeCodeGenerator.cs diff --git a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs index 0782c99..7d2ef23 100644 --- a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs @@ -4,6 +4,7 @@ using SolutionErp.Domain.Budgets; using SolutionErp.Domain.Contracts; using SolutionErp.Domain.Contracts.Details; using SolutionErp.Domain.Forms; +using SolutionErp.Domain.Hrm; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Master; using SolutionErp.Domain.Master.Catalogs; @@ -83,5 +84,16 @@ public interface IApplicationDbContext DbSet BudgetChangelogs { get; } DbSet BudgetDepartmentApprovals { get; } + // Phase 10.1 G-H1 (Mig 34 — S33) — Hồ sơ Nhân sự port từ NamGroup. + // 1 main + 5 satellite + 1 sequence. 1-1 với User qua UserId UNIQUE. + // 3 HĐLĐ satellite defer Plan H2 sau. + DbSet EmployeeProfiles { get; } + DbSet EmployeeWorkHistories { get; } + DbSet EmployeeEducations { get; } + DbSet EmployeeFamilyRelations { get; } + DbSet EmployeeSkills { get; } + DbSet EmployeeDocuments { get; } + DbSet EmployeeCodeSequences { get; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/Backend/SolutionErp.Application/Hrm/Services/IEmployeeCodeGenerator.cs b/src/Backend/SolutionErp.Application/Hrm/Services/IEmployeeCodeGenerator.cs new file mode 100644 index 0000000..70c48ab --- /dev/null +++ b/src/Backend/SolutionErp.Application/Hrm/Services/IEmployeeCodeGenerator.cs @@ -0,0 +1,16 @@ +namespace SolutionErp.Application.Hrm.Services; + +// Atomic sequence generator cho MaNhanVien — mirror IContractCodeGenerator +// + IPurchaseEvaluationCodeGenerator pattern. +// +// Format: "NV/{YYYY}/{Seq:D4}" +// - YYYY = năm hiện tại (UTC) +// - Seq = 4 chữ số tăng dần, reset per năm +// +// VD: NV/2026/0001, NV/2026/0002, ... ; sang 2027 reset NV/2027/0001. +// +// Transaction SERIALIZABLE để tránh race condition khi 2 admin tạo NV cùng lúc. +public interface IEmployeeCodeGenerator +{ + Task GenerateAsync(CancellationToken ct = default); +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeCodeSequence.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeCodeSequence.cs new file mode 100644 index 0000000..62bb1ea --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeCodeSequence.cs @@ -0,0 +1,15 @@ +namespace SolutionErp.Domain.Hrm; + +// Sequence generator cho MaNhanVien format "NV/{YYYY}/{Seq:D4}". +// Prefix = "NV/2026" (per year), LastSeq tăng dần. +// Reset per năm: prefix "NV/2027" sẽ start LastSeq=1 lại. +// Update atomic qua transaction SERIALIZABLE (mirror ContractCodeSequence). +// +// PK là Prefix string NOT Id Guid (mirror Contract/PE pattern). KHÔNG inherit +// BaseEntity vì không cần audit fields trên sequence table. +public class EmployeeCodeSequence +{ + public string Prefix { get; set; } = string.Empty; + public int LastSeq { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeDocument.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeDocument.cs new file mode 100644 index 0000000..57fdbb7 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeDocument.cs @@ -0,0 +1,26 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Hrm; + +// Satellite — File scan CCCD/Bằng/Chứng chỉ/HĐLĐ (NamGroup `CT_TAILIEU`). +// FK Cascade từ EmployeeProfile. +// +// File lưu local storage qua IFileStorage (mirror PurchaseEvaluationAttachment +// + ContractAttachment pattern). FilePath relative root storage dir. +public class EmployeeDocument : AuditableEntity +{ + public Guid EmployeeProfileId { get; set; } + + public EmployeeDocumentType DocumentType { get; set; } + + public string FileName { get; set; } = string.Empty; // Tên file gốc khi upload + public string FilePath { get; set; } = string.Empty; // Relative path trong storage + public long FileSize { get; set; } // Byte + public string ContentType { get; set; } = string.Empty; // MIME (vd "application/pdf") + + public DateOnly? IssueDate { get; set; } // Ngày cấp + public DateOnly? ExpiryDate { get; set; } // Ngày hết hạn + public string? Notes { get; set; } + + public EmployeeProfile? EmployeeProfile { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeEducation.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeEducation.cs new file mode 100644 index 0000000..83174ae --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeEducation.cs @@ -0,0 +1,25 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Hrm; + +// Satellite — Quá trình học vấn (NamGroup `CT_QUATRINHHOCVAN`). +// FK Cascade từ EmployeeProfile. +public class EmployeeEducation : AuditableEntity +{ + public Guid EmployeeProfileId { get; set; } + + public string SchoolName { get; set; } = string.Empty; + public string? Major { get; set; } // Chuyên ngành + + public DegreeLevel? DegreeLevel { get; set; } // Trình độ (CĐ/ĐH/Th.S/TS) + public EducationMode? EducationMode { get; set; } // Hình thức (Chính quy/Tại chức/Từ xa) + public GradeLevel? GradeLevel { get; set; } // Xếp loại (TB/Khá/Giỏi) + + public DateOnly? FromDate { get; set; } + public DateOnly? ToDate { get; set; } + public DateOnly? CertificateIssueDate { get; set; } // Ngày cấp bằng + + public string? Notes { get; set; } + + public EmployeeProfile? EmployeeProfile { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeFamilyRelation.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeFamilyRelation.cs new file mode 100644 index 0000000..b75d121 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeFamilyRelation.cs @@ -0,0 +1,20 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Hrm; + +// Satellite — Quan hệ gia đình (NamGroup `CT_QUANHEGIADINH`). +// FK Cascade từ EmployeeProfile. +public class EmployeeFamilyRelation : AuditableEntity +{ + public Guid EmployeeProfileId { get; set; } + + public string FullName { get; set; } = string.Empty; + public FamilyRelationKind Relationship { get; set; } + public int? BirthYear { get; set; } // Năm sinh (chỉ year, không cần DateOnly đủ) + + public string? Occupation { get; set; } // Nghề nghiệp + public string? CurrentAddress { get; set; } // Địa chỉ hiện tại + public string? Phone { get; set; } + + public EmployeeProfile? EmployeeProfile { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeProfile.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeProfile.cs new file mode 100644 index 0000000..c1c95dc --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeProfile.cs @@ -0,0 +1,137 @@ +using SolutionErp.Domain.Common; +using SolutionErp.Domain.Identity; + +namespace SolutionErp.Domain.Hrm; + +// Phase 10.1 G-H1 — Hồ sơ Nhân sự main entity (Mig 34). +// 1-1 với User qua UserId UNIQUE FK. AuditableEntity inherit cho soft delete. +// +// Port từ NamGroup CT_NHANSU (1675 NV) — Investigator field map đã verified với +// 10 NamGroup table. 5 satellite entity Phase 10.1 (defer 3 HĐLĐ Plan H2 sau): +// EmployeeWorkHistory + EmployeeEducation + EmployeeFamilyRelation + +// EmployeeSkill (polymorphic Kind) + EmployeeDocument. +// +// DiaChi dual-write 6 FK + freetext: spec gốc declare FK Province/District/Ward +// nhưng catalog chưa scaffold trong Mig 34 — DEFER FK constraint sang G-H2 khi +// thêm catalog Province/District/Ward. Plain nullable Guid? lưu giá trị tham +// chiếu tương lai + freetext snapshot song hành ngày đầu (lesson NamGroup drift). +// +// MaNhanVien format "NV/{YYYY}/{Seq:D4}" gen atomic SERIALIZABLE qua +// IEmployeeCodeGenerator (mirror IContractCodeGenerator pattern). +public class EmployeeProfile : AuditableEntity +{ + // ===== Identity link ===== + // 1-1 với User.Id (UNIQUE index). NV phải có user account login system. + public Guid UserId { get; set; } + + // Mã nhân viên format "NV/2026/0001" — gen atomic per-year reset sequence. + public string EmployeeCode { get; set; } = string.Empty; + + // ===== Trạng thái + phân loại ===== + public EmployeeStatus EmployeeStatus { get; set; } = EmployeeStatus.Active; + public Gender? Gender { get; set; } + public MaritalStatus? MaritalStatus { get; set; } + public EmployeeType? EmployeeType { get; set; } + + // ===== Thông tin cá nhân cơ bản ===== + public DateOnly? DateOfBirth { get; set; } + public string? BirthPlace { get; set; } // Nơi sinh + public string? Hometown { get; set; } // Nguyên quán + + // ===== Liên hệ ===== + public string? Phone { get; set; } // SDT cá nhân (INDEX) + public string? PersonalEmail { get; set; } // Email cá nhân (khác email login) + public string? InternalPhone { get; set; } // SDT nội bộ + + // ===== Dân tộc + tôn giáo + quốc tịch ===== + public string? Ethnicity { get; set; } // Dân tộc + public string? Religion { get; set; } // Tôn giáo + public string Nationality { get; set; } = "Việt Nam"; + + // ===== Giấy tờ tuỳ thân ===== + public string? IdCardNumber { get; set; } // CCCD/CMND + public DateOnly? IdCardIssueDate { get; set; } + public string? IdCardIssuePlace { get; set; } + + public string? TaxCode { get; set; } // MST cá nhân + public string? SocialInsuranceNumber { get; set; } // Số sổ BHXH + public string? PassportNumber { get; set; } + + // ===== Địa chỉ HKTT (Hộ khẩu thường trú) — dual-write FK + freetext ===== + // FK Province/District/Ward DEFER G-H2 khi catalog scaffold (plain Guid? ngày đầu). + public string? PermanentAddressText { get; set; } // Freetext snapshot full (vd "123 Nguyễn Văn A, P. Bến Nghé, Q.1, TP.HCM") + public Guid? PermanentProvinceId { get; set; } // FK→Provinces (defer) + public Guid? PermanentDistrictId { get; set; } // FK→Districts (defer) + public Guid? PermanentWardId { get; set; } // FK→Wards (defer) + public string? StreetAddressPermanent { get; set; } // Số nhà + tên đường + + // ===== Địa chỉ Tạm trú — dual-write FK + freetext ===== + public string? TemporaryAddressText { get; set; } + public Guid? TemporaryProvinceId { get; set; } + public Guid? TemporaryDistrictId { get; set; } + public Guid? TemporaryWardId { get; set; } + public string? StreetAddressTemporary { get; set; } + + // ===== Tuyển dụng + nghỉ việc ===== + public DateOnly? HireDate { get; set; } // Ngày vào làm + public DateOnly? ResignDate { get; set; } // Ngày nghỉ việc + + // ===== Liên hệ khẩn cấp (inline NOT satellite — chỉ 1 contact) ===== + public string? EmergencyContactName { get; set; } + public string? EmergencyContactPhone { get; set; } + public string? EmergencyContactAddress { get; set; } + + // ===== Trình độ + chức danh ===== + public string? Qualification { get; set; } // Trình độ chuyên môn cao nhất (vd "Thạc sĩ XD") + public string? AcademicTitle { get; set; } // Học hàm/học vị (vd "PGS.TS.") + + // ===== Vị trí công tác ===== + public string? WorkLocation { get; set; } // Nơi làm việc (công trường / VP) + public string? TimekeepingCode { get; set; } // Mã chấm công (sync máy chấm) + + // ===== Tài khoản ngân hàng ===== + public string? BankAccount { get; set; } + public string? BankName { get; set; } + public string? BankBranch { get; set; } + + // ===== Sức khoẻ ===== + public int? HeightCm { get; set; } + public int? WeightKg { get; set; } + public string? BloodType { get; set; } // "A+", "O-", "AB" ... + + // ===== Lương ===== + // decimal NOT double (tránh floating point error tài chính). + public decimal? BaseSalary { get; set; } // Lương cơ bản + public decimal? TotalSalary { get; set; } // Tổng lương (bao gồm phụ cấp) + + // ===== Phép ===== + // decimal(5,2) — vd 12.5 ngày phép. + public decimal? AnnualLeaveDays { get; set; } // Phép năm + public decimal? RemainingLeaveDays { get; set; } // Phép còn lại + public decimal? CompensatoryLeaveDays { get; set; } // Phép bù + public decimal? SeniorityLeaveDays { get; set; } // Phép thâm niên + + // ===== BHXH + BHYT ===== + public DateOnly? SocialInsuranceStartDate { get; set; } // Ngày bắt đầu đóng BHXH + public string? MedicalRegistrationPlace { get; set; } // Nơi đăng ký KCB ban đầu BHYT + + // ===== Đoàn thể ===== + public bool IsCommunistParty { get; set; } + public DateOnly? CommunistPartyJoinDate { get; set; } + public bool IsYouthUnion { get; set; } + public DateOnly? YouthUnionJoinDate { get; set; } + public bool IsTradeUnion { get; set; } + public DateOnly? TradeUnionJoinDate { get; set; } + + // ===== Khác ===== + public string? PhotoUrl { get; set; } // URL ảnh đại diện + public string? Notes { get; set; } // Ghi chú free text + + // ===== Navigation ===== + public User? User { get; set; } + public ICollection WorkHistories { get; set; } = new List(); + public ICollection Educations { get; set; } = new List(); + public ICollection FamilyRelations { get; set; } = new List(); + public ICollection Skills { get; set; } = new List(); + public ICollection Documents { get; set; } = new List(); +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeSkill.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeSkill.cs new file mode 100644 index 0000000..f596186 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeSkill.cs @@ -0,0 +1,30 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Hrm; + +// Satellite POLYMORPHIC — gộp 3 NamGroup table: +// CT_KYNANGVITINH (Kind=Computer) +// CT_NGOAINGU (Kind=Language) +// CT_KYNANGKHAC (Kind=Other) +// Decision em main: 1 entity polymorphic discriminator Kind enum để tránh +// 3 satellite riêng cho cùng pattern "skill". +// +// Field semantic mapping per Kind: +// Computer: Name=TenPhanMem (vd "AutoCAD"), Level=TrinhDo (vd "Thành thạo") +// Language: Name=TenNgoaiNgu (vd "Tiếng Anh"), LanguageId=ISO code "en"|"fr"|"zh"|"jp"|"vi", +// Level=BangCapChungChi (vd "IELTS 7.0", "TOEIC 800") +// Other: Name=TenKyNangKhac (vd "Lãnh đạo nhóm"), Level=free text +// +// FK Cascade từ EmployeeProfile. +public class EmployeeSkill : AuditableEntity +{ + public Guid EmployeeProfileId { get; set; } + + public SkillKind Kind { get; set; } + + public string Name { get; set; } = string.Empty; // Required (semantic theo Kind) + public string? LanguageId { get; set; } // ISO code chỉ set khi Kind=Language ("en"/"fr"/...) + public string? Level { get; set; } // TrinhDo / BangCapChungChi / free text + + public EmployeeProfile? EmployeeProfile { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/EmployeeWorkHistory.cs b/src/Backend/SolutionErp.Domain/Hrm/EmployeeWorkHistory.cs new file mode 100644 index 0000000..e71fda3 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/EmployeeWorkHistory.cs @@ -0,0 +1,23 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Hrm; + +// Satellite — Quá trình công tác (NamGroup `CT_QUATRINHCONGTAC`). +// Cookie-cutter port. FK Cascade từ EmployeeProfile (xoá NV → xoá history). +public class EmployeeWorkHistory : AuditableEntity +{ + public Guid EmployeeProfileId { get; set; } + + public string CompanyName { get; set; } = string.Empty; + public string? CompanyAddress { get; set; } + public string? Industry { get; set; } // Ngành nghề + + public DateOnly? FromDate { get; set; } + public DateOnly? ToDate { get; set; } + + public string? JobTitle { get; set; } // Chức danh + public string? JobDescription { get; set; } // Mô tả công việc + public string? ResignReason { get; set; } // Lý do nghỉ + + public EmployeeProfile? EmployeeProfile { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Hrm/Enums.cs b/src/Backend/SolutionErp.Domain/Hrm/Enums.cs new file mode 100644 index 0000000..1ee4d49 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Hrm/Enums.cs @@ -0,0 +1,84 @@ +namespace SolutionErp.Domain.Hrm; + +// Phase 10.1 G-H1 — Hồ sơ Nhân sự enum set. 10 enum gọn 1 file. +// Port từ NamGroup CT_NHANSU (1675 NV) catalog. Int storage (TS6 erasableSyntaxOnly +// FE mirror dùng const-object pattern, NOT enum). + +public enum EmployeeStatus +{ + Active = 1, // Đang làm việc + OnLeave = 2, // Nghỉ phép / Tạm hoãn HĐ + Resigned = 3, // Đã nghỉ việc +} + +public enum Gender +{ + Male = 1, + Female = 2, + Other = 3, +} + +public enum MaritalStatus +{ + Single = 1, // Độc thân + Married = 2, // Đã kết hôn + Divorced = 3, // Đã ly hôn + Widowed = 4, // Goá +} + +public enum EmployeeType +{ + FullTime = 1, // Chính thức + PartTime = 2, // Bán thời gian + Intern = 3, // Thực tập + Contractor = 4, // Khoán việc +} + +public enum DegreeLevel +{ + College = 1, // Cao đẳng + Bachelor = 2, // Đại học + Master = 3, // Thạc sĩ + PhD = 4, // Tiến sĩ +} + +public enum EducationMode +{ + FullTime = 1, // Chính quy + PartTime = 2, // Tại chức + Distance = 3, // Từ xa +} + +public enum GradeLevel +{ + Average = 1, // Trung bình + Good = 2, // Khá + Excellent = 3, // Giỏi +} + +public enum FamilyRelationKind +{ + Father = 1, + Mother = 2, + Spouse = 3, // Vợ/Chồng + Child = 4, // Con + Sibling = 5, // Anh/Chị/Em ruột + Other = 99, // Khác +} + +public enum SkillKind +{ + Computer = 1, // Kỹ năng vi tính (TenPhanMem + TrinhDo) + Language = 2, // Ngoại ngữ (LanguageId + BangCapChungChi) + Other = 3, // Kỹ năng khác (TenKyNangKhac + Level free text) +} + +public enum EmployeeDocumentType +{ + IdCard = 1, // CMND/CCCD scan + Passport = 2, // Hộ chiếu + Degree = 3, // Bằng cấp + Certificate = 4, // Chứng chỉ + LaborContract = 5, // HĐLĐ + Other = 99, +} diff --git a/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs b/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs index 79a8920..d7a221e 100644 --- a/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs +++ b/src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using SolutionErp.Application.Common.Interfaces; using SolutionErp.Application.Contracts.Services; using SolutionErp.Application.Forms.Services; +using SolutionErp.Application.Hrm.Services; using SolutionErp.Application.Notifications; using SolutionErp.Application.PurchaseEvaluations.Services; using SolutionErp.Application.Reports.Services; @@ -35,6 +36,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs index eb15b81..f7bc350 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs @@ -6,6 +6,7 @@ using SolutionErp.Domain.Budgets; using SolutionErp.Domain.Contracts; using SolutionErp.Domain.Contracts.Details; using SolutionErp.Domain.Forms; +using SolutionErp.Domain.Hrm; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Master; using SolutionErp.Domain.Master.Catalogs; @@ -80,6 +81,15 @@ public class ApplicationDbContext public DbSet BudgetChangelogs => Set(); public DbSet BudgetDepartmentApprovals => Set(); + // Phase 10.1 G-H1 (Mig 34 — S33) — Hồ sơ Nhân sự port từ NamGroup. + public DbSet EmployeeProfiles => Set(); + public DbSet EmployeeWorkHistories => Set(); + public DbSet EmployeeEducations => Set(); + public DbSet EmployeeFamilyRelations => Set(); + public DbSet EmployeeSkills => Set(); + public DbSet EmployeeDocuments => Set(); + public DbSet EmployeeCodeSequences => Set(); + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeCodeSequenceConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeCodeSequenceConfiguration.cs new file mode 100644 index 0000000..cc9bc2c --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeCodeSequenceConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — Sequence table cho MaNhanVien. PK Prefix string. +// Mirror ContractCodeSequenceConfiguration pattern. +public class EmployeeCodeSequenceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeCodeSequences"); + + e.HasKey(x => x.Prefix); + e.Property(x => x.Prefix).HasMaxLength(50); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeDocumentConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeDocumentConfiguration.cs new file mode 100644 index 0000000..44df994 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeDocumentConfiguration.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — File scan attachment (satellite). FK Cascade EmployeeProfile. +// INDEX DocumentType cho filter "tất cả bằng cấp của NV X". +public class EmployeeDocumentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeDocuments"); + + e.Property(x => x.FileName).HasMaxLength(255).IsRequired(); + e.Property(x => x.FilePath).HasMaxLength(500).IsRequired(); + e.Property(x => x.ContentType).HasMaxLength(100).IsRequired(); + e.Property(x => x.Notes).HasMaxLength(500); + + e.HasOne(x => x.EmployeeProfile) + .WithMany(p => p.Documents) + .HasForeignKey(x => x.EmployeeProfileId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(x => x.EmployeeProfileId); + e.HasIndex(x => x.DocumentType); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeEducationConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeEducationConfiguration.cs new file mode 100644 index 0000000..4999212 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeEducationConfiguration.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — Quá trình học vấn (satellite). FK Cascade EmployeeProfile. +public class EmployeeEducationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeEducations"); + + e.Property(x => x.SchoolName).HasMaxLength(200).IsRequired(); + e.Property(x => x.Major).HasMaxLength(200); + e.Property(x => x.Notes).HasMaxLength(500); + + e.HasOne(x => x.EmployeeProfile) + .WithMany(p => p.Educations) + .HasForeignKey(x => x.EmployeeProfileId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(x => x.EmployeeProfileId); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeFamilyRelationConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeFamilyRelationConfiguration.cs new file mode 100644 index 0000000..c6f1439 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeFamilyRelationConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — Quan hệ gia đình (satellite). FK Cascade EmployeeProfile. +public class EmployeeFamilyRelationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeFamilyRelations"); + + e.Property(x => x.FullName).HasMaxLength(200).IsRequired(); + e.Property(x => x.Occupation).HasMaxLength(200); + e.Property(x => x.CurrentAddress).HasMaxLength(500); + e.Property(x => x.Phone).HasMaxLength(20); + + e.HasOne(x => x.EmployeeProfile) + .WithMany(p => p.FamilyRelations) + .HasForeignKey(x => x.EmployeeProfileId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(x => x.EmployeeProfileId); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeProfileConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeProfileConfiguration.cs new file mode 100644 index 0000000..b640a48 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeProfileConfiguration.cs @@ -0,0 +1,101 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; +using SolutionErp.Domain.Identity; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — Hồ sơ Nhân sự main entity. +// 1-1 User qua UNIQUE UserId. FK Cascade Users (xoá user → wipe profile). +// EmployeeCode UNIQUE. Phone INDEX (lookup nhanh). +// IsDeleted INDEX (soft delete query filter). +// +// Province/District/Ward 6 cột nullable plain Guid? — KHÔNG declare FK +// constraint (catalog chưa scaffold trong Mig 34, defer G-H2 khi thêm catalog +// thì alter table add FK). +public class EmployeeProfileConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeProfiles"); + + // ===== EmployeeCode + UserId UNIQUE ===== + e.Property(x => x.EmployeeCode).HasMaxLength(50).IsRequired(); + e.HasIndex(x => x.EmployeeCode).IsUnique(); + + e.HasIndex(x => x.UserId).IsUnique(); + + // ===== 1-1 User FK Cascade ===== + e.HasOne(x => x.User) + .WithOne() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.Cascade); + + // ===== Cá nhân ===== + e.Property(x => x.BirthPlace).HasMaxLength(200); + e.Property(x => x.Hometown).HasMaxLength(200); + + e.Property(x => x.Phone).HasMaxLength(20); + e.HasIndex(x => x.Phone); // Non-unique (nhiều NV cùng nhà có thể chung SDT cố định) + e.Property(x => x.PersonalEmail).HasMaxLength(200); + e.Property(x => x.InternalPhone).HasMaxLength(20); + + e.Property(x => x.Ethnicity).HasMaxLength(50); + e.Property(x => x.Religion).HasMaxLength(50); + e.Property(x => x.Nationality).HasMaxLength(50).IsRequired(); + + // ===== Giấy tờ ===== + e.Property(x => x.IdCardNumber).HasMaxLength(20); + e.Property(x => x.IdCardIssuePlace).HasMaxLength(200); + e.Property(x => x.TaxCode).HasMaxLength(20); + e.Property(x => x.SocialInsuranceNumber).HasMaxLength(20); + e.Property(x => x.PassportNumber).HasMaxLength(20); + + // ===== Địa chỉ HKTT (no FK — defer G-H2) ===== + e.Property(x => x.PermanentAddressText).HasMaxLength(500); + e.Property(x => x.StreetAddressPermanent).HasMaxLength(200); + + // ===== Địa chỉ Tạm trú (no FK — defer G-H2) ===== + e.Property(x => x.TemporaryAddressText).HasMaxLength(500); + e.Property(x => x.StreetAddressTemporary).HasMaxLength(200); + + // ===== Khẩn cấp ===== + e.Property(x => x.EmergencyContactName).HasMaxLength(200); + e.Property(x => x.EmergencyContactPhone).HasMaxLength(20); + e.Property(x => x.EmergencyContactAddress).HasMaxLength(500); + + // ===== Trình độ + chức danh ===== + e.Property(x => x.Qualification).HasMaxLength(200); + e.Property(x => x.AcademicTitle).HasMaxLength(200); + + // ===== Vị trí ===== + e.Property(x => x.WorkLocation).HasMaxLength(200); + e.Property(x => x.TimekeepingCode).HasMaxLength(50); + + // ===== Ngân hàng ===== + e.Property(x => x.BankAccount).HasMaxLength(50); + e.Property(x => x.BankName).HasMaxLength(200); + e.Property(x => x.BankBranch).HasMaxLength(200); + + // ===== Sức khoẻ ===== + e.Property(x => x.BloodType).HasMaxLength(5); + + // ===== Lương + Phép — decimal precision ===== + e.Property(x => x.BaseSalary).HasColumnType("decimal(18,2)"); + e.Property(x => x.TotalSalary).HasColumnType("decimal(18,2)"); + e.Property(x => x.AnnualLeaveDays).HasColumnType("decimal(5,2)"); + e.Property(x => x.RemainingLeaveDays).HasColumnType("decimal(5,2)"); + e.Property(x => x.CompensatoryLeaveDays).HasColumnType("decimal(5,2)"); + e.Property(x => x.SeniorityLeaveDays).HasColumnType("decimal(5,2)"); + + // ===== BHXH ===== + e.Property(x => x.MedicalRegistrationPlace).HasMaxLength(200); + + // ===== Khác ===== + e.Property(x => x.PhotoUrl).HasMaxLength(500); + e.Property(x => x.Notes).HasMaxLength(2000); + + // ===== Soft delete index ===== + e.HasIndex(x => x.IsDeleted); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeSkillConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeSkillConfiguration.cs new file mode 100644 index 0000000..2fdd3ba --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeSkillConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — Skill polymorphic Kind=Computer/Language/Other. +// FK Cascade EmployeeProfile. INDEX Kind cho filter "tất cả ngoại ngữ của NV X". +public class EmployeeSkillConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeSkills"); + + e.Property(x => x.Name).HasMaxLength(200).IsRequired(); + e.Property(x => x.LanguageId).HasMaxLength(20); + e.Property(x => x.Level).HasMaxLength(200); + + e.HasOne(x => x.EmployeeProfile) + .WithMany(p => p.Skills) + .HasForeignKey(x => x.EmployeeProfileId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(x => x.EmployeeProfileId); + e.HasIndex(x => x.Kind); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeWorkHistoryConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeWorkHistoryConfiguration.cs new file mode 100644 index 0000000..285ce42 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/EmployeeWorkHistoryConfiguration.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF Mig 34 — Quá trình công tác (satellite). FK Cascade EmployeeProfile. +public class EmployeeWorkHistoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("EmployeeWorkHistories"); + + e.Property(x => x.CompanyName).HasMaxLength(200).IsRequired(); + e.Property(x => x.CompanyAddress).HasMaxLength(500); + e.Property(x => x.Industry).HasMaxLength(200); + + e.Property(x => x.JobTitle).HasMaxLength(200); + e.Property(x => x.JobDescription).HasMaxLength(2000); + e.Property(x => x.ResignReason).HasMaxLength(500); + + e.HasOne(x => x.EmployeeProfile) + .WithMany(p => p.WorkHistories) + .HasForeignKey(x => x.EmployeeProfileId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(x => x.EmployeeProfileId); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 2af4775..a90a699 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -7,6 +7,7 @@ using SolutionErp.Application.Contracts.Services; using SolutionErp.Domain.ApprovalWorkflowsV2; using SolutionErp.Domain.Contracts; using SolutionErp.Domain.Forms; +using SolutionErp.Domain.Hrm; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Master; using SolutionErp.Domain.Master.Catalogs; @@ -86,6 +87,11 @@ public static class DbInitializer await SeedAdminAsync(userManager, logger); await SeedDepartmentsAsync(db, logger); await SeedDemoUsersAsync(db, userManager, logger); + // Plan B G-H1 (Mig 34 S33 2026-05-26) — seed EmployeeProfile 1-1 với + // mọi user @solutions.com.vn. Idempotent. NOT gated DemoSeed flag + // (infrastructure data, mirror Mig 32 SeedSampleContractWorkflowV2 + // gotcha #51 lesson). Bro UAT update field nhạy cảm qua FE Hồ sơ NS. + await SeedDemoEmployeeProfilesAsync(db, userManager, logger); await SeedMenuTreeAsync(db, logger); await SeedAdminPermissionsAsync(db, roleManager, logger); await SeedDemoMasterDataAsync(db, logger); @@ -1919,4 +1925,99 @@ public static class DbInitializer logger.LogInformation("Seeded {Count} contract templates (active file check)", added); } } + + // Plan B G-H1 (Mig 34 — S33 2026-05-26) — seed EmployeeProfile cho mọi + // user @solutions.com.vn (30 demo + admin). Idempotent — skip nếu đã có + // EmployeeProfile. Sequential code NV/{Year}/0001..N. Field nhạy cảm + // (CMND/BHXH/Bank) PLACEHOLDER masked — bro UAT update real qua FE Hồ sơ + // NS Page (Task 5). + // + // Cũng seed EmployeeCodeSequence "NV/{Year}" với LastSeq=N → production + // gen tiếp từ N+1 (mirror PE/Contract CodeGen pattern). + // + // NOT gated DemoSeed:Disabled flag vì EmployeeProfile là INFRASTRUCTURE + // data (1-1 với User), không phải DEMO content như sample HĐ/PE. Gotcha + // #51 lesson (S29 Plan B): infra seed phải OUT of flag gate. + private static async Task SeedDemoEmployeeProfilesAsync( + ApplicationDbContext db, UserManager userManager, ILogger logger) + { + if (await db.EmployeeProfiles.AnyAsync()) + { + logger.LogInformation("SeedDemoEmployeeProfilesAsync: skip — đã có EmployeeProfile."); + return; + } + + var users = await userManager.Users + .Where(u => u.Email != null && u.Email.EndsWith("@solutions.com.vn")) + .OrderBy(u => u.Email) + .ToListAsync(); + + if (users.Count == 0) + { + logger.LogWarning("SeedDemoEmployeeProfilesAsync: no users @solutions.com.vn found — run SeedDemoUsersAsync first."); + return; + } + + var rng = new Random(20260526); // Deterministic seed for reproducibility + var year = DateTime.UtcNow.Year; + var seq = 0; + foreach (var u in users) + { + seq++; + var birthYear = 1970 + rng.Next(0, 26); // 1970-1995 → 30-55 tuổi + var hireYear = 2020 + (seq % 6); // 2020-2025 + var profile = new EmployeeProfile + { + Id = Guid.NewGuid(), + UserId = u.Id, + EmployeeCode = $"NV/{year}/{seq:D4}", + EmployeeStatus = SolutionErp.Domain.Hrm.EmployeeStatus.Active, + Gender = (seq % 2 == 0) ? SolutionErp.Domain.Hrm.Gender.Female : SolutionErp.Domain.Hrm.Gender.Male, + MaritalStatus = (seq % 3 == 0) ? SolutionErp.Domain.Hrm.MaritalStatus.Single : SolutionErp.Domain.Hrm.MaritalStatus.Married, + EmployeeType = SolutionErp.Domain.Hrm.EmployeeType.FullTime, + DateOfBirth = new DateOnly(birthYear, rng.Next(1, 13), rng.Next(1, 28)), + BirthPlace = "Hà Nội", + Hometown = "Hà Nội", + Phone = $"09{rng.Next(10000000, 99999999)}", + PersonalEmail = u.Email, + Nationality = "Việt Nam", + Ethnicity = "Kinh", + IdCardNumber = $"001099{seq:D6}", // PLACEHOLDER masked + IdCardIssueDate = new DateOnly(2020, 1, 15), + IdCardIssuePlace = "Cục CS QLHC về TTXH", + TaxCode = $"8{seq:D9}", + SocialInsuranceNumber = $"BHXH{seq:D8}", + PermanentAddressText = "Hà Nội (cập nhật qua Hồ sơ NS)", + HireDate = new DateOnly(hireYear, ((seq % 12) + 1), 1), + EmergencyContactName = "Người thân — cập nhật sau", + EmergencyContactPhone = $"09{rng.Next(10000000, 99999999)}", + Qualification = (seq % 4 == 0) ? "Thạc sĩ" : "Đại học", + WorkLocation = (seq % 3 == 0) ? "Công trường" : "Văn phòng", + BankAccount = $"19030000{seq:D4}", + BankName = "Techcombank", + BankBranch = "Hà Nội", + BaseSalary = 15_000_000m + (seq % 10) * 1_000_000m, + AnnualLeaveDays = 12m, + RemainingLeaveDays = 12m - (seq % 5), + SocialInsuranceStartDate = new DateOnly(hireYear, ((seq % 12) + 1), 1), + IsCommunistParty = false, + IsYouthUnion = (seq % 4 == 0), + IsTradeUnion = true, + CreatedAt = DateTime.UtcNow, + }; + db.EmployeeProfiles.Add(profile); + } + + db.EmployeeCodeSequences.Add(new EmployeeCodeSequence + { + Prefix = $"NV/{year}", + LastSeq = seq, + UpdatedAt = DateTime.UtcNow, + }); + + await db.SaveChangesAsync(); + logger.LogInformation( + "SeedDemoEmployeeProfilesAsync: seeded {Count} profiles + 1 sequence row NV/{Year} LastSeq={Seq}", + seq, year, seq); + } } diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.Designer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.Designer.cs new file mode 100644 index 0000000..dd6bd6e --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.Designer.cs @@ -0,0 +1,4712 @@ +// +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("20260526110207_AddEmployeeProfiles")] + partial class AddEmployeeProfiles + { + /// + 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.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.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.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.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.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/20260526110207_AddEmployeeProfiles.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.cs new file mode 100644 index 0000000..6dd634b --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260526110207_AddEmployeeProfiles.cs @@ -0,0 +1,356 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddEmployeeProfiles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EmployeeCodeSequences", + columns: table => new + { + Prefix = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + LastSeq = table.Column(type: "int", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EmployeeCodeSequences", x => x.Prefix); + }); + + migrationBuilder.CreateTable( + name: "EmployeeProfiles", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: false), + EmployeeCode = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + EmployeeStatus = table.Column(type: "int", nullable: false), + Gender = table.Column(type: "int", nullable: true), + MaritalStatus = table.Column(type: "int", nullable: true), + EmployeeType = table.Column(type: "int", nullable: true), + DateOfBirth = table.Column(type: "date", nullable: true), + BirthPlace = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Hometown = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Phone = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + PersonalEmail = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + InternalPhone = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + Ethnicity = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Religion = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Nationality = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + IdCardNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + IdCardIssueDate = table.Column(type: "date", nullable: true), + IdCardIssuePlace = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + TaxCode = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + SocialInsuranceNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + PassportNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + PermanentAddressText = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + PermanentProvinceId = table.Column(type: "uniqueidentifier", nullable: true), + PermanentDistrictId = table.Column(type: "uniqueidentifier", nullable: true), + PermanentWardId = table.Column(type: "uniqueidentifier", nullable: true), + StreetAddressPermanent = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + TemporaryAddressText = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + TemporaryProvinceId = table.Column(type: "uniqueidentifier", nullable: true), + TemporaryDistrictId = table.Column(type: "uniqueidentifier", nullable: true), + TemporaryWardId = table.Column(type: "uniqueidentifier", nullable: true), + StreetAddressTemporary = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + HireDate = table.Column(type: "date", nullable: true), + ResignDate = table.Column(type: "date", nullable: true), + EmergencyContactName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + EmergencyContactPhone = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + EmergencyContactAddress = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Qualification = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + AcademicTitle = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + WorkLocation = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + TimekeepingCode = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + BankAccount = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + BankName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + BankBranch = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + HeightCm = table.Column(type: "int", nullable: true), + WeightKg = table.Column(type: "int", nullable: true), + BloodType = table.Column(type: "nvarchar(5)", maxLength: 5, nullable: true), + BaseSalary = table.Column(type: "decimal(18,2)", nullable: true), + TotalSalary = table.Column(type: "decimal(18,2)", nullable: true), + AnnualLeaveDays = table.Column(type: "decimal(5,2)", nullable: true), + RemainingLeaveDays = table.Column(type: "decimal(5,2)", nullable: true), + CompensatoryLeaveDays = table.Column(type: "decimal(5,2)", nullable: true), + SeniorityLeaveDays = table.Column(type: "decimal(5,2)", nullable: true), + SocialInsuranceStartDate = table.Column(type: "date", nullable: true), + MedicalRegistrationPlace = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + IsCommunistParty = table.Column(type: "bit", nullable: false), + CommunistPartyJoinDate = table.Column(type: "date", nullable: true), + IsYouthUnion = table.Column(type: "bit", nullable: false), + YouthUnionJoinDate = table.Column(type: "date", nullable: true), + IsTradeUnion = table.Column(type: "bit", nullable: false), + TradeUnionJoinDate = table.Column(type: "date", nullable: true), + PhotoUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Notes = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + 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_EmployeeProfiles", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeProfiles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EmployeeDocuments", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EmployeeProfileId = table.Column(type: "uniqueidentifier", nullable: false), + DocumentType = table.Column(type: "int", nullable: false), + FileName = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + FilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + FileSize = table.Column(type: "bigint", nullable: false), + ContentType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + IssueDate = table.Column(type: "date", nullable: true), + ExpiryDate = table.Column(type: "date", nullable: true), + Notes = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + 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_EmployeeDocuments", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeDocuments_EmployeeProfiles_EmployeeProfileId", + column: x => x.EmployeeProfileId, + principalTable: "EmployeeProfiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EmployeeEducations", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EmployeeProfileId = table.Column(type: "uniqueidentifier", nullable: false), + SchoolName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Major = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + DegreeLevel = table.Column(type: "int", nullable: true), + EducationMode = table.Column(type: "int", nullable: true), + GradeLevel = table.Column(type: "int", nullable: true), + FromDate = table.Column(type: "date", nullable: true), + ToDate = table.Column(type: "date", nullable: true), + CertificateIssueDate = table.Column(type: "date", nullable: true), + Notes = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + 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_EmployeeEducations", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeEducations_EmployeeProfiles_EmployeeProfileId", + column: x => x.EmployeeProfileId, + principalTable: "EmployeeProfiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EmployeeFamilyRelations", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EmployeeProfileId = table.Column(type: "uniqueidentifier", nullable: false), + FullName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Relationship = table.Column(type: "int", nullable: false), + BirthYear = table.Column(type: "int", nullable: true), + Occupation = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + CurrentAddress = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Phone = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + 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_EmployeeFamilyRelations", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeFamilyRelations_EmployeeProfiles_EmployeeProfileId", + column: x => x.EmployeeProfileId, + principalTable: "EmployeeProfiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EmployeeSkills", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EmployeeProfileId = table.Column(type: "uniqueidentifier", nullable: false), + Kind = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + LanguageId = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: true), + Level = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + 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_EmployeeSkills", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeSkills_EmployeeProfiles_EmployeeProfileId", + column: x => x.EmployeeProfileId, + principalTable: "EmployeeProfiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EmployeeWorkHistories", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EmployeeProfileId = table.Column(type: "uniqueidentifier", nullable: false), + CompanyName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + CompanyAddress = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Industry = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + FromDate = table.Column(type: "date", nullable: true), + ToDate = table.Column(type: "date", nullable: true), + JobTitle = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + JobDescription = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + ResignReason = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + 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_EmployeeWorkHistories", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeWorkHistories_EmployeeProfiles_EmployeeProfileId", + column: x => x.EmployeeProfileId, + principalTable: "EmployeeProfiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeDocuments_DocumentType", + table: "EmployeeDocuments", + column: "DocumentType"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeDocuments_EmployeeProfileId", + table: "EmployeeDocuments", + column: "EmployeeProfileId"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeEducations_EmployeeProfileId", + table: "EmployeeEducations", + column: "EmployeeProfileId"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeFamilyRelations_EmployeeProfileId", + table: "EmployeeFamilyRelations", + column: "EmployeeProfileId"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeProfiles_EmployeeCode", + table: "EmployeeProfiles", + column: "EmployeeCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeProfiles_IsDeleted", + table: "EmployeeProfiles", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeProfiles_Phone", + table: "EmployeeProfiles", + column: "Phone"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeProfiles_UserId", + table: "EmployeeProfiles", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeSkills_EmployeeProfileId", + table: "EmployeeSkills", + column: "EmployeeProfileId"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeSkills_Kind", + table: "EmployeeSkills", + column: "Kind"); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeWorkHistories_EmployeeProfileId", + table: "EmployeeWorkHistories", + column: "EmployeeProfileId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EmployeeCodeSequences"); + + migrationBuilder.DropTable( + name: "EmployeeDocuments"); + + migrationBuilder.DropTable( + name: "EmployeeEducations"); + + migrationBuilder.DropTable( + name: "EmployeeFamilyRelations"); + + migrationBuilder.DropTable( + name: "EmployeeSkills"); + + migrationBuilder.DropTable( + name: "EmployeeWorkHistories"); + + migrationBuilder.DropTable( + name: "EmployeeProfiles"); + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index ee5e0e1..e0a0372 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1894,6 +1894,606 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations 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.Identity.MenuItem", b => { b.Property("Key") @@ -3731,6 +4331,72 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations 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.Identity.MenuItem", b => { b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent") @@ -3982,6 +4648,19 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations 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"); diff --git a/src/Backend/SolutionErp.Infrastructure/Services/EmployeeCodeGenerator.cs b/src/Backend/SolutionErp.Infrastructure/Services/EmployeeCodeGenerator.cs new file mode 100644 index 0000000..f039f4d --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Services/EmployeeCodeGenerator.cs @@ -0,0 +1,53 @@ +using System.Data; +using Microsoft.EntityFrameworkCore; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Application.Hrm.Services; +using SolutionErp.Domain.Hrm; + +namespace SolutionErp.Infrastructure.Services; + +// Mirror ContractCodeGenerator + PurchaseEvaluationCodeGenerator pattern. +// Format: "NV/{YYYY}/{Seq:D4}", reset per năm. +// +// SERIALIZABLE transaction tránh race khi 2 admin tạo NV đồng thời. +public class EmployeeCodeGenerator(IApplicationDbContext db, IDateTime dateTime) + : IEmployeeCodeGenerator +{ + public async Task GenerateAsync(CancellationToken ct = default) + { + var year = dateTime.UtcNow.Year; + var prefix = $"NV/{year}"; + + var context = (DbContext)db; + await using var tx = await context.Database + .BeginTransactionAsync(IsolationLevel.Serializable, ct); + try + { + var seq = await db.EmployeeCodeSequences + .FirstOrDefaultAsync(s => s.Prefix == prefix, ct); + if (seq is null) + { + seq = new EmployeeCodeSequence + { + Prefix = prefix, + LastSeq = 1, + UpdatedAt = dateTime.UtcNow, + }; + db.EmployeeCodeSequences.Add(seq); + } + else + { + seq.LastSeq += 1; + seq.UpdatedAt = dateTime.UtcNow; + } + await db.SaveChangesAsync(ct); + await tx.CommitAsync(ct); + return $"{prefix}/{seq.LastSeq:D4}"; + } + catch + { + await tx.RollbackAsync(ct); + throw; + } + } +}