[CLAUDE] PurchaseEvaluation: S22+4 Chunk A — BE attachment view endpoint + AdjustBudget command
Feature 1 (attachment preview):
- NEW `GET /api/purchase-evaluations/{id}/attachments/{attId}/view`
- Cùng handler download, override `Content-Disposition: inline` để FE nhúng iframe
- Permission: same scope GET phiếu (Plan E V2 strict scope)
Feature 2 (điều chỉnh ngân sách):
- NEW `AdjustPurchaseEvaluationBudgetCommand` + Handler + Validator
- NEW `PATCH /api/purchase-evaluations/{id}/budget-adjust` body
`{budgetId, budgetManualName, budgetManualAmount}`
- Phase + actor scope guard:
* DangSoanThao/TraLai → chỉ Drafter của phiếu
* ChoDuyet → Approver currentLevel (match ApproverUserId) — V2 only
* Admin → bypass tất cả
- Audit changelog với diff narrative: "Điều chỉnh ngân sách: link X→Y, số tiền A→Bđ [Drafter/Approver Bước/Cấp/Admin]"
- Tách riêng KHÔNG dùng UpdatePeDraft vì Approver scope KHÔNG nên được edit
Section 1 fields (TenGoiThau/DiaDiem/MoTa/PaymentTerms)
Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Test defer carry Plan C (UAT mode §7) — guard logic critical, ưu tiên cho S23+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -52,6 +52,18 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// S22+4 — Feature 2: Section "Điều chỉnh ngân sách" tách endpoint riêng.
|
||||
// Cho phép Drafter (DangSoanThao/TraLai) OR Approver currentLevel (ChoDuyet)
|
||||
// OR Admin adjust Budget* fields. Handler kiểm phase + actor scope.
|
||||
[HttpPatch("{id:guid}/budget-adjust")]
|
||||
public async Task<IActionResult> AdjustBudget(Guid id, [FromBody] AdjustBudgetBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new AdjustPurchaseEvaluationBudgetCommand(
|
||||
id, body.BudgetId, body.BudgetManualName, body.BudgetManualAmount), ct);
|
||||
return NoContent();
|
||||
}
|
||||
public record AdjustBudgetBody(Guid? BudgetId, string? BudgetManualName, decimal? BudgetManualAmount);
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionPeBody body, CancellationToken ct)
|
||||
{
|
||||
@ -182,6 +194,17 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
return File(f.Content, f.ContentType, f.FileName);
|
||||
}
|
||||
|
||||
// S22+4 — Inline view endpoint cho FE preview modal (PDF iframe, image <img>).
|
||||
// Cùng handler download, khác Content-Disposition (inline vs attachment).
|
||||
// Permission: same actor scope như GET phiếu (Plan E V2 strict).
|
||||
[HttpGet("{id:guid}/attachments/{attId:guid}/view")]
|
||||
public async Task<IActionResult> ViewAttachment(Guid id, Guid attId, CancellationToken ct)
|
||||
{
|
||||
var f = await mediator.Send(new DownloadPurchaseEvaluationAttachmentQuery(id, attId), ct);
|
||||
Response.Headers["Content-Disposition"] = $"inline; filename=\"{f.FileName}\"";
|
||||
return File(f.Content, f.ContentType);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}/attachments/{attId:guid}")]
|
||||
public async Task<IActionResult> DeleteAttachment(Guid id, Guid attId, CancellationToken ct)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user