[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
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:
@ -134,4 +134,64 @@ public class HrmConfigsController(IMediator mediator) : ControllerBase
|
||||
await mediator.Send(new DeleteOtPolicyCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ===== Vehicles (Mig 44 P11-C — S51) =====
|
||||
[HttpGet("vehicles")]
|
||||
public async Task<List<VehicleDto>> ListVehicles([FromQuery] string? q, [FromQuery] bool? isActive, CancellationToken ct)
|
||||
=> await mediator.Send(new ListVehiclesQuery(q, isActive), ct);
|
||||
|
||||
[HttpPost("vehicles")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> CreateVehicle([FromBody] CreateVehicleCommand body, CancellationToken ct)
|
||||
{
|
||||
var id = await mediator.Send(body, ct);
|
||||
return CreatedAtAction(nameof(ListVehicles), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("vehicles/{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> UpdateVehicle(Guid id, [FromBody] UpdateVehicleCommand body, CancellationToken ct)
|
||||
{
|
||||
if (id != body.Id) return BadRequest("Id mismatch");
|
||||
await mediator.Send(body, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("vehicles/{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> DeleteVehicle(Guid id, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteVehicleCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ===== Drivers (Mig 44 P11-C — S51) =====
|
||||
[HttpGet("drivers")]
|
||||
public async Task<List<DriverDto>> ListDrivers([FromQuery] string? q, [FromQuery] bool? isActive, CancellationToken ct)
|
||||
=> await mediator.Send(new ListDriversQuery(q, isActive), ct);
|
||||
|
||||
[HttpPost("drivers")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> CreateDriver([FromBody] CreateDriverCommand body, CancellationToken ct)
|
||||
{
|
||||
var id = await mediator.Send(body, ct);
|
||||
return CreatedAtAction(nameof(ListDrivers), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("drivers/{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> UpdateDriver(Guid id, [FromBody] UpdateDriverCommand body, CancellationToken ct)
|
||||
{
|
||||
if (id != body.Id) return BadRequest("Id mismatch");
|
||||
await mediator.Send(body, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("drivers/{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> DeleteDriver(Guid id, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteDriverCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +104,10 @@ public interface IApplicationDbContext
|
||||
DbSet<ShiftPattern> ShiftPatterns { get; }
|
||||
DbSet<OtPolicy> OtPolicies { get; }
|
||||
|
||||
// Phase 11 P11-C (Mig 44 — S51) — Danh mục xe công + tài xế reference VehicleBooking.
|
||||
DbSet<Vehicle> Vehicles { get; }
|
||||
DbSet<Driver> Drivers { get; }
|
||||
|
||||
// Phase 10.2 G-O2 (Mig 36 — S36) — Phòng họp + Booking + Attendee join.
|
||||
// Overlap check qua SERIALIZABLE tx + EXISTS handler (clean-room SOL,
|
||||
// mirror NamGroup TblBookingResource S50 pattern). FullCalendar v6 MIT FE.
|
||||
|
||||
@ -26,6 +26,8 @@ public record LeaveTypeDto(Guid Id, string Code, string Name, decimal DaysPerYea
|
||||
public record HolidayDto(Guid Id, int Year, DateOnly Date, string Name, bool IsRecurring, bool IsPaid, bool IsActive, string? Description);
|
||||
public record ShiftPatternDto(Guid Id, string Code, string Name, TimeOnly StartTime, TimeOnly EndTime, int BreakMinutes, string WorkDays, bool IsActive, string? Description);
|
||||
public record OtPolicyDto(Guid Id, string Code, string Name, decimal MultiplierWeekday, decimal MultiplierWeekend, decimal MultiplierHoliday, int MaxHoursPerDay, int MaxHoursPerMonth, int MaxHoursPerYear, bool IsActive, string? Description);
|
||||
public record VehicleDto(Guid Id, string Code, string Name, string LicensePlate, int SeatCount, bool IsActive, string? Description);
|
||||
public record DriverDto(Guid Id, string Code, string Name, string PhoneNumber, string LicenseNumber, string LicenseClass, bool IsActive, string? Description);
|
||||
|
||||
// ===== Region 1: LeaveType =====
|
||||
|
||||
@ -437,3 +439,201 @@ public class DeleteOtPolicyHandler(IApplicationDbContext db) : IRequestHandler<D
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Region 5: Vehicle (Mig 44 P11-C — S51) =====
|
||||
// Code UNIQUE filtered. Catalog xe công reference VehicleBooking workflow app.
|
||||
// Mirror Region 1 LeaveType EXACT (list Where !IsDeleted + filter q Code/Name + IsActive).
|
||||
|
||||
public record ListVehiclesQuery(string? Q = null, bool? IsActive = null) : IRequest<List<VehicleDto>>;
|
||||
public class ListVehiclesHandler(IApplicationDbContext db) : IRequestHandler<ListVehiclesQuery, List<VehicleDto>>
|
||||
{
|
||||
public async Task<List<VehicleDto>> Handle(ListVehiclesQuery req, CancellationToken ct)
|
||||
{
|
||||
var q = db.Vehicles.AsNoTracking().Where(x => !x.IsDeleted);
|
||||
if (!string.IsNullOrWhiteSpace(req.Q))
|
||||
{
|
||||
var s = req.Q.ToLower();
|
||||
q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s));
|
||||
}
|
||||
if (req.IsActive.HasValue) q = q.Where(x => x.IsActive == req.IsActive.Value);
|
||||
return await q.OrderBy(x => x.Code)
|
||||
.Select(x => new VehicleDto(x.Id, x.Code, x.Name, x.LicensePlate, x.SeatCount, x.IsActive, x.Description))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record CreateVehicleCommand(string Code, string Name, string LicensePlate, int SeatCount, string? Description) : IRequest<Guid>;
|
||||
public class CreateVehicleValidator : AbstractValidator<CreateVehicleCommand>
|
||||
{
|
||||
public CreateVehicleValidator()
|
||||
{
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.LicensePlate).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.SeatCount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
public class CreateVehicleHandler(IApplicationDbContext db) : IRequestHandler<CreateVehicleCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreateVehicleCommand req, CancellationToken ct)
|
||||
{
|
||||
if (await db.Vehicles.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
|
||||
throw new ConflictException($"Mã xe '{req.Code}' đã tồn tại.");
|
||||
var entity = new Vehicle
|
||||
{
|
||||
Code = req.Code,
|
||||
Name = req.Name,
|
||||
LicensePlate = req.LicensePlate,
|
||||
SeatCount = req.SeatCount,
|
||||
Description = req.Description,
|
||||
IsActive = true,
|
||||
};
|
||||
db.Vehicles.Add(entity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateVehicleCommand(Guid Id, string Code, string Name, string LicensePlate, int SeatCount, bool IsActive, string? Description) : IRequest;
|
||||
public class UpdateVehicleValidator : AbstractValidator<UpdateVehicleCommand>
|
||||
{
|
||||
public UpdateVehicleValidator()
|
||||
{
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.LicensePlate).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.SeatCount).GreaterThanOrEqualTo(0);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
public class UpdateVehicleHandler(IApplicationDbContext db) : IRequestHandler<UpdateVehicleCommand>
|
||||
{
|
||||
public async Task Handle(UpdateVehicleCommand req, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Vehicles.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
|
||||
?? throw new NotFoundException("Vehicle", req.Id);
|
||||
if (entity.Code != req.Code && await db.Vehicles.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
|
||||
throw new ConflictException($"Mã xe '{req.Code}' đã tồn tại.");
|
||||
entity.Code = req.Code;
|
||||
entity.Name = req.Name;
|
||||
entity.LicensePlate = req.LicensePlate;
|
||||
entity.SeatCount = req.SeatCount;
|
||||
entity.IsActive = req.IsActive;
|
||||
entity.Description = req.Description;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record DeleteVehicleCommand(Guid Id) : IRequest;
|
||||
public class DeleteVehicleHandler(IApplicationDbContext db) : IRequestHandler<DeleteVehicleCommand>
|
||||
{
|
||||
public async Task Handle(DeleteVehicleCommand req, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Vehicles.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
|
||||
?? throw new NotFoundException("Vehicle", req.Id);
|
||||
db.Vehicles.Remove(entity); // Soft delete via AuditingInterceptor
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Region 6: Driver (Mig 44 P11-C — S51) =====
|
||||
// Code UNIQUE filtered. Catalog tài xế reference VehicleBooking workflow app.
|
||||
// Mirror Region 5 Vehicle EXACT. Validator: PhoneNumber Max20 + LicenseNumber Max50 + LicenseClass Max20.
|
||||
|
||||
public record ListDriversQuery(string? Q = null, bool? IsActive = null) : IRequest<List<DriverDto>>;
|
||||
public class ListDriversHandler(IApplicationDbContext db) : IRequestHandler<ListDriversQuery, List<DriverDto>>
|
||||
{
|
||||
public async Task<List<DriverDto>> Handle(ListDriversQuery req, CancellationToken ct)
|
||||
{
|
||||
var q = db.Drivers.AsNoTracking().Where(x => !x.IsDeleted);
|
||||
if (!string.IsNullOrWhiteSpace(req.Q))
|
||||
{
|
||||
var s = req.Q.ToLower();
|
||||
q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s));
|
||||
}
|
||||
if (req.IsActive.HasValue) q = q.Where(x => x.IsActive == req.IsActive.Value);
|
||||
return await q.OrderBy(x => x.Code)
|
||||
.Select(x => new DriverDto(x.Id, x.Code, x.Name, x.PhoneNumber, x.LicenseNumber, x.LicenseClass, x.IsActive, x.Description))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record CreateDriverCommand(string Code, string Name, string PhoneNumber, string LicenseNumber, string LicenseClass, string? Description) : IRequest<Guid>;
|
||||
public class CreateDriverValidator : AbstractValidator<CreateDriverCommand>
|
||||
{
|
||||
public CreateDriverValidator()
|
||||
{
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.PhoneNumber).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.LicenseNumber).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.LicenseClass).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
public class CreateDriverHandler(IApplicationDbContext db) : IRequestHandler<CreateDriverCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreateDriverCommand req, CancellationToken ct)
|
||||
{
|
||||
if (await db.Drivers.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
|
||||
throw new ConflictException($"Mã tài xế '{req.Code}' đã tồn tại.");
|
||||
var entity = new Driver
|
||||
{
|
||||
Code = req.Code,
|
||||
Name = req.Name,
|
||||
PhoneNumber = req.PhoneNumber,
|
||||
LicenseNumber = req.LicenseNumber,
|
||||
LicenseClass = req.LicenseClass,
|
||||
Description = req.Description,
|
||||
IsActive = true,
|
||||
};
|
||||
db.Drivers.Add(entity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateDriverCommand(Guid Id, string Code, string Name, string PhoneNumber, string LicenseNumber, string LicenseClass, bool IsActive, string? Description) : IRequest;
|
||||
public class UpdateDriverValidator : AbstractValidator<UpdateDriverCommand>
|
||||
{
|
||||
public UpdateDriverValidator()
|
||||
{
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.PhoneNumber).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.LicenseNumber).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.LicenseClass).NotEmpty().MaximumLength(20);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
public class UpdateDriverHandler(IApplicationDbContext db) : IRequestHandler<UpdateDriverCommand>
|
||||
{
|
||||
public async Task Handle(UpdateDriverCommand req, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Drivers.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
|
||||
?? throw new NotFoundException("Driver", req.Id);
|
||||
if (entity.Code != req.Code && await db.Drivers.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
|
||||
throw new ConflictException($"Mã tài xế '{req.Code}' đã tồn tại.");
|
||||
entity.Code = req.Code;
|
||||
entity.Name = req.Name;
|
||||
entity.PhoneNumber = req.PhoneNumber;
|
||||
entity.LicenseNumber = req.LicenseNumber;
|
||||
entity.LicenseClass = req.LicenseClass;
|
||||
entity.IsActive = req.IsActive;
|
||||
entity.Description = req.Description;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record DeleteDriverCommand(Guid Id) : IRequest;
|
||||
public class DeleteDriverHandler(IApplicationDbContext db) : IRequestHandler<DeleteDriverCommand>
|
||||
{
|
||||
public async Task Handle(DeleteDriverCommand req, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Drivers.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
|
||||
?? throw new NotFoundException("Driver", req.Id);
|
||||
db.Drivers.Remove(entity); // Soft delete via AuditingInterceptor
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
17
src/Backend/SolutionErp.Domain/Hrm/Driver.cs
Normal file
17
src/Backend/SolutionErp.Domain/Hrm/Driver.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.Hrm;
|
||||
|
||||
// Phase 11 P11-C (Mig 44 — S51) — Danh mục tài xế.
|
||||
// Catalog lookup table reference cho Workflow Apps VehicleBooking (đặt xe công).
|
||||
// Sample seed 2 tài xế: "TX-01" + "TX-02".
|
||||
public class Driver : AuditableEntity
|
||||
{
|
||||
public string Code { get; set; } = string.Empty; // UNIQUE — "TX-01", "TX-02", ...
|
||||
public string Name { get; set; } = string.Empty; // Họ tên "Nguyễn Văn Tài"
|
||||
public string PhoneNumber { get; set; } = string.Empty; // SĐT liên hệ
|
||||
public string LicenseNumber { get; set; } = string.Empty; // Số GPLX
|
||||
public string LicenseClass { get; set; } = string.Empty; // Hạng GPLX "B2", "C", "D", "E"
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
16
src/Backend/SolutionErp.Domain/Hrm/Vehicle.cs
Normal file
16
src/Backend/SolutionErp.Domain/Hrm/Vehicle.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.Hrm;
|
||||
|
||||
// Phase 11 P11-C (Mig 44 — S51) — Danh mục xe công.
|
||||
// Catalog lookup table reference cho Workflow Apps VehicleBooking (đặt xe công).
|
||||
// Sample seed 2 xe: "XE-01" Toyota Innova 7 chỗ + "XE-02" Ford Transit 16 chỗ.
|
||||
public class Vehicle : AuditableEntity
|
||||
{
|
||||
public string Code { get; set; } = string.Empty; // UNIQUE — "XE-01", "XE-02", ...
|
||||
public string Name { get; set; } = string.Empty; // "Toyota Innova", "Ford Transit"
|
||||
public string LicensePlate { get; set; } = string.Empty; // Biển số "30A-12345"
|
||||
public int SeatCount { get; set; } // Số chỗ ngồi
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@ -90,6 +90,9 @@ public static class MenuKeys
|
||||
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
|
||||
// Phase 11 P11-C (Mig 44 — S51) — Danh mục xe công + tài xế (reference VehicleBooking).
|
||||
public const string HrmConfigVehicles = "Hrm_Config_Vehicles"; // Xe công
|
||||
public const string HrmConfigDrivers = "Hrm_Config_Drivers"; // Tài xế
|
||||
|
||||
// ============================================================
|
||||
// Module Văn phòng số (Phase 10.2 G-O1+ S34 2026-05-27).
|
||||
@ -147,6 +150,7 @@ public static class MenuKeys
|
||||
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
||||
Hrm, HrmHoSo, // Mig 34 — Phase 10.1
|
||||
HrmConfig, HrmConfigLeaveTypes, HrmConfigHolidays, HrmConfigShifts, HrmConfigOtPolicies, // Mig 35 — Phase 10.2 G-H2
|
||||
HrmConfigVehicles, HrmConfigDrivers, // Mig 44 — Phase 11 P11-C
|
||||
Off, OffDanhBa, // Phase 10.2 G-O1 — Văn phòng số
|
||||
OffPhongHop, OffPhongHopView, OffPhongHopManage, OffPhongHopBook, // Phase 10.2 G-O2 — Phòng họp
|
||||
OffDeXuat, OffDeXuatList, OffDeXuatCreate, OffDeXuatInbox, // Phase 10.3 G-O3 — Đề xuất
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
|
||||
Reference in New Issue
Block a user