[CLAUDE] Domain+App+Infra+FE-Admin+FE-User: S34 Plan 4 G-H2 Mig 35 schema foundation
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m30s

Phase 10.2 G-H2 Cấu hình HRM — 4 catalog lookup foundation deploy (BE CRUD +
FE forms DEFER S35 cho clean handoff).

Mig 35 `AddHrmConfigs` — 4 table mới:
- LeaveTypes (Code unique + Name + DaysPerYear decimal(5,2) + IsPaid +
  RequiresAttachment) — 5 sample seed (ANNUAL 12d + SICK 30d + MATERNITY 180d
  + COMPASSIONATE 3d + UNPAID 0d)
- Holidays (Year + Date UNIQUE composite + Name + IsRecurring + IsPaid) —
  10 sample VN 2026 (Tết Dương + 5 Tết Nguyên đán placeholder + Giỗ tổ +
  30/4 + 1/5 + 2/9 + Quốc khánh)
- ShiftPatterns (Code unique + Name + StartTime/EndTime TimeOnly + BreakMinutes +
  WorkDays comma string) — 3 sample (HC 8-17 T2-T6 + CA1 6-14 T2-T7 + CA2 14-22)
- OtPolicies (Code unique + 3 Multiplier decimal(4,2) + 3 MaxHours int) —
  1 sample STANDARD (1.5x/2.0x/3.0x weekday/weekend/holiday + 4h/40h/200h cap
  Luật Lao động VN 2019)

