314 lines
13 KiB
Markdown
314 lines
13 KiB
Markdown
|
||
|
||
# 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 `<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 doc’s authoring; update file paths if the repo structure changes.*
|