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
+292 -236
View File
@@ -1,19 +1,58 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import { Head, Link, useForm } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
faPlus,
faFlask,
faBolt,
faArrowsRotate,
faToggleOn,
faToggleOff,
faPaperPlane,
} from "@fortawesome/free-solid-svg-icons";
PlusIcon,
FlaskConicalIcon,
MailIcon,
PencilIcon,
SendIcon,
MoreVerticalIcon,
} 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 { Badge } from "@/Components/ui/badge";
import { Switch } from "@/Components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
const props = defineProps({
profiles: { type: Array, default: () => [] },
@@ -93,13 +132,15 @@ function submitEdit() {
if (form.password && form.password.trim() !== "") {
payload.password = form.password.trim();
}
form.transform(() => payload).put(route("admin.mail-profiles.update", editTarget.value.id), {
preserveScroll: true,
onSuccess: () => {
editOpen.value = false;
editTarget.value = null;
},
});
form
.transform(() => payload)
.put(route("admin.mail-profiles.update", editTarget.value.id), {
preserveScroll: true,
onSuccess: () => {
editOpen.value = false;
editTarget.value = null;
},
});
}
function toggleActive(p) {
@@ -131,251 +172,266 @@ const statusClass = (p) => {
<template>
<AdminLayout title="Mail profili">
<Head title="Mail profili" />
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl font-semibold text-gray-800 flex items-center gap-3">
Mail profili
<span class="text-xs font-medium text-gray-400">({{ profiles.length }})</span>
</h1>
<button
@click="openCreate"
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-500 shadow"
>
<FontAwesomeIcon :icon="faPlus" class="w-4 h-4" /> Nov profil
</button>
</div>
<div class="rounded-lg border bg-white overflow-hidden shadow-sm">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
<tr>
<th class="px-3 py-2 text-left">Ime</th>
<th class="px-3 py-2 text-left">Host</th>
<th class="px-3 py-2">Port</th>
<th class="px-3 py-2">Enc</th>
<th class="px-3 py-2">Aktivno</th>
<th class="px-3 py-2">Test</th>
<th class="px-3 py-2">Zadnji uspeh</th>
<th class="px-3 py-2">Napaka</th>
<th class="px-3 py-2">Akcije</th>
</tr>
</thead>
<tbody>
<tr
v-for="p in profiles"
:key="p.id"
class="border-t last:border-b hover:bg-gray-50"
>
<td class="px-3 py-2 font-medium text-gray-800">{{ p.name }}</td>
<td class="px-3 py-2">{{ p.host }}</td>
<td class="px-3 py-2 text-center">{{ p.port }}</td>
<td class="px-3 py-2 text-center">{{ p.encryption || "—" }}</td>
<td class="px-3 py-2 text-center">
<button
@click="toggleActive(p)"
class="text-indigo-600 hover:text-indigo-800"
:title="p.active ? 'Onemogoči' : 'Omogoči'"
>
<FontAwesomeIcon
:icon="p.active ? faToggleOn : faToggleOff"
class="w-5 h-5"
/>
</button>
</td>
<td class="px-3 py-2 text-center">
<span :class="['font-medium', statusClass(p)]">{{
p.test_status || "—"
}}</span>
</td>
<td class="px-3 py-2 text-xs text-gray-500">
{{ p.last_success_at ? new Date(p.last_success_at).toLocaleString() : "—" }}
</td>
<td
class="px-3 py-2 text-xs text-rose-600 max-w-[160px] truncate"
:title="p.last_error_message"
>
{{ p.last_error_message || "—" }}
</td>
<td class="px-3 py-2 flex items-center gap-2">
<button
@click="testConnection(p)"
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-600 border-amber-300 bg-amber-50 hover:bg-amber-100"
>
<FontAwesomeIcon :icon="faFlask" class="w-3.5 h-3.5" /> Test
</button>
<button
@click="sendTestEmail(p)"
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-emerald-700 border-emerald-300 bg-emerald-50 hover:bg-emerald-100"
title="Pošlji testni email"
>
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Pošlji test
</button>
<button
@click="openEdit(p)"
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-600 border-indigo-300 bg-indigo-50 hover:bg-indigo-100"
title="Uredi profil"
>
<FontAwesomeIcon :icon="faArrowsRotate" class="w-3.5 h-3.5" /> Uredi
</button>
</td>
</tr>
</tbody>
</table>
</div>
<CreateDialog
:show="createOpen"
title="Nov Mail profil"
max-width="2xl"
confirm-text="Shrani"
:processing="form.processing"
@close="closeCreate"
@confirm="submitCreate"
>
<form @submit.prevent="submitCreate" id="create-mail-profile" class="space-y-5">
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<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"
>
<MailIcon class="h-5 w-5" />
</div>
<div>
<CardTitle>Mail profili</CardTitle>
<CardDescription
>Upravljajte SMTP profile za pošiljanje e-pošte ({{
profiles.length
}})</CardDescription
>
</div>
</div>
<Button @click="openCreate">
<PlusIcon class="h-4 w-4 mr-2" />
Nov profil
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Ime</TableHead>
<TableHead>Host</TableHead>
<TableHead class="text-center">Port</TableHead>
<TableHead class="text-center">Enc</TableHead>
<TableHead class="text-center">Aktivno</TableHead>
<TableHead class="text-center">Status</TableHead>
<TableHead>Zadnji uspeh</TableHead>
<TableHead>Napaka</TableHead>
<TableHead class="text-right">Akcije</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="p in profiles" :key="p.id">
<TableCell class="font-medium">{{ p.name }}</TableCell>
<TableCell>{{ p.host }}</TableCell>
<TableCell class="text-center">{{ p.port }}</TableCell>
<TableCell class="text-center">
<Badge v-if="p.encryption" variant="outline">{{
p.encryption.toUpperCase()
}}</Badge>
<span v-else class="text-muted-foreground"></span>
</TableCell>
<TableCell class="text-center">
<Switch
:default-value="p.active"
@update:model-value="() => toggleActive(p)"
/>
</TableCell>
<TableCell class="text-center">
<Badge
v-if="p.test_status === 'success'"
variant="default"
class="bg-green-100 text-green-800 hover:bg-green-100"
>{{ p.test_status }}</Badge
>
<Badge v-else-if="p.test_status === 'failed'" variant="destructive">{{
p.test_status
}}</Badge>
<Badge
v-else-if="p.test_status === 'queued'"
variant="secondary"
class="bg-amber-100 text-amber-800 hover:bg-amber-100"
>{{ p.test_status }}</Badge
>
<span v-else class="text-muted-foreground">—</span>
</TableCell>
<TableCell class="text-sm text-muted-foreground">
{{
p.last_success_at ? new Date(p.last_success_at).toLocaleString() : ""
}}
</TableCell>
<TableCell
class="text-sm text-destructive max-w-[200px] truncate"
:title="p.last_error_message"
>
{{ p.last_error_message || "" }}
</TableCell>
<TableCell class="text-right">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="sm">
<MoreVerticalIcon class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Akcije</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem @click="testConnection(p)">
<FlaskConicalIcon class="h-4 w-4 mr-2" />
Test povezavo
</DropdownMenuItem>
<DropdownMenuItem @click="sendTestEmail(p)">
<SendIcon class="h-4 w-4 mr-2" />
Pošlji testni email
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="openEdit(p)">
<PencilIcon class="h-4 w-4 mr-2" />
Uredi profil
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<Dialog :open="createOpen" @update:open="(val) => (createOpen = val)">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Nov Mail profil</DialogTitle>
<DialogDescription
>Ustvarite nov SMTP profil za pošiljanje e-pošte</DialogDescription
>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="grid gap-4 grid-cols-2">
<div class="col-span-1">
<label class="label">Ime</label>
<input v-model="form.name" type="text" class="input" />
<div class="space-y-2">
<Label for="create-name">Ime</Label>
<Input id="create-name" v-model="form.name" type="text" />
</div>
<div>
<label class="label">Host</label>
<input v-model="form.host" type="text" class="input" />
<div class="space-y-2">
<Label for="create-host">Host</Label>
<Input id="create-host" v-model="form.host" type="text" />
</div>
<div>
<label class="label">Port</label>
<input v-model="form.port" type="number" class="input" />
<div class="space-y-2">
<Label for="create-port">Port</Label>
<Input id="create-port" v-model.number="form.port" type="number" />
</div>
<div>
<label class="label">Encryption</label>
<select v-model="form.encryption" class="input">
<option value="">(None)</option>
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
</select>
<div class="space-y-2">
<Label for="create-encryption">Encryption</Label>
<Select v-model="form.encryption">
<SelectTrigger id="create-encryption">
<SelectValue placeholder="Izberi..." />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">None</SelectItem>
<SelectItem value="tls">TLS</SelectItem>
<SelectItem value="ssl">SSL</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label class="label">Username</label>
<input v-model="form.username" type="text" class="input" />
<div class="space-y-2">
<Label for="create-username">Username</Label>
<Input id="create-username" v-model="form.username" type="text" />
</div>
<div>
<label class="label">Password</label>
<input
<div class="space-y-2">
<Label for="create-password">Password</Label>
<Input
id="create-password"
v-model="form.password"
type="password"
class="input"
autocomplete="new-password"
/>
</div>
<div>
<label class="label">From naslov</label>
<input v-model="form.from_address" type="email" class="input" />
<div class="space-y-2">
<Label for="create-from-address">From naslov</Label>
<Input id="create-from-address" v-model="form.from_address" type="email" />
</div>
<div>
<label class="label">From ime</label>
<input v-model="form.from_name" type="text" class="input" />
<div class="space-y-2">
<Label for="create-from-name">From ime</Label>
<Input id="create-from-name" v-model="form.from_name" type="text" />
</div>
<div>
<label class="label">Prioriteta</label>
<input v-model="form.priority" type="number" class="input" />
<div class="space-y-2">
<Label for="create-priority">Prioriteta</Label>
<Input id="create-priority" v-model.number="form.priority" type="number" />
</div>
</div>
</form>
</CreateDialog>
<!-- Edit Modal -->
<UpdateDialog
:show="editOpen"
title="Uredi Mail profil"
max-width="2xl"
confirm-text="Shrani"
:processing="form.processing"
@close="closeEdit"
@confirm="submitEdit"
>
<form @submit.prevent="submitEdit" id="edit-mail-profile" class="space-y-5">
</form>
<DialogFooter>
<Button variant="outline" @click="closeCreate" :disabled="form.processing"
>Prekliči</Button
>
<Button @click="submitCreate" :disabled="form.processing">Shrani</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog :open="editOpen" @update:open="(val) => (editOpen = val)">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Uredi Mail profil</DialogTitle>
<DialogDescription>Posodobite nastavitve SMTP profila</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="grid gap-4 grid-cols-2">
<div>
<label class="label">Ime</label>
<input v-model="form.name" type="text" class="input" />
<div class="space-y-2">
<Label for="edit-name">Ime</Label>
<Input id="edit-name" v-model="form.name" type="text" />
</div>
<div>
<label class="label">Host</label>
<input v-model="form.host" type="text" class="input" />
<div class="space-y-2">
<Label for="edit-host">Host</Label>
<Input id="edit-host" v-model="form.host" type="text" />
</div>
<div>
<label class="label">Port</label>
<input v-model="form.port" type="number" class="input" />
<div class="space-y-2">
<Label for="edit-port">Port</Label>
<Input id="edit-port" v-model.number="form.port" type="number" />
</div>
<div>
<label class="label">Encryption</label>
<select v-model="form.encryption" class="input">
<option value="">(None)</option>
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
</select>
<div class="space-y-2">
<Label for="edit-encryption">Encryption</Label>
<Select v-model="form.encryption">
<SelectTrigger id="edit-encryption">
<SelectValue placeholder="Izberi..." />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">None</SelectItem>
<SelectItem value="tls">TLS</SelectItem>
<SelectItem value="ssl">SSL</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label class="label">Username</label>
<input v-model="form.username" type="text" class="input" />
<div class="space-y-2">
<Label for="edit-username">Username</Label>
<Input id="edit-username" v-model="form.username" type="text" />
</div>
<div>
<label class="label">Password (nova, če spreminjaš)</label>
<input v-model="form.password" type="password" class="input" autocomplete="new-password" />
<div class="space-y-2">
<Label for="edit-password">Password (nova, če spreminjaš)</Label>
<Input
id="edit-password"
v-model="form.password"
type="password"
autocomplete="new-password"
/>
</div>
<div>
<label class="label">From naslov</label>
<input v-model="form.from_address" type="email" class="input" />
<div class="space-y-2">
<Label for="edit-from-address">From naslov</Label>
<Input id="edit-from-address" v-model="form.from_address" type="email" />
</div>
<div>
<label class="label">From ime</label>
<input v-model="form.from_name" type="text" class="input" />
<div class="space-y-2">
<Label for="edit-from-name">From ime</Label>
<Input id="edit-from-name" v-model="form.from_name" type="text" />
</div>
<div>
<label class="label">Prioriteta</label>
<input v-model="form.priority" type="number" class="input" />
<div class="space-y-2">
<Label for="edit-priority">Prioriteta</Label>
<Input id="edit-priority" v-model.number="form.priority" type="number" />
</div>
</div>
<p class="text-xs text-gray-500">Pusti geslo prazno, če želiš obdržati obstoječe.</p>
</form>
</UpdateDialog>
<p class="text-sm text-muted-foreground">
Pusti geslo prazno, če želiš obdržati obstoječe.
</p>
</form>
<DialogFooter>
<Button variant="outline" @click="closeEdit" :disabled="form.processing"
>Prekliči</Button
>
<Button @click="submitEdit" :disabled="form.processing">Shrani</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</AdminLayout>
</template>
<style scoped>
/* Utility replacements for @apply not processed in SFC scope build pipeline */
.input {
width: 100%;
border-radius: 0.375rem;
border: 1px solid var(--tw-color-gray-300, #d1d5db);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.input:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-color: #6366f1;
border-color: #6366f1;
box-shadow: 0 0 0 1px #6366f1;
}
.label {
display: block;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #6b7280;
margin-bottom: 0.25rem;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
.animate-fade-in {
animation: fade-in 0.25s ease;
}
</style>
<style scoped></style>