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
+165 -109
View File
@@ -1,10 +1,47 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import DialogModal from "@/Components/DialogModal.vue";
import { Head, useForm, router } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faPlus, faPen, faTrash, faToggleOn, faToggleOff } from "@fortawesome/free-solid-svg-icons";
import { Card, CardContent, 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import { Switch } from "@/Components/ui/switch";
import {
PhoneIcon,
PlusIcon,
PencilIcon,
Trash2Icon,
MoreVerticalIcon,
} from "lucide-vue-next";
const props = defineProps({
initialSenders: { type: Array, default: () => [] },
@@ -13,8 +50,9 @@ const props = defineProps({
// Use props directly so Inertia navigations refresh the list automatically
const senders = computed(() => props.initialSenders || []);
const profileById = computed(() => Object.fromEntries((props.profiles || []).map(p => [p.id, p])));
const profileById = computed(() =>
Object.fromEntries((props.profiles || []).map((p) => [p.id, p]))
);
// Create/Edit modal
const editOpen = ref(false);
@@ -71,7 +109,11 @@ async function submitEdit() {
}
async function toggleActive(s) {
await router.post(route("admin.sms-senders.toggle", s.id), {}, { preserveScroll: true });
await router.post(
route("admin.sms-senders.toggle", s.id),
{},
{ preserveScroll: true }
);
}
async function destroySender(s) {
@@ -83,117 +125,131 @@ async function destroySender(s) {
<template>
<AdminLayout title="SMS pošiljatelji">
<Head title="SMS pošiljatelji" />
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl font-semibold text-gray-800">SMS pošiljatelji</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 pošiljatelj
</button>
</div>
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<PhoneIcon class="h-5 w-5 text-muted-foreground" />
<CardTitle>SMS pošiljatelji</CardTitle>
</div>
<Button @click="openCreate">
<PlusIcon class="h-4 w-4 mr-2" />
Nov pošiljatelj
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Pošiljatelj</TableHead>
<TableHead>Številka</TableHead>
<TableHead>Profil</TableHead>
<TableHead>Aktiven</TableHead>
<TableHead>Opis</TableHead>
<TableHead class="text-right">Akcije</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="s in senders" :key="s.id">
<TableCell class="font-medium">{{ s.sname }}</TableCell>
<TableCell>{{ s.phone_number || "—" }}</TableCell>
<TableCell>{{ profileById[s.profile_id]?.name || "—" }}</TableCell>
<TableCell>
<Switch
:default-value="s.active"
@update:model-value="() => toggleActive(s)"
/>
</TableCell>
<TableCell class="text-muted-foreground">{{
s.description || ""
}}</TableCell>
<TableCell class="text-right">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreVerticalIcon class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="openEdit(s)">
<PencilIcon class="h-4 w-4 mr-2" />
Uredi
</DropdownMenuItem>
<DropdownMenuItem @click="destroySender(s)" class="text-destructive">
<Trash2Icon class="h-4 w-4 mr-2" />
Izbriši
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<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">Pošiljatelj</th>
<th class="px-3 py-2 text-left">Številka</th>
<th class="px-3 py-2 text-left">Profil</th>
<th class="px-3 py-2">Aktiven</th>
<th class="px-3 py-2 text-left">Opis</th>
<th class="px-3 py-2">Akcije</th>
</tr>
</thead>
<tbody>
<tr v-for="s in senders" :key="s.id" class="border-t last:border-b hover:bg-gray-50">
<td class="px-3 py-2 font-medium text-gray-800">{{ s.sname }}</td>
<td class="px-3 py-2 text-gray-700">{{ s.phone_number || '—' }}</td>
<td class="px-3 py-2">{{ profileById[s.profile_id]?.name || '—' }}</td>
<td class="px-3 py-2 text-center">
<span :class="s.active ? 'text-emerald-600' : 'text-rose-600'">{{ s.active ? 'Da' : 'Ne' }}</span>
</td>
<td class="px-3 py-2 text-gray-600">{{ s.description || '—' }}</td>
<td class="px-3 py-2">
<div class="flex items-center gap-2">
<button @click="openEdit(s)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-700 border-indigo-300 bg-indigo-50 hover:bg-indigo-100">
<FontAwesomeIcon :icon="faPen" class="w-3.5 h-3.5" /> Uredi
</button>
<button @click="toggleActive(s)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-700 border-amber-300 bg-amber-50 hover:bg-amber-100">
<FontAwesomeIcon :icon="s.active ? faToggleOn : faToggleOff" class="w-3.5 h-3.5" />
</button>
<button @click="destroySender(s)" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-rose-700 border-rose-300 bg-rose-50 hover:bg-rose-100">
<FontAwesomeIcon :icon="faTrash" class="w-3.5 h-3.5" /> Izbriši
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<DialogModal :show="editOpen" max-width="2xl" @close="() => (editOpen = false)">
<template #title> {{ editing ? 'Uredi pošiljatelja' : 'Nov pošiljatelj' }} </template>
<template #content>
<form @submit.prevent="submitEdit" id="edit-sms-sender" class="space-y-5">
<Dialog :open="editOpen" @update:open="(val) => (editOpen = val)">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>{{
editing ? "Uredi pošiljatelja" : "Nov pošiljatelj"
}}</DialogTitle>
</DialogHeader>
<form @submit.prevent="submitEdit" id="edit-sms-sender" class="space-y-4">
<div class="grid gap-4 grid-cols-2">
<div>
<label class="label">Profil</label>
<select v-model="form.profile_id" class="input">
<option :value="null" disabled>Izberi profil</option>
<option v-for="p in profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
<div class="space-y-2">
<Label>Profil</Label>
<Select v-model="form.profile_id">
<SelectTrigger>
<SelectValue placeholder="Izberi profil" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="p in profiles" :key="p.id" :value="p.id">
{{ p.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label class="label">Pošiljatelj (Sender ID) opcijsko</label>
<input v-model="form.sname" type="text" class="input" placeholder="npr. TEREN" />
<div class="space-y-2">
<Label>Pošiljatelj (Sender ID) — opcijsko</Label>
<Input v-model="form.sname" type="text" placeholder="npr. TEREN" />
</div>
<div>
<label class="label">Številka pošiljatelja (opcijsko)</label>
<input v-model="form.phone_number" type="text" class="input" placeholder="npr. +38640123456" />
<div class="space-y-2">
<Label>Številka pošiljatelja (opcijsko)</Label>
<Input
v-model="form.phone_number"
type="text"
placeholder="npr. +38640123456"
/>
</div>
<div class="col-span-2">
<label class="label">Opis (opcijsko)</label>
<input v-model="form.description" type="text" class="input" />
<div class="space-y-2 col-span-2">
<Label>Opis (opcijsko)</Label>
<Input v-model="form.description" type="text" />
</div>
<div>
<label class="label">Aktivno</label>
<select v-model="form.active" class="input">
<option :value="true">Da</option>
<option :value="false">Ne</option>
</select>
<div class="space-y-2">
<Label>Aktivno</Label>
<Select v-model="form.active">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem :value="true">Da</SelectItem>
<SelectItem :value="false">Ne</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</form>
</template>
<template #footer>
<button type="button" @click="() => (editOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Prekliči</button>
<button form="edit-sms-sender" type="submit" :disabled="form.processing" class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50">Shrani</button>
</template>
</DialogModal>
<DialogFooter>
<Button type="button" variant="outline" @click="() => (editOpen = false)"
>Prekliči</Button
>
<Button form="edit-sms-sender" type="submit" :disabled="form.processing"
>Shrani</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
</AdminLayout>
</template>
<style scoped>
.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;
}
</style>