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

314 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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.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 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`](../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 docs authoring; update file paths if the repo structure changes.*