SMS service

This commit is contained in:
Simon Pocrnjič
2025-10-24 21:39:10 +02:00
parent 3a2eed7dda
commit 930ac83604
52 changed files with 3830 additions and 36 deletions
+164
View File
@@ -0,0 +1,164 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import { Head, Link, router } from "@inertiajs/vue3";
import { ref, watch } from "vue";
const props = defineProps({
logs: { type: Object, required: true },
profiles: { type: Array, default: () => [] },
templates: { type: Array, default: () => [] },
filters: { type: Object, default: () => ({}) },
});
const f = ref({
status: props.filters.status ?? "",
profile_id: props.filters.profile_id ?? "",
template_id: props.filters.template_id ?? "",
search: props.filters.search ?? "",
from: props.filters.from ?? "",
to: props.filters.to ?? "",
});
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 });
}
watch(
() => [f.value.status, f.value.profile_id, f.value.template_id, f.value.from, f.value.to],
() => reload()
);
function clearFilters() {
f.value = { status: "", profile_id: "", template_id: "", search: "", from: "", to: "" };
reload();
}
</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>
</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>
<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">&nbsp;</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 }}
</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
/>
</div>
</div>
</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; }
.label { display: block; font-size: 0.65rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; margin-bottom: 0.25rem; }
</style>
+80
View File
@@ -0,0 +1,80 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import { Head, Link } from "@inertiajs/vue3";
const props = defineProps({ log: { type: Object, required: true } });
</script>
<template>
<AdminLayout title="SMS log">
<Head title="SMS log" />
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<Link :href="route('admin.sms-logs.index')" class="text-sm text-indigo-600 hover:underline"> Nazaj na dnevnike</Link>
<div class="text-gray-700 text-sm">#{{ log.id }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-4">
<div class="rounded-lg border bg-white p-4 shadow-sm">
<div class="font-semibold text-gray-800 mb-2">Sporočilo</div>
<pre class="text-sm whitespace-pre-wrap">{{ log.message }}</pre>
</div>
<div class="rounded-lg border bg-white p-4 shadow-sm">
<div class="font-semibold text-gray-800 mb-2">Meta</div>
<pre class="text-xs whitespace-pre-wrap">{{ JSON.stringify(log.meta || {}, null, 2) }}</pre>
</div>
</div>
<div class="space-y-4">
<div class="rounded-lg border bg-white p-4 shadow-sm">
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<div class="text-gray-500">Prejemnik</div>
<div class="text-gray-800">{{ log.to_number }}</div>
<div class="text-gray-500">Sender</div>
<div class="text-gray-800">{{ log.sender || '—' }}</div>
<div class="text-gray-500">Profil</div>
<div class="text-gray-800">{{ log.profile?.name || '—' }}</div>
<div class="text-gray-500">Predloga</div>
<div class="text-gray-800">{{ log.template?.slug || log.template?.name || '—' }}</div>
<div class="text-gray-500">Status</div>
<div class="text-gray-800">{{ log.status }}</div>
<div class="text-gray-500">Cena</div>
<div class="text-gray-800">{{ log.cost != null ? (Number(log.cost).toFixed(2) + ' ' + (log.currency || '')) : '—' }}</div>
<div class="text-gray-500">Provider ID</div>
<div class="text-gray-800">{{ log.provider_message_id || '—' }}</div>
<div class="text-gray-500">Čas</div>
<div class="text-gray-800">{{ new Date(log.created_at).toLocaleString() }}</div>
<div class="text-gray-500">Sent</div>
<div class="text-gray-800">{{ log.sent_at ? new Date(log.sent_at).toLocaleString() : '—' }}</div>
<div class="text-gray-500">Delivered</div>
<div class="text-gray-800">{{ log.delivered_at ? new Date(log.delivered_at).toLocaleString() : '—' }}</div>
<div class="text-gray-500">Failed</div>
<div class="text-gray-800">{{ log.failed_at ? new Date(log.failed_at).toLocaleString() : '—' }}</div>
<div class="text-gray-500">Napaka (koda)</div>
<div class="text-gray-800">{{ log.error_code || '—' }}</div>
<div class="text-gray-500">Napaka (opis)</div>
<div class="text-gray-800">{{ log.error_message || '—' }}</div>
</div>
</div>
</div>
</div>
</AdminLayout>
</template>
<style scoped>
.label { display: block; font-size: 0.65rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; margin-bottom: 0.25rem; }
</style>