[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:
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user