SMS service
This commit is contained in:
@@ -3,7 +3,7 @@ import { FwbBadge } from "flowbite-vue";
|
||||
import { EditIcon, PlusIcon, UserEditIcon, TrashBinIcon } from "@/Utilities/Icons";
|
||||
import CusTab from "./CusTab.vue";
|
||||
import CusTabs from "./CusTabs.vue";
|
||||
import { provide, ref, watch } from "vue";
|
||||
import { provide, ref, watch, computed } from "vue";
|
||||
import axios from "axios";
|
||||
import PersonUpdateForm from "./PersonUpdateForm.vue";
|
||||
import AddressCreateForm from "./AddressCreateForm.vue";
|
||||
@@ -14,6 +14,8 @@ import EmailUpdateForm from "./EmailUpdateForm.vue";
|
||||
import TrrCreateForm from "./TrrCreateForm.vue";
|
||||
import TrrUpdateForm from "./TrrUpdateForm.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { router, usePage } from "@inertiajs/vue3";
|
||||
|
||||
const props = defineProps({
|
||||
person: Object,
|
||||
@@ -32,6 +34,14 @@ const props = defineProps({
|
||||
phone_types: [],
|
||||
},
|
||||
},
|
||||
// Enable sending SMS buttons (only pass true for ClientCase person context)
|
||||
enableSms: { type: Boolean, default: false },
|
||||
// Required when enableSms=true to scope route correctly
|
||||
clientCaseUuid: { type: String, default: null },
|
||||
// Optional overrides; if not provided, falls back to Inertia page props
|
||||
smsProfiles: { type: Array, default: () => [] },
|
||||
smsSenders: { type: Array, default: () => [] },
|
||||
smsTemplates: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const drawerUpdatePerson = ref(false);
|
||||
@@ -187,6 +197,123 @@ const getTRRs = (p) => {
|
||||
if (Array.isArray(p?.bankAccounts)) return p.bankAccounts;
|
||||
return [];
|
||||
};
|
||||
|
||||
// SMS dialog state and actions (ClientCase person only)
|
||||
const showSmsDialog = ref(false);
|
||||
const smsTargetPhone = ref(null);
|
||||
const smsMessage = ref("");
|
||||
const smsSending = ref(false);
|
||||
|
||||
// Page-level props fallback for SMS metadata
|
||||
const page = usePage();
|
||||
// In Inertia Vue 3, page.props is already a reactive object (not a Ref),
|
||||
// so do NOT access .value here; otherwise, you'll get undefined and empty lists.
|
||||
const pageProps = computed(() => page?.props ?? {});
|
||||
const pageSmsProfiles = computed(() => {
|
||||
const fromProps =
|
||||
Array.isArray(props.smsProfiles) && props.smsProfiles.length
|
||||
? props.smsProfiles
|
||||
: null;
|
||||
return fromProps ?? pageProps.value?.sms_profiles ?? [];
|
||||
});
|
||||
const pageSmsSenders = computed(() => {
|
||||
const fromProps =
|
||||
Array.isArray(props.smsSenders) && props.smsSenders.length ? props.smsSenders : null;
|
||||
return fromProps ?? pageProps.value?.sms_senders ?? [];
|
||||
});
|
||||
const pageSmsTemplates = computed(() => {
|
||||
const fromProps =
|
||||
Array.isArray(props.smsTemplates) && props.smsTemplates.length
|
||||
? props.smsTemplates
|
||||
: null;
|
||||
return fromProps ?? pageProps.value?.sms_templates ?? [];
|
||||
});
|
||||
|
||||
// Selections
|
||||
const selectedProfileId = ref(null);
|
||||
const selectedSenderId = ref(null);
|
||||
const deliveryReport = ref(false);
|
||||
const selectedTemplateId = ref(null);
|
||||
|
||||
const sendersForSelectedProfile = computed(() => {
|
||||
if (!selectedProfileId.value) return pageSmsSenders.value;
|
||||
return (pageSmsSenders.value || []).filter(
|
||||
(s) => s.profile_id === selectedProfileId.value
|
||||
);
|
||||
});
|
||||
|
||||
watch(selectedProfileId, () => {
|
||||
// Clear sender selection if it doesn't belong to the chosen profile
|
||||
if (!selectedSenderId.value) return;
|
||||
const ok = sendersForSelectedProfile.value.some((s) => s.id === selectedSenderId.value);
|
||||
if (!ok) selectedSenderId.value = null;
|
||||
});
|
||||
|
||||
watch(selectedTemplateId, () => {
|
||||
if (!selectedTemplateId.value) return;
|
||||
const tpl = (pageSmsTemplates.value || []).find(
|
||||
(t) => t.id === selectedTemplateId.value
|
||||
);
|
||||
if (tpl && typeof tpl.content === "string") {
|
||||
smsMessage.value = tpl.content;
|
||||
}
|
||||
});
|
||||
|
||||
const openSmsDialog = (phone) => {
|
||||
if (!props.enableSms || !props.clientCaseUuid) return;
|
||||
smsTargetPhone.value = phone;
|
||||
smsMessage.value = "";
|
||||
showSmsDialog.value = true;
|
||||
// Defaults
|
||||
selectedProfileId.value =
|
||||
(pageSmsProfiles.value && pageSmsProfiles.value[0]?.id) || null;
|
||||
// If profile has default sender, try to preselect it
|
||||
if (selectedProfileId.value) {
|
||||
const prof = (pageSmsProfiles.value || []).find(
|
||||
(p) => p.id === selectedProfileId.value
|
||||
);
|
||||
if (prof && prof.default_sender_id) {
|
||||
selectedSenderId.value = prof.default_sender_id;
|
||||
} else {
|
||||
selectedSenderId.value = null;
|
||||
}
|
||||
} else {
|
||||
selectedSenderId.value = null;
|
||||
}
|
||||
deliveryReport.value = false;
|
||||
selectedTemplateId.value = null;
|
||||
};
|
||||
const closeSmsDialog = () => {
|
||||
showSmsDialog.value = false;
|
||||
smsTargetPhone.value = null;
|
||||
smsMessage.value = "";
|
||||
};
|
||||
const submitSms = () => {
|
||||
if (!smsTargetPhone.value || !smsMessage.value || !props.clientCaseUuid) {
|
||||
return;
|
||||
}
|
||||
smsSending.value = true;
|
||||
router.post(
|
||||
route("clientCase.phone.sms", {
|
||||
client_case: props.clientCaseUuid,
|
||||
phone_id: smsTargetPhone.value.id,
|
||||
}),
|
||||
{
|
||||
message: smsMessage.value,
|
||||
template_id: selectedTemplateId.value,
|
||||
profile_id: selectedProfileId.value,
|
||||
sender_id: selectedSenderId.value,
|
||||
delivery_report: !!deliveryReport.value,
|
||||
},
|
||||
{
|
||||
preserveScroll: true,
|
||||
onFinish: () => {
|
||||
smsSending.value = false;
|
||||
closeSmsDialog();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -303,9 +430,20 @@ const getTRRs = (p) => {
|
||||
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between">
|
||||
<div class="flex">
|
||||
<FwbBadge title type="yellow">+{{ phone.country_code }}</FwbBadge>
|
||||
<FwbBadge>{{ phone.type.name }}</FwbBadge>
|
||||
<FwbBadge>{{
|
||||
phone && phone.type && phone.type.name ? phone.type.name : "—"
|
||||
}}</FwbBadge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Send SMS only in ClientCase person context -->
|
||||
<button
|
||||
v-if="enableSms && clientCaseUuid"
|
||||
@click="openSmsDialog(phone)"
|
||||
title="Pošlji SMS"
|
||||
class="text-indigo-600 hover:text-indigo-800 text-xs border border-indigo-200 px-2 py-0.5 rounded"
|
||||
>
|
||||
SMS
|
||||
</button>
|
||||
<button>
|
||||
<EditIcon
|
||||
@click="operDrawerAddPhone(true, phone.id)"
|
||||
@@ -510,4 +648,89 @@ const getTRRs = (p) => {
|
||||
@close="closeConfirm"
|
||||
@confirm="onConfirmDelete"
|
||||
/>
|
||||
|
||||
<!-- Send SMS dialog -->
|
||||
<DialogModal :show="showSmsDialog" @close="closeSmsDialog">
|
||||
<template #title>Pošlji SMS</template>
|
||||
<template #content>
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-gray-600">
|
||||
Prejemnik: <span class="font-mono">{{ smsTargetPhone?.nu }}</span>
|
||||
<span v-if="smsTargetPhone?.country_code" class="ml-2 text-xs text-gray-500"
|
||||
>CC +{{ smsTargetPhone.country_code }}</span
|
||||
>
|
||||
</p>
|
||||
<!-- Profile & Sender selectors -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Profil</label>
|
||||
<select
|
||||
v-model="selectedProfileId"
|
||||
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="p in pageSmsProfiles" :key="p.id" :value="p.id">
|
||||
{{ p.name || "Profil #" + p.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Pošiljatelj</label>
|
||||
<select
|
||||
v-model="selectedSenderId"
|
||||
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="s in sendersForSelectedProfile" :key="s.id" :value="s.id">
|
||||
{{ s.name || s.phone || "Sender #" + s.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template selector -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Predloga</label>
|
||||
<select
|
||||
v-model="selectedTemplateId"
|
||||
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option :value="null">—</option>
|
||||
<option v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
|
||||
{{ t.name || "Predloga #" + t.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="block text-sm font-medium text-gray-700">Vsebina sporočila</label>
|
||||
<textarea
|
||||
v-model="smsMessage"
|
||||
rows="4"
|
||||
class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="Vpišite SMS vsebino..."
|
||||
></textarea>
|
||||
|
||||
<label class="inline-flex items-center gap-2 text-sm text-gray-700 mt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="deliveryReport"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Zahtevaj poročilo o dostavi
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="px-3 py-1 rounded border mr-2" @click="closeSmsDialog">
|
||||
Prekliči
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1 rounded bg-indigo-600 text-white disabled:opacity-50"
|
||||
:disabled="smsSending || !smsMessage"
|
||||
@click="submitSms"
|
||||
>
|
||||
Pošlji
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
faAt,
|
||||
faInbox,
|
||||
faFileLines,
|
||||
faMessage,
|
||||
faAddressBook,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import DropdownLink from "@/Components/DropdownLink.vue";
|
||||
@@ -134,6 +136,44 @@ const navGroups = computed(() => [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "sms",
|
||||
label: "SMS",
|
||||
items: [
|
||||
{
|
||||
key: "admin.sms-templates.index",
|
||||
label: "SMS predloge",
|
||||
route: "admin.sms-templates.index",
|
||||
icon: faFileLines,
|
||||
active: [
|
||||
"admin.sms-templates.index",
|
||||
"admin.sms-templates.create",
|
||||
"admin.sms-templates.edit",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-logs.index",
|
||||
label: "SMS dnevniki",
|
||||
route: "admin.sms-logs.index",
|
||||
icon: faInbox,
|
||||
active: ["admin.sms-logs.index", "admin.sms-logs.show"],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-senders.index",
|
||||
label: "SMS pošiljatelji",
|
||||
route: "admin.sms-senders.index",
|
||||
icon: faAddressBook,
|
||||
active: ["admin.sms-senders.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-profiles.index",
|
||||
label: "SMS profili",
|
||||
route: "admin.sms-profiles.index",
|
||||
icon: faGears,
|
||||
active: ["admin.sms-profiles.index"],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function isActive(patterns) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { faUserGroup, faKey, faGears, faFileWord, faEnvelopeOpenText, faInbox, faAt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faUserGroup, faKey, faGears, faFileWord, faEnvelopeOpenText, faInbox, faAt, faAddressBook, faFileLines } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const cards = [
|
||||
{
|
||||
@@ -62,6 +62,35 @@ const cards = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Komunikacije',
|
||||
items: [
|
||||
{
|
||||
title: 'SMS profili',
|
||||
description: 'Nastavitve SMS profilov, testno pošiljanje in stanje kreditov',
|
||||
route: 'admin.sms-profiles.index',
|
||||
icon: faGears,
|
||||
},
|
||||
{
|
||||
title: 'SMS pošiljatelji',
|
||||
description: 'Upravljanje nazivov pošiljateljev (Sender ID) za SMS profile',
|
||||
route: 'admin.sms-senders.index',
|
||||
icon: faAddressBook,
|
||||
},
|
||||
{
|
||||
title: 'SMS predloge',
|
||||
description: 'Tekstovne predloge za SMS obvestila in opomnike',
|
||||
route: 'admin.sms-templates.index',
|
||||
icon: faFileLines,
|
||||
},
|
||||
{
|
||||
title: 'SMS dnevniki',
|
||||
description: 'Pregled poslanih SMSov in statusov',
|
||||
route: 'admin.sms-logs.index',
|
||||
icon: faInbox,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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"> </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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,292 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { Head, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, watch } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPaperPlane, faCoins, faTags, faFlask } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
initialProfiles: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const profiles = ref(props.initialProfiles || []);
|
||||
|
||||
// Keep local ref in sync with Inertia prop changes (after router navigations)
|
||||
watch(
|
||||
() => props.initialProfiles,
|
||||
(val) => {
|
||||
profiles.value = val || [];
|
||||
}
|
||||
);
|
||||
|
||||
// Create profile modal
|
||||
const createOpen = ref(false);
|
||||
const createForm = useForm({
|
||||
name: "",
|
||||
active: true,
|
||||
api_username: "",
|
||||
api_password: "",
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
createForm.reset();
|
||||
createForm.active = true;
|
||||
createOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
try {
|
||||
createForm.processing = true;
|
||||
const payload = {
|
||||
name: createForm.name,
|
||||
active: !!createForm.active,
|
||||
api_username: createForm.api_username,
|
||||
api_password: createForm.api_password,
|
||||
};
|
||||
await router.post(route("admin.sms-profiles.store"), payload, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
createOpen.value = false;
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
createForm.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test send modal
|
||||
const testOpen = ref(false);
|
||||
const testTarget = ref(null);
|
||||
const testResult = ref(null);
|
||||
const testForm = useForm({
|
||||
to: "",
|
||||
message: "",
|
||||
sender_id: null,
|
||||
delivery_report: true,
|
||||
country_code: null,
|
||||
});
|
||||
|
||||
function openTest(p) {
|
||||
testForm.reset();
|
||||
testTarget.value = p;
|
||||
testResult.value = null;
|
||||
testOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitTest() {
|
||||
if (!testTarget.value) return;
|
||||
try {
|
||||
testForm.processing = true;
|
||||
const payload = {
|
||||
to: testForm.to,
|
||||
message: testForm.message,
|
||||
sender_id: testForm.sender_id,
|
||||
delivery_report: !!testForm.delivery_report,
|
||||
country_code: testForm.country_code,
|
||||
};
|
||||
await router.post(route("admin.sms-profiles.test-send", testTarget.value.id), payload, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testResult.value = null;
|
||||
testOpen.value = false;
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
testForm.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Balance & Price
|
||||
const balances = ref({});
|
||||
const quotes = ref({});
|
||||
|
||||
function fetchBalance(p) {
|
||||
window.axios.post(route("admin.sms-profiles.balance", p.id)).then((r) => {
|
||||
balances.value[p.id] = r.data.balance;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchPrice(p) {
|
||||
window.axios.post(route("admin.sms-profiles.price", p.id)).then((r) => {
|
||||
quotes.value[p.id] = r.data.quotes || [];
|
||||
});
|
||||
}
|
||||
|
||||
const formatDateTime = (s) => (s ? new Date(s).toLocaleString() : "—");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="SMS profili">
|
||||
<Head title="SMS profili" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800 flex items-center gap-3">
|
||||
SMS 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">Uporabnik</th>
|
||||
<th class="px-3 py-2">Aktiven</th>
|
||||
<th class="px-3 py-2">Pošiljatelji</th>
|
||||
<th class="px-3 py-2">Bilanca</th>
|
||||
<th class="px-3 py-2">Cena</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.api_username }}</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="p.active ? 'text-emerald-600' : 'text-rose-600'">{{ p.active ? 'Da' : 'Ne' }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-600">
|
||||
<span v-if="(p.senders||[]).length === 0">—</span>
|
||||
<span v-else>
|
||||
{{ p.senders.map(s => s.sname).join(', ') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="fetchBalance(p)" 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="faCoins" class="w-3.5 h-3.5" /> Pridobi
|
||||
</button>
|
||||
<span class="text-xs text-gray-600">{{ balances[p.id] ?? '—' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="fetchPrice(p)" 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="faTags" class="w-3.5 h-3.5" /> Cene
|
||||
</button>
|
||||
<span class="text-xs text-gray-600 truncate max-w-[200px]" :title="(quotes[p.id]||[]).join(', ')">
|
||||
{{ (quotes[p.id] || []).join(', ') || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<button
|
||||
@click="openTest(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 test SMS"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Test SMS
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create Profile Modal -->
|
||||
<DialogModal :show="createOpen" max-width="2xl" @close="() => (createOpen = false)">
|
||||
<template #title> Nov SMS profil </template>
|
||||
<template #content>
|
||||
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-5">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="createForm.name" type="text" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Aktivno</label>
|
||||
<select v-model="createForm.active" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">API uporabnik</label>
|
||||
<input v-model="createForm.api_username" type="text" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">API geslo</label>
|
||||
<input v-model="createForm.api_password" type="password" class="input" autocomplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" @click="() => (createOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Prekliči</button>
|
||||
<button form="create-sms-profile" type="submit" :disabled="createForm.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>
|
||||
|
||||
<!-- Test Send Modal -->
|
||||
<DialogModal :show="testOpen" max-width="2xl" @close="() => (testOpen = false)">
|
||||
<template #title> Testni SMS </template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input v-model="testForm.to" type="text" class="input" placeholder="+386..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda (opcijsko)</label>
|
||||
<input v-model="testForm.country_code" type="text" class="input" placeholder="SI" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="label">Sporočilo</label>
|
||||
<textarea v-model="testForm.message" class="input" rows="4"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result details removed; rely on flash message after redirect -->
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" @click="() => (testOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Zapri</button>
|
||||
<button type="button" @click="submitTest" :disabled="testForm.processing || !testTarget" class="px-4 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50">
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5 mr-1" /> Pošlji test
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</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>
|
||||
@@ -0,0 +1,199 @@
|
||||
<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";
|
||||
|
||||
const props = defineProps({
|
||||
initialSenders: { type: Array, default: () => [] },
|
||||
profiles: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
// 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])));
|
||||
|
||||
|
||||
// Create/Edit modal
|
||||
const editOpen = ref(false);
|
||||
const editing = ref(null);
|
||||
const form = useForm({
|
||||
profile_id: null,
|
||||
sname: "",
|
||||
phone_number: "",
|
||||
description: "",
|
||||
active: true,
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
form.reset();
|
||||
form.active = true;
|
||||
editOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(s) {
|
||||
editing.value = s;
|
||||
form.reset();
|
||||
form.profile_id = s.profile_id;
|
||||
form.sname = s.sname;
|
||||
form.phone_number = s.phone_number || "";
|
||||
form.description = s.description;
|
||||
form.active = !!s.active;
|
||||
editOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
try {
|
||||
form.processing = true;
|
||||
const payload = {
|
||||
profile_id: form.profile_id,
|
||||
sname: (form.sname || "").trim() || null,
|
||||
phone_number: form.phone_number || null,
|
||||
description: form.description || null,
|
||||
active: !!form.active,
|
||||
};
|
||||
if (editing.value) {
|
||||
await router.put(route("admin.sms-senders.update", editing.value.id), payload, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
} else {
|
||||
await router.post(route("admin.sms-senders.store"), payload, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
editOpen.value = false;
|
||||
} finally {
|
||||
form.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActive(s) {
|
||||
await router.post(route("admin.sms-senders.toggle", s.id), {}, { preserveScroll: true });
|
||||
}
|
||||
|
||||
async function destroySender(s) {
|
||||
if (!confirm(`Izbrišem pošiljatelja "${s.sname}"?`)) return;
|
||||
await router.delete(route("admin.sms-senders.destroy", s.id), { preserveScroll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<div>
|
||||
<label class="label">Pošiljatelj (Sender ID) — opcijsko</label>
|
||||
<input v-model="form.sname" type="text" class="input" 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>
|
||||
<div class="col-span-2">
|
||||
<label class="label">Opis (opcijsko)</label>
|
||||
<input v-model="form.description" type="text" class="input" />
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
@@ -0,0 +1,263 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { Head, Link, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, computed, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
template: { type: Object, default: null },
|
||||
profiles: { type: Array, default: () => [] },
|
||||
senders: { type: Array, default: () => [] },
|
||||
actions: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: props.template?.name ?? "",
|
||||
slug: props.template?.slug ?? "",
|
||||
content: props.template?.content ?? "",
|
||||
variables_json: props.template?.variables_json ?? {},
|
||||
is_active: props.template?.is_active ?? true,
|
||||
default_profile_id: props.template?.default_profile_id ?? null,
|
||||
default_sender_id: props.template?.default_sender_id ?? null,
|
||||
allow_custom_body: props.template?.allow_custom_body ?? false,
|
||||
action_id: props.template?.action_id ?? null,
|
||||
decision_id: props.template?.decision_id ?? null,
|
||||
});
|
||||
|
||||
const sendersByProfile = computed(() => {
|
||||
const map = {};
|
||||
(props.senders || []).forEach((s) => {
|
||||
if (!map[s.profile_id]) map[s.profile_id] = [];
|
||||
map[s.profile_id].push(s);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
function currentSendersForProfile(pid) {
|
||||
if (!pid) return [];
|
||||
return (sendersByProfile.value[pid] || []).filter((s) => s.active);
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (props.template?.id) {
|
||||
form.put(route("admin.sms-templates.update", props.template.id));
|
||||
} else {
|
||||
form.post(route("admin.sms-templates.store"), {
|
||||
onSuccess: (page) => {
|
||||
// If backend redirected, nothing to do. If returned JSON, navigate to edit.
|
||||
try {
|
||||
const id = page?.props?.template?.id; // in case of redirect props
|
||||
if (id) {
|
||||
router.visit(route("admin.sms-templates.edit", id));
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
onFinish: () => {},
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Test send panel
|
||||
const testForm = useForm({
|
||||
to: "",
|
||||
variables: {},
|
||||
profile_id: props.template?.default_profile_id ?? null,
|
||||
sender_id: props.template?.default_sender_id ?? null,
|
||||
country_code: null,
|
||||
delivery_report: true,
|
||||
custom_content: "",
|
||||
});
|
||||
const testResult = ref(null);
|
||||
|
||||
watch(
|
||||
() => testForm.profile_id,
|
||||
() => {
|
||||
// reset sender when profile changes
|
||||
testForm.sender_id = null;
|
||||
}
|
||||
);
|
||||
|
||||
async function submitTest() {
|
||||
if (!props.template?.id) {
|
||||
alert("Najprej shranite predlogo.");
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
to: testForm.to,
|
||||
variables: testForm.variables || {},
|
||||
profile_id: testForm.profile_id || null,
|
||||
sender_id: testForm.sender_id || null,
|
||||
country_code: testForm.country_code || null,
|
||||
delivery_report: !!testForm.delivery_report,
|
||||
custom_content: testForm.custom_content || null,
|
||||
};
|
||||
await router.post(route("admin.sms-templates.send-test", props.template.id), payload, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testResult.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout :title="props.template ? 'Uredi SMS predlogo' : 'Nova SMS predloga'">
|
||||
<Head :title="props.template ? 'Uredi SMS predlogo' : 'Nova SMS predloga'" />
|
||||
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('admin.sms-templates.index')" class="text-sm text-indigo-600 hover:underline">← Nazaj na seznam</Link>
|
||||
<div class="text-gray-700 text-sm" v-if="props.template">#{{ props.template.id }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="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>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Form -->
|
||||
<div class="lg:col-span-2 rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-5 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Ime</label>
|
||||
<input v-model="form.name" type="text" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Slug</label>
|
||||
<input v-model="form.slug" type="text" class="input" placeholder="npr. payment_reminder" />
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="label">Vsebina</label>
|
||||
<textarea v-model="form.content" rows="8" class="input" placeholder="Pozdravljen {{ person.first_name }}, ..."></textarea>
|
||||
<div class="text-[11px] text-gray-500 mt-1">
|
||||
Uporabite placeholderje npr. <code>{first_name}</code> ali
|
||||
<code v-pre>{{ person.first_name }}</code> – ob pošiljanju se vrednosti nadomestijo.
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="label">Dovoli lastno besedilo</label>
|
||||
<select v-model="form.allow_custom_body" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
<div class="text-[11px] text-gray-500 mt-1">Če je omogočeno, lahko pošiljatelj namesto vsebine predloge napiše poljubno besedilo.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Privzet profil</label>
|
||||
<select v-model="form.default_profile_id" class="input">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="p in props.profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Privzet sender</label>
|
||||
<select v-model="form.default_sender_id" class="input" :disabled="!form.default_profile_id">
|
||||
<option :value="null">—</option>
|
||||
<option v-for="s in currentSendersForProfile(form.default_profile_id)" :key="s.id" :value="s.id">
|
||||
{{ s.sname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Aktivno</label>
|
||||
<select v-model="form.is_active" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Akcija po pošiljanju</label>
|
||||
<select v-model="form.action_id" class="input">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="a in props.actions" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Odločitev</label>
|
||||
<select v-model="form.decision_id" class="input" :disabled="!form.action_id">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="d in (props.actions.find(x => x.id === form.action_id)?.decisions || [])" :key="d.id" :value="d.id">{{ d.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test send -->
|
||||
<div class="rounded-xl border bg-white/60 backdrop-blur-sm shadow-sm p-5 space-y-4">
|
||||
<div class="font-semibold text-gray-800">Testno pošiljanje</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input v-model="testForm.to" type="text" class="input" placeholder="+386..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda</label>
|
||||
<input v-model="testForm.country_code" type="text" class="input" placeholder="SI" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="testForm.profile_id" class="input">
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="p in props.profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Sender</label>
|
||||
<select v-model="testForm.sender_id" class="input" :disabled="!testForm.profile_id">
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="s in currentSendersForProfile(testForm.profile_id)" :key="s.id" :value="s.id">{{ s.sname }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="form.allow_custom_body" class="md:col-span-2">
|
||||
<label class="label">Lastno besedilo (opcijsko)</label>
|
||||
<textarea v-model="testForm.custom_content" rows="4" class="input" placeholder="Če je izpolnjeno, bo namesto predloge poslano to besedilo."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button type="button" @click="submitTest" :disabled="testForm.processing || !props.template" class="px-3 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50">Pošlji test</button>
|
||||
</div>
|
||||
<!-- Result details removed in favor of flash messages after redirect -->
|
||||
</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>
|
||||
@@ -0,0 +1,332 @@
|
||||
<script setup>
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { Head, useForm, Link, router } from "@inertiajs/vue3";
|
||||
import { computed, ref } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faPlus, faPen, faTrash, faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
initialTemplates: { type: Array, default: () => [] },
|
||||
profiles: { type: Array, default: () => [] },
|
||||
senders: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const templates = computed(() => props.initialTemplates || []);
|
||||
const profilesById = computed(() =>
|
||||
Object.fromEntries((props.profiles || []).map((p) => [p.id, p]))
|
||||
);
|
||||
const sendersById = computed(() =>
|
||||
Object.fromEntries((props.senders || []).map((s) => [s.id, s]))
|
||||
);
|
||||
const sendersByProfile = computed(() => {
|
||||
const map = {};
|
||||
(props.senders || []).forEach((s) => {
|
||||
if (!map[s.profile_id]) map[s.profile_id] = [];
|
||||
map[s.profile_id].push(s);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
// No manual reload; Inertia visits will refresh props
|
||||
|
||||
// Create/Edit modal
|
||||
const editOpen = ref(false);
|
||||
const editing = ref(null);
|
||||
const form = useForm({
|
||||
name: "",
|
||||
slug: "",
|
||||
content: "",
|
||||
variables_json: {},
|
||||
is_active: true,
|
||||
default_profile_id: null,
|
||||
default_sender_id: null,
|
||||
});
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
form.reset();
|
||||
form.is_active = true;
|
||||
editOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(t) {
|
||||
editing.value = t;
|
||||
form.reset();
|
||||
form.name = t.name;
|
||||
form.slug = t.slug;
|
||||
form.content = t.content;
|
||||
form.variables_json = t.variables_json || {};
|
||||
form.is_active = !!t.is_active;
|
||||
form.default_profile_id = t.default_profile_id || null;
|
||||
form.default_sender_id = t.default_sender_id || null;
|
||||
editOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
try {
|
||||
form.processing = true;
|
||||
const payload = {
|
||||
name: form.name,
|
||||
slug: form.slug,
|
||||
content: form.content,
|
||||
variables_json: form.variables_json || {},
|
||||
is_active: !!form.is_active,
|
||||
default_profile_id: form.default_profile_id || null,
|
||||
default_sender_id: form.default_sender_id || null,
|
||||
};
|
||||
if (editing.value) {
|
||||
await router.put(route("admin.sms-templates.update", editing.value.id), payload, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
} else {
|
||||
await router.post(route("admin.sms-templates.store"), payload, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
editOpen.value = false;
|
||||
} finally {
|
||||
form.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function destroyTemplate(t) {
|
||||
if (!confirm(`Izbrišem SMS predlogo "${t.name}"?`)) return;
|
||||
await router.delete(route("admin.sms-templates.destroy", t.id), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Test send modal
|
||||
const testOpen = ref(false);
|
||||
const testTarget = ref(null);
|
||||
const testForm = useForm({
|
||||
to: "",
|
||||
variables: {},
|
||||
profile_id: null,
|
||||
sender_id: null,
|
||||
country_code: null,
|
||||
delivery_report: true,
|
||||
});
|
||||
const testResult = ref(null);
|
||||
|
||||
function openTest(t) {
|
||||
testTarget.value = t;
|
||||
testForm.reset();
|
||||
testForm.delivery_report = true;
|
||||
testForm.profile_id = t.default_profile_id || null;
|
||||
testForm.sender_id = t.default_sender_id || null;
|
||||
testResult.value = null;
|
||||
testOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitTest() {
|
||||
if (!testTarget.value) return;
|
||||
const payload = {
|
||||
to: testForm.to,
|
||||
variables: testForm.variables || {},
|
||||
profile_id: testForm.profile_id || null,
|
||||
sender_id: testForm.sender_id || null,
|
||||
country_code: testForm.country_code || null,
|
||||
delivery_report: !!testForm.delivery_report,
|
||||
};
|
||||
await router.post(
|
||||
route("admin.sms-templates.send-test", testTarget.value.id),
|
||||
payload,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
testOpen.value = false;
|
||||
testResult.value = null;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function currentSenders() {
|
||||
const pid = form.default_profile_id;
|
||||
if (!pid) return [];
|
||||
return (sendersByProfile.value[pid] || []).filter((s) => s.active);
|
||||
}
|
||||
|
||||
function currentSendersForTest() {
|
||||
const pid = testForm.profile_id;
|
||||
if (!pid) return [];
|
||||
return (sendersByProfile.value[pid] || []).filter((s) => s.active);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminLayout title="SMS predloge">
|
||||
<Head title="SMS predloge" />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-semibold text-gray-800">SMS predloge</h1>
|
||||
<Link
|
||||
:href="route('admin.sms-templates.create')"
|
||||
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" /> Nova predloga
|
||||
</Link>
|
||||
</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">Slug</th>
|
||||
<th class="px-3 py-2 text-left">Privzet profil/sender</th>
|
||||
<th class="px-3 py-2">Aktivno</th>
|
||||
<th class="px-3 py-2">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="t in templates"
|
||||
:key="t.id"
|
||||
class="border-t last:border-b hover:bg-gray-50"
|
||||
>
|
||||
<td class="px-3 py-2 font-medium text-gray-800">{{ t.name }}</td>
|
||||
<td class="px-3 py-2 text-gray-600">{{ t.slug }}</td>
|
||||
<td class="px-3 py-2 text-gray-600">
|
||||
<span>{{ profilesById[t.default_profile_id]?.name || "—" }}</span>
|
||||
<span v-if="t.default_sender_id">
|
||||
/ {{ sendersById[t.default_sender_id]?.sname }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span :class="t.is_active ? 'text-emerald-600' : 'text-rose-600'">{{
|
||||
t.is_active ? "Da" : "Ne"
|
||||
}}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 flex items-center gap-2">
|
||||
<Link
|
||||
:href="route('admin.sms-templates.edit', t.id)"
|
||||
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
|
||||
</Link>
|
||||
<button
|
||||
@click="openTest(t)"
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5" /> Test
|
||||
</button>
|
||||
<button
|
||||
@click="destroyTemplate(t)"
|
||||
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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Edit/Create now handled on dedicated page -->
|
||||
|
||||
<!-- Test Send Modal -->
|
||||
<DialogModal :show="testOpen" max-width="2xl" @close="() => (testOpen = false)">
|
||||
<template #title> Testno pošiljanje </template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="label">Prejemnik (E.164)</label>
|
||||
<input
|
||||
v-model="testForm.to"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="+386..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Državna koda</label>
|
||||
<input
|
||||
v-model="testForm.country_code"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="SI"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Profil</label>
|
||||
<select v-model="testForm.profile_id" class="input">
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="p in props.profiles" :key="p.id" :value="p.id">
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Sender</label>
|
||||
<select
|
||||
v-model="testForm.sender_id"
|
||||
class="input"
|
||||
:disabled="!testForm.profile_id"
|
||||
>
|
||||
<option :value="null">(privzeti)</option>
|
||||
<option v-for="s in currentSendersForTest()" :key="s.id" :value="s.id">
|
||||
{{ s.sname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Dostavna poročila</label>
|
||||
<select v-model="testForm.delivery_report" class="input">
|
||||
<option :value="true">Da</option>
|
||||
<option :value="false">Ne</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result details removed in favor of flash messages after redirect -->
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
@click="() => (testOpen = false)"
|
||||
class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50"
|
||||
>
|
||||
Zapri
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitTest"
|
||||
:disabled="testForm.processing || !testTarget"
|
||||
class="px-4 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" class="w-3.5 h-3.5 mr-1" /> Pošlji test
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</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>
|
||||
@@ -274,6 +274,8 @@ const submitAttachSegment = () => {
|
||||
:types="types"
|
||||
tab-color="red-600"
|
||||
:person="client_case.person"
|
||||
:enable-sms="true"
|
||||
:client-case-uuid="client_case.uuid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ const props = defineProps({
|
||||
fieldJobsAssignedToday: Array,
|
||||
importsInProgress: Array,
|
||||
activeTemplates: Array,
|
||||
smsStats: Array,
|
||||
});
|
||||
|
||||
const kpiDefs = [
|
||||
@@ -318,6 +319,63 @@ function safeCaseHref(uuid, segment = null) {
|
||||
|
||||
<!-- Right side panels -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- SMS Overview -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
|
||||
>
|
||||
SMS stanje
|
||||
</h3>
|
||||
<div v-if="props.smsStats?.length" class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead
|
||||
class="bg-gray-50 dark:bg-gray-900/30 text-gray-600 dark:text-gray-300 text-xs uppercase tracking-wider"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Profil</th>
|
||||
<th class="px-3 py-2 text-left">Bilanca</th>
|
||||
<th class="px-3 py-2 text-left">Danes (skupaj)</th>
|
||||
<th class="px-3 py-2 text-left">Sent</th>
|
||||
<th class="px-3 py-2 text-left">Delivered</th>
|
||||
<th class="px-3 py-2 text-left">Failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="p in props.smsStats"
|
||||
:key="p.id"
|
||||
class="border-t last:border-b dark:border-gray-700"
|
||||
>
|
||||
<td class="px-3 py-2">
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{
|
||||
p.name
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-2 text-[11px]"
|
||||
:class="p.active ? 'text-emerald-600' : 'text-gray-400'"
|
||||
>{{ p.active ? "Aktiven" : "Neaktiven" }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-700 dark:text-gray-300">
|
||||
{{ p.balance ?? "—" }}
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ p.today?.total ?? 0 }}</td>
|
||||
<td class="px-3 py-2 text-sky-700">{{ p.today?.sent ?? 0 }}</td>
|
||||
<td class="px-3 py-2 text-emerald-700">
|
||||
{{ p.today?.delivered ?? 0 }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-rose-700">{{ p.today?.failed ?? 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Ni podatkov o SMS.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Health -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
|
||||
|
||||
@@ -44,8 +44,9 @@ const clientOptions = computed(() => {
|
||||
? props.clients
|
||||
: (Array.isArray(props.activities?.data) ? props.activities.data : [])
|
||||
.map((row) => {
|
||||
const cc = row.contract?.client_case || row.client_case;
|
||||
return cc ? { value: cc.uuid, label: cc.person?.full_name || "(neznana stranka)" } : null;
|
||||
const client = row.contract?.client_case?.client || row.client_case?.client;
|
||||
if (!client?.uuid) return null;
|
||||
return { value: client.uuid, label: client.person?.full_name || "(neznana stranka)" };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reduce((acc, cur) => {
|
||||
@@ -174,8 +175,8 @@ async function markRead(id) {
|
||||
<template #cell-partner="{ row }">
|
||||
<div class="truncate">
|
||||
{{
|
||||
(row.contract?.client_case?.person?.full_name) ||
|
||||
(row.client_case?.person?.full_name) ||
|
||||
(row.contract?.client_case?.client?.person?.full_name) ||
|
||||
(row.client_case?.client?.person?.full_name) ||
|
||||
'—'
|
||||
}}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user