Files
sciagent/docs/PDF_preview.md
T
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

13 KiB
Raw Blame History

Applicant PDF / report preview — re-implementation guide

This document describes how PDF and “draft” preview work in the DYD frontend and backend so you can reproduce behavior in another codebase or refactor safely.


1. Two different things called “PDF”

Path What it is Layout fidelity
Server PDF GET /api/reports/{reportId}/export/pdf Same as Xuất Word, then LibreOffice DOCX → PDF. Official template layout.
Client “draft” preview PdfExportDialog + PrintableReport + html2canvas + jspdf HTML recreation; not pixel-equal to Word. Used when server PDF fails (typical: LibreOffice missing).

Canonical document for official exports: the filled .docx. The PDF is a conversion, not a separately maintained template.

Reference implementation:


2. Why server PDF matches DOCX (margins, text, spacing)

ExportReportPdfHandler runs:

  1. ExportReportDocxQuery → bytes of the filled report .docx (identical pipeline to Xuất Word).
  2. SofficeConverter.WordToPdfAsync(wordBytes) → writes temp .docx, runs LibreOffice headless --convert-to pdf, reads input.pdf.

So the PDF is one layout engine pass over the merged OpenXML file. Field text and structural spacing come from that document; you are not duplicating merge logic for PDF.

Caveats: Font availability on the server, LibreOffice vs Microsoft Word subtle differences, and very long unbroken strings may change wrapping. For strict WYSIWYG with Word desktop, compare in both viewers.

Contrast: The React PrintableReport path does not use the Word template—it renders its own HTML. Use it only as a fallback preview, not as a legal duplicate of the official form.


3. User flows (where the preview opens)

3.1 My Reports (fe0/src/pages/MyReports.tsx)

  1. User clicks Xuất PDF on a row.
  2. App calls GET /api/reports/{id}/export/pdf and downloads the blob on success.
  3. If the error message contains LibreOffice or soffice, it alerts and opens PdfExportDialog with { reportId, initiativeId, reportCode } from the row (initiative id is set correctly).

3.2 Dashboard Overview — Panel 2 (fe0/src/pages/DashboardOverview.tsx)

  1. Xuất PDF calls the backend download path only (handleExportPdfBackend).
  2. On LibreOffice failure, fallback opens PdfExportDialog but currently passes initiativeId: ''. That disables useInitiative, so Section I of PrintableReport may show placeholders. Fix when re-implementing: pass selectedReport.initiativeId.

3.3 Component names

There is no symbol ApplicantPreviewPanel. The modal title is “Xem trước PDF” (PdfExportDialog).


4. Data loading for the client preview

PdfExportDialog renders PrintableReport and passes data from three React Query hooks (all under authenticated apiFetch):

Hook Endpoint Type (TS)
useInitiative(id) GET /api/initiatives/{id} InitiativeDetail
useReport(id) GET /api/reports/{id} Report
useDocumentsByReport(reportId) GET /api/documents/by-report/{reportId} DocumentListItem[]

Hooks use enabled: !!id. Empty initiativeId skips initiative fetch.

DocumentListItem includes optional content (JSON string) and summary. Each recognition document (Mẫu 0104) stores form state as JSON in content.

DocumentType enum: Description = 1, Application = 2, ContributionRatio = 3, Evaluation = 4.


5. PrintableReport structure (fallback HTML)

5.1 Root layout

  • ref on a single root div captured by html2canvas.
  • Width 794px (A4 at 96dpi), padding 40px, box-sizing: border-box.
  • Font: Times New Roman, 12px, line-height 1.5, black on white.
  • Optional prop schoolName (default: Đại học Y Dược TP. HCM).

5.2 Sections and data sources

Section Source Notes
Header (ministry, school, “BÁO CÁO SÁNG KIẾN”, export date) Static + new Date() vi-VN
Metadata grid (mã BC, mã SK, tiêu đề, đơn vị, trạng thái BC, dates) report, initiative Status via numeric map → Vietnamese label
I. Nội dung Sáng kiến initiative shortSummary, description, objectives, scopeOfApplication, expectedOutcomes, startDate/endDate, estimatedBudget
II. Kết quả thực tế report actualOutcomes, actualBudget, implementationNotes, challenges, lessonsLearned
III. Checklist 4 tài liệu documents Fixed order of four DocumentType values; codes, DOCUMENT_STATUS_LABELS, approvalDate; optional bullet list from summary
IV. Mẫu 01 Parse content for DocumentType.DescriptionDescriptionData Only if parsed object is truthy
V. Mẫu 02 DocumentType.ApplicationApplicationData Authors/support staff tables, classification labels
VI. Mẫu 03 DocumentType.ContributionRatioContributionData Participants + total % row
VII. Mẫu 04 DocumentType.EvaluationEvaluationData Scores table + total /100
Footer Static + date

Empty string fields use helper Paragraph → gray italic “(chưa có nội dung)”.

pageBreakBefore on IVVII does not create real breaks in the downloaded PDF from html2canvas; see section 6.


6. Download pipeline (PdfExportDialog → file)

  1. await document.fonts.ready if available (helps Vietnamese glyphs).
  2. html2canvas(ref, { scale: 2, backgroundColor: '#ffffff', useCORS: true, logging: false, windowWidth/Height: element scroll size }).
  3. canvas.toDataURL('image/png').
  4. jsPDF A4 portrait; image width = page width; height from aspect ratio.
  5. Multi-page: repeatedly addPage() and addImage with a negative Y offset to slice one tall image across pages (position = heightLeft - imgHeight). Limitation: content can be cut mid-line or mid-table.

Output filename: {reportCode}_{YYYYMMDD}.pdf (fallback codes: report?.code or BaoCao).

Dependencies: html2canvas, jspdf (see fe0/package.json).


7. JSON shapes (document.content) — examples

Parse with JSON.parse; invalid JSON → section omitted (or empty). Shapes are defined in PrintableReport.tsx and should stay aligned with the four tab forms that persist PUT /api/documents/{id}.

7.1 Mẫu 01 — DescriptionData (DocumentType.Description)

{
  "introduction": "Mở đầu...",
  "initiativeName": "Tên sáng kiến",
  "applicationField": "Lĩnh vực",
  "currentStatus": "Tình trạng giải pháp đã biết",
  "purpose": "Mục đích",
  "solutionContent": "Nội dung giải pháp",
  "implementationSteps": "Các bước thực hiện",
  "conditions": "Điều kiện áp dụng",
  "trialUnits": [
    { "id": 1, "name": "Đơn vị A", "address": "Địa chỉ", "field": "Lĩnh vực áp dụng" }
  ],
  "novelty": "Tính mới",
  "effectiveness": {
    "economic": "...",
    "social": "...",
    "teaching": "...",
    "productivity": "...",
    "quality": "...",
    "environment": "...",
    "safety": "..."
  },
  "confidentialInfo": "...",
  "submissionDate": "2026-01-15",
  "authorName": "..."
}

7.2 Mẫu 02 — ApplicationData (DocumentType.Application)

{
  "unitName": "Đơn vị chủ quản",
  "initiativeName": "Tên SK đề nghị",
  "investorName": "Chủ đầu tư",
  "applicationField": "Lĩnh vực",
  "firstApplyDate": "2025-06-01",
  "authors": [
    {
      "id": 1,
      "name": "Nguyễn Văn A",
      "dob": "01/01/1980",
      "workplace": "Khoa X",
      "title": "PGS",
      "qualification": "TS",
      "contributionPercent": 60
    }
  ],
  "initiativeClassification": "technical",
  "contentSummary": "Tóm tắt nội dung",
  "confidentialInfo": "",
  "conditions": "",
  "authorEvaluation": "",
  "trialEvaluation": "",
  "supportStaff": [
    {
      "id": 1,
      "name": "Trợ lý",
      "dob": "",
      "workplace": "",
      "title": "",
      "qualification": "",
      "supportContent": "Hỗ trợ hành chính"
    }
  ],
  "submissionDay": 10,
  "submissionMonth": 5,
  "submissionYear": "2026"
}

