[CLAUDE] App+Api: S35 Plan G-H2 Task 3 — HrmConfig BE CRUD 16 endpoint cookie-cutter
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m43s

Pattern 12-bis cumulative 3× (S29 Plan B + Chunk C + S35 G-H2). Mega scaffold mirror
Master/Catalogs/CatalogsFeatures.cs 334 LOC pattern → SolutionErp.Application/Hrm/
HrmConfigFeatures.cs 439 LOC. 4 region cookie-cutter cho 4 HRM catalog (LeaveTypes +
Holidays + ShiftPatterns + OtPolicies). 16 endpoint qua HrmConfigsController.cs 137 LOC.

## Scope (Implementer Case 2 ~25K spawn)
- HrmConfigFeatures.cs 4 region × (DTO + List + Create cmd/validator/handler + Update cmd/validator/handler + Delete cmd/handler) = 4 DTO + 4 List query + 4 Create + 4 Update + 4 Delete handler
- HrmConfigsController.cs URL `/api/hrm-configs/{kind}` × {leave-types, holidays, shifts, ot-policies} × 4 verb = 16 endpoint
- Authorization: Class `[Authorize]` + write `[Authorize(Roles="Admin")]` 12 attribute (4 × 3 write verb) mirror Catalogs precedent
- Conflict check: 8 AnyAsync IsDeleted (4 Create + 4 Update với composite Year+Date Holiday exclude-self via `x.Id != req.Id`)
- Validator: 8 AbstractValidator + 44 RuleFor — MaxLength match EF source-of-truth (Smart Friend Implementer caught spec drift, aligned)
- HRM entities NO HasQueryFilter (4 vs 9 file Master have it) — explicit `.Where(!IsDeleted)` in List/Conflict-check queries

## Build + Test verify
- dotnet build PASS 0 err (2 pre-existing DocxRenderer warning unrelated)
- dotnet test 130/130 PASS preserve (BE-only, no FE/test touch)
- FE files NOT touched (defer Task 4 G-H2 FE Admin next chunk)
- EF Config + DbInitializer + IApplicationDbContext: S34 done, no re-touch

## Pre-commit Reviewer Smart Friend 8× clean cumulative
S22+S25+S29×2+S33×2+S35×2 (FE forms + BE CRUD)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-28 09:51:18 +07:00
parent c3cd343bae
commit 909655c40d
2 changed files with 576 additions and 0 deletions

View File

@ -0,0 +1,137 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Hrm;
namespace SolutionErp.Api.Controllers;
// HRM Config API — 4 sub-resource (leave-types, holidays, shifts, ot-policies)
// dùng Mediator dispatch theo command/query type. Authorize = đăng nhập đủ
// (mọi role được đọc cho dropdown; ghi cần Admin).
// Pattern 12-bis mirror Master/Catalogs/CatalogsController.cs (S29 cumulative 2×).
[ApiController]
[Route("api/hrm-configs")]
[Authorize]
public class HrmConfigsController(IMediator mediator) : ControllerBase
{
// ===== Leave Types =====
[HttpGet("leave-types")]
public async Task<List<LeaveTypeDto>> ListLeaveTypes([FromQuery] string? q, [FromQuery] bool? isActive, CancellationToken ct)
=> await mediator.Send(new ListLeaveTypesQuery(q, isActive), ct);
[HttpPost("leave-types")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateLeaveType([FromBody] CreateLeaveTypeCommand body, CancellationToken ct)
{
var id = await mediator.Send(body, ct);
return CreatedAtAction(nameof(ListLeaveTypes), new { id }, new { id });
}
[HttpPut("leave-types/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateLeaveType(Guid id, [FromBody] UpdateLeaveTypeCommand body, CancellationToken ct)
{
if (id != body.Id) return BadRequest("Id mismatch");
await mediator.Send(body, ct);
return NoContent();
}
[HttpDelete("leave-types/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteLeaveType(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteLeaveTypeCommand(id), ct);
return NoContent();
}
// ===== Holidays =====
[HttpGet("holidays")]
public async Task<List<HolidayDto>> ListHolidays([FromQuery] string? q, [FromQuery] int? year, CancellationToken ct)
=> await mediator.Send(new ListHolidaysQuery(q, year), ct);
[HttpPost("holidays")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateHoliday([FromBody] CreateHolidayCommand body, CancellationToken ct)
{
var id = await mediator.Send(body, ct);
return CreatedAtAction(nameof(ListHolidays), new { id }, new { id });
}
[HttpPut("holidays/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateHoliday(Guid id, [FromBody] UpdateHolidayCommand body, CancellationToken ct)
{
if (id != body.Id) return BadRequest("Id mismatch");
await mediator.Send(body, ct);
return NoContent();
}
[HttpDelete("holidays/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteHoliday(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteHolidayCommand(id), ct);
return NoContent();
}
// ===== Shifts =====
[HttpGet("shifts")]
public async Task<List<ShiftPatternDto>> ListShifts([FromQuery] string? q, [FromQuery] bool? isActive, CancellationToken ct)
=> await mediator.Send(new ListShiftsQuery(q, isActive), ct);
[HttpPost("shifts")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateShift([FromBody] CreateShiftCommand body, CancellationToken ct)
{
var id = await mediator.Send(body, ct);
return CreatedAtAction(nameof(ListShifts), new { id }, new { id });
}
[HttpPut("shifts/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateShift(Guid id, [FromBody] UpdateShiftCommand body, CancellationToken ct)
{
if (id != body.Id) return BadRequest("Id mismatch");
await mediator.Send(body, ct);
return NoContent();
}
[HttpDelete("shifts/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteShift(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteShiftCommand(id), ct);
return NoContent();
}
// ===== OT Policies =====
[HttpGet("ot-policies")]
public async Task<List<OtPolicyDto>> ListOtPolicies([FromQuery] string? q, [FromQuery] bool? isActive, CancellationToken ct)
=> await mediator.Send(new ListOtPoliciesQuery(q, isActive), ct);
[HttpPost("ot-policies")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateOtPolicy([FromBody] CreateOtPolicyCommand body, CancellationToken ct)
{
var id = await mediator.Send(body, ct);
return CreatedAtAction(nameof(ListOtPolicies), new { id }, new { id });
}
[HttpPut("ot-policies/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateOtPolicy(Guid id, [FromBody] UpdateOtPolicyCommand body, CancellationToken ct)
{
if (id != body.Id) return BadRequest("Id mismatch");
await mediator.Send(body, ct);
return NoContent();
}
[HttpDelete("ot-policies/{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteOtPolicy(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteOtPolicyCommand(id), ct);
return NoContent();
}
}