[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
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:
137
src/Backend/SolutionErp.Api/Controllers/HrmConfigsController.cs
Normal file
137
src/Backend/SolutionErp.Api/Controllers/HrmConfigsController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user