Files
solution-erp/docs/flows/form-render-flow.md
pqhuy1987 49a5f57a50 [CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md:
- Conventions (naming, data types, audit fields, soft delete)
- Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin
- Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix)
- Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence)
- Mermaid ERD cho tung phase
- Migration workflow (create/apply/revert)
- Index strategy + unique indexes
- Backup/restore SQL
- Common pitfalls + SQL cheatsheet

docs/flows/ — 6 flow documentation:
- README.md: index
- auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist)
- permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy
- contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft
- contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI
- form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert
- sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify

Update references:
- CLAUDE.md (root): them 2 row Tai lieu quan trong
- docs/CLAUDE.md: update project layout voi flows/ + database/
- docs/STATUS.md: log docs addition
- docs/changelog/migration-todos.md: tick Phase 0 docs items

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:15:28 +07:00

8.2 KiB

Form Render Flow — Template Engine

Status: 📝 Planned (Phase 2) Mục tiêu: render .docx/.xlsx giống 100% mẫu gốc với field động đã điền Spec 8 form: ../forms-spec.md

1. Stack lựa chọn

Format Library Status Lý do
.docx render DocumentFormat.OpenXml 3.x Chọn mặc định Free, maintained by MS, dùng đủ cho template placeholder
.docx render (alternative) Aspose.Words Option phí Dễ dùng hơn, render PDF in-process, nhưng cần license
.xlsx render ClosedXML 0.105+ Chọn mặc định Free, LGPL, API dễ; support formula, merged cells, style
.xlsx render (alternative) EPPlus 7+ Không chọn License commercial sau v5; non-commercial license risky
PDF convert LibreOffice headless (soffice --convert-to pdf) Production path Free, chất lượng OK
PDF convert (dev only) Aspose.Words SaveAs PDF Option nếu mua Aspose In-process, không cần external binary

Quyết định Phase 2: OpenXml + ClosedXML + LibreOffice. Nếu render DOCX phức tạp quá (table lồng, heading numbering) → đánh giá lại Aspose.

2. Template placeholder syntax

Sử dụng {{fieldName}} cho text đơn giản và {{#loop}}...{{/loop}} cho bảng lặp.

Ví dụ trong file .docx (FO-002.05 Giao khoán):

Hợp đồng số: {{maHopDong}}
Ngày: {{ngayKy}}

Bên A: {{benA_tenCongTy}}
Địa chỉ: {{benA_diaChi}}
MST: {{benA_maSoThue}}

Bên B: {{benB_tenNCC}}
...

Danh mục công việc:
| STT | Hạng mục | Đơn giá | Khối lượng | Thành tiền |
| --- | -------- | ------- | ---------- | ---------- |
{{#hangMuc}}
| {{stt}} | {{tenHangMuc}} | {{donGia}} | {{khoiLuong}} | {{thanhTien}} |
{{/hangMuc}}

Tổng giá trị: {{giaTri}} VND

Field không có trong data → replace bằng rỗng hoặc (config).

3. Flow render .docx

sequenceDiagram
    participant Caller as API / Workflow
    participant REN as IFormRenderer
    participant DOCX as DocxRenderer
    participant OX as OpenXml
    participant FS as File Storage

    Caller->>REN: Render(templateId, dataDict)
    REN->>REN: Load ContractTemplate from DB<br/>(get TemplatePath + FieldSpec)

    REN->>DOCX: RenderDocx(templatePath, dataDict)
    DOCX->>FS: Copy template → temp/{guid}.docx
    DOCX->>OX: Open WordprocessingDocument

    DOCX->>DOCX: Find all Text elements<br/>(w:t nodes)
    loop Each w:t
        DOCX->>DOCX: Regex replace {{field}} → value
    end

    DOCX->>DOCX: Process {{#loop}} blocks:<br/>1. Find row/paragraph có placeholder loop<br/>2. Clone n-1 lần cho n items<br/>3. Replace field trong từng clone
    DOCX->>OX: Save + Close
    OX-->>DOCX: byte[] hoặc path
    DOCX-->>REN: file bytes + metadata
    REN-->>Caller: RenderResult{bytes, fileName, contentType}

4. Flow render .xlsx (FO-002.07 PO)

sequenceDiagram
    participant Caller
    participant REN as XlsxRenderer
    participant CX as ClosedXML
    participant FS

    Caller->>REN: RenderXlsx(templateId, dataDict)
    REN->>FS: Copy template xlsx → temp
    REN->>CX: Open XLWorkbook

    loop Each sheet
        REN->>CX: Iterate cells
        alt Cell value contains {{field}}
            REN->>CX: Replace value với data[field]<br/>(giữ style, format)
        end
    end

    loop Each named range "LoopHangMuc"
        REN->>REN: Insert n-1 rows, copy style, fill data
        REN->>REN: Adjust formula references (=SUM(...))
    end

    REN->>CX: Recalculate formulas
    REN->>CX: SaveAs byte[]
    CX-->>REN: xlsx bytes
    REN-->>Caller: RenderResult

ClosedXML edge: phải gọi workbook.CalculateMode = XLCalculateMode.Auto + workbook.FormulaParser.Calculate() trước khi save, nếu không formula tổng vẫn giữ giá trị cũ.

5. PDF convert flow

flowchart TD
    Start([Input: .docx bytes]) --> Save[Lưu temp file .docx]
    Save --> Run[Run: soffice --headless --convert-to pdf<br/>--outdir /tmp in.docx]
    Run -->|exit 0| Read[Read /tmp/in.pdf bytes]
    Run -->|exit != 0| Fail[Log error<br/>return docx thay vì pdf]
    Read --> Cleanup[Cleanup temp files]
    Cleanup --> Return([Return PDF bytes])
    Fail --> Return

Deploy prod:

  • Windows IIS app pool cần LibreOffice portable ở C:\Apps\LibreOffice + PATH env var
  • Subprocess timeout 30s (nếu > 30s → fail, user download docx thay)
  • Pool soffice instance (1 instance / 10 requests) — tránh spawn process mỗi request

6. API endpoints

Preview (không lưu)

POST /api/forms/render
Body: { templateId, data, outputFormat: "docx" | "pdf" }
Response: binary stream, Content-Disposition: inline (nếu pdf), attachment (nếu docx)

Finalize (lưu vào ContractAttachments)

POST /api/contracts/{id}/render-final
Body: { outputFormat: "docx" }
Response: { attachmentId, url }
Side effect: INSERT ContractAttachments (Purpose='Export')

7. Template management (admin)

sequenceDiagram
    actor Admin
    participant FE as fe-admin<br/>/master/contract-templates
    participant API as FormsController.UploadTemplate
    participant VAL as TemplateValidator
    participant DB
    participant FS

    Admin->>FE: Upload file .docx + metadata<br/>(formCode, name, contractType, fieldSpec JSON)
    FE->>API: POST /api/forms/templates<br/>multipart: file + metadata
    API->>VAL: ParsePlaceholders(file)
    VAL->>VAL: Extract all {{field}} tokens<br/>→ compare với fieldSpec
    alt Token không match spec
        VAL-->>API: warning "Field X trong template không có trong spec"
    end

    API->>FS: Save file to wwwroot/templates/{guid}.docx
    API->>DB: INSERT ContractTemplate<br/>(FormCode, Name, TemplatePath, FieldSpec, IsActive=true)
    API-->>FE: 201

8. Field types supported

Type Render behavior Example
string Replace direct "Công ty ABC"
text Replace + preserve line breaks (<w:br/>) "Dòng 1\nDòng 2"
number Format theo culture vi-VN 150000000"150.000.000"
decimal Giá tiền: thêm VND suffix 150000000"150.000.000 VND"
date Format dd/MM/yyyy 2026-04-21"21/04/2026"
boolean Checkbox symbol true"☑", false"☐"
reference Lookup entity name supplierId → "Công ty NCC PVL"
array Với {{#loop}} danh mục hạng mục

9. Edge cases

Case Xử lý
Template bị rename/xóa file vật lý Render throw FileNotFoundException → API 500, log error
Template có macro (.docm) Reject ở upload — chỉ accept .docx
Placeholder bị split giữa 2 <w:t> elements (Word quirk) DocxRenderer normalize trước: merge adjacent <w:t> trong cùng run
Field có ký tự đặc biệt (&, <, >) OpenXml auto XML-escape khi set Text value
Loop template có 0 items Remove template row hoàn toàn (không để lại placeholder row rỗng)
File size > 10MB Chunk upload hoặc reject (Phase 4 optimize)
Render concurrent (2 user render cùng template) Mỗi request lock copy riêng vào temp file → không conflict

10. Performance

Operation Budget Note
Load template from DB + FS < 100ms Cache FileBytes in IMemoryCache 1h
Render .docx (5 pages, 1 table 20 rows) < 500ms Stream-based, không full DOM
Convert PDF via LibreOffice 1-3s Bottleneck, consider pool
Full request (render + PDF + save attachment) < 5s Show spinner FE

11. Testing checklist (Phase 2)

  • FO-002.05 Giao khoán: render với 10 hạng mục → output giống mẫu 100%
  • FO-002.07 PO xlsx: formula =SUM(E5:E30) recalculate đúng
  • Template với ký tự Unicode (đ, ư, ă) → không lỗi encoding
  • Template với 0 items loop → không còn dòng trống
  • PDF convert với 50-page docx → timeout setting OK
  • Concurrent 10 request cùng 1 template → mỗi file output độc lập

12. Liên quan