234 lines
12 KiB
Vue
234 lines
12 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
/** FR-12 UI — detail ของเอกสาร: ค่าล่าสุดของทุก field + version history (ขยายแถวเห็นค่าตอนนั้น)
|
||
|
|
* + ปุ่ม re-generate: พาไปฟอร์มสร้างเอกสารแบบ prefill ค่าปัจจุบัน → ได้เอกสารใหม่ v1 (ไม่แตะเอกสารเดิม) */
|
||
|
|
import type { DocumentDetail, VersionListItem } from '~/types/api'
|
||
|
|
|
||
|
|
const route = useRoute()
|
||
|
|
const { apiJson, downloadFile } = useApi()
|
||
|
|
|
||
|
|
const documentId = computed(() => String(route.params.documentId))
|
||
|
|
|
||
|
|
const detail = ref<DocumentDetail | null>(null)
|
||
|
|
const versions = ref<VersionListItem[]>([])
|
||
|
|
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) ?? [])
|
||
|
|
|
||
|
|
function snapshotOf(v: VersionListItem) {
|
||
|
|
return normalizeSnapshot(v.fields, fieldOrderKeys.value)
|
||
|
|
}
|
||
|
|
|
||
|
|
/** โชว์ label ของ field ถ้ามีนิยาม ไม่มีก็โชว์ key ตรงๆ */
|
||
|
|
function fieldLabel(key: string): string {
|
||
|
|
return detail.value?.fields.find((f) => f.key === key)?.label ?? key
|
||
|
|
}
|
||
|
|
|
||
|
|
async function load() {
|
||
|
|
loading.value = true
|
||
|
|
loadError.value = null
|
||
|
|
try {
|
||
|
|
const [d, vs] = await Promise.all([
|
||
|
|
apiJson<DocumentDetail>(`/api/documents/${documentId.value}`),
|
||
|
|
apiJson<VersionListItem[]>(`/api/documents/${documentId.value}/versions`),
|
||
|
|
])
|
||
|
|
detail.value = d!
|
||
|
|
versions.value = vs ?? []
|
||
|
|
useHead({ title: `${d?.fileName} — e-Court Documents Versioning` })
|
||
|
|
} catch (err) {
|
||
|
|
loadError.value = err instanceof Error ? err.message : 'โหลดข้อมูลเอกสารไม่สำเร็จ'
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** re-generate — เข้าฟอร์มสร้างเอกสารของ template เดิมโดย prefill ค่าปัจจุบัน (flow เดิม ไม่แตะเอกสารฉบับนี้) */
|
||
|
|
function regenerate() {
|
||
|
|
if (!detail.value?.templateId) return
|
||
|
|
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>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<AuthGate>
|
||
|
|
<section class="space-y-8">
|
||
|
|
<div v-if="loading" class="flex items-center gap-3 py-10 text-sm text-slate-500 dark:text-slate-400">
|
||
|
|
<span
|
||
|
|
class="h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600 dark:border-slate-600 dark:border-t-slate-300"
|
||
|
|
/>
|
||
|
|
กำลังโหลด…
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<AlertBox v-else-if="loadError" type="error">
|
||
|
|
{{ loadError }}
|
||
|
|
<NuxtLink to="/" class="ml-1 font-medium underline">กลับหน้าหลัก</NuxtLink>
|
||
|
|
</AlertBox>
|
||
|
|
|
||
|
|
<template v-else-if="detail">
|
||
|
|
<!-- หัวเรื่อง + การกระทำ -->
|
||
|
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
|
|
<div>
|
||
|
|
<h1 class="text-2xl font-bold break-all">{{ detail.fileName }}</h1>
|
||
|
|
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||
|
|
<template v-if="detail.templateId">
|
||
|
|
จาก template
|
||
|
|
<NuxtLink
|
||
|
|
:to="`/templates/${detail.templateId}/documents`"
|
||
|
|
class="font-medium text-sky-700 hover:underline dark:text-sky-400"
|
||
|
|
>
|
||
|
|
{{ detail.templateName }}
|
||
|
|
</NuxtLink>
|
||
|
|
</template>
|
||
|
|
<template v-else>เอกสารที่ track จาก add-in (ไม่ได้สร้างจาก template)</template>
|
||
|
|
· สร้างเมื่อ {{ formatDateTime(detail.createdAt) }}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex flex-col items-end gap-1">
|
||
|
|
<button
|
||
|
|
v-if="detail.templateId"
|
||
|
|
type="button"
|
||
|
|
:disabled="!detail.templateIsActive"
|
||
|
|
:title="detail.templateIsActive ? '' : 'template นี้ถูก deactivate แล้ว — สร้างเอกสารใหม่ไม่ได้'"
|
||
|
|
class="rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||
|
|
@click="regenerate"
|
||
|
|
>
|
||
|
|
⟳ สร้างเอกสารใหม่จากข้อมูลนี้
|
||
|
|
</button>
|
||
|
|
<p v-if="detail.templateId && !detail.templateIsActive" class="text-xs text-slate-500 dark:text-slate-400">
|
||
|
|
template ถูก deactivate — เอกสารฉบับนี้ยังใช้ได้ แต่สร้างใหม่ไม่ได้
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ค่าล่าสุดของทุก field -->
|
||
|
|
<div class="space-y-3">
|
||
|
|
<h2 class="text-lg font-semibold">ค่า fields ปัจจุบัน</h2>
|
||
|
|
|
||
|
|
<div
|
||
|
|
v-if="detail.fields.length === 0"
|
||
|
|
class="rounded-md border border-dashed border-slate-300 p-6 text-center text-sm text-slate-500 dark:border-slate-600 dark:text-slate-400"
|
||
|
|
>
|
||
|
|
เอกสารนี้ไม่มี field values (ไม่ได้สร้างจาก template หรือ template ไม่มี fields)
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else class="overflow-x-auto rounded-lg border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-800">
|
||
|
|
<table class="w-full text-sm">
|
||
|
|
<thead class="border-b border-slate-200 text-left text-xs uppercase text-slate-500 dark:border-slate-700 dark:text-slate-400">
|
||
|
|
<tr>
|
||
|
|
<th class="px-4 py-3">field</th>
|
||
|
|
<th class="px-4 py-3">ค่า</th>
|
||
|
|
<th class="px-4 py-3">type</th>
|
||
|
|
<th class="px-4 py-3">อัปเดตเมื่อ</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
|
||
|
|
<tr v-for="f in detail.fields" :key="f.key">
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
{{ f.label ?? f.key }}
|
||
|
|
<span v-if="!f.label" class="ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900 dark:text-amber-300">
|
||
|
|
ไม่มีในนิยามปัจจุบัน
|
||
|
|
</span>
|
||
|
|
<span class="block font-mono text-xs text-slate-400">{{ f.key }}</span>
|
||
|
|
</td>
|
||
|
|
<td class="max-w-72 px-4 py-3 break-words">{{ f.value || '—' }}</td>
|
||
|
|
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">{{ f.type ?? '—' }}</td>
|
||
|
|
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">
|
||
|
|
{{ f.updatedAt ? formatDateTime(f.updatedAt) : 'ยังไม่เคยมีค่า' }}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- version history -->
|
||
|
|
<div class="space-y-3">
|
||
|
|
<h2 class="text-lg font-semibold">Version history ({{ versions.length }})</h2>
|
||
|
|
|
||
|
|
<div
|
||
|
|
v-if="versions.length === 0"
|
||
|
|
class="rounded-md border border-dashed border-slate-300 p-6 text-center text-sm text-slate-500 dark:border-slate-600 dark:text-slate-400"
|
||
|
|
>
|
||
|
|
ยังไม่มี version
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else class="overflow-x-auto rounded-lg border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-800">
|
||
|
|
<table class="w-full text-sm">
|
||
|
|
<thead class="border-b border-slate-200 text-left text-xs uppercase text-slate-500 dark:border-slate-700 dark:text-slate-400">
|
||
|
|
<tr>
|
||
|
|
<th class="px-4 py-3">version</th>
|
||
|
|
<th class="px-4 py-3">เวลา</th>
|
||
|
|
<th class="px-4 py-3">ผู้สร้าง</th>
|
||
|
|
<th class="px-4 py-3 text-right">การกระทำ</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
|
||
|
|
<template v-for="v in versions" :key="v.versionId">
|
||
|
|
<tr class="cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/40" @click="expandedVersionId = expandedVersionId === v.versionId ? null : v.versionId">
|
||
|
|
<td class="px-4 py-3 font-medium">v{{ v.versionNumber }}</td>
|
||
|
|
<td class="px-4 py-3">{{ formatDateTime(v.createdAt) }}</td>
|
||
|
|
<td class="px-4 py-3 font-mono text-xs text-slate-500 dark:text-slate-400">{{ v.createdByUserId.slice(0, 8) }}…</td>
|
||
|
|
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
:disabled="downloadingVersionId === v.versionId"
|
||
|
|
class="rounded-md border border-slate-300 px-2.5 py-1 text-xs hover:bg-slate-100 disabled:opacity-50 dark:border-slate-600 dark:hover:bg-slate-700"
|
||
|
|
@click.stop="download(v)"
|
||
|
|
>
|
||
|
|
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
|
||
|
|
</button>
|
||
|
|
<span class="ml-2 text-xs text-slate-400">{{ expandedVersionId === v.versionId ? '▲ ซ่อน fields' : '▼ ดู fields' }}</span>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr v-if="expandedVersionId === v.versionId">
|
||
|
|
<td colspan="4" class="bg-slate-50 px-4 py-4 dark:bg-slate-900">
|
||
|
|
<p class="mb-2 text-xs font-medium text-slate-500 dark:text-slate-400">ค่า fields ของ v{{ v.versionNumber }}</p>
|
||
|
|
<dl class="grid gap-x-6 gap-y-1.5 text-sm sm:grid-cols-2">
|
||
|
|
<div v-for="e in snapshotOf(v)" :key="e.key" class="flex gap-2">
|
||
|
|
<dt class="w-44 shrink-0 truncate text-slate-500 dark:text-slate-400" :title="e.key">
|
||
|
|
{{ fieldLabel(e.key) }}
|
||
|
|
</dt>
|
||
|
|
<dd class="break-words">
|
||
|
|
{{ e.value || '—' }}
|
||
|
|
<span v-if="e.missing" class="ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900 dark:text-amber-300">
|
||
|
|
หายจากเอกสาร
|
||
|
|
</span>
|
||
|
|
</dd>
|
||
|
|
</div>
|
||
|
|
<p v-if="snapshotOf(v).length === 0" class="text-sm text-slate-500 dark:text-slate-400">
|
||
|
|
version นี้ไม่มี snapshot ของ fields
|
||
|
|
</p>
|
||
|
|
</dl>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</template>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<AlertBox v-if="downloadError" type="error">{{ downloadError }}</AlertBox>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</section>
|
||
|
|
</AuthGate>
|
||
|
|
</template>
|