eCourt-Doc-Edit/web/app/pages/templates/new.vue
Suphonchai Phoonsawat 9bb4a48c00 Phase 10: field editor (FR-14)
เพิ่ม editor ต่อ template field — ผู้ดูแลเลือกรูปแบบช่องกรอกในฟอร์มสร้างเอกสารได้:
text → textbox/textarea, number/date → control ตาม type (presentation metadata
ล้วน — validation ยังยึด Type, ค่า field เป็น string ทั้ง flow, ไม่แตะ generate/sync/add-in)

- Backend: TemplateField.Editor (nullable, null = default ของ type) +
  migration AddTemplateFieldEditor + validate ตาม type ใน PUT templates
  (ค่าไม่รู้จัก/ไม่เข้ากับ type → 400, normalize-on-write, API คืน resolved เสมอ)
- Web: util fieldEditor.ts + editor select หน้าแก้ template (desktop+mobile,
  ซ่อนเมื่อ type มีทางเดียว) + ฟอร์มสร้างเอกสาร render ตาม editor +
  แก้ prefill re-generate ของ field date จาก ISO timestamp เต็ม (input[type=date]
  รับเฉพาะ yyyy-MM-dd เดิมแสดงว่าง)
- Docs: spec v6 (FR-14/AC-17) + plan Phase 10 + sub-plan field-editor.md +
  acceptance-review AC-17 + README/CLAUDE.md
- Tests: backend 60/60 (FieldEditorTests ใหม่ 8 กรณี) · web 46/46 + build ผ่าน

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-22 09:17:15 +07:00

165 lines
6.3 KiB
Vue

<script setup lang="ts">
/** FR-9 UI — upload template ใหม่: ระบบ detect fields (SDT tags) จากไฟล์ให้อัตโนมัติ */
import type { CreateTemplateResponse } from '~/types/api'
const { apiUpload } = useApi()
useHead({ title: 'สร้าง template ใหม่ — e-Court Documents Versioning' })
const name = ref('')
const description = ref('')
const file = ref<File | null>(null)
const fileInput = ref<HTMLInputElement | null>(null)
const submitting = ref(false)
const error = ref<string | null>(null)
const created = ref<CreateTemplateResponse | null>(null)
function onFileChange(e: Event) {
const input = e.target as HTMLInputElement
file.value = input.files?.[0] ?? null
}
async function submit() {
error.value = null
if (!name.value.trim()) {
error.value = 'กรอกชื่อ template ด้วย'
return
}
if (!file.value) {
error.value = 'เลือกไฟล์ .docx ของ template ด้วย'
return
}
if (!file.value.name.toLowerCase().endsWith('.docx')) {
error.value = 'รองรับเฉพาะไฟล์ .docx เท่านั้น'
return
}
const form = new FormData()
form.append('name', name.value.trim())
if (description.value.trim()) form.append('description', description.value.trim())
form.append('file', file.value)
submitting.value = true
try {
created.value = await apiUpload<CreateTemplateResponse>('/api/templates', form)
} catch (err) {
error.value = err instanceof Error ? err.message : 'อัปโหลด template ไม่สำเร็จ'
} finally {
submitting.value = false
}
}
/** เคลียร์ฟอร์มเพื่ออัปโหลดไฟล์ถัดไป */
function resetForm() {
created.value = null
name.value = ''
description.value = ''
file.value = null
if (fileInput.value) fileInput.value.value = ''
}
</script>
<template>
<AuthGate>
<section class="mx-auto max-w-xl space-y-6">
<h1 class="text-page-title">สร้าง template ใหม่</h1>
<!-- ผลลัพธ์หลังอัปโหลดสำเร็จ: field list ที่ detect ได้จากไฟล์ -->
<template v-if="created">
<AlertBox type="success">
อัปโหลดสำเร็จ — ตรวจพบ <strong>{{ created.fields.length }}</strong> fields จากไฟล์
</AlertBox>
<!-- มือถือ: card list -->
<ul class="space-y-3 md:hidden">
<li v-for="f in created.fields" :key="f.key" class="card space-y-1 p-4">
<p class="font-mono text-xs text-muted">{{ f.key }}</p>
<p class="text-sm font-medium">{{ f.label }}</p>
<p class="text-xs text-muted">type: {{ f.type }}</p>
</li>
</ul>
<!-- desktop: ตาราง -->
<div class="table-card hidden md:block">
<table class="w-full text-sm">
<thead class="table-head">
<tr>
<th class="px-4 py-3">key (SDT tag)</th>
<th class="px-4 py-3">label เริ่มต้น</th>
<th class="px-4 py-3">type เริ่มต้น</th>
</tr>
</thead>
<tbody class="divide-y divide-line">
<tr v-for="f in created.fields" :key="f.key">
<td class="px-4 py-2 font-mono text-xs">{{ f.key }}</td>
<td class="px-4 py-2">{{ f.label }}</td>
<td class="px-4 py-2">{{ f.type }}</td>
</tr>
</tbody>
</table>
</div>
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<button type="button" class="btn btn-secondary w-full sm:w-auto" @click="resetForm">
อัปโหลดอีกไฟล์
</button>
<NuxtLink :to="`/templates/${created.id}/edit`" class="btn btn-primary w-full sm:w-auto">
ตั้งค่า label/type/editor/required ต่อ →
</NuxtLink>
</div>
</template>
<form v-else class="space-y-4" @submit.prevent="submit">
<AlertBox v-if="error" type="error">{{ error }}</AlertBox>
<div>
<label for="tpl-name" class="label">ชื่อ template <span class="text-error-strong">*</span></label>
<input
id="tpl-name"
v-model="name"
type="text"
required
placeholder="เช่น สัญญาจ้างงาน"
class="input"
/>
</div>
<div>
<label for="tpl-desc" class="label">คำอธิบาย</label>
<textarea
id="tpl-desc"
v-model="description"
rows="2"
placeholder="คำอธิบายสั้นๆ ว่าเอกสารนี้ใช้ทำอะไร"
class="input"
/>
</div>
<div>
<label for="tpl-file" class="label">ไฟล์ template (.docx) <span class="text-error-strong">*</span></label>
<input
id="tpl-file"
ref="fileInput"
type="file"
accept=".docx"
required
class="input file:mr-3 file:rounded-pill file:border-0 file:bg-surface-muted file:px-3 file:py-1 file:text-sm file:font-medium"
@change="onFileChange"
/>
<p class="mt-1 text-xs text-muted">
สร้างไฟล์ด้วย Word แล้วใส่
<strong>Content Controls</strong> (Developer → Rich Text/Plain Text Control) โดยตั้งค่า
<span class="font-mono">Tag</span> ของแต่ละ control เป็น field key — ระบบจะ detect tags
ออกมาเป็นรายการ fields ให้อัตโนมัติ
</p>
</div>
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
<button type="submit" :disabled="submitting" class="btn btn-primary w-full sm:w-auto">
{{ submitting ? 'กำลังอัปโหลด…' : 'อัปโหลด template' }}
</button>
</div>
</form>
</section>
</AuthGate>
</template>