Classification values: technical | research | textbook (mapped to long Vietnamese labels in UI).

7.3 Mẫu 03 — ContributionData (DocumentType.ContributionRatio)

{
  "initiativeName": "Tên SK",
  "mainAuthor": "Tác giả chính",
  "position": "Trưởng khoa — Khoa X",
  "representativePercent": 40,
  "submissionDate": "2026-05-01",
  "participants": [
    { "id": 1, "fullName": "Nguyễn B", "workUnit": "Khoa Y", "contributionPercent": 40 }
  ],
  "digitalSignatureConfirmed": true
}

Total row sums participants[].contributionPercent (display only; not validated here).

7.4 Mẫu 04 — EvaluationData (DocumentType.Evaluation)

{
  "initiativeName": "Tên SK",
  "authorName": "Tác giả",
  "position": "Chức vụ",
  "evaluationDate": "2026-05-11",
  "noveltyLevel": "high",
  "noveltyScore": 35,
  "noveltyComment": "Nhận xét tính mới",
  "effectivenessLevel": "medium",
  "effectivenessScore": 45,
  "effectivenessComment": "Nhận xét hiệu quả",
  "conclusion": "Kết luận"
}

Levels: high | medium | low. Printed table shows shortened level text (split on ().


8. Field inventory (PrintableReport) — quick reference

8.1 InitiativeDetail → Section I and header

UI label (approx.) Field on InitiativeDetail
Mã Sáng kiến code
Tiêu đề SK title
Đơn vị chủ trì owningUnitName
Mô tả tóm tắt shortSummary
Mô tả chi tiết description
Mục tiêu objectives
Phạm vi áp dụng scopeOfApplication
Kết quả dự kiến expectedOutcomes
Thời gian áp dụng startDate, endDate (ISO)
Kinh phí dự toán estimatedBudget

8.2 Report → Section II and header

UI label Field on Report
Mã Báo cáo code
Trạng thái BC status (numeric → label map)
Ngày nộp BC submissionDate
Ngày duyệt BC approvalDate
Kết quả đạt được actualOutcomes
Kinh phí thực tế actualBudget
Ghi chú triển khai implementationNotes
Khó khăn challenges
Bài học lessonsLearned

8.3 DocumentListItem → Section III

UI Field
Loại type + DOCUMENT_TYPE_LABELS
code
Trạng thái status + DOCUMENT_STATUS_LABELS
Ngày duyệt approvalDate
Tóm tắt (bullets) summary

8.4 JSON document sections

See section 7 for keys. Every field in DescriptionData, ApplicationData, ContributionData, EvaluationData maps 1:1 to labels inside PrintableReport.tsx (search Paragraph label= and table headers).


9. Re-implementation checklist

  • Official PDF/Word: Call the same APIs (export/docx, export/pdf) so template fidelity stays server-side.
  • Fallback modal: Fetch initiative + report + documents; render a single scrollable column layout; optional: fix Dashboard Overview initiativeId on fallback.
  • Download: Match html2canvas + jspdf multi-page slicing or replace with a service that returns vector PDF (e.g. server-only preview URL).
  • i18n: Labels are hardcoded Vietnamese in PrintableReport.
  • Tests: Fixture objects for InitiativeDetail, Report, four DocumentListItem records with sample content JSON; snapshot or visual regression on the root div dimensions.

10. Embedded PDF preview (optional UX)

If you need an in-app preview that matches the official PDF (as in a browser PDF viewer with page count):

  1. GET /api/reports/{id}/export/pdfblobURL.createObjectURL(blob).
  2. Use <iframe src={url} /> or react-pdf / PDF.js on that URL.
  3. For side-by-side Word, same pattern with GET /api/reports/{id}/export/docx (browser may download; for in-browser preview consider a docx preview library or server-rendered HTML).

This path keeps one generation pipeline and avoids duplicating the Word layout in React.


Generated to match the DYD codebase layout as of the docs authoring; update file paths if the repo structure changes.