ปรับ look and feel

This commit is contained in:
Suphonchai Phoonsawat 2026-08-20 21:34:45 +07:00
parent 0148f48204
commit b47667f918
15 changed files with 915 additions and 523 deletions

View file

@ -71,10 +71,8 @@ onMounted(load)
<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 v-if="loading" class="flex items-center gap-3 py-10 text-sm text-muted">
<span class="spinner" />
กำลังโหลด…
</div>
@ -85,16 +83,13 @@ onMounted(load)
<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">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<h1 class="text-page-title break-all">{{ detail.fileName }}</h1>
<p class="mt-1 text-sm text-muted">
<template v-if="detail.templateId">
จาก template
<NuxtLink
:to="`/templates/${detail.templateId}/documents`"
class="font-medium text-sky-700 hover:underline dark:text-sky-400"
>
<NuxtLink :to="`/templates/${detail.templateId}/documents`" class="link">
{{ detail.templateName }}
</NuxtLink>
</template>
@ -103,18 +98,18 @@ onMounted(load)
</p>
</div>
<div class="flex flex-col items-end gap-1">
<div class="flex shrink-0 flex-col items-start gap-1 sm:items-end">
<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"
class="btn btn-primary w-full sm:w-auto"
@click="regenerate"
>
⟳ สร้างเอกสารใหม่จากข้อมูลนี้
</button>
<p v-if="detail.templateId && !detail.templateIsActive" class="text-xs text-slate-500 dark:text-slate-400">
<p v-if="detail.templateId && !detail.templateIsActive" class="text-xs text-muted">
template ถูก deactivate — เอกสารฉบับนี้ยังใช้ได้ แต่สร้างใหม่ไม่ได้
</p>
</div>
@ -122,109 +117,171 @@ onMounted(load)
<!-- ค่าล่าสุดของทุก field -->
<div class="space-y-3">
<h2 class="text-lg font-semibold">ค่า fields ปัจจุบัน</h2>
<h2 class="text-section-title">ค่า 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"
>
<div v-if="detail.fields.length === 0" class="empty-state">
เอกสารนี้ไม่มี 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>
<template v-else>
<!-- มือถือ: card list -->
<ul class="space-y-3 md:hidden">
<li v-for="f in detail.fields" :key="f.key" class="card space-y-1 p-4">
<p class="text-sm font-medium">
{{ f.label ?? f.key }}
<span v-if="!f.label" class="badge badge-accent ml-1">ไม่มีในนิยามปัจจุบัน</span>
</p>
<p class="font-mono text-xs text-muted">{{ f.key }}</p>
<p class="break-words text-sm">{{ f.value || '—' }}</p>
<p class="text-xs text-muted">
{{ f.type ?? '—' }} ·
{{ f.updatedAt ? formatDateTime(f.updatedAt) : 'ยังไม่เคยมีค่า' }}
</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">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-line">
<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="badge badge-accent ml-1">ไม่มีในนิยามปัจจุบัน</span>
<span class="block font-mono text-xs text-muted">{{ 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-muted">{{ f.type ?? '—' }}</td>
<td class="px-4 py-3 text-muted">
{{ f.updatedAt ? formatDateTime(f.updatedAt) : 'ยังไม่เคยมีค่า' }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<!-- version history -->
<div class="space-y-3">
<h2 class="text-lg font-semibold">Version history ({{ versions.length }})</h2>
<h2 class="text-section-title">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-if="versions.length === 0" class="empty-state">ยังไม่มี 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>
<template v-else>
<!-- มือถือ: card ต่อ version (กดขยายดู fields ได้เหมือนแถวตาราง) -->
<ul class="space-y-3 md:hidden">
<li v-for="v in versions" :key="v.versionId" class="card p-4">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="font-medium">v{{ v.versionNumber }}</p>
<p class="mt-1 text-xs text-muted">{{ formatDateTime(v.createdAt) }}</p>
<p class="mt-0.5 font-mono text-xs text-muted">{{ v.createdByUserId.slice(0, 8) }}…</p>
</div>
<div class="flex shrink-0 flex-col items-end gap-2">
<button
type="button"
:disabled="downloadingVersionId === v.versionId"
class="btn btn-secondary btn-xs"
@click.stop="download(v)"
>
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
</button>
<button
type="button"
class="text-xs text-muted"
@click="expandedVersionId = expandedVersionId === v.versionId ? null : v.versionId"
>
{{ expandedVersionId === v.versionId ? '▲ ซ่อน fields' : '▼ ดู fields' }}
</button>
</div>
</div>
<div v-if="expandedVersionId === v.versionId" class="mt-3 rounded-card bg-surface-muted p-3">
<p class="mb-2 text-xs font-medium text-muted">ค่า fields ของ v{{ v.versionNumber }}</p>
<dl class="space-y-1.5 text-sm">
<div v-for="e in snapshotOf(v)" :key="e.key" class="flex gap-2">
<dt class="w-36 shrink-0 truncate text-muted" :title="e.key">
{{ fieldLabel(e.key) }}
</dt>
<dd class="min-w-0 break-words">
{{ e.value || '—' }}
<span v-if="e.missing" class="badge badge-accent ml-1">หายจากเอกสาร</span>
</dd>
</div>
<p v-if="snapshotOf(v).length === 0" class="text-sm text-muted">
version นี้ไม่มี snapshot ของ fields
</p>
</dl>
</div>
</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">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>
<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>
</thead>
<tbody class="divide-y divide-line">
<template v-for="v in versions" :key="v.versionId">
<tr
class="cursor-pointer hover:bg-surface-muted/60"
@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-muted">{{ v.createdByUserId.slice(0, 8) }}…</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
<button
type="button"
:disabled="downloadingVersionId === v.versionId"
class="btn btn-secondary btn-xs"
@click.stop="download(v)"
>
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
</button>
<span class="ml-2 text-xs text-muted">
{{ expandedVersionId === v.versionId ? '▲ ซ่อน fields' : '▼ ดู fields' }}
</span>
</td>
</tr>
<tr v-if="expandedVersionId === v.versionId">
<td colspan="4" class="bg-surface-muted px-4 py-4">
<p class="mb-2 text-xs font-medium text-muted">ค่า 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-muted" :title="e.key">
{{ fieldLabel(e.key) }}
</dt>
<dd class="break-words">
{{ e.value || '—' }}
<span v-if="e.missing" class="badge badge-accent ml-1">หายจากเอกสาร</span>
</dd>
</div>
<p v-if="snapshotOf(v).length === 0" class="text-sm text-muted">
version นี้ไม่มี snapshot ของ fields
</p>
</dl>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
<AlertBox v-if="downloadError" type="error">{{ downloadError }}</AlertBox>
</div>
</template>

View file

@ -121,10 +121,8 @@ onMounted(load)
<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"
/>
<div v-if="loading" class="flex items-center gap-3 py-10 text-sm text-muted">
<span class="spinner" />
กำลังโหลด template…
</div>
@ -140,48 +138,39 @@ onMounted(load)
สร้างเอกสารสำเร็จ — บันทึกเป็น <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>
<div class="card p-6">
<h1 class="text-page-title">{{ 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>
<dt class="w-32 shrink-0 text-muted">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>
<dt class="w-32 shrink-0 text-muted">version</dt>
<dd>{{ result.versionNumber }} (ต้นทางของ version history)</dd>
</div>
</dl>
<div class="mt-5 flex flex-wrap items-center gap-3">
<div class="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap">
<NuxtLink to="/" class="btn btn-secondary w-full sm:w-auto">กลับหน้าหลัก</NuxtLink>
<button type="button" class="btn btn-secondary w-full sm:w-auto" @click="resetForm">
สร้างอีกฉบับ
</button>
<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"
class="btn btn-primary w-full sm:w-auto"
@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">
<div class="panel-info">
<p class="font-medium text-primary">ขั้นต่อไป</p>
<ol class="mt-2 list-decimal space-y-1 pl-5">
<li>เปิดไฟล์ที่ดาวน์โหลดใน Word (Desktop หรือ Online)</li>
<li>
เปิด task pane ของ add-in <strong>e-Court Documents Versioning</strong> — ระบบจะตรวจพบว่าเป็น
@ -195,8 +184,8 @@ onMounted(load)
<!-- ฟอร์มกรอกค่า -->
<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">
<h1 class="text-page-title">{{ detail.name }}</h1>
<p v-if="detail.description" class="mt-1 text-sm text-muted">
{{ detail.description }}
</p>
</div>
@ -214,10 +203,10 @@ onMounted(load)
v-for="f in sortedFields"
:key="f.key"
>
<label :for="`field-${f.key}`" class="mb-1 block text-sm font-medium">
<label :for="`field-${f.key}`" class="label">
{{ 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>
<span v-if="f.isRequired" class="text-error-strong">*</span>
<span class="ml-1 font-mono text-xs font-normal text-muted">{{ f.key }}</span>
</label>
<input
@ -225,31 +214,28 @@ onMounted(load)
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' : ''"
class="input"
:class="issues[f.key] ? 'border-error' : ''"
/>
<p v-if="issues[f.key]" class="mt-1 text-xs text-red-600 dark:text-red-400">{{ issues[f.key] }}</p>
<p v-if="issues[f.key]" class="mt-1 text-xs text-error-strong">{{ 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"
>
<div v-if="sortedFields.length === 0" class="empty-state">
template นี้ไม่มี field definitions —
<NuxtLink :to="`/templates/${detail.id}/edit`" class="font-medium text-sky-600 hover:underline dark:text-sky-400">
ตรวจสอบที่หน้าจัดการ template
</NuxtLink>
<NuxtLink :to="`/templates/${detail.id}/edit`" class="link">ตรวจสอบที่หน้าจัดการ template</NuxtLink>
</div>
<AlertBox v-if="submitError" type="error">{{ submitError }}</AlertBox>
<button
type="submit"
:disabled="submitting || sortedFields.length === 0 || !detail.isActive"
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>
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
<button
type="submit"
:disabled="submitting || sortedFields.length === 0 || !detail.isActive"
class="btn btn-primary w-full sm:w-auto"
>
{{ submitting ? 'กำลังสร้างเอกสาร…' : 'สร้างเอกสาร (version 1)' }}
</button>
</div>
</form>
</template>
</section>