Admin panel updated with shadcn-vue components
This commit is contained in:
@@ -1,7 +1,35 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link, router } from "@inertiajs/vue3";
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import {
|
||||
MessageSquareIcon,
|
||||
FilterIcon,
|
||||
XIcon,
|
||||
EyeIcon,
|
||||
MessageSquareTextIcon,
|
||||
} 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import Pagination from "@/Components/Pagination.vue";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
logs: { type: Object, required: true },
|
||||
@@ -23,11 +51,20 @@ function reload() {
|
||||
const query = Object.fromEntries(
|
||||
Object.entries(f.value).filter(([_, v]) => v !== null && v !== undefined && v !== "")
|
||||
);
|
||||
router.get(route("admin.sms-logs.index"), query, { preserveScroll: true, preserveState: true });
|
||||
router.get(route("admin.sms-logs.index"), query, {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [f.value.status, f.value.profile_id, f.value.template_id, f.value.from, f.value.to],
|
||||
() => [
|
||||
f.value.status,
|
||||
f.value.profile_id,
|
||||
f.value.template_id,
|
||||
f.value.from,
|
||||
f.value.to,
|
||||
],
|
||||
() => reload()
|
||||
);
|
||||
|
||||
@@ -35,130 +72,222 @@ function clearFilters() {
|
||||
f.value = { status: "", profile_id: "", template_id: "", search: "", from: "", to: "" };
|
||||
reload();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ key: "created_at", label: "Čas", sortable: false },
|
||||
{ key: "to_number", label: "Prejemnik", sortable: false },
|
||||
{ key: "sender", label: "Sender", sortable: false },
|
||||
{ key: "profile", label: "Profil", sortable: false },
|
||||
{ key: "template", label: "Predloga", sortable: false },
|
||||
{ key: "status", label: "Status", sortable: false },
|
||||
{ key: "cost", label: "Cena", sortable: false },
|
||||
{ key: "provider_message_id", label: "Provider ID", sortable: false },
|
||||
{ key: "actions", label: "", sortable: false },
|
||||
];
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (status === "queued") return "secondary";
|
||||
if (status === "sent") return "default";
|
||||
if (status === "delivered") return "default";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (status === "queued") return "bg-amber-100 text-amber-800 hover:bg-amber-100";
|
||||
if (status === "sent") return "bg-sky-100 text-sky-800 hover:bg-sky-100";
|
||||
if (status === "delivered") return "bg-green-100 text-green-800 hover:bg-green-100";
|
||||
if (status === "failed") return "bg-red-100 text-red-800 hover:bg-red-100";
|
||||
return "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="SMS dnevniki">
|
||||
<Head title="SMS dnevniki" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800">SMS dnevniki</h1>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-white p-4 shadow-sm mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-3">
|
||||
<div>
|
||||
<label class="label">Status</label>
|
||||
<select v-model="f.status" class="input">
|
||||
<option value="">Vsi</option>
|
||||
<option value="queued">queued</option>
|
||||
<option value="sent">sent</option>
|
||||
<option value="delivered">delivered</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<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"
|
||||
>
|
||||
<MessageSquareIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>SMS dnevniki</CardTitle>
|
||||
<CardDescription>Pregled poslanih SMS sporočil</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="f.profile_id" class="input">
|
||||
<option value="">Vsi</option>
|
||||
<option v-for="p in profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Predloga</label>
|
||||
<select v-model="f.template_id" class="input">
|
||||
<option value="">Vse</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Od</label>
|
||||
<input type="date" v-model="f.from" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Do</label>
|
||||
<input type="date" v-model="f.to" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Iskanje</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="f.search"
|
||||
class="input"
|
||||
placeholder="to, sender, provider id, message"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<button type="button" class="px-3 py-1.5 rounded border text-sm bg-gray-50 hover:bg-gray-100" @click="reload">Filtriraj</button>
|
||||
<button type="button" class="px-3 py-1.5 rounded border text-sm bg-white hover:bg-gray-50" @click="clearFilters">Počisti</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</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">Čas</th>
|
||||
<th class="px-3 py-2 text-left">Prejemnik</th>
|
||||
<th class="px-3 py-2 text-left">Sender</th>
|
||||
<th class="px-3 py-2 text-left">Profil</th>
|
||||
<th class="px-3 py-2 text-left">Predloga</th>
|
||||
<th class="px-3 py-2 text-left">Status</th>
|
||||
<th class="px-3 py-2 text-left">Cena</th>
|
||||
<th class="px-3 py-2 text-left">Provider ID</th>
|
||||
<th class="px-3 py-2"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs.data" :key="log.id" class="border-t last:border-b hover:bg-gray-50">
|
||||
<td class="px-3 py-2">{{ new Date(log.created_at).toLocaleString() }}</td>
|
||||
<td class="px-3 py-2">{{ log.to_number }}</td>
|
||||
<td class="px-3 py-2">{{ log.sender || '—' }}</td>
|
||||
<td class="px-3 py-2">{{ log.profile?.name || '—' }}</td>
|
||||
<td class="px-3 py-2">{{ log.template?.slug || log.template?.name || '—' }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<span :class="{
|
||||
'text-amber-600': log.status === 'queued',
|
||||
'text-sky-700': log.status === 'sent',
|
||||
'text-emerald-700': log.status === 'delivered',
|
||||
'text-rose-700': log.status === 'failed',
|
||||
}">{{ log.status }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ log.cost != null ? (Number(log.cost).toFixed(2) + ' ' + (log.currency || '')) : '—' }}</td>
|
||||
<td class="px-3 py-2">{{ log.provider_message_id || '—' }}</td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
<Link :href="route('admin.sms-logs.show', log.id)" class="text-xs px-2 py-1 rounded border text-gray-700 bg-gray-50 hover:bg-gray-100">Ogled</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!logs.data || logs.data.length === 0">
|
||||
<td colspan="9" class="px-3 py-6 text-center text-sm text-gray-500">Ni vnosov.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="px-3 py-2 border-t flex items-center justify-between text-xs text-gray-600">
|
||||
<div>
|
||||
Prikaz {{ logs.from || 0 }}–{{ logs.to || 0 }} od {{ logs.total || 0 }}
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<FilterIcon class="h-4 w-4" />
|
||||
<CardTitle class="text-base">Filtri</CardTitle>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Link
|
||||
v-for="l in logs.links"
|
||||
:key="l.label + l.url"
|
||||
:href="l.url || '#'"
|
||||
v-html="l.label"
|
||||
class="px-2 py-1 rounded border"
|
||||
:class="[l.active ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white hover:bg-gray-50']"
|
||||
preserve-scroll
|
||||
preserve-state
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-status">Status</Label>
|
||||
<Select v-model="f.status">
|
||||
<SelectTrigger id="filter-status">
|
||||
<SelectValue placeholder="Vsi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vsi</SelectItem>
|
||||
<SelectItem value="queued">queued</SelectItem>
|
||||
<SelectItem value="sent">sent</SelectItem>
|
||||
<SelectItem value="delivered">delivered</SelectItem>
|
||||
<SelectItem value="failed">failed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-profile">Profil</Label>
|
||||
<Select v-model="f.profile_id">
|
||||
<SelectTrigger id="filter-profile">
|
||||
<SelectValue placeholder="Vsi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vsi</SelectItem>
|
||||
<SelectItem v-for="p in profiles" :key="p.id" :value="p.id">{{
|
||||
p.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-template">Predloga</Label>
|
||||
<Select v-model="f.template_id">
|
||||
<SelectTrigger id="filter-template">
|
||||
<SelectValue placeholder="Vse" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vse</SelectItem>
|
||||
<SelectItem v-for="t in templates" :key="t.id" :value="t.id">{{
|
||||
t.name
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-from">Od</Label>
|
||||
<Input id="filter-from" type="date" v-model="f.from" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-to">Do</Label>
|
||||
<Input id="filter-to" type="date" v-model="f.to" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-search">Iskanje</Label>
|
||||
<Input
|
||||
id="filter-search"
|
||||
type="text"
|
||||
v-model="f.search"
|
||||
placeholder="to, sender, provider id"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<Button size="sm" @click="reload">
|
||||
<FilterIcon class="h-4 w-4" />
|
||||
Filtriraj
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" @click="clearFilters">
|
||||
<XIcon class="h-4 w-4" />
|
||||
Počisti
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<AppCard
|
||||
title=""
|
||||
padding="none"
|
||||
class="p-0! gap-0"
|
||||
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||
body-class=""
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<MessageSquareTextIcon size="18" />
|
||||
<CardTitle class="uppercase">Poslani</CardTitle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="logs.data"
|
||||
:meta="logs"
|
||||
:page-size="25"
|
||||
:page-size-options="[10, 15, 25, 50, 100]"
|
||||
route-name="admin.sms-logs.index"
|
||||
>
|
||||
<template #cell-created_at="{ row }">
|
||||
<span class="text-sm">{{ new Date(row.created_at).toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-to_number="{ row }">
|
||||
<span class="text-sm">{{ row.to_number }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-sender="{ row }">
|
||||
<span class="text-sm text-muted-foreground">{{ row.sender || "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-profile="{ row }">
|
||||
<span class="text-sm">{{ row.profile?.name || "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-template="{ row }">
|
||||
<Badge v-if="row.template" variant="outline">{{
|
||||
row.template.slug || row.template.name
|
||||
}}</Badge>
|
||||
<span v-else class="text-sm text-muted-foreground">—</span>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<Badge
|
||||
:variant="getStatusVariant(row.status)"
|
||||
:class="getStatusClass(row.status)"
|
||||
>
|
||||
{{ row.status }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-cost="{ row }">
|
||||
<span class="text-sm">
|
||||
{{
|
||||
row.cost != null
|
||||
? Number(row.cost).toFixed(2) + " " + (row.currency || "")
|
||||
: "—"
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-provider_message_id="{ row }">
|
||||
<span class="text-sm text-muted-foreground truncate max-w-[150px] block">{{
|
||||
row.provider_message_id || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex justify-end">
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="route('admin.sms-logs.show', row.id)">
|
||||
<EyeIcon class="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input { width: 100%; border-radius: 0.375rem; border: 1px solid #d1d5db; padding: 0.5rem 0.75rem; font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.input:focus { outline: 2px solid transparent; outline-offset: 2px; 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>
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user