Admin panel updated with shadcn-vue components

This commit is contained in:
Simon Pocrnjič
2026-01-05 18:27:35 +01:00
parent 70a5d015e0
commit c4d9ecb39e
37 changed files with 5407 additions and 3740 deletions
@@ -1,327 +1,352 @@
<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>
<div class="flex-1 min-w-[320px] space-y-6">
<Card>
<CardHeader>
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3">
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
<FileTextIcon class="h-5 w-5" />
</div>
<div>
<CardTitle>{{ template.name }}</CardTitle>
<CardDescription class="flex flex-wrap gap-3 mt-1">
<span class="inline-flex items-center gap-1">
<span>Slug:</span>
<Badge variant="secondary" class="text-xs">{{ template.slug }}</Badge>
</span>
<span class="inline-flex items-center gap-1">
<span>Verzija:</span>
<Badge variant="secondary" class="text-xs">v{{ template.version }}</Badge>
</span>
<Badge :variant="template.active ? 'default' : 'outline'" class="text-xs">
{{ template.active ? "Aktivna" : "Neaktivna" }}
</Badge>
</CardDescription>
</div>
</div>
<div class="flex items-center gap-2">
<form @submit.prevent="toggleActive">
<Button
type="submit"
:variant="template.active ? 'destructive' : 'default'"
size="sm"
:disabled="toggleForm.processing"
>
<PowerOffIcon v-if="template.active" class="h-4 w-4 mr-2" />
<Power v-else class="h-4 w-4 mr-2" />
{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}
</Button>
</form>
<Button size="sm" variant="outline" as-child>
<Link :href="route('admin.document-templates.show', template.id)">
<EyeIcon class="h-4 w-4 mr-2" />
Ogled
</Link>
</Button>
</div>
</div>
</CardHeader>
</Card>
<form @submit.prevent="submit" class="space-y-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- 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}"
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<Settings2Icon class="h-4 w-4" />
<CardTitle class="text-base">Osnovne nastavitve</CardTitle>
</div>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="output_filename_pattern">Izlazna datoteka (pattern)</Label>
<Input
id="output_filename_pattern"
v-model="form.output_filename_pattern"
placeholder="POVRACILO_{contract.reference}"
class="font-mono text-sm"
/>
<p class="text-xs text-muted-foreground">
Tokens npr. {contract.reference}
</p>
</div>
<div class="space-y-2">
<Label for="date_format">Privzeti format datuma</Label>
<Input
id="date_format"
v-model="form.date_format"
placeholder="d.m.Y"
class="font-mono text-sm"
/>
</div>
</div>
<div class="flex items-center gap-2">
<Checkbox
id="fail_on_unresolved"
:default-value="form.fail_on_unresolved"
@update:model-value="(val) => (form.fail_on_unresolved = val)"
/>
<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>
<Label for="fail_on_unresolved" class="cursor-pointer font-normal">
Prekini če token ni rešen (fail on unresolved)
</Label>
</div>
</CardContent>
</Card>
<!-- 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>
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<FileTextIcon class="h-4 w-4" />
<CardTitle class="text-base">Formatiranje</CardTitle>
</div>
</CardHeader>
<CardContent>
<div class="grid md:grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="number_decimals">Decimalna mesta</Label>
<Input
id="number_decimals"
v-model.number="form.number_decimals"
type="number"
min="0"
max="6"
/>
</div>
<div class="space-y-2">
<Label for="decimal_separator">Decimalni separator</Label>
<Input
id="decimal_separator"
v-model="form.decimal_separator"
maxlength="2"
/>
</div>
<div class="space-y-2">
<Label for="thousands_separator">Tisočice separator</Label>
<Input
id="thousands_separator"
v-model="form.thousands_separator"
maxlength="2"
/>
</div>
<div class="space-y-2">
<Label for="currency_symbol">Znak valute</Label>
<Input
id="currency_symbol"
v-model="form.currency_symbol"
maxlength="8"
/>
</div>
<div class="space-y-2">
<Label for="currency_position">Pozicija valute</Label>
<Select v-model="form.currency_position">
<SelectTrigger id="currency_position">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">(privzeto)</SelectItem>
<SelectItem value="before">Pred</SelectItem>
<SelectItem value="after">Za</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex items-center gap-2 pt-8">
<Checkbox
id="currency_space"
:default-value="form.currency_space"
@update:model-value="(val) => (form.currency_space = val)"
/>
<Label for="currency_space" class="cursor-pointer font-normal">
Presledek pred/za valuto
</Label>
</div>
</div>
</CardContent>
</Card>
<!-- 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>
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<ActivityIcon class="h-4 w-4" />
<CardTitle class="text-base">Aktivnost</CardTitle>
</div>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="action_id">Akcija</Label>
<Select v-model="form.action_id" @update:model-value="handleActionChange">
<SelectTrigger id="action_id">
<SelectValue placeholder="(brez)" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">(brez)</SelectItem>
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="decision_id">Odločitev</Label>
<Select v-model="form.decision_id" :disabled="!currentActionDecisions.length">
<SelectTrigger id="decision_id">
<SelectValue placeholder="(brez)" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">(brez)</SelectItem>
<SelectItem v-for="d in currentActionDecisions" :key="d.id" :value="d.id">
{{ d.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2 md:col-span-2">
<Label for="activity_note_template">Predloga opombe aktivnosti</Label>
<Textarea
id="activity_note_template"
v-model="form.activity_note_template"
rows="3"
placeholder="Besedilo aktivnosti..."
/>
<p class="text-xs text-muted-foreground">
Tokeni npr. {contract.reference}
</p>
</div>
</div>
</CardContent>
</Card>
<!-- 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">
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<button type="button" :class="[btnBase, btnOutline]" @click="addCustomDefault">
Dodaj vrstico
</button>
<CodeIcon class="h-4 w-4" />
<CardTitle class="text-base">Custom tokens (privzete vrednosti)</CardTitle>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
</CardHeader>
<CardContent class="space-y-4">
<Button type="button" variant="outline" size="sm" @click="addCustomDefault">
Dodaj vrstico
</Button>
<div class="grid grid-cols-1 gap-3">
<div
v-for="(row, idx) in customRows"
:key="idx"
class="grid grid-cols-12 items-center gap-2"
class="grid grid-cols-12 items-start 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
<div class="col-span-4 space-y-1">
<Input
v-model="row.key"
placeholder="custom ključ (npr. order_id)"
class="font-mono text-sm"
/>
</div>
<div class="col-span-5 space-y-1">
<Textarea
v-if="row.type === 'text'"
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
<Input
v-else
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 class="col-span-2 space-y-1">
<Select v-model="row.type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="text">text</SelectItem>
</SelectContent>
</Select>
</div>
<div class="col-span-1 flex items-center pt-2">
<Button type="button" variant="ghost" size="icon" @click="removeCustomDefault(idx)">
<XIcon class="h-4 w-4" />
</Button>
</div>
</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 class="text-xs text-muted-foreground">
Uporabite v predlogi kot <code class="px-1 py-0.5 bg-muted rounded text-xs" v-pre>{{custom.your_key}}</code>. Manjkajoče vrednosti se privzeto izpraznijo.
</p>
</div>
</div>
</CardContent>
</Card>
<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
>
<Button type="submit" :disabled="form.processing">
<SaveIcon class="h-4 w-4 mr-2" />
{{ form.processing ? "Shranjevanje" : "Shrani spremembe" }}
</Button>
<Button variant="outline" as-child>
<Link :href="route('admin.document-templates.show', template.id)">
<XIcon class="h-4 w-4 mr-2" />
Prekliči
</Link>
</Button>
</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
<Card>
<CardHeader>
<CardTitle class="text-sm">Meta podatki</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Velikost:</span>
<Badge variant="secondary">{{ (template.file_size / 1024).toFixed(1) }} KB</Badge>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Hash:</span>
<code class="text-xs">{{ template.file_hash?.substring(0, 12) }}…</code>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Engine:</span>
<Badge variant="outline">{{ template.engine }}</Badge>
</div>
</div>
<Separator />
<Button variant="outline" size="sm" class="w-full" as-child>
<a :href="'/storage/' + template.file_path" target="_blank">
Prenesi izvorni DOCX
</a>
</Button>
</CardContent>
</Card>
<Card v-if="template.tokens?.length">
<CardHeader>
<CardTitle class="text-sm">Tokens</CardTitle>
<CardDescription>{{ template.tokens.length }} tokenov</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-wrap gap-1.5 max-h-48 overflow-auto">
<Badge
v-for="t in template.tokens"
:key="t"
variant="secondary"
class="font-mono text-xs"
>
</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>
{{ t }}
</Badge>
</div>
</CardContent>
</Card>
</aside>
</div>
</AdminLayout>
@@ -331,6 +356,16 @@
import { computed, reactive } from "vue";
import { useForm, Link, router } from "@inertiajs/vue3";
import AdminLayout from "@/Layouts/AdminLayout.vue";
import { Settings2Icon, FileTextIcon, ActivityIcon, CodeIcon, SaveIcon, XIcon, EyeIcon, Power, PowerOffIcon } from "lucide-vue-next";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Badge } from "@/Components/ui/badge";
import { Separator } from "@/Components/ui/separator";
// Button style utility classes
const btnBase =