433 lines
17 KiB
Vue
433 lines
17 KiB
Vue
<template>
|
|
<AdminLayout title="Uredi predlogo">
|
|
<div class="mb-6 flex flex-col lg:flex-row lg:items-start gap-6">
|
|
<div class="flex-1 min-w-[320px]">
|
|
<div class="flex items-start justify-between gap-4 mb-4">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold tracking-tight">{{ template.name }}</h1>
|
|
<p class="text-xs text-gray-500 mt-1 flex flex-wrap gap-3">
|
|
<span class="inline-flex items-center gap-1"
|
|
><span class="text-gray-400">Slug:</span
|
|
><span class="font-medium">{{ template.slug }}</span></span
|
|
>
|
|
<span class="inline-flex items-center gap-1"
|
|
><span class="text-gray-400">Verzija:</span
|
|
><span class="font-medium">v{{ template.version }}</span></span
|
|
>
|
|
<span
|
|
class="inline-flex items-center gap-1"
|
|
:class="template.active ? 'text-emerald-600' : 'text-gray-400'"
|
|
><span
|
|
class="w-1.5 h-1.5 rounded-full"
|
|
:class="template.active ? 'bg-emerald-500' : 'bg-gray-300'"
|
|
/>
|
|
{{ template.active ? "Aktivna" : "Neaktivna" }}</span
|
|
>
|
|
</p>
|
|
</div>
|
|
<form @submit.prevent="toggleActive" class="flex items-center gap-2">
|
|
<button
|
|
type="submit"
|
|
:class="[btnBase, template.active ? btnWarn : btnOutline]"
|
|
:disabled="toggleForm.processing"
|
|
>
|
|
<span v-if="toggleForm.processing">...</span>
|
|
<span v-else>{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}</span>
|
|
</button>
|
|
<Link
|
|
:href="route('admin.document-templates.show', template.id)"
|
|
:class="[btnBase, btnOutline]"
|
|
>Ogled</Link
|
|
>
|
|
</form>
|
|
</div>
|
|
|
|
<form @submit.prevent="submit" class="space-y-8">
|
|
<!-- Osnovno -->
|
|
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
|
Osnovne nastavitve
|
|
</h2>
|
|
</div>
|
|
<div class="grid md:grid-cols-2 gap-6">
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600"
|
|
>Izlazna datoteka (pattern)</span
|
|
>
|
|
<input
|
|
v-model="form.output_filename_pattern"
|
|
type="text"
|
|
class="input input-bordered w-full input-sm"
|
|
placeholder="POVRACILO_{contract.reference}"
|
|
/>
|
|
<span class="text-[11px] text-gray-500"
|
|
>Tokens npr. {contract.reference}</span
|
|
>
|
|
</label>
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600"
|
|
>Privzeti format datuma</span
|
|
>
|
|
<input
|
|
v-model="form.date_format"
|
|
type="text"
|
|
class="input input-bordered w-full input-sm"
|
|
placeholder="d.m.Y"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<label class="flex items-center gap-2 text-xs font-medium text-gray-600">
|
|
<input
|
|
id="fail_on_unresolved"
|
|
type="checkbox"
|
|
v-model="form.fail_on_unresolved"
|
|
class="checkbox checkbox-xs"
|
|
/>
|
|
<span>Prekini če token ni rešen (fail on unresolved)</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Formatiranje -->
|
|
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
|
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
|
Formatiranje
|
|
</h2>
|
|
<div class="grid md:grid-cols-3 gap-5">
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Decimalna mesta</span>
|
|
<input
|
|
v-model.number="form.number_decimals"
|
|
type="number"
|
|
min="0"
|
|
max="6"
|
|
class="input input-bordered w-full input-sm"
|
|
/>
|
|
</label>
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Decimalni separator</span>
|
|
<input
|
|
v-model="form.decimal_separator"
|
|
type="text"
|
|
maxlength="2"
|
|
class="input input-bordered w-full input-sm"
|
|
/>
|
|
</label>
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Tisocice separator</span>
|
|
<input
|
|
v-model="form.thousands_separator"
|
|
type="text"
|
|
maxlength="2"
|
|
class="input input-bordered w-full input-sm"
|
|
/>
|
|
</label>
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Znak valute</span>
|
|
<input
|
|
v-model="form.currency_symbol"
|
|
type="text"
|
|
maxlength="8"
|
|
class="input input-bordered w-full input-sm"
|
|
/>
|
|
</label>
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Pozicija valute</span>
|
|
<select
|
|
v-model="form.currency_position"
|
|
class="select select-bordered select-sm w-full"
|
|
>
|
|
<option :value="null">(privzeto)</option>
|
|
<option value="before">Pred</option>
|
|
<option value="after">Za</option>
|
|
</select>
|
|
</label>
|
|
<label
|
|
class="flex items-center gap-2 space-y-0 pt-6 text-xs font-medium text-gray-600"
|
|
>
|
|
<input
|
|
id="currency_space"
|
|
type="checkbox"
|
|
v-model="form.currency_space"
|
|
class="checkbox checkbox-xs"
|
|
/>
|
|
<span>Presledek pred/za valuto</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Aktivnost -->
|
|
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
|
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
|
Aktivnost
|
|
</h2>
|
|
<div class="grid md:grid-cols-2 gap-6">
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Akcija</span>
|
|
<select
|
|
v-model="form.action_id"
|
|
class="select select-bordered select-sm w-full"
|
|
@change="handleActionChange"
|
|
>
|
|
<option :value="null">(brez)</option>
|
|
<option v-for="a in actions" :key="a.id" :value="a.id">
|
|
{{ a.name }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label class="space-y-1 block">
|
|
<span class="text-xs font-medium text-gray-600">Odločitev</span>
|
|
<select
|
|
v-model="form.decision_id"
|
|
class="select select-bordered select-sm w-full"
|
|
:disabled="!currentActionDecisions.length"
|
|
>
|
|
<option :value="null">(brez)</option>
|
|
<option v-for="d in currentActionDecisions" :key="d.id" :value="d.id">
|
|
{{ d.name }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label class="space-y-1 md:col-span-2 block">
|
|
<span class="text-xs font-medium text-gray-600"
|
|
>Predloga opombe aktivnosti</span
|
|
>
|
|
<textarea
|
|
v-model="form.activity_note_template"
|
|
rows="3"
|
|
class="textarea textarea-bordered w-full text-xs"
|
|
placeholder="Besedilo aktivnosti..."
|
|
/>
|
|
<span class="text-[11px] text-gray-500"
|
|
>Tokeni npr. {contract.reference}</span
|
|
>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom tokens defaults -->
|
|
<div class="bg-white border rounded-lg shadow-sm p-5 space-y-5">
|
|
<h2 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
|
Custom tokens (privzete vrednosti)
|
|
</h2>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" :class="[btnBase, btnOutline]" @click="addCustomDefault">
|
|
Dodaj vrstico
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div
|
|
v-for="(row, idx) in customRows"
|
|
:key="idx"
|
|
class="grid grid-cols-12 items-center gap-2"
|
|
>
|
|
<input
|
|
v-model="row.key"
|
|
type="text"
|
|
class="input input-bordered input-sm w-full col-span-4"
|
|
placeholder="custom ključ (npr. order_id)"
|
|
/>
|
|
<template v-if="row.type === 'text'">
|
|
<textarea
|
|
v-model="row.value"
|
|
rows="3"
|
|
class="textarea textarea-bordered w-full text-xs col-span-5"
|
|
placeholder="privzeta vrednost"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<input
|
|
v-model="row.value"
|
|
type="text"
|
|
class="input input-bordered input-sm w-full col-span-5"
|
|
placeholder="privzeta vrednost"
|
|
/>
|
|
</template>
|
|
<select v-model="row.type" class="select select-bordered select-sm w-full col-span-2">
|
|
<option value="string">string</option>
|
|
<option value="number">number</option>
|
|
<option value="date">date</option>
|
|
<option value="text">text</option>
|
|
</select>
|
|
<button type="button" class="btn btn-ghost btn-xs col-span-1" @click="removeCustomDefault(idx)">✕</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-[11px] text-gray-500">
|
|
Uporabite v predlogi kot <code v-pre>{{custom.your_key}}</code>. Manjkajoče vrednosti se privzeto izpraznijo.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 pt-2">
|
|
<button
|
|
type="submit"
|
|
:class="[btnBase, btnPrimary]"
|
|
:disabled="form.processing"
|
|
>
|
|
<span v-if="form.processing">Shranjevanje…</span>
|
|
<span v-else>Shrani spremembe</span>
|
|
</button>
|
|
<Link
|
|
:href="route('admin.document-templates.show', template.id)"
|
|
:class="[btnBase, btnOutline]"
|
|
>Prekliči</Link
|
|
>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Side meta panel -->
|
|
<aside class="w-full lg:w-72 space-y-6">
|
|
<div class="bg-white border rounded-lg shadow-sm p-4 space-y-3">
|
|
<h3 class="text-xs font-semibold tracking-wide text-gray-600 uppercase">
|
|
Meta
|
|
</h3>
|
|
<ul class="text-xs text-gray-600 space-y-1">
|
|
<li>
|
|
<span class="text-gray-400">Velikost:</span>
|
|
<span class="font-medium"
|
|
>{{ (template.file_size / 1024).toFixed(1) }} KB</span
|
|
>
|
|
</li>
|
|
<li>
|
|
<span class="text-gray-400">Hash:</span>
|
|
<span class="font-mono">{{ template.file_hash?.substring(0, 12) }}…</span>
|
|
</li>
|
|
<li>
|
|
<span class="text-gray-400">Engine:</span>
|
|
<span class="font-medium">{{ template.engine }}</span>
|
|
</li>
|
|
</ul>
|
|
<a
|
|
:href="'/storage/' + template.file_path"
|
|
target="_blank"
|
|
class="text-[11px] inline-flex items-center gap-1 text-indigo-600 hover:underline"
|
|
>Prenesi izvorni DOCX →</a
|
|
>
|
|
</div>
|
|
<div
|
|
v-if="template.tokens?.length"
|
|
class="bg-white border rounded-lg shadow-sm p-4"
|
|
>
|
|
<h3 class="text-xs font-semibold tracking-wide text-gray-600 uppercase mb-2">
|
|
Tokens ({{ template.tokens.length }})
|
|
</h3>
|
|
<div class="flex flex-wrap gap-1.5 max-h-48 overflow-auto pr-1">
|
|
<span
|
|
v-for="t in template.tokens"
|
|
:key="t"
|
|
class="px-1.5 py-0.5 bg-gray-100 rounded text-[11px] font-mono"
|
|
>{{ t }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</AdminLayout>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, reactive } from "vue";
|
|
import { useForm, Link, router } from "@inertiajs/vue3";
|
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
|
|
|
// Button style utility classes
|
|
const btnBase =
|
|
"inline-flex items-center justify-center gap-1 rounded-md border text-xs font-medium px-3 py-1.5 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
const btnPrimary = "bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500";
|
|
const btnOutline = "bg-white text-gray-700 border-gray-300 hover:bg-gray-50";
|
|
const btnWarn = "bg-amber-500 border-amber-500 text-white hover:bg-amber-400";
|
|
|
|
const props = defineProps({
|
|
template: Object,
|
|
actions: Array,
|
|
});
|
|
|
|
const form = useForm({
|
|
output_filename_pattern: props.template.output_filename_pattern || "",
|
|
date_format: props.template.date_format || "",
|
|
fail_on_unresolved: props.template.fail_on_unresolved ?? false,
|
|
number_decimals: props.template.formatting_options?.number_decimals ?? 2,
|
|
decimal_separator: props.template.formatting_options?.decimal_separator ?? ",",
|
|
thousands_separator: props.template.formatting_options?.thousands_separator ?? ".",
|
|
currency_symbol: props.template.formatting_options?.currency_symbol ?? "€",
|
|
currency_position: props.template.formatting_options?.currency_position ?? "after",
|
|
currency_space: props.template.formatting_options?.currency_space ?? true,
|
|
action_id: props.template.action_id ?? null,
|
|
decision_id: props.template.decision_id ?? null,
|
|
activity_note_template: props.template.activity_note_template || "",
|
|
// meta will include custom_defaults on submit
|
|
meta: props.template.meta || {},
|
|
});
|
|
|
|
const toggleForm = useForm({});
|
|
|
|
const currentActionDecisions = computed(() => {
|
|
if (!form.action_id) {
|
|
return [];
|
|
}
|
|
const a = props.actions.find((a) => a.id === form.action_id);
|
|
return a ? a.decisions : [];
|
|
});
|
|
|
|
function handleActionChange() {
|
|
if (!currentActionDecisions.value.some((d) => d.id === form.decision_id)) {
|
|
form.decision_id = null;
|
|
}
|
|
}
|
|
|
|
function submit() {
|
|
// Build meta.custom_defaults object from rows
|
|
const entries = customRows
|
|
.filter((r) => (r.key || "").trim() !== "")
|
|
.reduce((acc, r) => {
|
|
acc[r.key.trim()] = r.value ?? "";
|
|
return acc;
|
|
}, {});
|
|
const types = customRows
|
|
.filter((r) => (r.key || "").trim() !== "")
|
|
.reduce((acc, r) => {
|
|
acc[r.key.trim()] = r.type || 'string';
|
|
return acc;
|
|
}, {});
|
|
form.meta = Object.assign({}, form.meta || {}, { custom_defaults: entries, custom_default_types: types });
|
|
form.put(route("admin.document-templates.settings.update", props.template.id));
|
|
}
|
|
|
|
function toggleActive() {
|
|
toggleForm.post(route("admin.document-templates.toggle", props.template.id), {
|
|
preserveScroll: true,
|
|
});
|
|
}
|
|
|
|
// Custom defaults rows state
|
|
const baseDefaults = (props.template.meta && props.template.meta.custom_defaults) || {};
|
|
const baseTypes = (props.template.meta && props.template.meta.custom_default_types) || {};
|
|
// Gather detected custom tokens from template.tokens
|
|
const detectedCustoms = Array.isArray(props.template.tokens)
|
|
? props.template.tokens.filter((t) => typeof t === 'string' && t.startsWith('custom.')).map((t) => t.replace(/^custom\./, ''))
|
|
: [];
|
|
// Build a union of keys from defaults, types, and detected tokens
|
|
const allKeysSet = new Set([
|
|
...Object.keys(baseDefaults || {}),
|
|
...Object.keys(baseTypes || {}),
|
|
...detectedCustoms,
|
|
]);
|
|
const allKeys = Array.from(allKeysSet);
|
|
const customRows = reactive(
|
|
allKeys.length
|
|
? allKeys.map((k) => ({ key: k, value: baseDefaults[k] ?? '', type: baseTypes[k] || 'string' }))
|
|
: [{ key: '', value: '', type: 'string' }]
|
|
);
|
|
|
|
function addCustomDefault() {
|
|
customRows.push({ key: "", value: "", type: 'string' });
|
|
}
|
|
|
|
function removeCustomDefault(idx) {
|
|
customRows.splice(idx, 1);
|
|
if (!customRows.length) customRows.push({ key: "", value: "", type: 'string' });
|
|
}
|
|
</script>
|