13 KiB
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:
- Dialog:
fe0/src/components/PdfExportDialog.tsx - Layout:
fe0/src/components/PrintableReport.tsx - Hooks:
fe0/src/api/hooks.ts - PDF handler:
src/Backend/DYD.Application/Features/Reports/ExportReportPdf.cs - LibreOffice wrapper:
src/Backend/DYD.Application/Common/Export/SofficeConverter.cs
2. Why server PDF matches DOCX (margins, text, spacing)
ExportReportPdfHandler runs:
ExportReportDocxQuery→ bytes of the filled report.docx(identical pipeline to Xuất Word).SofficeConverter.WordToPdfAsync(wordBytes)→ writes temp.docx, runs LibreOffice headless--convert-to pdf, readsinput.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)
- User clicks Xuất PDF on a row.
- App calls
GET /api/reports/{id}/export/pdfand downloads the blob on success. - If the error message contains
LibreOfficeorsoffice, it alerts and opensPdfExportDialogwith{ reportId, initiativeId, reportCode }from the row (initiative id is set correctly).
3.2 Dashboard Overview — Panel 2 (fe0/src/pages/DashboardOverview.tsx)
- Xuất PDF calls the backend download path only (
handleExportPdfBackend). - On LibreOffice failure, fallback opens
PdfExportDialogbut currently passesinitiativeId: ''. That disablesuseInitiative, so Section I ofPrintableReportmay show placeholders. Fix when re-implementing: passselectedReport.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 01–04) 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
refon a single rootdivcaptured byhtml2canvas.- 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.Description → DescriptionData |
Only if parsed object is truthy |
| V. Mẫu 02 | DocumentType.Application → ApplicationData |
Authors/support staff tables, classification labels |
| VI. Mẫu 03 | DocumentType.ContributionRatio → ContributionData |
Participants + total % row |
| VII. Mẫu 04 | DocumentType.Evaluation → EvaluationData |
Scores table + total /100 |
| Footer | Static + date |
Empty string fields use helper Paragraph → gray italic “(chưa có nội dung)”.
pageBreakBefore on IV–VII does not create real breaks in the downloaded PDF from html2canvas; see section 6.
6. Download pipeline (PdfExportDialog → file)
await document.fonts.readyif available (helps Vietnamese glyphs).html2canvas(ref, { scale: 2, backgroundColor: '#ffffff', useCORS: true, logging: false, windowWidth/Height: element scroll size }).canvas.toDataURL('image/png').jsPDFA4 portrait; image width = page width; height from aspect ratio.- Multi-page: repeatedly
addPage()andaddImagewith 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 |
| Mã | 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
initiativeIdon fallback. - Download: Match
html2canvas+jspdfmulti-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, fourDocumentListItemrecords with samplecontentJSON; snapshot or visual regression on the rootdivdimensions.
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):
GET /api/reports/{id}/export/pdf→blob→URL.createObjectURL(blob).- Use
<iframe src={url} />orreact-pdf/ PDF.js on that URL. - 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 doc’s authoring; update file paths if the repo structure changes.