[CLAUDE] App+Api+FE-Admin: Form template builder (upload + edit + delete)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m44s
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:
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user