

# 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.*
