phase 6 เบื้องต้น
This commit is contained in:
parent
1bb850c6b8
commit
93570d38d8
20 changed files with 1715 additions and 34 deletions
222
web/app/pages/documents/create/[templateId].vue
Normal file
222
web/app/pages/documents/create/[templateId].vue
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<script setup lang="ts">
|
||||
/** FR-10 UI — dynamic form สร้างเอกสารจาก field definitions ของ template → generate เป็น v1 + ดาวน์โหลด */
|
||||
import type { GenerateDocumentResponse, TemplateDetail } from '~/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
const { apiJson, downloadFile } = useApi()
|
||||
|
||||
const templateId = computed(() => String(route.params.templateId))
|
||||
const detail = ref<TemplateDetail | null>(null)
|
||||
const values = ref<Record<string, string>>({})
|
||||
const issues = ref<Record<string, string>>({})
|
||||
|
||||
const loading = ref(true)
|
||||
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))
|
||||
|
||||
useHead({
|
||||
title: computed(() =>
|
||||
detail.value
|
||||
? `สร้างเอกสาร: ${detail.value.name} — e-Court Documents Versioning`
|
||||
: 'สร้างเอกสาร — e-Court Documents Versioning',
|
||||
),
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
detail.value = (await apiJson<TemplateDetail>(`/api/templates/${templateId.value}`))!
|
||||
values.value = Object.fromEntries(detail.value.fields.map((f) => [f.key, '']))
|
||||
} catch (err) {
|
||||
loadError.value = err instanceof Error ? err.message : 'โหลด template ไม่สำเร็จ'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** validate ฝั่ง client (required/type) ก่อนยิง backend — backend ตรวจซ้ำอีกชั้นเสมอ */
|
||||
async function submit() {
|
||||
submitError.value = null
|
||||
downloadError.value = null
|
||||
issues.value = Object.fromEntries(
|
||||
validateFieldValues(sortedFields.value, values.value).map((i) => [i.key, i.message]),
|
||||
)
|
||||
if (Object.keys(issues.value).length > 0) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
result.value = await apiJson<GenerateDocumentResponse>('/api/documents/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ templateId: templateId.value, values: values.value }),
|
||||
})
|
||||
window.scrollTo({ top: 0 })
|
||||
} catch (err) {
|
||||
submitError.value = err instanceof Error ? err.message : 'สร้างเอกสารไม่สำเร็จ'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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 เดิม */
|
||||
function resetForm() {
|
||||
result.value = null
|
||||
submitError.value = null
|
||||
downloadError.value = null
|
||||
issues.value = {}
|
||||
values.value = Object.fromEntries((detail.value?.fields ?? []).map((f) => [f.key, '']))
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthGate>
|
||||
<section class="mx-auto max-w-2xl space-y-6">
|
||||
<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"
|
||||
/>
|
||||
กำลังโหลด template…
|
||||
</div>
|
||||
|
||||
<AlertBox v-else-if="loadError" type="error">
|
||||
{{ loadError }}
|
||||
<NuxtLink to="/" class="ml-1 font-medium underline">กลับหน้าหลัก</NuxtLink>
|
||||
</AlertBox>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<!-- ผลลัพธ์: สร้างสำเร็จเป็น v1 แล้ว -->
|
||||
<template v-if="result">
|
||||
<AlertBox type="success">
|
||||
สร้างเอกสารสำเร็จ — บันทึกเป็น <strong>version {{ result.versionNumber }}</strong> แล้ว
|
||||
</AlertBox>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-800">
|
||||
<h1 class="text-xl font-bold">{{ detail.name }}</h1>
|
||||
<dl class="mt-4 space-y-2 text-sm">
|
||||
<div class="flex gap-2">
|
||||
<dt class="w-32 shrink-0 text-slate-500 dark:text-slate-400">document id</dt>
|
||||
<dd class="break-all font-mono text-xs leading-6">{{ result.documentId }}</dd>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<dt class="w-32 shrink-0 text-slate-500 dark:text-slate-400">version</dt>
|
||||
<dd>{{ result.versionNumber }} (ต้นทางของ version history)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="mt-5 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="downloading"
|
||||
class="rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700 disabled:opacity-50"
|
||||
@click="download"
|
||||
>
|
||||
{{ downloading ? 'กำลังเตรียมไฟล์…' : '⬇ ดาวน์โหลด .docx' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-slate-300 px-4 py-2 text-sm hover:bg-slate-100 dark:border-slate-600 dark:hover:bg-slate-800"
|
||||
@click="resetForm"
|
||||
>
|
||||
สร้างอีกฉบับ
|
||||
</button>
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="rounded-md border border-slate-300 px-4 py-2 text-sm hover:bg-slate-100 dark:border-slate-600 dark:hover:bg-slate-800"
|
||||
>
|
||||
กลับหน้าหลัก
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<AlertBox v-if="downloadError" type="error" class="mt-4">{{ downloadError }}</AlertBox>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-sky-200 bg-sky-50 p-4 text-sm dark:border-sky-800 dark:bg-sky-950">
|
||||
<p class="font-medium text-sky-800 dark:text-sky-300">ขั้นต่อไป</p>
|
||||
<ol class="mt-2 list-decimal space-y-1 pl-5 text-sky-800 dark:text-sky-200">
|
||||
<li>เปิดไฟล์ที่ดาวน์โหลดใน Word (Desktop หรือ Online)</li>
|
||||
<li>
|
||||
เปิด task pane ของ add-in <strong>e-Court Documents Versioning</strong> — ระบบจะตรวจพบว่าเป็น
|
||||
tracked document และแสดง <strong>version 1</strong> ทันที โดยไม่ต้องกด Start Versioning
|
||||
</li>
|
||||
<li>แก้ไขเอกสารต่อแล้วกด <strong>Save & Version</strong> เพื่อบันทึก version ถัดไป</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ฟอร์มกรอกค่า -->
|
||||
<form v-else class="space-y-5" @submit.prevent="submit" novalidate>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ detail.name }}</h1>
|
||||
<p v-if="detail.description" class="mt-1 text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ detail.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="f in sortedFields"
|
||||
:key="f.key"
|
||||
>
|
||||
<label :for="`field-${f.key}`" class="mb-1 block text-sm font-medium">
|
||||
{{ f.label }}
|
||||
<span v-if="f.isRequired" class="text-red-500">*</span>
|
||||
<span class="ml-1 font-mono text-xs font-normal text-slate-400">{{ f.key }}</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
:id="`field-${f.key}`"
|
||||
v-model="values[f.key]"
|
||||
:type="f.type === 'number' ? 'number' : f.type === 'date' ? 'date' : 'text'"
|
||||
:step="f.type === 'number' ? 'any' : undefined"
|
||||
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"
|
||||
:class="issues[f.key] ? 'border-red-400 dark:border-red-500' : ''"
|
||||
/>
|
||||
<p v-if="issues[f.key]" class="mt-1 text-xs text-red-600 dark:text-red-400">{{ issues[f.key] }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="sortedFields.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"
|
||||
>
|
||||
template นี้ไม่มี field definitions —
|
||||
<NuxtLink :to="`/templates/${detail.id}/edit`" class="font-medium text-sky-600 hover:underline dark:text-sky-400">
|
||||
ตรวจสอบที่หน้าจัดการ template
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<AlertBox v-if="submitError" type="error">{{ submitError }}</AlertBox>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="submitting || sortedFields.length === 0"
|
||||
class="rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700 disabled:opacity-50"
|
||||
>
|
||||
{{ submitting ? 'กำลังสร้างเอกสาร…' : 'สร้างเอกสาร (version 1)' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
</section>
|
||||
</AuthGate>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue