Added call later, option to limit auto mail so for a client person email you can limit which decision activity will be send to that specific email and moved SMS packages from admin panel to default app view
This commit is contained in:
@@ -0,0 +1,810 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, router, useForm } from "@inertiajs/vue3";
|
||||
import { ref, computed, nextTick } from "vue";
|
||||
import axios from "axios";
|
||||
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 { Textarea } from "@/Components/ui/textarea";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
|
||||
import {
|
||||
PackageIcon,
|
||||
PhoneIcon,
|
||||
UsersIcon,
|
||||
SearchIcon,
|
||||
SaveIcon,
|
||||
ArrowLeftIcon,
|
||||
FilterIcon,
|
||||
CalendarIcon,
|
||||
CheckCircle2Icon,
|
||||
XCircleIcon,
|
||||
BadgeCheckIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { fmtDateDMY } from "@/Utilities/functions";
|
||||
import { upperFirst } from "lodash";
|
||||
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
|
||||
import AppRangeDatePicker from "@/Components/app/ui/AppRangeDatePicker.vue";
|
||||
|
||||
const props = defineProps({
|
||||
profiles: { type: Array, default: () => [] },
|
||||
senders: { type: Array, default: () => [] },
|
||||
templates: { type: Array, default: () => [] },
|
||||
segments: { type: Array, default: () => [] },
|
||||
clients: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const creatingFromContracts = ref(false);
|
||||
|
||||
const createMode = ref("numbers"); // 'numbers' | 'contracts'
|
||||
const form = useForm({
|
||||
type: "sms",
|
||||
name: "",
|
||||
description: "",
|
||||
profile_id: null,
|
||||
sender_id: null,
|
||||
template_id: null,
|
||||
delivery_report: false,
|
||||
body: "",
|
||||
numbers: "", // one per line
|
||||
});
|
||||
|
||||
const filteredSenders = computed(() => {
|
||||
if (!form.profile_id) return props.senders;
|
||||
return props.senders.filter((s) => s.profile_id === form.profile_id);
|
||||
});
|
||||
|
||||
function onTemplateChange() {
|
||||
const template = props.templates.find((t) => t.id === form.template_id);
|
||||
if (template?.content) {
|
||||
form.body = template.content;
|
||||
} else {
|
||||
form.body = "";
|
||||
}
|
||||
}
|
||||
|
||||
function submitCreate() {
|
||||
const lines = (form.numbers || "")
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (!lines.length) return;
|
||||
if (!form.profile_id && !form.template_id) {
|
||||
alert("Izberi SMS profil ali predlogo.");
|
||||
return;
|
||||
}
|
||||
if (!form.template_id && !form.body) {
|
||||
alert("Vnesi vsebino sporočila ali izberi predlogo.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: "sms",
|
||||
name: form.name || `SMS paket ${new Date().toLocaleString()}`,
|
||||
description: form.description || "",
|
||||
items: lines.map((number) => ({
|
||||
number,
|
||||
payload: {
|
||||
profile_id: form.profile_id,
|
||||
sender_id: form.sender_id,
|
||||
template_id: form.template_id,
|
||||
delivery_report: !!form.delivery_report,
|
||||
body: form.body && form.body.trim() ? form.body.trim() : null,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
router.post(route("packages.store"), payload, {
|
||||
onSuccess: () => {
|
||||
router.visit(route("packages.index"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Contracts mode state & actions
|
||||
const contracts = ref({
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
});
|
||||
const segmentId = ref(null);
|
||||
const search = ref("");
|
||||
const clientId = ref(null);
|
||||
const startDateRange = ref({ start: null, end: null });
|
||||
const promiseDateRange = ref({ start: null, end: null });
|
||||
const onlyMobile = ref(false);
|
||||
const onlyValidated = ref(false);
|
||||
const loadingContracts = ref(false);
|
||||
|
||||
// Transform clients for AppCombobox
|
||||
const clientItems = computed(() =>
|
||||
props.clients.map((c) => ({
|
||||
value: c.id,
|
||||
label: c.name,
|
||||
}))
|
||||
);
|
||||
const selectedContractIds = ref(new Set());
|
||||
const perPage = ref(25);
|
||||
|
||||
// DataTable columns definition
|
||||
const contractColumns = [
|
||||
{ accessorKey: "reference", header: "Pogodba" },
|
||||
{
|
||||
id: "person",
|
||||
accessorFn: (row) => row.person?.full_name || "—",
|
||||
header: "Primer",
|
||||
},
|
||||
{
|
||||
id: "client",
|
||||
accessorFn: (row) => row.client?.name || "—",
|
||||
header: "Stranka",
|
||||
},
|
||||
{ accessorKey: "start_date", header: "Datum začetka" },
|
||||
{ accessorKey: "promise_date", header: "Zadnja obljuba" },
|
||||
{
|
||||
id: "selected_phone",
|
||||
accessorFn: (row) => row.selected_phone?.number || "—",
|
||||
header: "Izbrana številka",
|
||||
},
|
||||
{
|
||||
id: "segment",
|
||||
accessorFn: (row) => upperFirst(row.segment?.name) || "—",
|
||||
header: "Segment",
|
||||
},
|
||||
{ accessorKey: "no_phone_reason", header: "Opomba" },
|
||||
];
|
||||
|
||||
function onSelectionChange(selectedKeys) {
|
||||
// selectedKeys are indices from the table
|
||||
const newSelection = new Set();
|
||||
selectedKeys.forEach((key) => {
|
||||
const index = parseInt(key);
|
||||
if (contracts.value.data[index]) {
|
||||
newSelection.add(contracts.value.data[index].id);
|
||||
}
|
||||
});
|
||||
selectedContractIds.value = newSelection;
|
||||
}
|
||||
|
||||
async function loadContracts(url = null) {
|
||||
loadingContracts.value = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (segmentId.value) params.append("segment_id", segmentId.value);
|
||||
if (search.value) params.append("q", search.value);
|
||||
if (clientId.value) params.append("client_id", clientId.value);
|
||||
if (startDateRange.value?.start)
|
||||
params.append("start_date_from", startDateRange.value.start);
|
||||
if (startDateRange.value?.end)
|
||||
params.append("start_date_to", startDateRange.value.end);
|
||||
if (promiseDateRange.value?.start)
|
||||
params.append("promise_date_from", promiseDateRange.value.start);
|
||||
if (promiseDateRange.value?.end)
|
||||
params.append("promise_date_to", promiseDateRange.value.end);
|
||||
if (onlyMobile.value) params.append("only_mobile", "1");
|
||||
if (onlyValidated.value) params.append("only_validated", "1");
|
||||
params.append("per_page", perPage.value);
|
||||
|
||||
const target = url || `${route("packages.contracts")}?${params.toString()}`;
|
||||
const { data: json } = await axios.get(target, {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
|
||||
// Wait for next tick before updating to avoid Vue reconciliation issues
|
||||
await nextTick();
|
||||
|
||||
contracts.value = {
|
||||
data: json.data || [],
|
||||
meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
};
|
||||
} finally {
|
||||
loadingContracts.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectContract(id) {
|
||||
const s = selectedContractIds.value;
|
||||
if (s.has(id)) {
|
||||
s.delete(id);
|
||||
} else {
|
||||
s.add(id);
|
||||
}
|
||||
selectedContractIds.value = new Set(Array.from(s));
|
||||
}
|
||||
|
||||
// Get row selection state for DataTable
|
||||
const rowSelection = computed(() => {
|
||||
const selection = {};
|
||||
contracts.value.data.forEach((contract, index) => {
|
||||
if (selectedContractIds.value.has(contract.id)) {
|
||||
selection[index.toString()] = true;
|
||||
}
|
||||
});
|
||||
return selection;
|
||||
});
|
||||
|
||||
// Computed key to force DataTable re-render on page change
|
||||
const tableKey = computed(() => {
|
||||
return `contracts-${contracts.value.meta.current_page}-${contracts.value.data.length}`;
|
||||
});
|
||||
|
||||
function clearSelection() {
|
||||
selectedContractIds.value = new Set();
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
if (page < 1 || page > contracts.value.meta.last_page) return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (segmentId.value) params.append("segment_id", segmentId.value);
|
||||
if (search.value) params.append("q", search.value);
|
||||
if (clientId.value) params.append("client_id", clientId.value);
|
||||
if (startDateRange.value?.start)
|
||||
params.append("start_date_from", startDateRange.value.start);
|
||||
if (startDateRange.value?.end) params.append("start_date_to", startDateRange.value.end);
|
||||
if (promiseDateRange.value?.start)
|
||||
params.append("promise_date_from", promiseDateRange.value.start);
|
||||
if (promiseDateRange.value?.end)
|
||||
params.append("promise_date_to", promiseDateRange.value.end);
|
||||
if (onlyMobile.value) params.append("only_mobile", "1");
|
||||
if (onlyValidated.value) params.append("only_validated", "1");
|
||||
params.append("per_page", perPage.value);
|
||||
params.append("page", page);
|
||||
|
||||
const url = `${route("packages.contracts")}?${params.toString()}`;
|
||||
loadContracts(url);
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
segmentId.value = null;
|
||||
clientId.value = null;
|
||||
search.value = "";
|
||||
startDateRange.value = { start: null, end: null };
|
||||
promiseDateRange.value = { start: null, end: null };
|
||||
onlyMobile.value = false;
|
||||
onlyValidated.value = false;
|
||||
contracts.value = {
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
function submitCreateFromContracts() {
|
||||
const ids = Array.from(selectedContractIds.value);
|
||||
if (!ids.length) return;
|
||||
|
||||
const visibleById = new Map((contracts.value.data || []).map((c) => [c.id, c]));
|
||||
const selectedVisible = ids.map((id) => visibleById.get(id)).filter(Boolean);
|
||||
if (selectedVisible.length && selectedVisible.every((c) => !c?.selected_phone)) {
|
||||
alert("Za izbrane pogodbe ni mogoče najti prejemnikov (telefonov).");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: "sms",
|
||||
name: form.name || `SMS paket (segment) ${new Date().toLocaleString()}`,
|
||||
description: form.description || "",
|
||||
payload: {
|
||||
profile_id: form.profile_id,
|
||||
sender_id: form.sender_id,
|
||||
template_id: form.template_id,
|
||||
delivery_report: !!form.delivery_report,
|
||||
body: form.body && form.body.trim() ? form.body.trim() : null,
|
||||
},
|
||||
contract_ids: ids,
|
||||
};
|
||||
|
||||
creatingFromContracts.value = true;
|
||||
router.post(route("packages.store-from-contracts"), payload, {
|
||||
onSuccess: () => {
|
||||
router.visit(route("packages.index"));
|
||||
},
|
||||
onError: (errors) => {
|
||||
const first = errors && Object.values(errors)[0];
|
||||
if (first) {
|
||||
alert(String(first));
|
||||
}
|
||||
},
|
||||
onFinish: () => {
|
||||
creatingFromContracts.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const numbersCount = computed(() => {
|
||||
return (form.numbers || "")
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean).length;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Ustvari SMS paket">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<Link :href="route('packages.index')">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<PackageIcon class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Ustvari SMS paket</h1>
|
||||
<p class="text-sm text-muted-foreground">Pošlji SMS sporočila v paketu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<Tabs v-model="createMode" class="w-full">
|
||||
<TabsList class="flex flex-row justify-baseline py-4">
|
||||
<TabsTrigger value="numbers" class="p-3">
|
||||
<span class="flex gap-2 items-center align-middle justify-center">
|
||||
<PhoneIcon class="h-5 w-5" />Vnos številk
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="contracts" class="p-3">
|
||||
<span class="flex gap-2 items-center align-middle justify-center">
|
||||
<UsersIcon class="h-5 w-5" />Iz pogodb (segment)
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- Package Details Card -->
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Podatki o paketu</CardTitle>
|
||||
<CardDescription>Osnovne informacije in SMS nastavitve</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Ime paketa</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
placeholder="Npr. SMS kampanja december 2024"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="description">Opis</Label>
|
||||
<Input
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
placeholder="Neobvezen opis paketa"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- SMS Configuration -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold mb-4">SMS nastavitve</h3>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label>SMS profil</Label>
|
||||
<Select v-model="form.profile_id">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Izberi profil" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">—</SelectItem>
|
||||
<SelectItem v-for="p in profiles" :key="p.id" :value="p.id">
|
||||
{{ p.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Pošiljatelj</Label>
|
||||
<Select v-model="form.sender_id">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Izberi pošiljatelja" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">—</SelectItem>
|
||||
<SelectItem v-for="s in filteredSenders" :key="s.id" :value="s.id">
|
||||
{{ s.sname }}
|
||||
<span v-if="s.phone_number" class="text-muted-foreground">
|
||||
({{ s.phone_number }})
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Predloga</Label>
|
||||
<Select v-model="form.template_id" @update:model-value="onTemplateChange">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Izberi predlogo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">—</SelectItem>
|
||||
<SelectItem v-for="t in templates" :key="t.id" :value="t.id">
|
||||
{{ t.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="body">Vsebina sporočila</Label>
|
||||
<Textarea
|
||||
id="body"
|
||||
v-model="form.body"
|
||||
rows="4"
|
||||
placeholder="Vsebina SMS sporočila..."
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
:model-value="form.delivery_report"
|
||||
@update:model-value="(val) => (form.delivery_report = val)"
|
||||
id="delivery-report"
|
||||
:disabled="true"
|
||||
/>
|
||||
<Label for="delivery-report" class="cursor-pointer text-sm">
|
||||
Zahtevaj delivery report
|
||||
</Label>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ form.body?.length || 0 }} znakov
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Numbers Mode -->
|
||||
<TabsContent value="numbers">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Telefonske številke</CardTitle>
|
||||
<CardDescription
|
||||
>Vnesi telefonske številke prejemnikov (ena na vrstico)</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Textarea
|
||||
v-model="form.numbers"
|
||||
rows="10"
|
||||
placeholder="+38640123456 +38640123457 +38641234567"
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
<strong>{{ numbersCount }}</strong>
|
||||
{{
|
||||
numbersCount === 1
|
||||
? "številka"
|
||||
: numbersCount < 5
|
||||
? "številke"
|
||||
: "številk"
|
||||
}}
|
||||
</p>
|
||||
<Badge v-if="numbersCount > 0" variant="secondary">
|
||||
<CheckCircle2Icon class="h-3 w-3 mr-1" />
|
||||
Pripravljeno
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
@click="router.visit(route('packages.index'))"
|
||||
variant="outline"
|
||||
>
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button
|
||||
@click="submitCreate"
|
||||
:disabled="numbersCount === 0 || (!form.profile_id && !form.template_id)"
|
||||
>
|
||||
<SaveIcon class="h-4 w-4 mr-2" />
|
||||
Ustvari paket
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- Contracts Mode -->
|
||||
<TabsContent value="contracts">
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Filtri za pogodbe</CardTitle>
|
||||
<CardDescription
|
||||
>Najdi prejemnike glede na pogodbe in segmente</CardDescription
|
||||
>
|
||||
</div>
|
||||
<Badge variant="outline" class="text-xs">
|
||||
<FilterIcon class="h-3 w-3 mr-1" />
|
||||
Napredno iskanje
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- Basic filters -->
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label>Segment</Label>
|
||||
<Select v-model="segmentId" @update:model-value="loadContracts()">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Vsi segmenti" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="null">Vsi segmenti</SelectItem>
|
||||
<SelectItem v-for="s in segments" :key="s.id" :value="s.id">
|
||||
{{ s.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Stranka</Label>
|
||||
<AppCombobox
|
||||
v-model="clientId"
|
||||
:items="clientItems"
|
||||
placeholder="Vse stranke"
|
||||
search-placeholder="Išči stranko..."
|
||||
empty-text="Stranka ni najdena."
|
||||
button-class="w-full"
|
||||
@update:model-value="loadContracts()"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Iskanje po referenci</Label>
|
||||
<Input
|
||||
v-model="search"
|
||||
@keyup.enter="loadContracts()"
|
||||
placeholder="Vnesi referenco..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Date filters -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<CalendarIcon class="h-4 w-4" />
|
||||
Datumski filtri
|
||||
</h4>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-muted-foreground">Datum začetka pogodbe</p>
|
||||
<AppRangeDatePicker
|
||||
v-model="startDateRange"
|
||||
placeholder="Izberi obdobje"
|
||||
button-class="w-full"
|
||||
:number-of-months="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-muted-foreground">Datum obljube plačila</p>
|
||||
<AppRangeDatePicker
|
||||
v-model="promiseDateRange"
|
||||
placeholder="Izberi obdobje"
|
||||
button-class="w-full"
|
||||
:number-of-months="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Phone filters -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold mb-3">Telefonski filtri</h4>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
:model-value="onlyMobile"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
onlyMobile = val;
|
||||
}
|
||||
"
|
||||
id="only-mobile"
|
||||
/>
|
||||
<Label for="only-mobile" class="cursor-pointer text-sm">
|
||||
Samo mobilne številke
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
:model-value="onlyValidated"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
onlyValidated = val;
|
||||
}
|
||||
"
|
||||
id="only-validated"
|
||||
/>
|
||||
<Label for="only-validated" class="cursor-pointer text-sm">
|
||||
Samo potrjene številke
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Button @click="loadContracts()">
|
||||
<SearchIcon class="h-4 w-4" />
|
||||
Išči pogodbe
|
||||
</Button>
|
||||
<Button @click="resetFilters" variant="outline">
|
||||
<XCircleIcon class="h-4 w-4" />
|
||||
Počisti filtre
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Results -->
|
||||
<Card v-if="contracts.data.length > 0 || loadingContracts">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Rezultati iskanja (do 500 zapisov)</CardTitle>
|
||||
<CardDescription v-if="contracts.meta.total > 0">
|
||||
Najdeno {{ contracts.meta.total }}
|
||||
{{
|
||||
contracts.meta.total === 1
|
||||
? "pogodba"
|
||||
: contracts.meta.total < 5
|
||||
? "pogodbe"
|
||||
: "pogodb"
|
||||
}}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<!-- Create Button -->
|
||||
<div class="flex justify-end gap-2" v-if="selectedContractIds.size > 0">
|
||||
<Badge
|
||||
v-if="selectedContractIds.size > 0"
|
||||
variant="secondary"
|
||||
class="text-sm"
|
||||
>
|
||||
<CheckCircle2Icon class="h-3 w-3" />
|
||||
Izbrano: {{ selectedContractIds.size }}
|
||||
</Badge>
|
||||
<Button
|
||||
@click="router.visit(route('packages.index'))"
|
||||
variant="outline"
|
||||
>
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button
|
||||
@click="submitCreateFromContracts"
|
||||
:disabled="selectedContractIds.size === 0 || creatingFromContracts"
|
||||
>
|
||||
<SaveIcon class="h-4 w-4" />
|
||||
Ustvari paket ({{ selectedContractIds.size }}
|
||||
{{
|
||||
selectedContractIds.size === 1
|
||||
? "pogodba"
|
||||
: selectedContractIds.size < 5
|
||||
? "pogodbe"
|
||||
: "pogodb"
|
||||
}})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<DataTableNew2
|
||||
v-if="!loadingContracts"
|
||||
:key="tableKey"
|
||||
:columns="contractColumns"
|
||||
:data="contracts.data"
|
||||
:enableRowSelection="true"
|
||||
:rowSelection="rowSelection"
|
||||
:showPagination="true"
|
||||
:page-size="50"
|
||||
:page-size-options="[10, 15, 25, 50, 100]"
|
||||
:showToolbar="false"
|
||||
@selection:change="onSelectionChange"
|
||||
>
|
||||
<template #cell-reference="{ row }">
|
||||
<div v-if="row.original" class="space-y-1">
|
||||
<p class="font-medium">{{ row.original.reference || "—" }}</p>
|
||||
<p class="text-xs text-muted-foreground font-mono">
|
||||
#{{ row.original.id }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-person="{ row }">
|
||||
<span v-if="row.original" class="text-xs">{{
|
||||
row.original.person?.full_name || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-client="{ row }">
|
||||
<span v-if="row.original" class="text-xs">{{
|
||||
row.original.client?.name || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-start_date="{ row }">
|
||||
{{ fmtDateDMY(row.start_date) || "—" }}
|
||||
</template>
|
||||
|
||||
<template #cell-promise_date="{ row }">
|
||||
{{ fmtDateDMY(row.promise_date) || "—" }}
|
||||
</template>
|
||||
|
||||
<template #cell-selected_phone="{ row }">
|
||||
<div v-if="row.selected_phone" class="space-y-1">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<span>{{ row.selected_phone.number }}</span>
|
||||
<span
|
||||
><Badge
|
||||
v-if="row.selected_phone.validated"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
<BadgeCheckIcon />
|
||||
Potrjena
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="destructive"
|
||||
class="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums text-accent"
|
||||
>
|
||||
Nepotrjena
|
||||
</Badge></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-xs text-destructive">Ni telefonske št.</span>
|
||||
</template>
|
||||
|
||||
<template #cell-no_phone_reason="{ row }">
|
||||
<span v-if="row.original" class="text-xs text-muted-foreground">{{
|
||||
row.original.no_phone_reason || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
|
||||
<div v-else class="text-center text-muted-foreground py-24">Nalaganje...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,176 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import { Card, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import { PackageIcon, PlusIcon, Trash2Icon, EyeIcon } from "lucide-vue-next";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
import { fmtDateTime } from "@/Utilities/functions";
|
||||
|
||||
const props = defineProps({
|
||||
packages: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const deletingId = ref(null);
|
||||
const packageToDelete = ref(null);
|
||||
const showDeleteDialog = ref(false);
|
||||
|
||||
const columns = [
|
||||
{ accessorKey: "id", header: "ID" },
|
||||
{ accessorKey: "name", header: "Ime" },
|
||||
{ accessorKey: "type", header: "Tip" },
|
||||
{ accessorKey: "status", header: "Status" },
|
||||
{ accessorKey: "total_items", header: "Skupaj" },
|
||||
{ accessorKey: "sent_count", header: "Poslano" },
|
||||
{ accessorKey: "failed_count", header: "Neuspešno" },
|
||||
{ accessorKey: "finished_at", header: "Zaključeno" },
|
||||
{ accessorKey: "actions", header: "", enableSorting: false },
|
||||
];
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (["queued", "running"].includes(status)) return "secondary";
|
||||
if (status === "completed") return "default";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
function goShow(id) {
|
||||
router.visit(route("packages.show", id));
|
||||
}
|
||||
|
||||
function openDeleteDialog(pkg) {
|
||||
if (!pkg || pkg.status !== "draft") return;
|
||||
packageToDelete.value = pkg;
|
||||
showDeleteDialog.value = true;
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
if (!packageToDelete.value) return;
|
||||
deletingId.value = packageToDelete.value.id;
|
||||
router.delete(route("packages.destroy", packageToDelete.value.id), {
|
||||
onSuccess: () => {
|
||||
router.reload({ only: ["packages"] });
|
||||
},
|
||||
onFinish: () => {
|
||||
deletingId.value = null;
|
||||
showDeleteDialog.value = false;
|
||||
packageToDelete.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="SMS paketi">
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<PackageIcon class="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle>SMS paketi</CardTitle>
|
||||
</div>
|
||||
<Link :href="route('packages.create')">
|
||||
<Button>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
Nov paket
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<AppCard
|
||||
title=""
|
||||
padding="none"
|
||||
class="p-0! gap-0"
|
||||
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||
body-class=""
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<PackageIcon size="18" />
|
||||
<CardTitle class="uppercase">Paketi</CardTitle>
|
||||
</div>
|
||||
</template>
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="packages.data"
|
||||
:meta="packages"
|
||||
route-name="packages.index"
|
||||
>
|
||||
<template #cell-name="{ row }">
|
||||
<span class="text-sm">{{ row.name ?? "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-type="{ row }">
|
||||
<Badge variant="outline" class="uppercase">{{ row.type }}</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<Badge :variant="getStatusVariant(row.status)">{{ row.status }}</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-finished_at="{ row }">
|
||||
<span class="text-xs text-muted-foreground">{{
|
||||
fmtDateTime(row.finished_at) ?? "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button @click="goShow(row.id)" variant="ghost" size="sm">
|
||||
<EyeIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="row.status === 'draft'"
|
||||
@click="openDeleteDialog(row)"
|
||||
:disabled="deletingId === row.id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Trash2Icon class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<AlertDialog v-model:open="showDeleteDialog">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Izbriši paket?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Ali ste prepričani, da želite izbrisati paket
|
||||
<strong v-if="packageToDelete"
|
||||
>#{{ packageToDelete.id }} -
|
||||
{{ packageToDelete.name || "Brez imena" }}</strong
|
||||
>? Tega dejanja ni mogoče razveljaviti.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Prekliči</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
@click="confirmDelete"
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Izbriši
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,337 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import Pagination from "@/Components/Pagination.vue";
|
||||
import {
|
||||
PackageIcon,
|
||||
ArrowLeftIcon,
|
||||
PlayIcon,
|
||||
XCircleIcon,
|
||||
RefreshCwIcon,
|
||||
CopyIcon,
|
||||
} from "lucide-vue-next";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
package: { type: Object, required: true },
|
||||
items: { type: Object, required: true },
|
||||
preview: { type: [Object, null], default: null },
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ accessorKey: "id", header: "ID" },
|
||||
{ accessorKey: "target", header: "Prejemnik" },
|
||||
{ accessorKey: "message", header: "Sporočilo" },
|
||||
{ accessorKey: "status", header: "Status" },
|
||||
{ accessorKey: "last_error", header: "Napaka" },
|
||||
{ accessorKey: "provider_message_id", header: "Provider ID" },
|
||||
{ accessorKey: "cost", header: "Cena" },
|
||||
{ accessorKey: "currency", header: "Valuta" },
|
||||
];
|
||||
|
||||
function getStatusVariant(status) {
|
||||
if (["queued", "processing"].includes(status)) return "secondary";
|
||||
if (status === "sent") return "default";
|
||||
if (status === "failed") return "destructive";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
const refreshing = ref(false);
|
||||
let timer = null;
|
||||
|
||||
const isRunning = computed(() => ["queued", "running"].includes(props.package.status));
|
||||
|
||||
// Derive a summary of payload/template/body from the first item on the page.
|
||||
// Assumption: payload is the same across items in a package (both flows use a common payload).
|
||||
const firstItem = computed(() =>
|
||||
props.items?.data && props.items.data.length ? props.items.data[0] : null
|
||||
);
|
||||
const firstPayload = computed(() =>
|
||||
firstItem.value ? firstItem.value.payload_json || {} : {}
|
||||
);
|
||||
const messageBody = computed(() => {
|
||||
const b = firstPayload.value?.body;
|
||||
if (typeof b === "string") {
|
||||
const t = b.trim();
|
||||
return t.length ? t : null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const payloadSummary = computed(() => ({
|
||||
profile_id: firstPayload.value?.profile_id ?? null,
|
||||
sender_id: firstPayload.value?.sender_id ?? null,
|
||||
template_id: firstPayload.value?.template_id ?? null,
|
||||
delivery_report: !!firstPayload.value?.delivery_report,
|
||||
}));
|
||||
|
||||
function reload() {
|
||||
refreshing.value = true;
|
||||
router.reload({
|
||||
only: ["package", "items"],
|
||||
onFinish: () => (refreshing.value = false),
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
});
|
||||
}
|
||||
|
||||
function dispatchPkg() {
|
||||
router.post(
|
||||
route("packages.dispatch", props.package.id),
|
||||
{},
|
||||
{ onSuccess: reload }
|
||||
);
|
||||
}
|
||||
function cancelPkg() {
|
||||
router.post(
|
||||
route("packages.cancel", props.package.id),
|
||||
{},
|
||||
{ onSuccess: reload }
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isRunning.value) {
|
||||
timer = setInterval(reload, 3000);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
async function copyText(text) {
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
// Fallback for older browsers
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
} catch (_) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :title="`Paket #${package.id}`">
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<PackageIcon class="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<CardTitle>Paket #{{ package.id }}</CardTitle>
|
||||
<CardDescription class="font-mono"
|
||||
>UUID: {{ package.uuid }}</CardDescription
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="route('packages.index')">
|
||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||
Nazaj
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="['draft', 'failed'].includes(package.status)"
|
||||
@click="dispatchPkg"
|
||||
size="sm"
|
||||
>
|
||||
<PlayIcon class="h-4 w-4 mr-2" />
|
||||
Zaženi
|
||||
</Button>
|
||||
<Button v-if="isRunning" @click="cancelPkg" variant="destructive" size="sm">
|
||||
<XCircleIcon class="h-4 w-4 mr-2" />
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button v-if="!isRunning" @click="reload" variant="outline" size="sm">
|
||||
<RefreshCwIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div class="grid sm:grid-cols-4 gap-3 mb-4">
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Status</CardDescription>
|
||||
<CardTitle class="text-xl uppercase">{{ package.status }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Skupaj</CardDescription>
|
||||
<CardTitle class="text-xl">{{ package.total_items }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Poslano</CardDescription>
|
||||
<CardTitle class="text-xl text-emerald-700">{{ package.sent_count }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardDescription>Neuspešno</CardDescription>
|
||||
<CardTitle class="text-xl text-rose-700">{{ package.failed_count }}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Payload / Message preview -->
|
||||
<div class="mb-4 grid gap-3 sm:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Sporočilo</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<template v-if="preview && preview.content">
|
||||
<div class="text-sm whitespace-pre-wrap mb-3">{{ preview.content }}</div>
|
||||
<Button @click="copyText(preview.content)" size="sm" variant="outline">
|
||||
<CopyIcon class="h-3.5 w-3.5 mr-2" />
|
||||
Kopiraj
|
||||
</Button>
|
||||
<p
|
||||
v-if="preview.source === 'template' && preview.template"
|
||||
class="mt-3 text-xs text-muted-foreground"
|
||||
>
|
||||
Predloga: {{ preview.template.name }} (#{{ preview.template.id }})
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="messageBody" class="text-sm whitespace-pre-wrap">
|
||||
{{ messageBody }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">
|
||||
<template v-if="payloadSummary.template_id">
|
||||
Uporabljena bo predloga #{{ payloadSummary.template_id }}.
|
||||
</template>
|
||||
<template v-else> Vsebina sporočila ni določena. </template>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base">Meta / Nastavitve pošiljanja</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl class="text-sm grid grid-cols-3 gap-y-2">
|
||||
<dt class="col-span-1 text-muted-foreground">Profil</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.profile_id ?? "—" }}</dd>
|
||||
<dt class="col-span-1 text-muted-foreground">Pošiljatelj</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.sender_id ?? "—" }}</dd>
|
||||
<dt class="col-span-1 text-muted-foreground">Predloga</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.template_id ?? "—" }}</dd>
|
||||
<dt class="col-span-1 text-muted-foreground">Delivery report</dt>
|
||||
<dd class="col-span-2">{{ payloadSummary.delivery_report ? "da" : "ne" }}</dd>
|
||||
</dl>
|
||||
<div
|
||||
v-if="
|
||||
package.meta && (package.meta.source || package.meta.skipped !== undefined)
|
||||
"
|
||||
class="mt-3 pt-3 border-t text-xs text-muted-foreground"
|
||||
>
|
||||
<span v-if="package.meta.source" class="mr-3"
|
||||
>Vir: {{ package.meta.source }}</span
|
||||
>
|
||||
<span v-if="package.meta.skipped !== undefined"
|
||||
>Preskočeno: {{ package.meta.skipped }}</span
|
||||
>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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">
|
||||
<PackageIcon size="18" />
|
||||
<CardTitle class="uppercase">Uvozi</CardTitle>
|
||||
</div>
|
||||
</template>
|
||||
<DataTableNew2
|
||||
:columns="columns"
|
||||
:data="items.data"
|
||||
:meta="items"
|
||||
route-name="packages.show"
|
||||
:route-params="{ id: package.id }"
|
||||
>
|
||||
<template #cell-target="{ row }">
|
||||
<span class="text-sm">{{
|
||||
(row.target_json && row.target_json.number) || "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-message="{ row }">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xs max-w-[420px] line-clamp-2 whitespace-pre-wrap">
|
||||
{{ row.rendered_preview || "—" }}
|
||||
</div>
|
||||
<Button
|
||||
v-if="row.rendered_preview"
|
||||
@click="copyText(row.rendered_preview)"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<CopyIcon class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<Badge :variant="getStatusVariant(row.status)">{{ row.status }}</Badge>
|
||||
</template>
|
||||
|
||||
<template #cell-last_error="{ row }">
|
||||
<span class="text-xs text-rose-700">{{ row.last_error ?? "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-provider_message_id="{ row }">
|
||||
<span class="font-mono text-xs text-muted-foreground">{{
|
||||
row.provider_message_id ?? "—"
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-cost="{ row }">
|
||||
<span class="text-sm">{{ row.cost ?? "—" }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-currency="{ row }">
|
||||
<span class="text-sm">{{ row.currency ?? "—" }}</span>
|
||||
</template>
|
||||
</DataTableNew2>
|
||||
</AppCard>
|
||||
|
||||
<div v-if="refreshing" class="mt-2 text-xs text-muted-foreground">
|
||||
Osveževanje ...
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user