Fix template index: initialize settings forms after Inertia update (watch) to prevent undefined output_filename_pattern error
This commit is contained in:
parent
4ca2d07e7f
commit
18fb04fe65
271
resources/js/Pages/Admin/DocumentTemplates/Index.vue
Normal file
271
resources/js/Pages/Admin/DocumentTemplates/Index.vue
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Link, useForm, router } from "@inertiajs/vue3";
|
||||
import { computed, reactive, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
// Group by slug => versions desc
|
||||
const grouped = computed(() => {
|
||||
const map = {};
|
||||
props.templates.forEach((t) => {
|
||||
if (!map[t.slug]) map[t.slug] = [];
|
||||
map[t.slug].push(t);
|
||||
});
|
||||
Object.values(map).forEach((arr) => arr.sort((a, b) => b.version - a.version));
|
||||
return map;
|
||||
});
|
||||
|
||||
// Inertia form for uploading new template version
|
||||
const form = useForm({
|
||||
name: "Povzetek pogodbe",
|
||||
slug: "contract-summary",
|
||||
file: null,
|
||||
});
|
||||
|
||||
function handleFile(e) {
|
||||
form.file = e.target.files[0];
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!form.file) {
|
||||
return;
|
||||
}
|
||||
form.post(route("admin.document-templates.store"), {
|
||||
forceFormData: true,
|
||||
onSuccess: () => {
|
||||
form.reset("file");
|
||||
// clear input value manually (optional)
|
||||
const fileInput = document.getElementById("template-file-input");
|
||||
if (fileInput) fileInput.value = "";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Per-template settings forms (useForm instances) for optimistic updates
|
||||
const settingsForms = reactive({});
|
||||
const settingsSaved = reactive({});
|
||||
props.templates.forEach(t => {
|
||||
if (!settingsForms[t.id]) {
|
||||
settingsForms[t.id] = useForm({
|
||||
output_filename_pattern: t.output_filename_pattern || '',
|
||||
date_format: t.date_format || '',
|
||||
fail_on_unresolved: t.fail_on_unresolved ? 1 : 0,
|
||||
number_decimals: t.formatting_options?.number_decimals ?? 2,
|
||||
decimal_separator: t.formatting_options?.decimal_separator ?? ',',
|
||||
thousands_separator: t.formatting_options?.thousands_separator ?? '.',
|
||||
currency_symbol: t.formatting_options?.currency_symbol ?? '€',
|
||||
currency_position: t.formatting_options?.currency_position ?? 'after',
|
||||
currency_space: t.formatting_options?.currency_space ? 1 : 0,
|
||||
default_date_format: t.formatting_options?.default_date_format || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for newly added templates (e.g. after uploading a new version) and lazily initialize missing settings forms
|
||||
watch(
|
||||
() => props.templates,
|
||||
(list) => {
|
||||
list.forEach((t) => {
|
||||
if (!settingsForms[t.id]) {
|
||||
settingsForms[t.id] = useForm({
|
||||
output_filename_pattern: t.output_filename_pattern || '',
|
||||
date_format: t.date_format || '',
|
||||
fail_on_unresolved: t.fail_on_unresolved ? 1 : 0,
|
||||
number_decimals: t.formatting_options?.number_decimals ?? 2,
|
||||
decimal_separator: t.formatting_options?.decimal_separator ?? ',',
|
||||
thousands_separator: t.formatting_options?.thousands_separator ?? '.',
|
||||
currency_symbol: t.formatting_options?.currency_symbol ?? '€',
|
||||
currency_position: t.formatting_options?.currency_position ?? 'after',
|
||||
currency_space: t.formatting_options?.currency_space ? 1 : 0,
|
||||
default_date_format: t.formatting_options?.default_date_format || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function submitSettings(id) {
|
||||
const f = settingsForms[id];
|
||||
f.put(route('admin.document-templates.settings.update', id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
settingsSaved[id] = true;
|
||||
setTimeout(() => { settingsSaved[id] = false; }, 2000);
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Dokumentne predloge">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold mb-1">Dokumentne predloge</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
Upravljanje verzij DOCX predlog za generiranje dokumentov.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
@submit.prevent="submit"
|
||||
class="flex items-center gap-3 text-sm bg-white p-2 rounded border"
|
||||
>
|
||||
<input type="text" v-model="form.name" class="hidden" />
|
||||
<input type="text" v-model="form.slug" class="hidden" />
|
||||
<input
|
||||
id="template-file-input"
|
||||
type="file"
|
||||
required
|
||||
accept=".docx"
|
||||
class="text-xs"
|
||||
@change="handleFile"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing || !form.file"
|
||||
class="px-3 py-1.5 rounded bg-emerald-600 text-white disabled:opacity-50"
|
||||
>
|
||||
Nova verzija
|
||||
</button>
|
||||
<div v-if="form.progress" class="w-28 h-1 bg-gray-200 rounded overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-emerald-500 transition-all"
|
||||
:style="{ width: form.progress.percentage + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="form.errors.file" class="text-xs text-rose-600">
|
||||
{{ form.errors.file }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="(versions, slug) in grouped"
|
||||
:key="slug"
|
||||
class="bg-white border rounded"
|
||||
>
|
||||
<div class="px-4 py-3 border-b flex items-center justify-between">
|
||||
<div class="font-medium">
|
||||
{{ versions[0].name }} <span class="text-xs text-gray-500">({{ slug }})</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<form
|
||||
v-if="versions[0]"
|
||||
method="post"
|
||||
:action="route('admin.document-templates.toggle', versions[0].id)"
|
||||
>
|
||||
<input type="hidden" name="_method" value="POST" />
|
||||
<button
|
||||
class="px-2 py-1 rounded text-xs"
|
||||
:class="
|
||||
versions[0].active
|
||||
? 'bg-amber-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
"
|
||||
>
|
||||
{{ versions[0].active ? "Deaktiviraj" : "Aktiviraj" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
<div
|
||||
v-for="v in versions"
|
||||
:key="v.id"
|
||||
class="px-4 py-3 text-sm flex items-center justify-between"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
>Verzija v{{ v.version }}
|
||||
<span
|
||||
v-if="v.id === versions[0].id"
|
||||
class="text-emerald-600 text-xs font-semibold"
|
||||
>zadnja</span
|
||||
></span
|
||||
>
|
||||
<span class="text-xs text-gray-500"
|
||||
>Hash: {{ v.file_hash?.substring(0, 10) }}… | Velikost:
|
||||
{{ (v.file_size / 1024).toFixed(1) }} KB</span
|
||||
>
|
||||
<a
|
||||
class="text-xs text-indigo-600 hover:underline"
|
||||
:href="'/storage/' + v.file_path"
|
||||
target="_blank"
|
||||
>Prenesi</a
|
||||
>
|
||||
<div v-if="v.id === versions[0].id" class="mt-3 pt-3 border-t space-y-2">
|
||||
<form @submit.prevent="submitSettings(v.id)" class="grid gap-2 md:grid-cols-4 text-xs items-end">
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
<span class="font-medium">Vzorec imena</span>
|
||||
<input name="output_filename_pattern" v-model="settingsForms[v.id].output_filename_pattern" placeholder="{slug}_{generation.date}.docx" class="border rounded px-2 py-1" />
|
||||
<span v-if="settingsForms[v.id].errors.output_filename_pattern" class="text-rose-600">{{ settingsForms[v.id].errors.output_filename_pattern }}</span>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="font-medium">Format datuma</span>
|
||||
<input name="date_format" v-model="settingsForms[v.id].date_format" placeholder="Y-m-d" class="border rounded px-2 py-1" />
|
||||
<span v-if="settingsForms[v.id].errors.date_format" class="text-rose-600">{{ settingsForms[v.id].errors.date_format }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 mt-5">
|
||||
<input type="checkbox" name="fail_on_unresolved" true-value="1" false-value="0" v-model="settingsForms[v.id].fail_on_unresolved" />
|
||||
<span>Fail na nerešene</span>
|
||||
</label>
|
||||
<div class="md:col-span-4 mt-2 p-3 bg-gray-50 rounded border border-gray-200 grid gap-2 md:grid-cols-6">
|
||||
<div class="col-span-6 text-[10px] uppercase tracking-wide text-gray-500 font-semibold">Formatiranje števil / valute</div>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>Decimale</span>
|
||||
<input type="number" min="0" max="6" name="number_decimals" v-model="settingsForms[v.id].number_decimals" class="border rounded px-2 py-1" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>Decimalno</span>
|
||||
<input name="decimal_separator" v-model="settingsForms[v.id].decimal_separator" class="border rounded px-2 py-1" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>Tisočice</span>
|
||||
<input name="thousands_separator" v-model="settingsForms[v.id].thousands_separator" class="border rounded px-2 py-1" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>Simbol</span>
|
||||
<input name="currency_symbol" v-model="settingsForms[v.id].currency_symbol" class="border rounded px-2 py-1" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>Pozicija</span>
|
||||
<select name="currency_position" v-model="settingsForms[v.id].currency_position" class="border rounded px-2 py-1">
|
||||
<option value="before">Pred</option>
|
||||
<option value="after">Za</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 mt-5">
|
||||
<input type="checkbox" name="currency_space" true-value="1" false-value="0" v-model="settingsForms[v.id].currency_space" />
|
||||
<span>Presledek</span>
|
||||
</label>
|
||||
<div class="col-span-6 border-t my-1"></div>
|
||||
<div class="col-span-6 text-[10px] uppercase tracking-wide text-gray-500 font-semibold mt-1">Datumi</div>
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
<span>Privzeti datum</span>
|
||||
<input name="default_date_format" v-model="settingsForms[v.id].default_date_format" placeholder="d.m.Y" class="border rounded px-2 py-1" />
|
||||
</label>
|
||||
<div class="md:col-span-4 text-xs text-gray-500 flex items-center">Uporabi npr. d.m.Y ali Y-m-d. Posamezni tokeni lahko dobijo specifičen format (nadgradnja kasneje).</div>
|
||||
</div>
|
||||
<div class="md:col-span-4 flex gap-2 items-center">
|
||||
<button :disabled="settingsForms[v.id].processing" class="px-3 py-1.5 bg-indigo-600 text-white rounded disabled:opacity-50">{{ settingsForms[v.id].processing ? 'Shranjevanje...' : 'Shrani' }}</button>
|
||||
<span v-if="settingsSaved[v.id]" class="text-emerald-600">Shranjeno</span>
|
||||
<span class="text-gray-400">Placeholders: {slug} {version} {generation.date} {generation.timestamp}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs"
|
||||
:class="v.active ? 'text-emerald-600' : 'text-gray-400'"
|
||||
>{{ v.active ? "Aktivno" : "Neaktivno" }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
Loading…
Reference in New Issue
Block a user