Admin panel updated with shadcn-vue components
This commit is contained in:
@@ -1,104 +1,259 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { Head, Link, router } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link, router } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import { MailIcon, FilterIcon, ExternalLinkIcon } 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
logs: Object,
|
||||
templates: Array,
|
||||
filters: Object,
|
||||
})
|
||||
});
|
||||
|
||||
const form = ref({
|
||||
status: props.filters?.status || '',
|
||||
to: props.filters?.to || '',
|
||||
subject: props.filters?.subject || '',
|
||||
template_id: props.filters?.template_id || '',
|
||||
date_from: props.filters?.date_from || '',
|
||||
date_to: props.filters?.date_to || '',
|
||||
})
|
||||
status: props.filters?.status || "",
|
||||
to: props.filters?.to || "",
|
||||
subject: props.filters?.subject || "",
|
||||
template_id: props.filters?.template_id || "",
|
||||
date_from: props.filters?.date_from || "",
|
||||
date_to: props.filters?.date_to || "",
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
router.get(route('admin.email-logs.index'), {
|
||||
...form.value,
|
||||
}, { preserveState: true, preserveScroll: true })
|
||||
router.get(
|
||||
route("admin.email-logs.index"),
|
||||
{
|
||||
...form.value,
|
||||
},
|
||||
{ preserveState: true, preserveScroll: true }
|
||||
);
|
||||
}
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (status === "sent") return "default";
|
||||
if (status === "queued" || status === "sending") return "secondary";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "created_at",
|
||||
label: "Datum",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "to_email",
|
||||
label: "Prejemnik",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "subject",
|
||||
label: "Zadeva",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "template",
|
||||
label: "Predloga",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "duration_ms",
|
||||
label: "Trajanje",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "Dejanja",
|
||||
sortable: false,
|
||||
align: "right",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="Email Logs">
|
||||
<Head title="Email Logs" />
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="text-xl font-semibold text-gray-800">Email Logs</h1>
|
||||
<div class="mt-3 grid grid-cols-1 md:grid-cols-6 gap-2">
|
||||
<select v-model="form.status" class="input">
|
||||
<option value="">All statuses</option>
|
||||
<option value="queued">Queued</option>
|
||||
<option value="sending">Sending</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="bounced">Bounced</option>
|
||||
<option value="deferred">Deferred</option>
|
||||
</select>
|
||||
<input v-model="form.to" placeholder="To email" class="input" />
|
||||
<input v-model="form.subject" placeholder="Subject" class="input" />
|
||||
<select v-model="form.template_id" class="input">
|
||||
<option value="">All templates</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<input v-model="form.date_from" type="date" class="input" />
|
||||
<input v-model="form.date_to" type="date" class="input" />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button @click="applyFilters" class="px-3 py-1.5 text-xs rounded border bg-gray-50 hover:bg-gray-100">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<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>Email Logs</CardTitle>
|
||||
<CardDescription>
|
||||
Pregled vseh poslanih in čakajočih e-poštnih sporočil
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="applyFilters" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="status">Status</Label>
|
||||
<Select v-model="form.status">
|
||||
<SelectTrigger id="status">
|
||||
<SelectValue placeholder="All statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">All statuses</SelectItem>
|
||||
<SelectItem value="queued">Queued</SelectItem>
|
||||
<SelectItem value="sending">Sending</SelectItem>
|
||||
<SelectItem value="sent">Sent</SelectItem>
|
||||
<SelectItem value="failed">Failed</SelectItem>
|
||||
<SelectItem value="bounced">Bounced</SelectItem>
|
||||
<SelectItem value="deferred">Deferred</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="to">To email</Label>
|
||||
<Input id="to" v-model="form.to" placeholder="prejemnik@email.com" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="subject">Subject</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
v-model="form.subject"
|
||||
placeholder="Iskanje po zadevi"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="template">Template</Label>
|
||||
<Select v-model="form.template_id">
|
||||
<SelectTrigger id="template">
|
||||
<SelectValue placeholder="All templates" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">All templates</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="date_from">Od datuma</Label>
|
||||
<Input id="date_from" v-model="form.date_from" type="date" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="date_to">Do datuma</Label>
|
||||
<Input id="date_to" v-model="form.date_to" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" size="sm">
|
||||
<FilterIcon class="h-4 w-4 mr-2" />
|
||||
Filtriraj
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm">
|
||||
<div class="overflow-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600">
|
||||
<tr>
|
||||
<th class="text-left p-2">Date</th>
|
||||
<th class="text-left p-2">Status</th>
|
||||
<th class="text-left p-2">To</th>
|
||||
<th class="text-left p-2">Subject</th>
|
||||
<th class="text-left p-2">Template</th>
|
||||
<th class="text-left p-2">Duration</th>
|
||||
<th class="text-left p-2">\#</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs.data" :key="log.id" class="border-t">
|
||||
<td class="p-2 whitespace-nowrap">{{ new Date(log.created_at).toLocaleString() }}</td>
|
||||
<td class="p-2"><span class="inline-flex items-center px-2 py-0.5 rounded text-xs border" :class="{
|
||||
'bg-green-50 text-green-700 border-green-200': log.status === 'sent',
|
||||
'bg-amber-50 text-amber-700 border-amber-200': log.status === 'queued' || log.status === 'sending',
|
||||
'bg-red-50 text-red-700 border-red-200': log.status === 'failed',
|
||||
}">{{ log.status }}</span></td>
|
||||
<td class="p-2 truncate max-w-[220px]">
|
||||
{{ log.to_email || (Array.isArray(log.to_recipients) && log.to_recipients.length ? log.to_recipients.join(', ') : '-') }}
|
||||
</td>
|
||||
<td class="p-2 truncate max-w-[320px]">{{ log.subject }}</td>
|
||||
<td class="p-2 truncate max-w-[220px]">{{ log.template?.name || '-' }}</td>
|
||||
<td class="p-2">{{ log.duration_ms ? log.duration_ms + ' ms' : '-' }}</td>
|
||||
<td class="p-2"><Link :href="route('admin.email-logs.show', log.id)" class="text-indigo-600 hover:underline">Open</Link></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-2 border-t text-xs text-gray-600 flex items-center justify-between">
|
||||
<div>Showing {{ logs.from }}-{{ logs.to }} of {{ logs.total }}</div>
|
||||
<div class="flex gap-2">
|
||||
<Link v-for="link in logs.links" :key="link.url || link.label" :href="link.url || '#'" :class="['px-2 py-1 rounded border text-xs', { 'bg-indigo-600 text-white border-indigo-600': link.active, 'pointer-events-none opacity-50': !link.url } ]" v-html="link.label" />
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<MailIcon size="18" />
|
||||
<CardTitle class="uppercase">Uvozi</CardTitle>
|
||||
</div>
|
||||
</template>
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="logs.data"
|
||||
:meta="logs"
|
||||
:show-toolbar="false"
|
||||
:show-pagination="true"
|
||||
route-name="admin.email-logs.index"
|
||||
:preserve-state="true"
|
||||
:preserve-scroll="true"
|
||||
empty-text="Ni e-poštnih dnevnikov"
|
||||
empty-description="Začnite z ustvarjanjem e-poštnih sporočil"
|
||||
>
|
||||
<template #cell-created_at="{ value }">
|
||||
<div class="whitespace-nowrap text-sm">
|
||||
{{ new Date(value).toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ value }">
|
||||
<Badge :variant="getStatusVariant(value)">
|
||||
{{ value }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-to_email="{ row }">
|
||||
<div class="max-w-55 truncate">
|
||||
{{
|
||||
row.to_email ||
|
||||
(Array.isArray(row.to_recipients) && row.to_recipients.length
|
||||
? row.to_recipients.join(", ")
|
||||
: "-")
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-subject="{ value }">
|
||||
<div class="max-w-[320px] truncate">{{ value }}</div>
|
||||
</template>
|
||||
|
||||
<template #cell-template="{ row }">
|
||||
<div class="max-w-55 truncate">{{ row.template?.name || "-" }}</div>
|
||||
</template>
|
||||
|
||||
<template #cell-duration_ms="{ value }">
|
||||
<span v-if="value" class="text-xs text-muted-foreground">
|
||||
{{ value }} ms
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground">-</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<Button size="sm" variant="ghost" as-child>
|
||||
<Link :href="route('admin.email-logs.show', row.id)">
|
||||
<ExternalLinkIcon class="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
</div>
|
||||
</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; }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { Head, Link } from '@inertiajs/vue3'
|
||||
import { MailIcon, ArrowLeftIcon, ClockIcon, UserIcon, AlertCircleIcon, FileTextIcon, CodeIcon } from 'lucide-vue-next'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card'
|
||||
import { Button } from '@/Components/ui/button'
|
||||
import { Badge } from '@/Components/ui/badge'
|
||||
import { Separator } from '@/Components/ui/separator'
|
||||
|
||||
const props = defineProps({
|
||||
log: Object,
|
||||
@@ -11,50 +16,124 @@ const props = defineProps({
|
||||
<AdminLayout title="Email Log">
|
||||
<Head title="Email Log" />
|
||||
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Link :href="route('admin.email-logs.index')" class="text-sm text-gray-600 hover:text-gray-800">Back</Link>
|
||||
<h1 class="text-xl font-semibold text-gray-800">Email Log #{{ props.log.id }}</h1>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Created: {{ new Date(props.log.created_at).toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<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>Email Log #{{ props.log.id }}</CardTitle>
|
||||
<CardDescription>
|
||||
Ustvarjeno: {{ new Date(props.log.created_at).toLocaleString() }}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="route('admin.email-logs.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-4 space-y-2">
|
||||
<div class="text-sm"><span class="font-semibold">Status:</span> {{ props.log.status }}</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold">To:</span>
|
||||
<template v-if="props.log.to_email">
|
||||
{{ props.log.to_email }} {{ props.log.to_name ? '(' + props.log.to_name + ')' : '' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="Array.isArray(props.log.to_recipients) && props.log.to_recipients.length">
|
||||
{{ props.log.to_recipients.join(', ') }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="text-sm"><span class="font-semibold">Subject:</span> {{ props.log.subject }}</div>
|
||||
<div class="text-sm"><span class="font-semibold">Template:</span> {{ props.log.template?.name || '-' }}</div>
|
||||
<!-- Message ID removed per request -->
|
||||
<div class="text-sm"><span class="font-semibold">Attempts:</span> {{ props.log.attempt }}</div>
|
||||
<div class="text-sm"><span class="font-semibold">Duration:</span> {{ props.log.duration_ms ? props.log.duration_ms + ' ms' : '-' }}</div>
|
||||
<div v-if="props.log.error_message" class="text-sm text-red-700"><span class="font-semibold">Error:</span> {{ props.log.error_message }}</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Podrobnosti</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground">Status:</span>
|
||||
<Badge
|
||||
:variant="
|
||||
props.log.status === 'sent' ? 'default' :
|
||||
props.log.status === 'queued' || props.log.status === 'sending' ? 'secondary' :
|
||||
props.log.status === 'failed' ? 'destructive' : 'outline'
|
||||
"
|
||||
>
|
||||
{{ props.log.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium flex items-center gap-2">
|
||||
<UserIcon class="h-4 w-4" />
|
||||
Prejemnik
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<template v-if="props.log.to_email">
|
||||
{{ props.log.to_email }} {{ props.log.to_name ? '(' + props.log.to_name + ')' : '' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="Array.isArray(props.log.to_recipients) && props.log.to_recipients.length">
|
||||
{{ props.log.to_recipients.join(', ') }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium">Zadeva</div>
|
||||
<div class="text-sm text-muted-foreground">{{ props.log.subject }}</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground">Predloga:</span>
|
||||
<Badge variant="outline">{{ props.log.template?.name || '-' }}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground">Poskusi:</span>
|
||||
<Badge variant="secondary">{{ props.log.attempt }}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<ClockIcon class="h-4 w-4" />
|
||||
Trajanje:
|
||||
</span>
|
||||
<span class="text-sm">{{ props.log.duration_ms ? props.log.duration_ms + ' ms' : '-' }}</span>
|
||||
</div>
|
||||
<div v-if="props.log.error_message" class="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<div class="flex items-start gap-2">
|
||||
<AlertCircleIcon class="h-4 w-4 text-destructive mt-0.5" />
|
||||
<div>
|
||||
<div class="text-sm font-medium text-destructive">Napaka</div>
|
||||
<div class="text-xs text-destructive/80 mt-1">{{ props.log.error_message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<FileTextIcon class="h-4 w-4" />
|
||||
Besedilo
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre class="text-xs whitespace-pre-wrap break-words bg-muted p-3 rounded-md">{{ props.log.body?.body_text || '' }}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-4">
|
||||
<div class="label">Text</div>
|
||||
<pre class="text-xs whitespace-pre-wrap break-words">{{ props.log.body?.body_text || '' }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-4">
|
||||
<div class="label">HTML</div>
|
||||
<iframe :srcdoc="props.log.body?.body_html || ''" class="w-full h-[480px] border rounded bg-white"></iframe>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<CodeIcon class="h-4 w-4" />
|
||||
HTML vsebina
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<iframe :srcdoc="props.log.body?.body_html || ''" class="w-full h-[480px] border rounded-md bg-white"></iframe>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label { display:block; font-size: 0.7rem; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:#6b7280; margin-bottom:0.25rem; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user