Test commit to new origin
This commit is contained in:
@@ -43,9 +43,9 @@ const fmtDateDMY = (v) => {
|
||||
<template #header> </template>
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="mx-auto max-w-4x1 py-3">
|
||||
<div class="pb-3">
|
||||
<div class="pb-3 px-3">
|
||||
<SectionTitle>
|
||||
<template #title>Primeri</template>
|
||||
</SectionTitle>
|
||||
|
||||
@@ -255,7 +255,7 @@ const submitAttachSegment = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="border-l-4 border-blue-400">
|
||||
<Card class="border-l-4 border-blue-400 p-0">
|
||||
<div class="p-3 flex justify-between items-center">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
@@ -271,8 +271,8 @@ const submitAttachSegment = () => {
|
||||
</div>
|
||||
<div class="pt-1" :hidden="clientDetails">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<Card class="p-0">
|
||||
<div class="p-3">
|
||||
<PersonInfoGrid
|
||||
:types="types"
|
||||
:person="client.person"
|
||||
@@ -285,8 +285,8 @@ const submitAttachSegment = () => {
|
||||
<!-- Case details -->
|
||||
<div class="pt-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card class="border-l-4 border-red-400">
|
||||
<div class="mx-auto max-w-4x1 p-3 flex items-center justify-between">
|
||||
<Card class="border-l-4 border-red-400 p-0">
|
||||
<div class="p-3 flex items-center justify-between">
|
||||
<SectionTitle>
|
||||
<template #title>{{ client_case.person.full_name }}</template>
|
||||
</SectionTitle>
|
||||
@@ -309,8 +309,8 @@ const submitAttachSegment = () => {
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<Card class="p-0">
|
||||
<div class="p-3">
|
||||
<PersonInfoGrid
|
||||
:types="types"
|
||||
tab-color="red-600"
|
||||
@@ -326,8 +326,8 @@ const submitAttachSegment = () => {
|
||||
<!-- Contracts section -->
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1">
|
||||
<Card class="p-0">
|
||||
<div>
|
||||
<div class="p-3">
|
||||
<SectionTitle>
|
||||
<template #title> Pogodbe </template>
|
||||
@@ -369,9 +369,9 @@ const submitAttachSegment = () => {
|
||||
<!-- Activities section -->
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1">
|
||||
<div class="flex justify-between p-4">
|
||||
<Card class="p-0">
|
||||
<div>
|
||||
<div class="flex justify-between p-3">
|
||||
<SectionTitle>
|
||||
<template #title>Aktivnosti</template>
|
||||
</SectionTitle>
|
||||
@@ -410,8 +410,8 @@ const submitAttachSegment = () => {
|
||||
<!-- Documents section -->
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1">
|
||||
<Card class="p-0">
|
||||
<div>
|
||||
<div class="p-4">
|
||||
<SectionTitle>
|
||||
<template #title>Dokumenti</template>
|
||||
|
||||
@@ -132,7 +132,7 @@ function formatDate(value) {
|
||||
<!-- Header card (matches Client/Show header style) -->
|
||||
<div class="pt-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card class="border-l-4 border-blue-400">
|
||||
<Card class="border-l-4 border-blue-400 p-0">
|
||||
<div class="p-3 flex justify-between items-center">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
@@ -146,8 +146,8 @@ function formatDate(value) {
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<Card class="p-0">
|
||||
<div class="p-3">
|
||||
<PersonInfoGrid
|
||||
:types="types"
|
||||
:person="client.person"
|
||||
|
||||
@@ -165,8 +165,8 @@ const fmtCurrency = (v) => {
|
||||
<template #header> </template>
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<CardHeader class="p-5">
|
||||
<Card class="p-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Naročniki</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
|
||||
@@ -63,7 +63,7 @@ function applySearch() {
|
||||
<template #header></template>
|
||||
<div class="pt-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card class="border-l-4 border-blue-400">
|
||||
<Card class="border-l-4 border-blue-400 p-0!">
|
||||
<div class="p-3 flex justify-between items-center">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
@@ -77,8 +77,8 @@ function applySearch() {
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<Card class="p-0!">
|
||||
<div class="p-3">
|
||||
<PersonInfoGrid
|
||||
:types="types"
|
||||
:person="client.person"
|
||||
|
||||
@@ -1,620 +0,0 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { usePage, Link } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faUsers,
|
||||
faUserPlus,
|
||||
faClipboardList,
|
||||
faFileLines,
|
||||
faCloudArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFileContract } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
kpis: Object,
|
||||
activities: Array,
|
||||
trends: Object,
|
||||
systemHealth: Object,
|
||||
staleCases: Array,
|
||||
fieldJobsAssignedToday: Array,
|
||||
importsInProgress: Array,
|
||||
activeTemplates: Array,
|
||||
smsStats: Array,
|
||||
});
|
||||
|
||||
const kpiDefs = [
|
||||
{ key: "clients_total", label: "Vse stranke", icon: faUsers, route: "client" },
|
||||
{ key: "clients_new_7d", label: "Nove (7d)", icon: faUserPlus, route: "client" },
|
||||
{
|
||||
key: "field_jobs_today",
|
||||
label: "Terenske danes",
|
||||
icon: faClipboardList,
|
||||
route: "fieldjobs.index",
|
||||
},
|
||||
{
|
||||
key: "documents_today",
|
||||
label: "Dokumenti danes",
|
||||
icon: faFileLines,
|
||||
route: "clientCase",
|
||||
},
|
||||
{
|
||||
key: "active_imports",
|
||||
label: "Aktivni uvozi",
|
||||
icon: faCloudArrowUp,
|
||||
route: "imports.index",
|
||||
},
|
||||
{
|
||||
key: "active_contracts",
|
||||
label: "Aktivne pogodbe",
|
||||
icon: faFileContract,
|
||||
route: "clientCase",
|
||||
},
|
||||
];
|
||||
|
||||
const page = usePage();
|
||||
|
||||
// Simple sparkline path generator
|
||||
function sparkline(values) {
|
||||
if (!values || !values.length) {
|
||||
return "";
|
||||
}
|
||||
const max = Math.max(...values) || 1;
|
||||
const h = 24;
|
||||
const w = 60;
|
||||
const step = w / (values.length - 1 || 1);
|
||||
return values
|
||||
.map(
|
||||
(v, i) =>
|
||||
`${i === 0 ? "M" : "L"}${(i * step).toFixed(2)},${(h - (v / max) * h).toFixed(2)}`
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
// Remove single relatedTarget helper and replace with multi-link builder
|
||||
function buildRelated(a) {
|
||||
const links = [];
|
||||
// Only client case link (other routes not defined yet)
|
||||
if (a.client_case_uuid || a.client_case_id) {
|
||||
const caseParam = a.client_case_uuid || a.client_case_id;
|
||||
try {
|
||||
// Prefer Ziggy when available and force stringification here
|
||||
const href = String(route("clientCase.show", { client_case: caseParam }));
|
||||
links.push({
|
||||
type: "client_case",
|
||||
label: "Primer",
|
||||
href,
|
||||
});
|
||||
} catch (e) {
|
||||
// Safe fallback to a best-effort URL to avoid breaking render
|
||||
links.push({
|
||||
type: "client_case",
|
||||
label: "Primer",
|
||||
href: `/client-cases/${caseParam}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
// Robust time formatter to avoid fixed 02:00:00 (timezone / fallback issues)
|
||||
function formatJobTime(ts) {
|
||||
if (!ts) return "";
|
||||
try {
|
||||
const d = new Date(ts);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
// Show HH:MM (24h) and seconds only if non-zero seconds
|
||||
const pad = (n) => n.toString().padStart(2, "0");
|
||||
const h = pad(d.getHours());
|
||||
const m = pad(d.getMinutes());
|
||||
const s = d.getSeconds();
|
||||
return s ? `${h}:${m}:${pad(s)}` : `${h}:${m}`;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Safely build a client case href using Ziggy when available, with a plain fallback.
|
||||
function safeCaseHref(uuid, segment = null) {
|
||||
if (!uuid) {
|
||||
return "#";
|
||||
}
|
||||
try {
|
||||
const params = { client_case: uuid };
|
||||
if (segment != null) {
|
||||
params.segment = segment;
|
||||
}
|
||||
return String(route("clientCase.show", params));
|
||||
} catch (e) {
|
||||
return segment != null
|
||||
? `/client-cases/${uuid}?segment=${segment}`
|
||||
: `/client-cases/${uuid}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Nadzorna plošča">
|
||||
<template #header> </template>
|
||||
|
||||
<div class="max-w-7xl mx-auto space-y-10 py-6">
|
||||
<!-- KPI Cards with trends -->
|
||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||
<Link
|
||||
v-for="k in kpiDefs"
|
||||
:key="k.key"
|
||||
:href="route(k.route)"
|
||||
class="group relative bg-white border rounded-xl shadow-sm px-4 py-5 flex flex-col gap-3 hover:border-indigo-300 hover:shadow transition focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-10 w-10 rounded-md bg-indigo-50 text-indigo-600 group-hover:bg-indigo-100"
|
||||
>
|
||||
<FontAwesomeIcon :icon="k.icon" class="w-5 h-5" />
|
||||
</span>
|
||||
<span class="text-[11px] text-gray-400 uppercase tracking-wide">{{
|
||||
k.label
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<span class="text-2xl font-semibold tracking-tight text-gray-900">{{
|
||||
props.kpis?.[k.key] ?? "—"
|
||||
}}</span>
|
||||
<span
|
||||
class="text-[10px] text-indigo-500 opacity-0 group-hover:opacity-100 transition"
|
||||
>Odpri →</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="trends" class="mt-1 h-6">
|
||||
<svg
|
||||
v-if="k.key === 'clients_new_7d'"
|
||||
:viewBox="'0 0 60 24'"
|
||||
class="w-full h-6 overflow-visible"
|
||||
>
|
||||
<path
|
||||
:d="sparkline(trends.clients_new)"
|
||||
fill="none"
|
||||
class="stroke-indigo-400"
|
||||
stroke-width="2"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="k.key === 'documents_today'"
|
||||
:viewBox="'0 0 60 24'"
|
||||
class="w-full h-6"
|
||||
>
|
||||
<path
|
||||
:d="sparkline(trends.documents_new)"
|
||||
fill="none"
|
||||
class="stroke-emerald-400"
|
||||
stroke-width="2"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="k.key === 'field_jobs_today'"
|
||||
:viewBox="'0 0 60 24'"
|
||||
class="w-full h-6"
|
||||
>
|
||||
<path
|
||||
:d="sparkline(trends.field_jobs)"
|
||||
fill="none"
|
||||
class="stroke-amber-400"
|
||||
stroke-width="2"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="k.key === 'active_imports'"
|
||||
:viewBox="'0 0 60 24'"
|
||||
class="w-full h-6"
|
||||
>
|
||||
<path
|
||||
:d="sparkline(trends.imports_new)"
|
||||
fill="none"
|
||||
class="stroke-fuchsia-400"
|
||||
stroke-width="2"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-3 gap-8">
|
||||
<!-- Activity Feed -->
|
||||
<div class="lg:col-span-1 space-y-4">
|
||||
<div class="bg-white border rounded-xl shadow-sm p-5 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
|
||||
Aktivnost
|
||||
</h3>
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-100 text-sm" v-if="activities">
|
||||
<li
|
||||
v-for="a in activityItems"
|
||||
:key="a.id"
|
||||
class="py-2 flex items-start gap-3"
|
||||
>
|
||||
<span class="w-2 h-2 mt-2 rounded-full bg-indigo-400" />
|
||||
<div class="flex-1 min-w-0 space-y-1">
|
||||
<p class="text-gray-700 line-clamp-2">
|
||||
{{ a.note || "Dogodek" }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-[11px] text-gray-400">{{
|
||||
new Date(a.created_at).toLocaleString()
|
||||
}}</span>
|
||||
<Link
|
||||
v-for="l in a.links"
|
||||
:key="l.type + l.href"
|
||||
:href="l.href"
|
||||
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-600 hover:bg-indigo-100 font-medium tracking-wide"
|
||||
>{{ l.label }}</Link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="!activities?.length"
|
||||
class="py-4 text-xs text-gray-500 text-center"
|
||||
>
|
||||
Ni zabeleženih aktivnosti.
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else class="animate-pulse space-y-2">
|
||||
<li v-for="n in 5" :key="n" class="h-5 bg-gray-100 rounded" />
|
||||
</ul>
|
||||
<div class="pt-1 flex justify-between items-center text-[11px]">
|
||||
<Link
|
||||
:href="route('dashboard')"
|
||||
class="inline-flex items-center gap-1 font-medium text-indigo-600 hover:underline"
|
||||
>Več kmalu
|
||||
<FontAwesomeIcon :icon="faArrowUpRightFromSquare" class="w-3 h-3"
|
||||
/></Link>
|
||||
<span v-if="systemHealth" class="text-gray-400"
|
||||
>Posodobljeno
|
||||
{{ new Date(systemHealth.generated_at).toLocaleTimeString() }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side panels -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- SMS Overview -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 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 text-gray-600 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"
|
||||
>
|
||||
<td class="px-3 py-2">
|
||||
<span class="font-medium text-gray-900">{{ 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">
|
||||
{{ 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">Ni podatkov o SMS.</div>
|
||||
</div>
|
||||
|
||||
<!-- System Health -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
|
||||
System Health
|
||||
</h3>
|
||||
<div
|
||||
v-if="systemHealth"
|
||||
class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-[11px] uppercase text-gray-400">Queue backlog</span>
|
||||
<span class="font-semibold text-gray-800">{{
|
||||
systemHealth.queue_backlog ?? "—"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-[11px] uppercase text-gray-400">Failed jobs</span>
|
||||
<span class="font-semibold text-gray-800">{{
|
||||
systemHealth.failed_jobs ?? "—"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-[11px] uppercase text-gray-400"
|
||||
>Last activity (min)</span
|
||||
>
|
||||
<span
|
||||
class="font-semibold text-gray-800"
|
||||
:title="
|
||||
systemHealth.last_activity_iso
|
||||
? new Date(systemHealth.last_activity_iso).toLocaleString()
|
||||
: ''
|
||||
"
|
||||
>{{
|
||||
Math.max(0, parseInt(systemHealth.last_activity_minutes ?? 0))
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-[11px] uppercase text-gray-400">Generated</span>
|
||||
<span class="font-semibold text-gray-800">{{
|
||||
new Date(systemHealth.generated_at).toLocaleTimeString()
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="grid sm:grid-cols-4 gap-4 animate-pulse">
|
||||
<div v-for="n in 4" :key="n" class="h-10 bg-gray-100 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Field Jobs Trend (7 dni) -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 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>
|
||||
<linearGradient id="fjc" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.35" />
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
<div class="mt-2 flex gap-2 text-[10px] text-gray-400">
|
||||
<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 rounded" />
|
||||
</div>
|
||||
|
||||
<!-- Stale Cases -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
|
||||
Stari primeri brez aktivnosti
|
||||
</h3>
|
||||
<ul v-if="staleCases" class="divide-y divide-gray-100 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
|
||||
v-if="c?.uuid"
|
||||
:href="safeCaseHref(c.uuid)"
|
||||
class="text-indigo-600 hover:underline font-medium"
|
||||
>{{ c.client_ref || c.uuid.slice(0, 8) }}</Link
|
||||
>
|
||||
<span v-else class="text-gray-700 font-medium">{{
|
||||
c.client_ref || "Primer"
|
||||
}}</span>
|
||||
<p class="text-[11px] text-gray-400">
|
||||
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 text-amber-600"
|
||||
>Stale</span
|
||||
>
|
||||
</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 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Field Jobs Assigned Today -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
|
||||
Današnje dodelitve terenskih
|
||||
</h3>
|
||||
<ul v-if="fieldJobsAssignedToday" class="divide-y divide-gray-100 text-sm">
|
||||
<li
|
||||
v-for="f in fieldJobsAssignedToday"
|
||||
:key="f.id"
|
||||
class="py-2 flex items-start justify-between gap-3"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-gray-700 text-sm font-medium">
|
||||
#{{ f.id }}
|
||||
<template v-if="f.contract">
|
||||
·
|
||||
<Link
|
||||
v-if="f.contract.client_case_uuid"
|
||||
:href="
|
||||
safeCaseHref(f.contract.client_case_uuid, f.contract.segment_id)
|
||||
"
|
||||
class="text-indigo-600 hover:underline"
|
||||
>
|
||||
{{ f.contract.reference || f.contract.uuid?.slice(0, 8) }}
|
||||
</Link>
|
||||
<span v-else class="text-gray-700">{{
|
||||
f.contract.reference || f.contract.uuid?.slice(0, 8)
|
||||
}}</span>
|
||||
<span v-if="f.contract.person_full_name" class="text-gray-500">
|
||||
– {{ f.contract.person_full_name }}
|
||||
</span>
|
||||
</template>
|
||||
</p>
|
||||
<p class="text-[11px] text-gray-400">
|
||||
{{ formatJobTime(f.created_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="f.priority"
|
||||
class="text-[10px] px-2 py-0.5 rounded bg-rose-50 text-rose-600"
|
||||
>Prioriteta</span
|
||||
>
|
||||
</div>
|
||||
</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 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Imports In Progress -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
|
||||
Uvozi v teku
|
||||
</h3>
|
||||
<ul v-if="importsInProgress" class="divide-y divide-gray-100 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 truncate">
|
||||
{{ im.file_name }}
|
||||
</p>
|
||||
<span
|
||||
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-600"
|
||||
>{{ im.status }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-gray-100 rounded overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-indigo-500"
|
||||
:style="{ width: (im.progress_pct || 0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-[10px] text-gray-400">
|
||||
{{ 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 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Document Templates -->
|
||||
<div class="bg-white border rounded-xl shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
|
||||
Aktivne predloge dokumentov
|
||||
</h3>
|
||||
<ul v-if="activeTemplates" class="divide-y divide-gray-100 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 font-medium truncate">
|
||||
{{ t.name }}
|
||||
</p>
|
||||
<p class="text-[11px] text-gray-400">
|
||||
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 text-indigo-600 hover:bg-indigo-100"
|
||||
>Uredi</Link
|
||||
>
|
||||
</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 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ...end of right side panels -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import SimpleKpiCard from "./Partials/SimpleKpiCard.vue";
|
||||
import ActivityFeed from "./Partials/ActivityFeed.vue";
|
||||
import SmsOverview from "./Partials/SmsOverview.vue";
|
||||
import CompletedFieldJobsTrend from "./Partials/CompletedFieldJobsTrend.vue";
|
||||
import FieldJobsAssignedToday from "./Partials/FieldJobsAssignedToday.vue";
|
||||
import { Users, FileText, Banknote, CalendarCheck } from "lucide-vue-next";
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
|
||||
const props = defineProps({
|
||||
kpis: Object,
|
||||
@@ -41,6 +41,8 @@ const formatBalance = (amount) => {
|
||||
label="Aktivni stranke"
|
||||
:value="kpis?.active_clients"
|
||||
:icon="Users"
|
||||
icon-bg="bg-chart-2/10"
|
||||
icon-color="text-chart-2"
|
||||
/>
|
||||
<SimpleKpiCard
|
||||
label="Aktivne pogodbe"
|
||||
|
||||
Reference in New Issue
Block a user