# 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`](../fe0/src/components/PdfExportDialog.tsx) - Layout: [`fe0/src/components/PrintableReport.tsx`](../fe0/src/components/PrintableReport.tsx) - Hooks: [`fe0/src/api/hooks.ts`](../fe0/src/api/hooks.ts) - PDF handler: [`src/Backend/DYD.Application/Features/Reports/ExportReportPdf.cs`](../src/Backend/DYD.Application/Features/Reports/ExportReportPdf.cs) - LibreOffice wrapper: [`src/Backend/DYD.Application/Common/Export/SofficeConverter.cs`](../src/Backend/DYD.Application/Common/Export/SofficeConverter.cs) --- ## 2. Why server PDF matches DOCX (margins, text, spacing) [`ExportReportPdfHandler`](../src/Backend/DYD.Application/Features/Reports/ExportReportPdf.cs) 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 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 - `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.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) 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`](../fe0/src/components/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`) ```json { "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`) ```json { "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`) ```json { "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`) ```json { "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`](../fe0/src/components/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/pdf` → `blob` → `URL.createObjectURL(blob)`. 2. Use `