[CLAUDE] Domain+App+Infra+Api+FE-Admin+FE-User: S37 Mig 37 enum + Plan G-O3 Đề xuất full-stack
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m53s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m53s
Phase 10.3 G-O3 Đề xuất (Proposal) — Mig 37 enum extend +5 values + Mig 38 Proposal schema + BE CQRS 8 endpoint + FE 2 app SHA256 IDENTICAL. Mig 37 (em main solo): extend ApprovalWorkflowApplicableType enum +5 values ProposalGeneral=4 / LeaveRequest=5 / OtRequest=6 / VehicleBooking=7 / ItTicket=8 cookie-cutter Mig 22 pattern (Up/Down empty — enum mức Domain). Mig 38 (em main solo): 4 entity Proposal (Code DX/YYYY/NNN) + ProposalAttachment + ProposalLevelOpinion (UNIQUE composite PEId+LevelId mirror PE Mig 26) + ProposalCodeSequence (Prefix PK atomic seq). 4 EF Config + 2 DbContext mod. BE CQRS (em main solo ~700 LOC ProposalFeatures.cs sau Implementer truncate phase exploration gotcha #53 5th + 529 Overload): - 4 Header handler (List paged + GetById detail + Create + UpdateDraft owner-OR-admin) - 4 Workflow handler (Submit gen MaDeXuat atomic + Approve UPSERT LevelOpinion advance + Reject + Return) - SERIALIZABLE transaction CodeGen - DTOs nested LevelOpinion với Step+Level metadata JOIN ProposalsController 8 endpoint /api/proposals (List/GetById/Create/Update/Submit/Approve/Reject/Return) class-level [Authorize] + handler-level owner-OR-admin guard. DbInitializer: SeedSampleProposalWorkflowV2Async ~40 LOC seed QT-DX-V2-001 IsUserSelectable=true NOT gated DemoSeed per gotcha #51. SeedMenuTreeAsync +4 row (Off_DeXuat sub-group + 3 leaf). FE 2 app (em main solo + Implementer 529 fail fallback): - types/proposal.ts × 2 SHA256 IDENTICAL 95607052ff1138f2 - ProposalsListPage.tsx × 2 IDENTICAL 603f0d9cf74cd09a — table 6 cột + Status badge + filter - ProposalCreatePage.tsx × 2 IDENTICAL 6aed3a76563dd576 — Form Header card - ProposalDetailPage.tsx × 2 IDENTICAL 3dc229ea8dcc9bc0 — 3 Section + WorkflowActions - Pattern 16-bis 8× cumulative (App.tsx + menuKeys + Layout staticMap 3 entry) Verify: - dotnet build PASS 0 error 2 warning pre-existing DocxRenderer - dotnet test 130/130 PASS baseline preserve - npm build × 2 PASS (fe-admin 14.72s + fe-user 6.40s) - SHA256 verify 4 file × 2 app all IDENTICAL Pattern reinforced cumulative S37: - Pattern 12-bis cross-module mirror 11× (PE V2 → Proposal V2 ApproveV2) - Pattern 16-bis 4-place mirror cross-app 8× - gotcha #53 5th occurrence Implementer mid-exploration truncation + 529 Overload 1× — em main solo fallback proven Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,86 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SolutionErp.Application.Office;
|
||||
|
||||
namespace SolutionErp.Api.Controllers;
|
||||
|
||||
// Phase 10.3 G-O3 (S37 2026-05-28) — Đề xuất REST endpoint.
|
||||
// Class-level [Authorize] — any authenticated user can list/create/view.
|
||||
// Per-action ownership/role enforcement trong handler (Drafter OR Admin for write;
|
||||
// Approver-of-current-Level OR Admin for approve/reject/return).
|
||||
[ApiController]
|
||||
[Route("api/proposals")]
|
||||
[Authorize]
|
||||
public class ProposalsController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetList(
|
||||
[FromQuery] int? status,
|
||||
[FromQuery] Guid? departmentId,
|
||||
[FromQuery] Guid? drafterUserId,
|
||||
[FromQuery] bool inboxOnly = false,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? search = null)
|
||||
=> Ok(await mediator.Send(new GetProposalsQuery(status, departmentId, drafterUserId, inboxOnly, page, pageSize, search)));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var dto = await mediator.Send(new GetProposalByIdQuery(id));
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateProposalCommand cmd)
|
||||
{
|
||||
var id = await mediator.Send(cmd);
|
||||
return CreatedAtAction(nameof(GetById), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateProposalDraftBody body)
|
||||
{
|
||||
await mediator.Send(new UpdateProposalDraftCommand(id, body.Title, body.Description,
|
||||
body.AmountEstimate, body.DepartmentId, body.ApprovalWorkflowId));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/submit")]
|
||||
public async Task<IActionResult> Submit(Guid id)
|
||||
{
|
||||
await mediator.Send(new SubmitProposalCommand(id));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
public async Task<IActionResult> Approve(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new ApproveProposalCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reject")]
|
||||
public async Task<IActionResult> Reject(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new RejectProposalCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/return")]
|
||||
public async Task<IActionResult> Return(Guid id, [FromBody] ApprovalActionBody body)
|
||||
{
|
||||
await mediator.Send(new ReturnProposalCommand(id, body.Comment));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record UpdateProposalDraftBody(
|
||||
string Title,
|
||||
string? Description,
|
||||
decimal? AmountEstimate,
|
||||
Guid? DepartmentId,
|
||||
Guid? ApprovalWorkflowId);
|
||||
|
||||
public record ApprovalActionBody(string? Comment);
|
||||
}
|
||||
Reference in New Issue
Block a user