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:
parent
618328ffa7
commit
39d610a781
39 changed files with 1387 additions and 212 deletions
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
/** FR-12 UI — detail ของเอกสาร: ค่าล่าสุดของทุก field + version history (ขยายแถวเห็นค่าตอนนั้น)
|
||||
* + ปุ่ม re-generate: พาไปฟอร์มสร้างเอกสารแบบ prefill ค่าปัจจุบัน → ได้เอกสารใหม่ v1 (ไม่แตะเอกสารเดิม) */
|
||||
* + ปุ่ม re-generate: พาไปฟอร์มสร้างเอกสารแบบ prefill ค่าปัจจุบัน → ได้เอกสารใหม่ v0 (ไม่แตะเอกสารเดิม) */
|
||||
import type { DocumentDetail, VersionListItem } from '~/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
const { apiJson, downloadFile } = useApi()
|
||||
const { apiJson } = useApi()
|
||||
const { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion } = useVersionOpen()
|
||||
|
||||
const documentId = computed(() => String(route.params.documentId))
|
||||
|
||||
|
|
@ -14,8 +15,6 @@ const loading = ref(true)
|
|||
const loadError = ref<string | null>(null)
|
||||
|
||||
const expandedVersionId = ref<string | null>(null)
|
||||
const downloadingVersionId = ref<string | null>(null)
|
||||
const downloadError = ref<string | null>(null)
|
||||
|
||||
const fieldOrderKeys = computed(() => detail.value?.fields.filter((f) => f.label !== null).map((f) => f.key) ?? [])
|
||||
|
||||
|
|
@ -52,19 +51,6 @@ function regenerate() {
|
|||
navigateTo(`/documents/create/${detail.value.templateId}?from=${detail.value.id}`)
|
||||
}
|
||||
|
||||
async function download(v: VersionListItem) {
|
||||
downloadingVersionId.value = v.versionId
|
||||
downloadError.value = null
|
||||
try {
|
||||
const base = (detail.value?.fileName ?? 'document').replace(/\.docx$/i, '')
|
||||
await downloadFile(`/api/documents/${documentId.value}/versions/${v.versionId}/download`, `${base}_v${v.versionNumber}.docx`)
|
||||
} catch (err) {
|
||||
downloadError.value = err instanceof Error ? err.message : 'ดาวน์โหลดไม่สำเร็จ'
|
||||
} finally {
|
||||
downloadingVersionId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
|
|
@ -190,12 +176,14 @@ onMounted(load)
|
|||
<div class="flex shrink-0 flex-col items-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="downloadingVersionId === v.versionId"
|
||||
:disabled="openingVersionId !== null"
|
||||
class="btn btn-secondary btn-xs"
|
||||
@click.stop="download(v)"
|
||||
@click.stop="openVersion(documentId, v.versionId)"
|
||||
>
|
||||
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
|
||||
{{ openingVersionId !== null ? '…' : 'เปิดใน Word' }}
|
||||
</button>
|
||||
<button v-if="v.hasPdf" type="button" class="btn btn-secondary btn-xs"
|
||||
:disabled="openingVersionId !== null" @click.stop="openVersion(documentId, v.versionId, 'pdf')">PDF</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-muted"
|
||||
|
|
@ -257,12 +245,14 @@ onMounted(load)
|
|||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="downloadingVersionId === v.versionId"
|
||||
:disabled="openingVersionId !== null"
|
||||
class="btn btn-secondary btn-xs"
|
||||
@click.stop="download(v)"
|
||||
@click.stop="openVersion(documentId, v.versionId)"
|
||||
>
|
||||
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
|
||||
{{ openingVersionId !== null ? '…' : 'เปิดใน Word' }}
|
||||
</button>
|
||||
<button v-if="v.hasPdf" type="button" class="btn btn-secondary btn-xs ml-2"
|
||||
:disabled="openingVersionId !== null" @click.stop="openVersion(documentId, v.versionId, 'pdf')">PDF</button>
|
||||
<span class="ml-2 text-xs text-muted">
|
||||
{{ expandedVersionId === v.versionId ? '▲ ซ่อน fields' : '▼ ดู fields' }}
|
||||
</span>
|
||||
|
|
@ -292,6 +282,11 @@ onMounted(load)
|
|||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<AlertBox v-if="fallbackUrl">
|
||||
<p v-if="openNotice">{{ openNotice }}</p>
|
||||
<a :href="fallbackUrl" target="_blank" rel="noopener noreferrer" class="link">เปิด / ดาวน์โหลดไฟล์สำรอง</a>
|
||||
<p class="text-xs text-muted">ลิงก์มีอายุสั้น หากหมดอายุให้กดเปิดไฟล์อีกครั้ง</p>
|
||||
</AlertBox>
|
||||
<AlertBox v-if="downloadError" type="error">{{ downloadError }}</AlertBox>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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 & 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue