[CLAUDE] App+Infra+FE-Admin: DynamicForm + .doc/.xls auto-convert on upload
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m17s

Tier 3 iter2 — form builder UI dùng FieldSpec thay raw JSON textarea.

FE:
- DynamicForm component — parse FieldSpec JSON (record of FieldDef với
  label/type/required/placeholder/hint/options) và render inputs
  dynamic theo type: text/textarea/number/date/currency/select.
- FormsPage render dialog thêm toggle Form ↔ JSON (segmented control).
  Mặc định Form mode khi template có FieldSpec, JSON mode khi không.
  Khi mở dialog cho row khác, reset formValues + chọn đúng default mode.
- parseFieldSpec helper trả { spec, error } — UI báo lỗi nếu JSON
  không parse được, fallback JSON textarea.

BE — generalize converter thành IDocumentConverter:
- IPdfConverter → IDocumentConverter (ConvertAsync(bytes, src, tgt, ct))
  — đủ gánh cả pdf, docx, xlsx targets.
- LibreOfficeDocumentConverter — 1 shell-out pattern cho mọi conversion
  (docx→pdf, doc→docx, xls→xlsx, xlsx→pdf), target arg truyền vào
  --convert-to.
- ExportTemplatePdfCommand update dùng "pdf" target.

Auto-convert .doc/.xls trên upload:
- Validator accept thêm .doc/.xls (thêm note "sẽ tự convert").
- UploadContractTemplateCommandHandler: nếu ext là doc/xls → read stream
  → converter.ConvertAsync → lưu file .docx/.xlsx thay vì format gốc.
  File rendering pipeline (DocxRenderer/XlsxRenderer) chỉ support docx/
  xlsx — convert đảm bảo consistent.
- Display FileName preserve original name nhưng đổi extension.

Unblock 3 file .doc legacy template — admin giờ upload .doc bình thường,
system tự convert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 21:35:05 +07:00
parent 6bbd894d96
commit e45909712b
6 changed files with 300 additions and 56 deletions

View File

@ -1,9 +1,14 @@
namespace SolutionErp.Application.Common.Interfaces;
// Convert .docx/.xlsx bytes to PDF. The Infrastructure impl shells out to
// LibreOffice headless (soffice.exe --headless --convert-to pdf). Future swap:
// QuestPDF re-render if we want zero external dep, or Aspose.Words for quality.
public interface IPdfConverter
// Convert document bytes between office formats. Infrastructure impl shells out
// to LibreOffice headless (soffice.exe --headless --convert-to TARGET).
//
// Supported source/target combos are whatever LibreOffice supports, commonly:
// docx → pdf, doc → docx, xlsx → pdf, xls → xlsx
//
// Future swap: QuestPDF for PDF-only, Aspose.Words for quality, or cloud API —
// without touching callers.
public interface IDocumentConverter
{
Task<byte[]> ConvertAsync(byte[] sourceBytes, string sourceExt, CancellationToken ct = default);
Task<byte[]> ConvertAsync(byte[] sourceBytes, string sourceExt, string targetExt, CancellationToken ct = default);
}