[CLAUDE] Hrm: P11-C Vehicle+Driver catalogs (Mig 44) + gotcha #57 filtered-unique 3 HRM catalog (Mig 45)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m18s

P11-C: extend HrmConfigs +2 kind (Vehicle/Driver) declarative. Mig 44 AddVehicleAndDriverCatalogs (2 table filtered-unique Code, tables 91->93). Domain entity + EF config (filtered day-1) + 2 DbSet + HrmConfigFeatures Region5/6 CRUD + Controller +2 route-group (GET public / write Roles=Admin) + MenuKeys +2 +All (auto Admin perm) + DbInitializer 2 menu leaf + idempotent seed 2 veh/2 drv. FE declarative KIND_CONFIG +2 kind x2 app (SHA256 mirror) + 4-place (types/page/menuKeys/Layout staticMap), :kind-driven no new route.

gotcha #57 (bundled; OtPolicy missed in backlog, caught via grep) - Mig 45 FilterHrmCatalogUniqueIndexesByIsDeleted: LeaveType+ShiftPattern+OtPolicy bare .IsUnique() -> .HasFilter([IsDeleted]=0) (recreate-on-soft-deleted-slot 500 fix, mirror Holiday Mig 43). Tests +5 HrmConfigFilteredUniqueTests (181->186 PASS) test-before RED->GREEN. Reviewer caught FE<->BE Driver required-field mismatch -> fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-08 10:32:28 +07:00
parent f8179c5fbd
commit 30a99aa03f
27 changed files with 14144 additions and 16 deletions

View File

@ -97,6 +97,10 @@ public class ApplicationDbContext
public DbSet<ShiftPattern> ShiftPatterns => Set<ShiftPattern>();
public DbSet<OtPolicy> OtPolicies => Set<OtPolicy>();
// Phase 11 P11-C (Mig 44 — S51) — Danh mục xe công + tài xế.
public DbSet<Vehicle> Vehicles => Set<Vehicle>();
public DbSet<Driver> Drivers => Set<Driver>();
// Phase 10.2 G-O2 (Mig 36 — S36) — Phòng họp + Booking + Attendee join.
public DbSet<MeetingRoom> MeetingRooms => Set<MeetingRoom>();
public DbSet<MeetingBooking> MeetingBookings => Set<MeetingBooking>();

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 44 P11-C (S51) — Danh mục tài xế. Catalog standalone, no FK.
// UNIQUE Code filtered WHERE IsDeleted=0 (gotcha #57 — soft-deleted slot reusable,
// khớp app-level !IsDeleted check + pattern HolidayConfiguration/Catalogs/Contract/PE).
public class DriverConfiguration : IEntityTypeConfiguration<Driver>
{
public void Configure(EntityTypeBuilder<Driver> e)
{
e.ToTable("Drivers");
e.Property(x => x.Code).HasMaxLength(50).IsRequired();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.PhoneNumber).HasMaxLength(20).IsRequired();
e.Property(x => x.LicenseNumber).HasMaxLength(50).IsRequired();
e.Property(x => x.LicenseClass).HasMaxLength(20).IsRequired();
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0");
}
}

View File

@ -16,6 +16,6 @@ public class LeaveTypeConfiguration : IEntityTypeConfiguration<LeaveType>
e.Property(x => x.DaysPerYear).HasColumnType("decimal(5,2)");
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique();
e.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 45 (S51 gotcha #57) — soft-deleted slot reusable, khớp handler !IsDeleted
}
}

View File

@ -19,6 +19,6 @@ public class OtPolicyConfiguration : IEntityTypeConfiguration<OtPolicy>
e.Property(x => x.MultiplierHoliday).HasColumnType("decimal(4,2)");
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique();
e.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 45 (S51 gotcha #57) — soft-deleted slot reusable, khớp handler !IsDeleted
}
}

View File

@ -16,6 +16,6 @@ public class ShiftPatternConfiguration : IEntityTypeConfiguration<ShiftPattern>
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();
e.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 45 (S51 gotcha #57) — soft-deleted slot reusable, khớp handler !IsDeleted
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
// EF Mig 44 P11-C (S51) — Danh mục xe công. Catalog standalone, no FK.
// UNIQUE Code filtered WHERE IsDeleted=0 (gotcha #57 — soft-deleted slot reusable,
// khớp app-level !IsDeleted check + pattern HolidayConfiguration/Catalogs/Contract/PE).
public class VehicleConfiguration : IEntityTypeConfiguration<Vehicle>
{
public void Configure(EntityTypeBuilder<Vehicle> e)
{
e.ToTable("Vehicles");
e.Property(x => x.Code).HasMaxLength(50).IsRequired();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.LicensePlate).HasMaxLength(20).IsRequired();
e.Property(x => x.Description).HasMaxLength(500);
e.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0");
}
}

View File

