sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 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.*
|
||||
Reference in New Issue
Block a user