[CLAUDE] Workflow: wire ApproveV2 + LevelOpinions cho 4 WorkflowApps module (Phase 11 P11-A)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m6s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m6s
Wire full approval workflow V2 cho Leave/OT/Travel/Vehicle — cookie-cutter
mirror Proposal (Mig 38). Trước đây skeleton Phase 1 (Create+List), giờ
ApproveV2 advance-level + UPSERT LevelOpinion + atomic codegen.
Schema (Mig 41 WireWorkflowAppsApprovalV2 — 84→89 tables, pure additive):
- 4 bảng {Leave,Ot,Travel,Vehicle}LevelOpinions (UNIQUE composite + Cascade
parent + Restrict Level — mirror ProposalLevelOpinion)
- 1 bảng WorkflowAppCodeSequences (shared atomic MaDonTu, Prefix-keyed)
- 4 cột RejectedFromStatus (smart return tracking)
- enum ApprovalWorkflowApplicableType.TravelRequest = 9
Application (LeaveOt + TravelVehicle ApprovalFeatures.cs — 30 handler):
- GetById detail (Include LevelOpinions + JOIN Step/Level) · UpdateDraft
- Submit (gen MaDonTu + DaGuiDuyet + level=1, verify ApplicableType per module)
- Approve (verify actor==ApproverUserId OR Admin, UPSERT opinion latest-write-wins,
advance level OR terminal DaDuyet, empty comment → placeholder)
- Reject (→TuChoi) · Return (→TraLai + RejectedFromStatus)
Api: 4 controller +6 route mỗi cái (GET/{id}, PUT/{id}, submit/approve/reject/return)
Infra: DbInitializer seed 4 workflow V2 mẫu (QT-NP/OT/CT/XE-V2-001) → UAT test ngay
FE: WorkflowAppDetailPage.tsx declarative 4-kind (fe-admin+fe-user SHA256 identical)
— workflow status + opinion timeline + action buttons; gỡ banner skeleton + row nav
Tests: +11 WorkflowAppApproveV2Tests (130→141 PASS) — state machine + UPSERT
invariant + guards + codegen + forbidden + placeholder (Leave full + Ot smoke)
Verify: build 0 error · 141 test PASS · FE build ×2 · reviewer checklist
(ApplicableType per-module + cross-module DbSet + [Authorize] — no copy-paste bug)
Known-minor (unreachable): Reject/Return actor-check skip nếu CurrentApprovalLevelOrder
null — nhưng DaGuiDuyet luôn có set (defer hardening).
ItTicket KHÔNG đụng (kanban, no workflow V2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -14,10 +14,63 @@ public class LeaveRequestsController(IMediator mediator) : ControllerBase
|
||||
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
|
||||
=> Ok(await mediator.Send(new GetLeaveRequestsQuery(status, requesterUserId, page, pageSize)));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var dto = await mediator.Send(new GetLeaveRequestByIdQuery(id));
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateLeaveRequestCommand cmd)
|
||||
{
|
||||
var id = await mediator.Send(cmd);
|
||||
return Created(string.Empty, new { id });
|
||||
return CreatedAtAction(nameof(GetById), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateLeaveRequestDraftBody body)
|
||||
{
|
||||
await mediator.Send(new UpdateLeaveRequestDraftCommand(id, body.LeaveTypeId, body.StartDate,
|
||||
body.EndDate, body.NumDays, body.Reason, body.ApprovalWorkflowId));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/submit")]
|
||||
public async Task<IActionResult> Submit(Guid id)
|
||||
{
|
||||
await mediator.Send(new SubmitLeaveRequestCommand(id));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
public async Task<IActionResult> Approve(Guid id, [FromBody] WorkflowActionBody body)
|
||||
{
|
||||
await mediator.Send(new ApproveLeaveRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reject")]
|
||||
public async Task<IActionResult> Reject(Guid id, [FromBody] WorkflowActionBody body)
|
||||
{
|
||||
await mediator.Send(new RejectLeaveRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/return")]
|
||||
public async Task<IActionResult> Return(Guid id, [FromBody] WorkflowActionBody body)
|
||||
{
|
||||
await mediator.Send(new ReturnLeaveRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record UpdateLeaveRequestDraftBody(
|
||||
Guid LeaveTypeId,
|
||||
DateTime StartDate,
|
||||
DateTime EndDate,
|
||||
decimal NumDays,
|
||||
string Reason,
|
||||
Guid? ApprovalWorkflowId);
|
||||
|
||||
public record WorkflowActionBody(string? Comment);
|
||||
}
|
||||
|
||||
@ -14,10 +14,64 @@ public class OtRequestsController(IMediator mediator) : ControllerBase
|
||||
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
|
||||
=> Ok(await mediator.Send(new GetOtRequestsQuery(status, requesterUserId, page, pageSize)));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var dto = await mediator.Send(new GetOtRequestByIdQuery(id));
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateOtRequestCommand cmd)
|
||||
{
|
||||
var id = await mediator.Send(cmd);
|
||||
return Created(string.Empty, new { id });
|
||||
return CreatedAtAction(nameof(GetById), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateOtRequestDraftBody body)
|
||||
{
|
||||
await mediator.Send(new UpdateOtRequestDraftCommand(id, body.OtDate, body.StartTime,
|
||||
body.EndTime, body.Hours, body.Reason, body.OtPolicyId, body.ApprovalWorkflowId));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/submit")]
|
||||
public async Task<IActionResult> Submit(Guid id)
|
||||
{
|
||||
await mediator.Send(new SubmitOtRequestCommand(id));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
public async Task<IActionResult> Approve(Guid id, [FromBody] WorkflowActionBody body)
|
||||
{
|
||||
await mediator.Send(new ApproveOtRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reject")]
|
||||
public async Task<IActionResult> Reject(Guid id, [FromBody] WorkflowActionBody body)
|
||||
{
|
||||
await mediator.Send(new RejectOtRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/return")]
|
||||
public async Task<IActionResult> Return(Guid id, [FromBody] WorkflowActionBody body)
|
||||
{
|
||||
await mediator.Send(new ReturnOtRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record UpdateOtRequestDraftBody(
|
||||
DateTime OtDate,
|
||||
TimeSpan StartTime,
|
||||
TimeSpan EndTime,
|
||||
decimal Hours,
|
||||
string Reason,
|
||||
Guid? OtPolicyId,
|
||||
Guid? ApprovalWorkflowId);
|
||||
|
||||
public record WorkflowActionBody(string? Comment);
|
||||
}
|
||||
|
||||
@ -14,10 +14,64 @@ public class TravelRequestsController(IMediator mediator) : ControllerBase
|
||||
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
|
||||
=> Ok(await mediator.Send(new GetTravelRequestsQuery(status, requesterUserId, page, pageSize)));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var dto = await mediator.Send(new GetTravelRequestByIdQuery(id));
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateTravelRequestCommand cmd)
|
||||
{
|
||||
var id = await mediator.Send(cmd);
|
||||
return Created(string.Empty, new { id });
|
||||
return CreatedAtAction(nameof(GetById), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTravelRequestBody body)
|
||||
{
|
||||
await mediator.Send(new UpdateTravelRequestDraftCommand(id, body.Destination, body.StartDate,
|
||||
body.EndDate, body.NumDays, body.Purpose, body.EstimatedCost, body.ApprovalWorkflowId));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/submit")]
|
||||
public async Task<IActionResult> Submit(Guid id)
|
||||
{
|
||||
await mediator.Send(new SubmitTravelRequestCommand(id));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
public async Task<IActionResult> Approve(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new ApproveTravelRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reject")]
|
||||
public async Task<IActionResult> Reject(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new RejectTravelRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/return")]
|
||||
public async Task<IActionResult> Return(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new ReturnTravelRequestCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record UpdateTravelRequestBody(
|
||||
string Destination,
|
||||
DateTime StartDate,
|
||||
DateTime EndDate,
|
||||
int NumDays,
|
||||
string Purpose,
|
||||
decimal? EstimatedCost,
|
||||
Guid? ApprovalWorkflowId);
|
||||
|
||||
public record ApprovalActionBody(string? Comment);
|
||||
}
|
||||
|
||||
@ -14,10 +14,65 @@ public class VehicleBookingsController(IMediator mediator) : ControllerBase
|
||||
public async Task<IActionResult> GetList([FromQuery] int? status, [FromQuery] Guid? requesterUserId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
|
||||
=> Ok(await mediator.Send(new GetVehicleBookingsQuery(status, requesterUserId, page, pageSize)));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var dto = await mediator.Send(new GetVehicleBookingByIdQuery(id));
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateVehicleBookingCommand cmd)
|
||||
{
|
||||
var id = await mediator.Send(cmd);
|
||||
return Created(string.Empty, new { id });
|
||||
return CreatedAtAction(nameof(GetById), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVehicleBookingBody body)
|
||||
{
|
||||
await mediator.Send(new UpdateVehicleBookingDraftCommand(id, body.VehicleLicense, body.VehicleName,
|
||||
body.StartAt, body.EndAt, body.Destination, body.Purpose, body.DriverName, body.ApprovalWorkflowId));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/submit")]
|
||||
public async Task<IActionResult> Submit(Guid id)
|
||||
{
|
||||
await mediator.Send(new SubmitVehicleBookingCommand(id));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
public async Task<IActionResult> Approve(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new ApproveVehicleBookingCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reject")]
|
||||
public async Task<IActionResult> Reject(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new RejectVehicleBookingCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/return")]
|
||||
public async Task<IActionResult> Return(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new ReturnVehicleBookingCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record UpdateVehicleBookingBody(
|
||||
string VehicleLicense,
|
||||
string? VehicleName,
|
||||
DateTime StartAt,
|
||||
DateTime EndAt,
|
||||
string Destination,
|
||||
string Purpose,
|
||||
string? DriverName,
|
||||
Guid? ApprovalWorkflowId);
|
||||
|
||||
public record ApprovalActionBody(string? Comment);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user