@ -1755,6 +1755,9 @@ public static class DbInitializer
(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"),
// Phase 11 P11-C (Mig 44 — S51). 2 catalog leaf xe công + tài xế.
(MenuKeys.HrmConfigVehicles, "Xe công", MenuKeys.HrmConfig, 5, "Car"),
(MenuKeys.HrmConfigDrivers, "Tài xế", MenuKeys.HrmConfig, 6, "UserCog"),
// 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.
@ -2331,9 +2334,11 @@ public static class DbInitializer
if (await db.LeaveTypes.AnyAsync()
&& await db.Holidays.AnyAsync()
&& await db.ShiftPatterns.AnyAsync()
&& await db.OtPolicies.AnyAsync())
&& await db.OtPolicies.AnyAsync()
&& await db.Vehicles.AnyAsync()
&& await db.Drivers.AnyAsync())
{
logger.LogInformation("SeedHrmConfigsAsync: skip — đã có 4 catalog.");
logger.LogInformation("SeedHrmConfigsAsync: skip — đã có 6 catalog.");
return;
}
@ -2435,8 +2440,44 @@ public static class DbInitializer
});
}
// 2 Vehicle sample (Mig 44 P11-C — reference VehicleBooking dropdown ngày 1).
if (!await db.Vehicles.AnyAsync())
{
db.Vehicles.AddRange(
new Vehicle
{
Code = "XE-01", Name = "Toyota Innova", LicensePlate = "30A-12345",
SeatCount = 7,
Description = "Xe 7 chỗ đưa đón công tác nội thành.",
},
new Vehicle
{
Code = "XE-02", Name = "Ford Transit", LicensePlate = "30A-67890",
SeatCount = 16,
Description = "Xe 16 chỗ đưa đón đoàn / công tác tỉnh.",
});
}
// 2 Driver sample (Mig 44 P11-C — reference VehicleBooking dropdown ngày 1).
if (!await db.Drivers.AnyAsync())
{
db.Drivers.AddRange(
new Driver
{
Code = "TX-01", Name = "Nguyễn Văn Tài", PhoneNumber = "0901234567",
LicenseNumber = "012345678901", LicenseClass = "D",
Description = "Tài xế xe 7-16 chỗ, GPLX hạng D.",
},
new Driver
{
Code = "TX-02", Name = "Trần Văn Lái", PhoneNumber = "0907654321",
LicenseNumber = "098765432109", LicenseClass = "E",
Description = "Tài xế xe khách lớn, GPLX hạng E.",
});
}
await db.SaveChangesAsync();
logger.LogInformation("SeedHrmConfigsAsync: seeded 5 LeaveTypes + 10 Holidays 2026 + 3 ShiftPatterns + 1 OtPolicy default.");
logger.LogInformation("SeedHrmConfigsAsync: seeded 5 LeaveTypes + 10 Holidays 2026 + 3 ShiftPatterns + 1 OtPolicy + 2 Vehicles + 2 Drivers.");
}
// Plan G-O2 (Mig 36 — S36 2026-05-28). 4 sample MeetingRoom seed.

View File

@ -0,0 +1,88 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddVehicleAndDriverCatalogs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Drivers",
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),
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
LicenseNumber = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
LicenseClass = table.Column<string>(type: "nvarchar(20)", maxLength: 20, 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_Drivers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Vehicles",
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),
LicensePlate = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
SeatCount = 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_Vehicles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Drivers_Code",
table: "Drivers",
column: "Code",
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_Vehicles_Code",
table: "Vehicles",
column: "Code",
unique: true,
filter: "[IsDeleted] = 0");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Drivers");
migrationBuilder.DropTable(
name: "Vehicles");
}
}
}

View File

@ -0,0 +1,81 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class FilterHrmCatalogUniqueIndexesByIsDeleted : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ShiftPatterns_Code",
table: "ShiftPatterns");
migrationBuilder.DropIndex(
name: "IX_OtPolicies_Code",
table: "OtPolicies");
migrationBuilder.DropIndex(
name: "IX_LeaveTypes_Code",
table: "LeaveTypes");
migrationBuilder.CreateIndex(
name: "IX_ShiftPatterns_Code",
table: "ShiftPatterns",
column: "Code",
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_OtPolicies_Code",
table: "OtPolicies",
column: "Code",
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_LeaveTypes_Code",
table: "LeaveTypes",
column: "Code",
unique: true,
filter: "[IsDeleted] = 0");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ShiftPatterns_Code",
table: "ShiftPatterns");
migrationBuilder.DropIndex(
name: "IX_OtPolicies_Code",
table: "OtPolicies");
migrationBuilder.DropIndex(
name: "IX_LeaveTypes_Code",
table: "LeaveTypes");
migrationBuilder.CreateIndex(
name: "IX_ShiftPatterns_Code",
table: "ShiftPatterns",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OtPolicies_Code",
table: "OtPolicies",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_LeaveTypes_Code",
table: "LeaveTypes",
column: "Code",
unique: true);
}
}
}

View File

@ -1894,6 +1894,74 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("ContractTemplates", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Hrm.Driver", 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<string>("LicenseClass")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("LicenseNumber")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Drivers", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeCodeSequence", b =>
{
b.Property<string>("Prefix")
@ -2670,7 +2738,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("LeaveTypes", (string)null);
});
@ -2740,7 +2809,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("OtPolicies", (string)null);
});
@ -2806,11 +2876,73 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("ShiftPatterns", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Hrm.Vehicle", 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<string>("LicensePlate")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("SeatCount")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Vehicles", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.Property<string>("Key")