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:
Simon Pocrnjič
2026-03-08 21:42:39 +01:00
parent c16dd51199
commit b0d2aa93ab
32 changed files with 1103 additions and 174 deletions
+337
View File
@@ -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>