Phase 11: version PDF (FR-15)

- เลข version ใหม่: เว็บ-generate = v0 ฉบับตั้งต้น (ไม่มี PDF), add-in save (Start Versioning + Save & Version) = v1+ มี PDF เสมอ
- PDF ผลิตจาก Word จริงผ่าน task pane เท่านั้น (getFileAsync Pdf + runtime guard, manifest ไม่เพิ่ม requirement); capture/upload เป็น best-effort — docx ผ่านแม้ PDF ล้ม + MessageBar เตือนตาม hasPdf
- Backend: DocumentVersion.PdfObjectKey (nullable, migration AddVersionPdf) + sibling .pdf ใน bucket docx-versions · multipart part pdf (ผิด type → 400) · /download + /download-link รับ ?format=pdf (ไม่มี → 404) · presigned ฝังชื่อไฟล์ไทย + _vN (RFC 5987)
- Web: เปิดไฟล์แทน blob-download — PDF แท็บใหม่, docx ผ่าน ms-word:ofe|u| พร้อม heuristic fallback ลิงก์สำรอง (useVersionOpen, versionDownload) · ปุ่ม "เปิดใน Word" ที่หน้า success ของฟอร์มสร้างเอกสาร
- Spec v7 + CONTEXT.md + ADR 0001 + แผน docs/plans/version-pdf.md
- Automated QA: backend 68 / add-in 63 / web 55 ผ่าน · manual acceptance ผ่านบน Word for Mac (2026-09-08) รวม fidelity + track changes — ค้างเฉพาะ Windows / Word on the web

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Suphonchai Phoonsawat 2026-09-08 14:01:13 +07:00
parent 618328ffa7
commit 39d610a781
39 changed files with 1387 additions and 212 deletions

View file

@ -1,10 +1,11 @@
<script setup lang="ts">
/** FR-10 UI — dynamic form สร้างเอกสารจาก field definitions ของ template → generate เป็น v1 + ดาวน์โหลด
/** FR-10 UI — dynamic form สร้างเอกสารจาก field definitions ของ template → generate เป็น v0 + ดาวน์โหลด
* FR-12 — รองรับ re-generate: ?from={documentId} → prefill ค่าล่าสุดของเอกสารนั้นให้แก้ก่อนสร้าง */
import type { DocumentDetail, GenerateDocumentResponse, TemplateDetail } from '~/types/api'
const route = useRoute()
const { apiJson, downloadFile } = useApi()
const { apiJson } = useApi()
const { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion, resetOpen } = useVersionOpen()
const templateId = computed(() => String(route.params.templateId))
const detail = ref<TemplateDetail | null>(null)
@ -23,8 +24,6 @@ const loadError = ref<string | null>(null)
const submitting = ref(false)
const submitError = ref<string | null>(null)
const result = ref<GenerateDocumentResponse | null>(null)
const downloading = ref(false)
const downloadError = ref<string | null>(null)
const sortedFields = computed(() => [...(detail.value?.fields ?? [])].sort((a, b) => a.sortOrder - b.sortOrder))
@ -98,22 +97,9 @@ async function submit() {
}
}
async function download() {
if (!result.value) return
downloading.value = true
downloadError.value = null
try {
const name = detail.value ? `${detail.value.name}_v${result.value.versionNumber}.docx` : 'document_v1.docx'
await downloadFile(result.value.downloadUrl, name)
} catch (err) {
downloadError.value = err instanceof Error ? err.message : 'ดาวน์โหลดไม่สำเร็จ'
} finally {
downloading.value = false
}
}
/** กลับไปกรอกฟอร์มใหม่เพื่อสร้างอีกฉบับจาก template เดิม (ถ้าเป็น re-generate → คืนค่า prefill ไม่ใช่ฟอร์มว่าง) */
function resetForm() {
resetOpen()
result.value = null
submitError.value = null
downloadError.value = null
@ -139,7 +125,7 @@ onMounted(load)
</AlertBox>
<template v-else-if="detail">
<!-- ผลลัพธ์: สร้างสำเร็จเป็น v1 แล้ว -->
<!-- ผลลัพธ์: สร้างสำเร็จเป็น v0 แล้ว -->
<template v-if="result">
<AlertBox type="success">
สร้างเอกสารสำเร็จ — บันทึกเป็น <strong>version {{ result.versionNumber }}</strong> แล้ว
@ -165,13 +151,18 @@ onMounted(load)
</button>
<button
type="button"
:disabled="downloading"
:disabled="openingVersionId !== null"
class="btn btn-primary w-full sm:w-auto"
@click="download"
@click="openVersion(result.documentId, result.versionId)"
>
{{ downloading ? 'กำลังเตรียมไฟล์…' : '⬇ ดาวน์โหลด .docx' }}
{{ openingVersionId !== null ? 'กำลังเตรียมไฟล์…' : 'เปิดใน Word' }}
</button>
</div>
<AlertBox v-if="fallbackUrl" class="mt-4">
<p v-if="openNotice">{{ openNotice }}</p>
<a :href="fallbackUrl" target="_blank" rel="noopener noreferrer" class="link">ดาวน์โหลด .docx สำรอง</a>
<p class="text-xs text-muted">ลิงก์มีอายุสั้น หากหมดอายุให้กดเปิดใน Word อีกครั้ง</p>
</AlertBox>
<AlertBox v-if="downloadError" type="error" class="mt-4">{{ downloadError }}</AlertBox>
</div>
@ -181,7 +172,7 @@ onMounted(load)
<li>เปิดไฟล์ที่ดาวน์โหลดใน Word (Desktop หรือ Online)</li>
<li>
เปิด task pane ของ add-in <strong>e-Court Documents Versioning</strong> — ระบบจะตรวจพบว่าเป็น
tracked document และแสดง <strong>version 1</strong> ทันที โดยไม่ต้องกด Start Versioning
tracked document และแสดง <strong>version 0</strong> ทันที โดยไม่ต้องกด Start Versioning
</li>
<li>แก้ไขเอกสารต่อแล้วกด <strong>Save &amp; Version</strong> เพื่อบันทึก version ถัดไป</li>
</ol>
@ -278,7 +269,7 @@ onMounted(load)
:disabled="submitting || sortedFields.length === 0 || !detail.isActive"
class="btn btn-primary w-full sm:w-auto"
>
{{ submitting ? 'กำลังสร้างเอกสาร…' : 'สร้างเอกสาร (version 1)' }}
{{ submitting ? 'กำลังสร้างเอกสาร…' : 'สร้างเอกสาร (version 0)' }}
</button>
</div>
</form>