Files:
- Domain/Hrm/{LeaveType,Holiday,ShiftPattern,OtPolicy}.cs (4 entity AuditableEntity)
- Infrastructure/Persistence/Configurations/{LeaveType,Holiday,ShiftPattern,OtPolicy}Configuration.cs (4 EF Config UNIQUE indexes)
- IApplicationDbContext + ApplicationDbContext +4 DbSet
- Migrations/20260527075940_AddHrmConfigs.{cs,Designer.cs} + Snapshot updated (3-file rule)
- DbInitializer.SeedHrmConfigsAsync ~120 LOC seed sample (NOT gated DemoSeed per gotcha #51)
- MenuKeys.cs +HrmConfig sub-group + 4 leaf (LeaveTypes/Holidays/Shifts/OtPolicies) Order=2 dưới Hrm
- DbInitializer.SeedMenuTreeAsync +5 entry (sub-group + 4 leaf)
- fe-admin + fe-user menuKeys.ts +5 const mirror BE (Pattern 16-bis sync)

Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS baseline preserve
- Mig 35 applied LocalDB SolutionErp_Dev — verified via dotnet ef database update
- 4 catalog table created + 5+10+3+1 = 19 sample row seed

Defer S35:
- Task 2 BE CQRS 4 catalog CRUD (16 endpoint) — Implementer Case 2 cookie-cutter
- Task 4 FE 2 app 4 catalog page (list/create/edit dialog) — Implementer Case 2

Cumulative S34 mig: 34 → 35 (+1). Tables 67 → 71 (+4). Menu keys 64 → 69 (+5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-27 15:04:10 +07:00
parent e506cd8135
commit 07b3f3b284
17 changed files with 5739 additions and 0 deletions

View File

@ -34,6 +34,12 @@ export const MenuKeys = {
// Module Hồ sơ Nhân sự (Mig 34 — Phase 10.1 G-H1 Session 33, 2026-05-26) // Module Hồ sơ Nhân sự (Mig 34 — Phase 10.1 G-H1 Session 33, 2026-05-26)
Hrm: 'Hrm', Hrm: 'Hrm',
HrmHoSo: 'Hrm_HoSo', HrmHoSo: 'Hrm_HoSo',
// Cấu hình HRM (Mig 35 — Phase 10.2 G-H2 Session 34, 2026-05-27)
HrmConfig: 'Hrm_Config',
HrmConfigLeaveTypes: 'Hrm_Config_LeaveTypes',
HrmConfigHolidays: 'Hrm_Config_Holidays',
HrmConfigShifts: 'Hrm_Config_Shifts',
HrmConfigOtPolicies: 'Hrm_Config_OtPolicies',
// Module Văn phòng số — Danh bạ nội bộ (Phase 10.2 G-O1 Session 34, 2026-05-27) // Module Văn phòng số — Danh bạ nội bộ (Phase 10.2 G-O1 Session 34, 2026-05-27)
Off: 'Off', Off: 'Off',
OffDanhBa: 'Off_DanhBa', OffDanhBa: 'Off_DanhBa',

View File

@ -34,6 +34,12 @@ export const MenuKeys = {
// Module Hồ sơ Nhân sự (Mig 34 — Phase 10.1 G-H1 Session 33, 2026-05-26) // Module Hồ sơ Nhân sự (Mig 34 — Phase 10.1 G-H1 Session 33, 2026-05-26)
Hrm: 'Hrm', Hrm: 'Hrm',
HrmHoSo: 'Hrm_HoSo', HrmHoSo: 'Hrm_HoSo',
// Cấu hình HRM (Mig 35 — Phase 10.2 G-H2 Session 34, 2026-05-27)
HrmConfig: 'Hrm_Config',
HrmConfigLeaveTypes: 'Hrm_Config_LeaveTypes',
HrmConfigHolidays: 'Hrm_Config_Holidays',
HrmConfigShifts: 'Hrm_Config_Shifts',
HrmConfigOtPolicies: 'Hrm_Config_OtPolicies',
// Module Văn phòng số — Danh bạ nội bộ (Phase 10.2 G-O1 Session 34, 2026-05-27) // Module Văn phòng số — Danh bạ nội bộ (Phase 10.2 G-O1 Session 34, 2026-05-27)
Off: 'Off', Off: 'Off',
OffDanhBa: 'Off_DanhBa', OffDanhBa: 'Off_DanhBa',

View File

@ -95,5 +95,13 @@ public interface IApplicationDbContext
DbSet<EmployeeDocument> EmployeeDocuments { get; } DbSet<EmployeeDocument> EmployeeDocuments { get; }
DbSet<EmployeeCodeSequence> EmployeeCodeSequences { get; } DbSet<EmployeeCodeSequence> EmployeeCodeSequences { get; }
// Phase 10.2 G-H2 (Mig 35 — S34) — Cấu hình HRM 4 catalog lookup.
// LeaveTypes + Holidays + ShiftPatterns + OtPolicies. Reference G-O4
// LeaveRequest workflow + G-P1 chấm công OT calc business logic.
DbSet<LeaveType> LeaveTypes { get; }
DbSet<Holiday> Holidays { get; }
DbSet<ShiftPattern> ShiftPatterns { get; }
DbSet<OtPolicy> OtPolicies { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }

View File

@ -0,0 +1,24 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Hrm;
// Phase 10.2 G-H2 (Mig 35 — S34 2026-05-27) — Ngày lễ Việt Nam.
// Catalog admin maintain per year. IsRecurring=true (vd 1/1) → auto roll-over
// năm sau (admin bulk clone). Non-recurring (vd Tết Nguyên đán date thay đổi
// theo Âm lịch) → admin tạo mới mỗi năm.
//
// Sample seed 10 ngày lễ VN 2026: Tết Dương lịch (1/1) + Tết Nguyên đán
// (5 ngày 28/1-1/2 đầu năm tới) + Giỗ tổ Hùng Vương (14/3 ÂL ~2026-04-20) +
// 30/4 + 1/5 + 2/9 + Quốc khánh 3/9.
public class Holiday : AuditableEntity
{
public int Year { get; set; } // 2026
public DateOnly Date { get; set; } // UNIQUE composite (Year, Date)
public string Name { get; set; } = string.Empty; // "Tết Dương lịch"
public bool IsRecurring { get; set; } // 1/1 lặp mỗi năm, Tết Âm lịch không
public bool IsPaid { get; set; } = true; // Có lương / Không lương (mặc định có)
public bool IsActive { get; set; } = true;
public string? Description { get; set; }
}

View File

@ -0,0 +1,25 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Hrm;
// Phase 10.2 G-H2 (Mig 35 — S34 2026-05-27) — Loại phép.
// Catalog lookup table cho HRM Workflow Apps (G-O4 LeaveRequest).
// Sample seed 5 loại: ANNUAL (12d) + SICK (30d) + MATERNITY (180d) +
// COMPASSIONATE (3d) + UNPAID (0d, không giới hạn).
public class LeaveType : AuditableEntity
{
public string Code { get; set; } = string.Empty; // UNIQUE — "ANNUAL", "SICK", ...
public string Name { get; set; } = string.Empty; // "Phép năm", "Phép ốm"
// Số ngày max được nghỉ trong năm. 0 = không giới hạn (UNPAID).
// decimal(5,2) cho phép half-day (vd 0.5d).
public decimal DaysPerYear { get; set; }
public bool IsPaid { get; set; } = true; // Có lương / Không lương
// Yêu cầu đính kèm giấy tờ (vd Sick → giấy bác sĩ, Maternity → giấy KS).
public bool RequiresAttachment { get; set; }
public bool IsActive { get; set; } = true;
public string? Description { get; set; }
}

View File

@ -0,0 +1,32 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Hrm;
// Phase 10.2 G-H2 (Mig 35 — S34 2026-05-27) — Chính sách tăng ca (OT).
// Catalog admin define hệ số OT theo loại ngày + giới hạn max hour.
// 1 OtPolicy hoạt động "default" cho toàn công ty (IsActive=true unique).
//
// Sample seed 1 default:
// STANDARD: Weekday 1.5x / Weekend 2.0x / Holiday 3.0x
// MaxHoursPerDay=4h, MaxHoursPerMonth=40h (Luật Lao động VN cap)
//
// Future G-P1 Chấm công attendance + G-O4 OtRequest workflow sẽ reference
// OtPolicy.IsActive=true để calc lương + validate approval.
public class OtPolicy : AuditableEntity
{
public string Code { get; set; } = string.Empty; // UNIQUE — "STANDARD"
public string Name { get; set; } = string.Empty; // "Chính sách OT chuẩn"
// Hệ số nhân lương OT decimal(4,2) — vd 1.5 / 2.0 / 3.0.
public decimal MultiplierWeekday { get; set; }
public decimal MultiplierWeekend { get; set; }
public decimal MultiplierHoliday { get; set; }
// Giới hạn OT — vd Luật Lao động VN max 4h/day, 40h/month, 200h/year.
public int MaxHoursPerDay { get; set; }
public int MaxHoursPerMonth { get; set; }
public int MaxHoursPerYear { get; set; }
public bool IsActive { get; set; } = true;
public string? Description { get; set; }
}

View File

@ -0,0 +1,30 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Hrm;
// Phase 10.2 G-H2 (Mig 35 — S34 2026-05-27) — Ca làm việc.
// Catalog admin define ca làm — assign cho NV qua EmployeeProfile (defer Phase 1.5
// nếu user cần per-NV shift assignment) hoặc default "HC" cho toàn công ty.
//
// Sample seed 3 ca:
// HC (Hành chính) — 8:00-17:00 Mon-Fri, nghỉ trưa 60 phút
// CA1 (Ca sáng) — 6:00-14:00 Mon-Sat, nghỉ trưa 30 phút
// CA2 (Ca chiều) — 14:00-22:00 Mon-Sat, nghỉ giữa ca 30 phút
public class ShiftPattern : AuditableEntity
{
public string Code { get; set; } = string.Empty; // UNIQUE — "HC", "CA1", "CA2"
public string Name { get; set; } = string.Empty; // "Hành chính", "Ca sáng"
public TimeOnly StartTime { get; set; } // 08:00
public TimeOnly EndTime { get; set; } // 17:00
public int BreakMinutes { get; set; } // Phút nghỉ trưa / giữa ca
// Comma-separated weekday codes: "Mon,Tue,Wed,Thu,Fri" cho HC,
// "Mon,Tue,Wed,Thu,Fri,Sat" cho CA1/CA2 (working Sat).
// FE Designer multi-select chuyển → comma string. Flexibility cao hơn bitmask int.
public string WorkDays { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public string? Description { get; set; }
}

View File

@ -84,6 +84,12 @@ public static class MenuKeys
// ============================================================ // ============================================================
public const string Hrm = "Hrm"; // root group public const string Hrm = "Hrm"; // root group
public const string HrmHoSo = "Hrm_HoSo"; // Hồ sơ Nhân sự (list + detail + edit) public const string HrmHoSo = "Hrm_HoSo"; // Hồ sơ Nhân sự (list + detail + edit)
// Phase 10.2 G-H2 (Mig 35 — S34) — Cấu hình HRM 4 catalog lookup.
public const string HrmConfig = "Hrm_Config"; // sub-group cấu hình
public const string HrmConfigLeaveTypes = "Hrm_Config_LeaveTypes"; // Loại phép
public const string HrmConfigHolidays = "Hrm_Config_Holidays"; // Ngày lễ
public const string HrmConfigShifts = "Hrm_Config_Shifts"; // Ca làm việc
public const string HrmConfigOtPolicies = "Hrm_Config_OtPolicies"; // Chính sách OT
// ============================================================ // ============================================================
// Module Văn phòng số (Phase 10.2 G-O1+ S34 2026-05-27). // Module Văn phòng số (Phase 10.2 G-O1+ S34 2026-05-27).
@ -119,6 +125,7 @@ public static class MenuKeys
PurchaseEvaluations, PurchaseEvaluations,
Budgets, BudgetList, BudgetCreate, BudgetPending, Budgets, BudgetList, BudgetCreate, BudgetPending,
Hrm, HrmHoSo, // Mig 34 — Phase 10.1 Hrm, HrmHoSo, // Mig 34 — Phase 10.1
HrmConfig, HrmConfigLeaveTypes, HrmConfigHolidays, HrmConfigShifts, HrmConfigOtPolicies, // Mig 35 — Phase 10.2 G-H2
Off, OffDanhBa, // Phase 10.2 G-O1 — Văn phòng số Off, OffDanhBa, // Phase 10.2 G-O1 — Văn phòng số
System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows, System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows,
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22 ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22

View File

@ -90,6 +90,12 @@ public class ApplicationDbContext
public DbSet<EmployeeDocument> EmployeeDocuments => Set<EmployeeDocument>(); public DbSet<EmployeeDocument> EmployeeDocuments => Set<EmployeeDocument>();
public DbSet<EmployeeCodeSequence> EmployeeCodeSequences => Set<EmployeeCodeSequence>(); public DbSet<EmployeeCodeSequence> EmployeeCodeSequences => Set<EmployeeCodeSequence>();
// Phase 10.2 G-H2 (Mig 35 — S34) — Cấu hình HRM 4 catalog lookup.
public DbSet<LeaveType> LeaveTypes => Set<LeaveType>();
public DbSet<Holiday> Holidays => Set<Holiday>();
public DbSet<ShiftPattern> ShiftPatterns => Set<ShiftPattern>();
public DbSet<OtPolicy> OtPolicies => Set<OtPolicy>();
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 35 G-H2 (S34) — Ngày lễ. UNIQUE composite (Year, Date).
public class HolidayConfiguration : IEntityTypeConfiguration<Holiday>
{
public void Configure(EntityTypeBuilder<Holiday> e)
{
e.ToTable("Holidays");
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => new { x.Year, x.Date }).IsUnique();
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 35 G-H2 (S34) — Loại phép. Catalog standalone, no FK.
public class LeaveTypeConfiguration : IEntityTypeConfiguration<LeaveType>
{
public void Configure(EntityTypeBuilder<LeaveType> e)
{
e.ToTable("LeaveTypes");
e.Property(x => x.Code).HasMaxLength(50).IsRequired();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.DaysPerYear).HasColumnType("decimal(5,2)");
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique();
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 35 G-H2 (S34) — Chính sách OT. Catalog standalone.
// Future business logic: G-P1 chấm công + G-O4 OtRequest reference IsActive=true.
public class OtPolicyConfiguration : IEntityTypeConfiguration<OtPolicy>
{
public void Configure(EntityTypeBuilder<OtPolicy> e)
{
e.ToTable("OtPolicies");
e.Property(x => x.Code).HasMaxLength(50).IsRequired();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.MultiplierWeekday).HasColumnType("decimal(4,2)");
e.Property(x => x.MultiplierWeekend).HasColumnType("decimal(4,2)");
e.Property(x => x.MultiplierHoliday).HasColumnType("decimal(4,2)");
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique();
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 35 G-H2 (S34) — Ca làm việc. Catalog standalone.
public class ShiftPatternConfiguration : IEntityTypeConfiguration<ShiftPattern>
{
public void Configure(EntityTypeBuilder<ShiftPattern> e)
{
e.ToTable("ShiftPatterns");
e.Property(x => x.Code).HasMaxLength(20).IsRequired();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.WorkDays).HasMaxLength(100).IsRequired(); // "Mon,Tue,Wed,Thu,Fri"
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique();
}
}

View File

@ -92,6 +92,9 @@ public static class DbInitializer
// (infrastructure data, mirror Mig 32 SeedSampleContractWorkflowV2 // (infrastructure data, mirror Mig 32 SeedSampleContractWorkflowV2
// gotcha #51 lesson). Bro UAT update field nhạy cảm qua FE Hồ sơ NS. // gotcha #51 lesson). Bro UAT update field nhạy cảm qua FE Hồ sơ NS.
await SeedDemoEmployeeProfilesAsync(db, userManager, logger); await SeedDemoEmployeeProfilesAsync(db, userManager, logger);
// Plan G-H2 (Mig 35 S34 2026-05-27) — Cấu hình HRM 4 catalog. Infrastructure
// data (NOT gated DemoSeed flag) — Workflow Apps reference cần data ngày 1.
await SeedHrmConfigsAsync(db, logger);
await SeedMenuTreeAsync(db, logger); await SeedMenuTreeAsync(db, logger);
await SeedAdminPermissionsAsync(db, roleManager, logger); await SeedAdminPermissionsAsync(db, roleManager, logger);
await SeedDemoMasterDataAsync(db, logger); await SeedDemoMasterDataAsync(db, logger);
@ -1484,6 +1487,13 @@ public static class DbInitializer
(MenuKeys.Hrm, "Nhân sự", null, 28, "UserCircle"), (MenuKeys.Hrm, "Nhân sự", null, 28, "UserCircle"),
(MenuKeys.HrmHoSo, "Hồ sơ Nhân sự", MenuKeys.Hrm, 1, "ContactRound"), (MenuKeys.HrmHoSo, "Hồ sơ Nhân sự", MenuKeys.Hrm, 1, "ContactRound"),
// Phase 10.2 G-H2 (Mig 35 — S34). Sub-group "Cấu hình HRM" + 4 catalog leaf.
(MenuKeys.HrmConfig, "Cấu hình HRM", MenuKeys.Hrm, 2, "Settings2"),
(MenuKeys.HrmConfigLeaveTypes, "Loại phép", MenuKeys.HrmConfig, 1, "CalendarOff"),
(MenuKeys.HrmConfigHolidays, "Ngày lễ", MenuKeys.HrmConfig, 2, "PartyPopper"),
(MenuKeys.HrmConfigShifts, "Ca làm việc", MenuKeys.HrmConfig, 3, "Clock"),
(MenuKeys.HrmConfigOtPolicies, "Chính sách OT", MenuKeys.HrmConfig, 4, "TimerReset"),
// Module Văn phòng số (Phase 10.2 G-O1+ S34). 1 root + leaf Danh bạ. // Module Văn phòng số (Phase 10.2 G-O1+ S34). 1 root + leaf Danh bạ.
// Future leaf: Off_PhongHop (G-O2) + workflow apps Off_DeXuat/DonTu/DatXe/ItTicket. // Future leaf: Off_PhongHop (G-O2) + workflow apps Off_DeXuat/DonTu/DatXe/ItTicket.
(MenuKeys.Off, "Văn phòng số", null, 29, "Briefcase"), (MenuKeys.Off, "Văn phòng số", null, 29, "Briefcase"),
@ -2029,4 +2039,121 @@ public static class DbInitializer
"SeedDemoEmployeeProfilesAsync: seeded {Count} profiles + 1 sequence row NV/{Year} LastSeq={Seq}", "SeedDemoEmployeeProfilesAsync: seeded {Count} profiles + 1 sequence row NV/{Year} LastSeq={Seq}",
seq, year, seq); seq, year, seq);
} }
// Phase 10.2 G-H2 (Mig 35 — S34 2026-05-27) — Cấu hình HRM 4 catalog seed sample.
// INFRASTRUCTURE seed (NOT gated DemoSeed flag — gotcha #51 lesson).
// Workflow Apps Phase 10.3 (G-O4 LeaveRequest, G-P1 chấm công OT) reference
// 4 catalog active rows ngày 1 → admin có thể tinh chỉnh sau qua Designer UI.
private static async Task SeedHrmConfigsAsync(ApplicationDbContext db, ILogger logger)
{
if (await db.LeaveTypes.AnyAsync()
&& await db.Holidays.AnyAsync()
&& await db.ShiftPatterns.AnyAsync()
&& await db.OtPolicies.AnyAsync())
{
logger.LogInformation("SeedHrmConfigsAsync: skip — đã có 4 catalog.");
return;
}
// 5 LeaveType — Luật Lao động VN 2019 reference.
if (!await db.LeaveTypes.AnyAsync())
{
db.LeaveTypes.AddRange(
new LeaveType
{
Code = "ANNUAL", Name = "Phép năm", DaysPerYear = 12m, IsPaid = true,
RequiresAttachment = false,
Description = "Phép năm tiêu chuẩn. Luật quy định tối thiểu 12 ngày/năm cho lao động làm việc đủ 12 tháng.",
},
new LeaveType
{
Code = "SICK", Name = "Phép ốm", DaysPerYear = 30m, IsPaid = true,
RequiresAttachment = true,
Description = "Nghỉ ốm có giấy bác sĩ. BHXH chi trả 75% lương.",
},
new LeaveType
{
Code = "MATERNITY", Name = "Phép thai sản", DaysPerYear = 180m, IsPaid = true,
RequiresAttachment = true,
Description = "Phép thai sản 6 tháng (180 ngày) cho lao động nữ. BHXH chi trả 100%.",
},
new LeaveType
{
Code = "COMPASSIONATE", Name = "Phép việc riêng", DaysPerYear = 3m, IsPaid = true,
RequiresAttachment = false,
Description = "Cưới hỏi / hiếu / việc gia đình quan trọng. Luật quy định 1-3 ngày tùy lý do.",
},
new LeaveType
{
Code = "UNPAID", Name = "Phép không lương", DaysPerYear = 0m, IsPaid = false,
RequiresAttachment = false,
Description = "Nghỉ không lương — không giới hạn, cần BOD duyệt.",
});
}
// 10 Holiday VN 2026 (placeholder Tết Nguyên đán date — admin update khi có lịch chính thức).
if (!await db.Holidays.AnyAsync())
{
db.Holidays.AddRange(
new Holiday { Year = 2026, Date = new DateOnly(2026, 1, 1), Name = "Tết Dương lịch", IsRecurring = true },
// Tết Nguyên đán 2026 (Bính Ngọ) — 17/2 mùng 1 Âm — 5 ngày 16-20/2
new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 16), Name = "Tết Nguyên đán (30 Tết)", IsRecurring = false },
new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 17), Name = "Tết Nguyên đán (Mùng 1)", IsRecurring = false },
new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 18), Name = "Tết Nguyên đán (Mùng 2)", IsRecurring = false },
new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 19), Name = "Tết Nguyên đán (Mùng 3)", IsRecurring = false },
new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 20), Name = "Tết Nguyên đán (Mùng 4)", IsRecurring = false },
// Giỗ tổ Hùng Vương 10/3 Âm — 2026 placeholder 14/4
new Holiday { Year = 2026, Date = new DateOnly(2026, 4, 14), Name = "Giỗ tổ Hùng Vương", IsRecurring = false },
new Holiday { Year = 2026, Date = new DateOnly(2026, 4, 30), Name = "Giải phóng miền Nam", IsRecurring = true },
new Holiday { Year = 2026, Date = new DateOnly(2026, 5, 1), Name = "Quốc tế Lao động", IsRecurring = true },
new Holiday { Year = 2026, Date = new DateOnly(2026, 9, 2), Name = "Quốc khánh", IsRecurring = true });
}
// 3 ShiftPattern — hành chính + 2 ca xoay.
if (!await db.ShiftPatterns.AnyAsync())
{
db.ShiftPatterns.AddRange(
new ShiftPattern
{
Code = "HC", Name = "Hành chính",
StartTime = new TimeOnly(8, 0), EndTime = new TimeOnly(17, 0),
BreakMinutes = 60, WorkDays = "Mon,Tue,Wed,Thu,Fri",
Description = "Ca hành chính chuẩn 8-17h, nghỉ trưa 60 phút, T2-T6.",
},
new ShiftPattern
{
Code = "CA1", Name = "Ca sáng",
StartTime = new TimeOnly(6, 0), EndTime = new TimeOnly(14, 0),
BreakMinutes = 30, WorkDays = "Mon,Tue,Wed,Thu,Fri,Sat",
Description = "Ca sáng 6-14h, nghỉ giữa ca 30 phút, T2-T7.",
},
new ShiftPattern
{
Code = "CA2", Name = "Ca chiều",
StartTime = new TimeOnly(14, 0), EndTime = new TimeOnly(22, 0),
BreakMinutes = 30, WorkDays = "Mon,Tue,Wed,Thu,Fri,Sat",
Description = "Ca chiều 14-22h, nghỉ giữa ca 30 phút, T2-T7.",
});
}
// 1 OtPolicy default — Luật Lao động VN cap.
if (!await db.OtPolicies.AnyAsync())
{
db.OtPolicies.Add(new OtPolicy
{
Code = "STANDARD",
Name = "Chính sách OT chuẩn",
MultiplierWeekday = 1.5m,
MultiplierWeekend = 2.0m,
MultiplierHoliday = 3.0m,
MaxHoursPerDay = 4,
MaxHoursPerMonth = 40,
MaxHoursPerYear = 200,
Description = "Hệ số 1.5x/2.0x/3.0x theo Luật Lao động VN 2019. Max 4h/ngày, 40h/tháng, 200h/năm.",
});
}
await db.SaveChangesAsync();
logger.LogInformation("SeedHrmConfigsAsync: seeded 5 LeaveTypes + 10 Holidays 2026 + 3 ShiftPatterns + 1 OtPolicy default.");
}
} }

