Package and individual mail sender, new report, and other changes

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Simon Pocrnjič
2026-05-11 21:32:30 +02:00
parent b6bfa17980
commit e3bc5da7e3
49 changed files with 4754 additions and 249 deletions
+242
View File
@@ -0,0 +1,242 @@
<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 DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
import {
MailIcon,
ArrowLeftIcon,
PlayIcon,
XCircleIcon,
RefreshCwIcon,
} 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 },
});
const columns = [
{ accessorKey: "id", header: "ID" },
{ accessorKey: "target", header: "Prejemnik" },
{ accessorKey: "subject", header: "Zadeva" },
{ accessorKey: "status", header: "Status" },
{ accessorKey: "last_error", header: "Napaka" },
];
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));
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 payloadSummary = computed(() => ({
mail_profile_id: firstPayload.value?.mail_profile_id ?? null,
template_id: firstPayload.value?.template_id ?? null,
subject: firstPayload.value?.subject ?? null,
}));
function reload() {
refreshing.value = true;
router.reload({
only: ["package", "items"],
onFinish: () => (refreshing.value = false),
preserveScroll: true,
preserveState: true,
});
}
function dispatchPkg() {
router.post(
route("packages.email.dispatch", props.package.id),
{},
{ onSuccess: reload }
);
}
function cancelPkg() {
router.post(
route("packages.email.cancel", props.package.id),
{},
{ onSuccess: reload }
);
}
onMounted(() => {
if (isRunning.value) {
timer = setInterval(reload, 3000);
}
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
<template>
<AppLayout :title="`E-mail paket #${package.id}`">
<Card class="mb-4">
<CardHeader>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<MailIcon class="h-5 w-5 text-muted-foreground" />
<div>
<CardTitle>E-mail 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.email.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>
<!-- E-mail settings summary -->
<Card class="mb-4">
<CardHeader>
<CardTitle class="text-base">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">E-mail profil</dt>
<dd class="col-span-2">{{ payloadSummary.mail_profile_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">Zadeva</dt>
<dd class="col-span-2">{{ payloadSummary.subject ?? "—" }}</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>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center gap-2">
<MailIcon size="18" />
<CardTitle class="uppercase">Uvozi</CardTitle>
</div>
</template>
<DataTableNew2
:columns="columns"
:data="items.data"
:meta="items"
route-name="packages.email.show"
:route-params="{ id: package.id }"
>
<template #cell-target="{ row }">
<span class="text-sm">{{
(row.target_json && row.target_json.email) || "—"
}}</span>
</template>
<template #cell-subject="{ row }">
<span class="text-xs text-muted-foreground">{{
(row.result_json && row.result_json.subject) ||
(row.payload_json && row.payload_json.subject) ||
"—"
}}</span>
</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>
</DataTableNew2>
</AppCard>
<div v-if="refreshing" class="mt-2 text-xs text-muted-foreground">
Osveževanje ...
</div>
</AppLayout>
</template>