Mail support testing faze

This commit is contained in:
Simon Pocrnjič
2025-10-07 21:57:10 +02:00
parent 175111bed4
commit b9ca8244ef
18 changed files with 1279 additions and 101 deletions
@@ -0,0 +1,272 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import DialogModal from "@/Components/DialogModal.vue";
import { Head, Link, useForm } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
faPlus,
faFlask,
faBolt,
faArrowsRotate,
faToggleOn,
faToggleOff,
} from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
profiles: { type: Array, default: () => [] },
});
const createOpen = ref(false);
const editTarget = ref(null);
const form = useForm({
name: "",
host: "",
port: 587,
encryption: "tls",
username: "",
password: "",
from_address: "",
from_name: "",
priority: 10,
});
function openCreate() {
form.reset();
createOpen.value = true;
editTarget.value = null;
}
function closeCreate() {
if (form.processing) return;
createOpen.value = false;
}
function submitCreate() {
form.post(route("admin.mail-profiles.store"), {
preserveScroll: true,
onSuccess: () => {
createOpen.value = false;
},
});
}
function toggleActive(p) {
window.axios
.post(route("admin.mail-profiles.toggle", p.id))
.then(() => window.location.reload());
}
function testConnection(p) {
window.axios
.post(route("admin.mail-profiles.test", p.id))
.then(() => window.location.reload());
}
const statusClass = (p) => {
if (p.test_status === "success") return "text-emerald-600";
if (p.test_status === "failed") return "text-rose-600";
if (p.test_status === "queued") return "text-amber-500";
return "text-gray-400";
};
</script>
<template>
<AdminLayout title="Mail profili">
<Head title="Mail profili" />
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl font-semibold text-gray-800 flex items-center gap-3">
Mail 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">Host</th>
<th class="px-3 py-2">Port</th>
<th class="px-3 py-2">Enc</th>
<th class="px-3 py-2">Aktivno</th>
<th class="px-3 py-2">Test</th>
<th class="px-3 py-2">Zadnji uspeh</th>
<th class="px-3 py-2">Napaka</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.host }}</td>
<td class="px-3 py-2 text-center">{{ p.port }}</td>
<td class="px-3 py-2 text-center">{{ p.encryption || "—" }}</td>
<td class="px-3 py-2 text-center">
<button
@click="toggleActive(p)"
class="text-indigo-600 hover:text-indigo-800"
:title="p.active ? 'Onemogoči' : 'Omogoči'"
>
<FontAwesomeIcon
:icon="p.active ? faToggleOn : faToggleOff"
class="w-5 h-5"
/>
</button>
</td>
<td class="px-3 py-2 text-center">
<span :class="['font-medium', statusClass(p)]">{{
p.test_status || "—"
}}</span>
</td>
<td class="px-3 py-2 text-xs text-gray-500">
{{ p.last_success_at ? new Date(p.last_success_at).toLocaleString() : "—" }}
</td>
<td
class="px-3 py-2 text-xs text-rose-600 max-w-[160px] truncate"
:title="p.last_error_message"
>
{{ p.last_error_message || "—" }}
</td>
<td class="px-3 py-2 flex items-center gap-2">
<button
@click="testConnection(p)"
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-amber-600 border-amber-300 bg-amber-50 hover:bg-amber-100"
>
<FontAwesomeIcon :icon="faFlask" class="w-3.5 h-3.5" /> Test
</button>
<button
class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border text-indigo-600 border-indigo-300 bg-indigo-50 hover:bg-indigo-100"
>
<FontAwesomeIcon :icon="faArrowsRotate" class="w-3.5 h-3.5" /> Shrani
</button>
</td>
</tr>
</tbody>
</table>
</div>
<DialogModal :show="createOpen" max-width="2xl" @close="closeCreate">
<template #title> Nov Mail profil </template>
<template #content>
<form @submit.prevent="submitCreate" id="create-mail-profile" class="space-y-5">
<div class="grid gap-4 grid-cols-2">
<div class="col-span-1">
<label class="label">Ime</label>
<input v-model="form.name" type="text" class="input" />
</div>
<div>
<label class="label">Host</label>
<input v-model="form.host" type="text" class="input" />
</div>
<div>
<label class="label">Port</label>
<input v-model="form.port" type="number" class="input" />
</div>
<div>
<label class="label">Encryption</label>
<select v-model="form.encryption" class="input">
<option value="">(None)</option>
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
</select>
</div>
<div>
<label class="label">Username</label>
<input v-model="form.username" type="text" class="input" />
</div>
<div>
<label class="label">Password</label>
<input
v-model="form.password"
type="password"
class="input"
autocomplete="new-password"
/>
</div>
<div>
<label class="label">From naslov</label>
<input v-model="form.from_address" type="email" class="input" />
</div>
<div>
<label class="label">From ime</label>
<input v-model="form.from_name" type="text" class="input" />
</div>
<div>
<label class="label">Prioriteta</label>
<input v-model="form.priority" type="number" class="input" />
</div>
</div>
</form>
</template>
<template #footer>
<button
type="button"
@click="closeCreate"
class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50"
>
Prekliči
</button>
<button
form="create-mail-profile"
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>
/* Utility replacements for @apply not processed in SFC scope build pipeline */
.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;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
.animate-fade-in {
animation: fade-in 0.25s ease;
}
</style>
+225 -58
View File
@@ -11,7 +11,7 @@ import {
faCloudArrowUp,
faArrowUpRightFromSquare,
} from "@fortawesome/free-solid-svg-icons";
import { faFileContract } from '@fortawesome/free-solid-svg-icons';
import { faFileContract } from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
kpis: Object,
@@ -45,7 +45,12 @@ const kpiDefs = [
icon: faCloudArrowUp,
route: "imports.index",
},
{ key: "active_contracts", label: "Aktivne pogodbe", icon: faFileContract, route: "clientCase" },
{
key: "active_contracts",
label: "Aktivne pogodbe",
icon: faFileContract,
route: "clientCase",
},
];
const page = usePage();
@@ -97,6 +102,19 @@ function buildRelated(a) {
const activityItems = computed(() =>
(props.activities || []).map((a) => ({ ...a, links: buildRelated(a) }))
);
// Format stale days label: never negative; '<1 dan' if 0<=value<1; else integer with proper suffix.
function formatStaleDaysLabel(value) {
const num = Number.parseFloat(value);
if (Number.isNaN(num)) {
return "—";
}
if (num < 1) {
return "<1 dan";
}
const whole = Math.floor(num);
return whole === 1 ? "1 dan" : whole + " dni";
}
</script>
<template>
@@ -269,8 +287,14 @@ const activityItems = computed(() =>
<!-- Right side panels -->
<div class="lg:col-span-2 space-y-8">
<!-- System Health -->
<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">System Health</h3>
<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"
>
System Health
</h3>
<div
v-if="systemHealth"
class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm"
@@ -326,8 +350,14 @@ const activityItems = computed(() =>
</div>
<!-- Completed Field Jobs Trend (7 dni) -->
<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">Zaključena terenska dela (7 dni)</h3>
<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"
>
Zaključena terenska dela (7 dni)
</h3>
<div v-if="trends" class="h-24">
<svg viewBox="0 0 140 60" class="w-full h-full">
<defs>
@@ -336,91 +366,228 @@ const activityItems = computed(() =>
<stop offset="100%" stop-color="#6366f1" stop-opacity="0" />
</linearGradient>
</defs>
<path v-if="trends.field_jobs_completed" :d="sparkline(trends.field_jobs_completed)" stroke="#6366f1" stroke-width="2" fill="none" stroke-linejoin="round" stroke-linecap="round" />
<path
v-if="trends.field_jobs_completed"
:d="sparkline(trends.field_jobs_completed)"
stroke="#6366f1"
stroke-width="2"
fill="none"
stroke-linejoin="round"
stroke-linecap="round"
/>
</svg>
<div class="mt-2 flex gap-2 text-[10px] text-gray-400 dark:text-gray-500">
<span v-for="(l,i) in trends.labels" :key="i" class="flex-1 truncate text-center">{{ l.slice(5) }}</span>
<span
v-for="(l, i) in trends.labels"
:key="i"
class="flex-1 truncate text-center"
>{{ l.slice(5) }}</span
>
</div>
</div>
<div v-else class="h-24 animate-pulse bg-gray-100 dark:bg-gray-700 rounded" />
</div>
<!-- Stale Cases -->
<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">Stari primeri brez aktivnosti</h3>
<ul v-if="staleCases" class="divide-y divide-gray-100 dark:divide-gray-700 text-sm">
<li v-for="c in staleCases" :key="c.id" class="py-2 flex items-center justify-between">
<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"
>
Stari primeri brez aktivnosti
</h3>
<ul
v-if="staleCases"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
>
<li
v-for="c in staleCases"
:key="c.id"
class="py-2 flex items-center justify-between"
>
<div class="min-w-0">
<Link :href="route('clientCase.show', c.uuid)" class="text-indigo-600 dark:text-indigo-400 hover:underline font-medium">{{ c.client_ref || c.uuid.slice(0,8) }}</Link>
<p class="text-[11px] text-gray-400 dark:text-gray-500">Staro: {{ c.days_stale }} dni</p>
<Link
:href="route('clientCase.show', c.uuid)"
class="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
>{{ c.client_ref || c.uuid.slice(0, 8) }}</Link
>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
Brez aktivnosti: {{ formatStaleDaysLabel(c.days_without_activity ?? c.days_stale) }}
</p>
</div>
<span class="text-[10px] px-2 py-0.5 rounded bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-300">Stale</span>
<span
class="text-[10px] px-2 py-0.5 rounded bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-300"
>Stale</span
>
</li>
<li
v-if="!staleCases.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni starih primerov.
</li>
<li v-if="!staleCases.length" class="py-4 text-xs text-gray-500 text-center">Ni starih primerov.</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 5" :key="n" class="h-5 bg-gray-100 dark:bg-gray-700 rounded" />
<div
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
/>
</div>
</div>
<!-- Field Jobs Assigned Today -->
<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">Današnje dodelitve terenskih</h3>
<ul v-if="fieldJobsAssignedToday" class="divide-y divide-gray-100 dark:divide-gray-700 text-sm">
<li v-for="f in fieldJobsAssignedToday" :key="f.id" class="py-2 flex items-center justify-between">
<div class="min-w-0">
<p class="text-gray-700 dark:text-gray-300 text-sm">#{{ f.id }}</p>
<p class="text-[11px] text-gray-400 dark:text-gray-500">{{ (f.assigned_at || f.created_at) ? new Date(f.assigned_at || f.created_at).toLocaleTimeString() : '' }}</p>
</div>
<span v-if="f.priority" class="text-[10px] px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-900/30 text-rose-600 dark:text-rose-300">Prioriteta</span>
</li>
<li v-if="!fieldJobsAssignedToday.length" class="py-4 text-xs text-gray-500 text-center">Ni dodelitev.</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 5" :key="n" class="h-5 bg-gray-100 dark:bg-gray-700 rounded" />
</div>
</div>
<!-- Imports In Progress -->
<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">Uvozi v teku</h3>
<ul v-if="importsInProgress" class="divide-y divide-gray-100 dark:divide-gray-700 text-sm">
<li v-for="im in importsInProgress" :key="im.id" class="py-2 space-y-1">
<div class="flex items-center justify-between">
<p class="font-medium text-gray-700 dark:text-gray-300 truncate">{{ im.file_name }}</p>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300">{{ im.status }}</span>
<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"
>
Današnje dodelitve terenskih
</h3>
<ul
v-if="fieldJobsAssignedToday"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
>
<li
v-for="f in fieldJobsAssignedToday"
:key="f.id"
class="py-2 flex items-center justify-between"
>
<div class="min-w-0">
<p class="text-gray-700 dark:text-gray-300 text-sm">#{{ f.id }}</p>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
{{
f.assigned_at || f.created_at
? new Date(f.assigned_at || f.created_at).toLocaleTimeString()
: ""
}}
</p>
</div>
<div class="w-full h-2 bg-gray-100 dark:bg-gray-700 rounded overflow-hidden">
<div class="h-full bg-indigo-500 dark:bg-indigo-400" :style="{ width: (im.progress_pct || 0) + '%' }"></div>
</div>
<p class="text-[10px] text-gray-400 dark:text-gray-500">{{ im.imported_rows }}/{{ im.total_rows }} (veljavnih: {{ im.valid_rows }}, neveljavnih: {{ im.invalid_rows }})</p>
<span
v-if="f.priority"
class="text-[10px] px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-900/30 text-rose-600 dark:text-rose-300"
>Prioriteta</span
>
</li>
<li
v-if="!fieldJobsAssignedToday.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni dodelitev.
</li>
<li v-if="!importsInProgress.length" class="py-4 text-xs text-gray-500 text-center">Ni aktivnih uvozov.</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 4" :key="n" class="h-5 bg-gray-100 dark:bg-gray-700 rounded" />
<div
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
/>
</div>
</div>
<!-- Imports In Progress -->
<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"
>
Uvozi v teku
</h3>
<ul
v-if="importsInProgress"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
>
<li v-for="im in importsInProgress" :key="im.id" class="py-2 space-y-1">
<div class="flex items-center justify-between">
<p class="font-medium text-gray-700 dark:text-gray-300 truncate">
{{ im.file_name }}
</p>
<span
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300"
>{{ im.status }}</span
>
</div>
<div
class="w-full h-2 bg-gray-100 dark:bg-gray-700 rounded overflow-hidden"
>
<div
class="h-full bg-indigo-500 dark:bg-indigo-400"
:style="{ width: (im.progress_pct || 0) + '%' }"
></div>
</div>
<p class="text-[10px] text-gray-400 dark:text-gray-500">
{{ im.imported_rows }}/{{ im.total_rows }} (veljavnih:
{{ im.valid_rows }}, neveljavnih: {{ im.invalid_rows }})
</p>
</li>
<li
v-if="!importsInProgress.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni aktivnih uvozov.
</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div
v-for="n in 4"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
/>
</div>
</div>
<!-- Active Document Templates -->
<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">Aktivne predloge dokumentov</h3>
<ul v-if="activeTemplates" class="divide-y divide-gray-100 dark:divide-gray-700 text-sm">
<li v-for="t in activeTemplates" :key="t.id" class="py-2 flex items-center justify-between">
<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"
>
Aktivne predloge dokumentov
</h3>
<ul
v-if="activeTemplates"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
>
<li
v-for="t in activeTemplates"
:key="t.id"
class="py-2 flex items-center justify-between"
>
<div class="min-w-0">
<p class="text-gray-700 dark:text-gray-300 font-medium truncate">{{ t.name }}</p>
<p class="text-[11px] text-gray-400 dark:text-gray-500">v{{ t.version }} · {{ new Date(t.updated_at).toLocaleDateString() }}</p>
<p class="text-gray-700 dark:text-gray-300 font-medium truncate">
{{ t.name }}
</p>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
v{{ t.version }} · {{ new Date(t.updated_at).toLocaleDateString() }}
</p>
</div>
<Link :href="route('admin.document-templates.edit', t.id)" class="text-[10px] px-2 py-0.5 rounded bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-800/60">Uredi</Link>
<Link
:href="route('admin.document-templates.edit', t.id)"
class="text-[10px] px-2 py-0.5 rounded bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-800/60"
>Uredi</Link
>
</li>
<li
v-if="!activeTemplates.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni aktivnih predlog.
</li>
<li v-if="!activeTemplates.length" class="py-4 text-xs text-gray-500 text-center">Ni aktivnih predlog.</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 5" :key="n" class="h-5 bg-gray-100 dark:bg-gray-700 rounded" />
<div
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
/>
</div>
</div>
<!-- ...end of right side panels -->
<!-- ...end of right side panels -->
</div>
</div>
</div>