View File

@ -0,0 +1,159 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddHrmConfigs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Holidays",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Year = table.Column<int>(type: "int", nullable: false),
Date = table.Column<DateOnly>(type: "date", nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
IsRecurring = table.Column<bool>(type: "bit", nullable: false),
IsPaid = table.Column<bool>(type: "bit", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Holidays", x => x.Id);
});
migrationBuilder.CreateTable(
name: "LeaveTypes",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
DaysPerYear = table.Column<decimal>(type: "decimal(5,2)", nullable: false),
IsPaid = table.Column<bool>(type: "bit", nullable: false),
RequiresAttachment = table.Column<bool>(type: "bit", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LeaveTypes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OtPolicies",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
MultiplierWeekday = table.Column<decimal>(type: "decimal(4,2)", nullable: false),
MultiplierWeekend = table.Column<decimal>(type: "decimal(4,2)", nullable: false),
MultiplierHoliday = table.Column<decimal>(type: "decimal(4,2)", nullable: false),
MaxHoursPerDay = table.Column<int>(type: "int", nullable: false),
MaxHoursPerMonth = table.Column<int>(type: "int", nullable: false),
MaxHoursPerYear = table.Column<int>(type: "int", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OtPolicies", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ShiftPatterns",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
StartTime = table.Column<TimeOnly>(type: "time", nullable: false),
EndTime = table.Column<TimeOnly>(type: "time", nullable: false),
BreakMinutes = table.Column<int>(type: "int", nullable: false),
WorkDays = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ShiftPatterns", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Holidays_Year_Date",
table: "Holidays",
columns: new[] { "Year", "Date" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_LeaveTypes_Code",
table: "LeaveTypes",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OtPolicies_Code",
table: "OtPolicies",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ShiftPatterns_Code",
table: "ShiftPatterns",
column: "Code",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Holidays");
migrationBuilder.DropTable(
name: "LeaveTypes");
migrationBuilder.DropTable(
name: "OtPolicies");
migrationBuilder.DropTable(
name: "ShiftPatterns");
}
}
}

View File

@ -2494,6 +2494,262 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("EmployeeWorkHistories", (string)null); b.ToTable("EmployeeWorkHistories", (string)null);
}); });
modelBuilder.Entity("SolutionErp.Domain.Hrm.Holiday", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<DateOnly>("Date")
.HasColumnType("date");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<bool>("IsPaid")
.HasColumnType("bit");
b.Property<bool>("IsRecurring")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Year", "Date")
.IsUnique();
b.ToTable("Holidays", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("DaysPerYear")
.HasColumnType("decimal(5,2)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<bool>("IsPaid")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("RequiresAttachment")
.HasColumnType("bit");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("LeaveTypes", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Hrm.OtPolicy", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<int>("MaxHoursPerDay")
.HasColumnType("int");
b.Property<int>("MaxHoursPerMonth")
.HasColumnType("int");
b.Property<int>("MaxHoursPerYear")
.HasColumnType("int");
b.Property<decimal>("MultiplierHoliday")
.HasColumnType("decimal(4,2)");
b.Property<decimal>("MultiplierWeekday")
.HasColumnType("decimal(4,2)");
b.Property<decimal>("MultiplierWeekend")
.HasColumnType("decimal(4,2)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("OtPolicies", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Hrm.ShiftPattern", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("BreakMinutes")
.HasColumnType("int");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<TimeOnly>("EndTime")
.HasColumnType("time");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<TimeOnly>("StartTime")
.HasColumnType("time");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("WorkDays")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("ShiftPatterns", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")