sciagent code + Gitea Actions CI/CD
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thinh Lam
2026-06-30 09:38:30 +07:00
commit 688fac73e9
1167 changed files with 158244 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
# 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.*