[CLAUDE] App+Api+FE-Admin: Form template builder (upload + edit + delete)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m44s

Admin giờ có thể quản lý template HĐ hoàn toàn qua UI — không cần dev
đụng vào file system hay seed data.

BE (FormFeatures.cs + FormsController.cs):
- UploadContractTemplateCommand (multipart): validate FormCode
  (regex [A-Za-z0-9._-]+, unique), file <= 10MB, ext .docx/.xlsx,
  FieldSpec phải là JSON hợp lệ hoặc null. Ghi file vào
  wwwroot/templates/{formCode}_{guid:N}.{ext} để tránh collision
  + path traversal.
- UpdateContractTemplateCommand: sửa metadata + FieldSpec + IsActive
  (không đụng file — chỉ DB).
- DeleteContractTemplateCommand: soft delete qua IsActive=false
  (historical contracts ref template này vẫn resolve).
- Endpoints: POST /api/forms/templates (multipart),
  PUT /api/forms/templates/{id}, DELETE /api/forms/templates/{id}.
  RequestSizeLimit 12MB (validator caps 10MB).

FE (FormsPage.tsx admin):
- PageHeader action button "Upload template" mở dialog mới
- Row actions: Download (render existing), Pencil (edit), Trash (xóa
  confirm) thay vì chỉ có 1 nút Render — row hover reveals clearly
- Upload dialog: file picker với file: pseudo-element brand styled,
  FormCode (required, font-mono), Tên, Loại HĐ select, Mô tả,
  FieldSpec JSON textarea với placeholder example
- Edit dialog: same fields minus file (FormCode disabled, edit chỉ
  cập nhật metadata), có checkbox Kích hoạt
- Shared form submit handler — same dialog cho upload (__new) + edit

Foundation sẵn cho form builder thật (render UI từ FieldSpec JSON
đang là text field — iteration sau sẽ parse + render form dynamic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 21:15:35 +07:00
parent c52186bed0
commit 166d26c1d8
3 changed files with 515 additions and 25 deletions

View File

@ -28,4 +28,50 @@ public class FormsController(IMediator mediator) : ControllerBase
var result = await mediator.Send(new RenderTemplateCommand(id, data), ct);
return File(result.Content, result.ContentType, result.FileName);
}
// ==================== Admin CRUD ====================
// Upload new template — multipart form with file + metadata fields
[HttpPost("templates")]
[RequestSizeLimit(12_000_000)] // ~12 MB (validator caps at 10 MB)
public async Task<ActionResult<ContractTemplateDto>> Upload(
IFormFile file,
[FromForm] string formCode,
[FromForm] string name,
[FromForm] ContractType? contractType = null,
[FromForm] string? description = null,
[FromForm] string? fieldSpec = null,
CancellationToken ct = default)
{
if (file is null || file.Length == 0)
return BadRequest(new { detail = "Chưa chọn file template." });
await using var stream = file.OpenReadStream();
var dto = await mediator.Send(new UploadContractTemplateCommand(
formCode, name, contractType, description, fieldSpec,
file.FileName, file.ContentType, file.Length, stream), ct);
return Ok(dto);
}
[HttpPut("templates/{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateContractTemplateBody body, CancellationToken ct)
{
await mediator.Send(new UpdateContractTemplateCommand(
id, body.Name, body.ContractType, body.Description, body.FieldSpec, body.IsActive), ct);
return NoContent();
}
[HttpDelete("templates/{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteContractTemplateCommand(id), ct);
return NoContent();
}
}
public record UpdateContractTemplateBody(
string Name,
ContractType? ContractType,
string? Description,
string? FieldSpec,
bool IsActive);