Changes to phone view, fixed infinity scroll issues with page refresh, updated design a bit

This commit is contained in:
Simon Pocrnjič 2026-04-18 12:28:15 +02:00
parent 8f8c5c5a12
commit 92f54f7103
5 changed files with 727 additions and 753 deletions

View File

@ -17,6 +17,11 @@ public function index(Request $request): \Inertia\Response
$search = $request->input('search');
$clientFilter = $request->input('client');
// On full page loads, always start from page 1
if (! $request->header('X-Inertia-Partial-Data')) {
$request->merge(['pending' => 1, 'processed' => 1]);
}
$eagerLoad = [
'contract' => function ($q) {
$q->with([
@ -97,6 +102,11 @@ public function completedToday(Request $request): \Inertia\Response
$search = $request->input('search');
$clientFilter = $request->input('client');
// On full page loads, always start from page 1
if (! $request->header('X-Inertia-Partial-Data')) {
$request->merge(['completed' => 1]);
}
$start = now()->startOfDay();
$end = now()->endOfDay();

View File

@ -25,6 +25,7 @@ import {
} from "@/Components/ui/select";
import { Switch } from "@/Components/ui/switch";
import { Button } from "@/Components/ui/button";
import { ScrollArea } from "@/Components/ui/scroll-area";
const props = defineProps({
show: { type: Boolean, default: false },
@ -452,7 +453,8 @@ const open = computed({
</DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit" class="space-y-4">
<ScrollArea class="max-h-[65vh] pr-1">
<form @submit.prevent="onSubmit" class="space-y-4 pr-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField v-slot="{ value, handleChange }" name="profile_id">
<FormItem>
@ -582,8 +584,8 @@ const open = computed({
</span>
</div>
<p class="text-[11px] text-gray-500 leading-snug">
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake, ki
ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake,
ki ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS2). V tem
primeru je največja dolžina enega SMS sporočila 70 znakov (pri daljših
sporočilih 67 znakov na del), medtem ko je pri GSM7 160 znakov (pri daljših
@ -604,6 +606,7 @@ const open = computed({
</FormItem>
</FormField>
</form>
</ScrollArea>
<DialogFooter>
<Button variant="outline" @click="closeSmsDialog" :disabled="processing">

View File

@ -308,7 +308,7 @@ const closeSearch = () => (searchOpen.value = false);
</div>
<!-- Page Heading -->
<header v-if="$slots.header" class="bg-white border-b border-gray-200 shadow-sm">
<header v-if="$slots.header" class="sticky top-16 z-20 bg-white border-b border-gray-200 shadow-sm dark:bg-gray-900 dark:border-gray-700">
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
<Breadcrumbs
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"

View File

@ -18,10 +18,11 @@ import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
import { Separator } from "@/Components/ui/separator";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/Components/ui/collapsible";
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/Components/ui/accordion";
import {
Dialog,
DialogContent,
@ -46,7 +47,6 @@ import { Checkbox } from "@/Components/ui/checkbox";
import {
ArrowLeft,
CheckCircle2,
ChevronDown,
FileText,
Calendar,
Euro,
@ -321,7 +321,7 @@ const clientSummary = computed(() => {
<div class="py-4 sm:py-6">
<div class="mx-auto max-w-5xl px-2 sm:px-4 space-y-4">
<!-- Client details (account holder) -->
<Card>
<Card class="gap-3">
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Building2 class="w-5 h-5 text-gray-500" />
@ -340,8 +340,8 @@ const clientSummary = computed(() => {
</Card>
<!-- Person (case person) -->
<Card>
<CardHeader>
<Card class="gap-3">
<CardHeader class="px-3">
<CardTitle class="flex items-center gap-2 text-base">
<User class="w-5 h-5 text-gray-500" />
<span class="truncate">{{ client_case.person.full_name }}</span>
@ -354,7 +354,7 @@ const clientSummary = computed(() => {
{{ client_case.person.description }}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent class="px-3">
<Separator class="mb-4" />
<PersonDetailPhone
:types="types"
@ -365,35 +365,32 @@ const clientSummary = computed(() => {
</Card>
<!-- Contracts assigned to me -->
<Card>
<CardHeader>
<Card class="p-0 pt-3 gap-1">
<CardHeader class="px-4">
<CardTitle class="flex items-center gap-2">
<FileText class="w-5 h-5" />
Pogodbe
</CardTitle>
</CardHeader>
<CardContent class="space-y-3">
<CardContent class="p-2">
<Card
v-for="c in contracts"
:key="c.uuid || c.id"
class="border-l-4 border-l-indigo-500 overflow-hidden"
class="overflow-hidden p-0 gap-3"
>
<!-- Contract header: reference + type badge -->
<CardHeader class="pb-2">
<div class="flex items-center gap-2 flex-wrap">
<CardHeader class="p-3 pb-2">
<div class="flex items-center flex-wrap">
<CardTitle class="text-base font-semibold">
{{ c.reference || c.uuid }}
{{ c.reference || "Šifra pogodbe ni določena" }}
</CardTitle>
<Badge v-if="c.type?.name" variant="secondary" class="text-[11px]">
{{ c.type.name }}
</Badge>
</div>
</CardHeader>
<!-- Balance row -->
<div
v-if="c.account"
class="mx-4 mb-3 rounded-xl bg-red-50 dark:bg-red-950/20 border border-red-100 dark:border-red-900 px-4 py-3 flex items-center justify-between"
class="mx-3 rounded-xl bg-red-50 dark:bg-red-950/20 border border-red-100 dark:border-red-900 px-2 py-2 flex items-center justify-between"
>
<div class="flex items-center gap-2 text-red-500">
<Euro class="w-4 h-4 shrink-0" />
@ -413,50 +410,50 @@ const clientSummary = computed(() => {
v-if="
c.description || c.last_object || (c.meta && Object.keys(c.meta).length)
"
class="pt-0 px-4 space-y-0"
class="pt-0 px-0 space-y-0"
>
<!-- Description -->
<template v-if="c.description">
<Separator class="mb-3" />
<Collapsible>
<CollapsibleTrigger
class="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 group w-full py-1"
<!-- Description + Meta Accordion -->
<template v-if="c.description || (c.meta && Object.keys(c.meta).length)">
<Separator />
<Accordion type="multiple" class="w-full">
<AccordionItem
v-if="c.description"
value="description"
class="border-b-0"
>
<ChevronDown
class="w-3.5 h-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180 shrink-0"
/>
<span class="uppercase tracking-wide font-medium">Opis</span>
</CollapsibleTrigger>
<CollapsibleContent>
<AccordionTrigger
class="px-3 py-2 text-xs font-medium uppercase tracking-wide hover:no-underline"
>
Opis
</AccordionTrigger>
<AccordionContent class="px-3 pb-3">
<p
class="mt-1.5 mb-2 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line rounded-lg bg-gray-50 dark:bg-gray-800/50 px-3 py-2.5"
class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line rounded-lg bg-gray-50 dark:bg-gray-800/50 px-3 py-2.5"
>
{{ c.description }}
</p>
</CollapsibleContent>
</Collapsible>
</template>
<!-- Meta -->
<template v-if="c.meta && Object.keys(c.meta).length">
<Separator class="mb-3" :class="c.description ? 'mt-2' : 'mt-0'" />
<Collapsible>
<CollapsibleTrigger
class="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 group w-full py-1"
</AccordionContent>
</AccordionItem>
<AccordionItem
v-if="c.meta && Object.keys(c.meta).length"
value="meta"
class="border-b-0"
:class="c.description ? 'border-t' : ''"
>
<ChevronDown
class="w-3.5 h-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180 shrink-0"
/>
<span class="uppercase tracking-wide font-medium"
>Dodatni podatki</span
<AccordionTrigger
class="px-3 py-2 text-xs font-medium uppercase tracking-wide hover:no-underline"
>
<span class="ml-auto text-gray-400 font-normal">{{
Object.keys(c.meta).length
}}</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div>
<span class="mr-1">Dodatni podatki</span>
<Badge
class="bg-blue-500 text-white dark:bg-blue-600 h-5 min-w-5 rounded-full px-2 font-mono tabular-nums"
>{{ Object.keys(c.meta).length }}</Badge
>
</div>
</AccordionTrigger>
<AccordionContent class="pb-2">
<div
class="mt-1.5 mb-2 divide-y divide-gray-100 dark:divide-gray-700 rounded-lg border border-gray-100 dark:border-gray-700 overflow-hidden"
class="divide-y divide-gray-100 dark:divide-gray-700 rounded-lg border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div
v-for="(val, key) in c.meta"
@ -480,8 +477,9 @@ const clientSummary = computed(() => {
</span>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</AccordionContent>
</AccordionItem>
</Accordion>
</template>
<!-- Last object -->

View File

@ -11,22 +11,9 @@ import {
SelectValue,
} from "@/Components/ui/select";
import { Skeleton } from "@/Components/ui/skeleton";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/Components/ui/tabs";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
import { router } from "@inertiajs/vue3";
import {
computed,
defineComponent,
h,
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import { computed, defineComponent, h, onMounted, onUnmounted, ref, watch } from "vue";
import { useDebounceFn } from "@vueuse/core";
import {
CalendarDays,
@ -55,7 +42,9 @@ const isCompleted = computed(() => props.view_mode === "completed-today");
const search = ref(props.filters.search || "");
const clientFilter = ref(props.filters.client || "all");
const isFiltering = ref(false);
const showFilters = ref(!!(props.filters.search || (props.filters.client && props.filters.client !== "all")));
const showFilters = ref(
!!(props.filters.search || (props.filters.client && props.filters.client !== "all"))
);
const debouncedFilter = useDebounceFn(() => performFilter(), 500);
watch(search, () => debouncedFilter());
@ -85,7 +74,7 @@ function performFilter() {
onError: () => {
isFiltering.value = false;
},
},
}
);
}
@ -157,14 +146,13 @@ function loadMore(listRef, pageRef, lastPageRef, loadingRef, propKey, pageParam)
const newData = props[propKey]?.data ?? [];
appendUnique(listRef, newData);
pageRef.value = nextPage;
lastPageRef.value =
props[propKey]?.last_page ?? lastPageRef.value;
lastPageRef.value = props[propKey]?.last_page ?? lastPageRef.value;
loadingRef.value = false;
},
onError: () => {
loadingRef.value = false;
},
},
}
);
}
@ -173,7 +161,7 @@ function makeObserver(sentinelRef, loadFn) {
(entries) => {
if (entries[0].isIntersecting) loadFn();
},
{ rootMargin: "200px" },
{ rootMargin: "200px" }
);
if (sentinelRef.value) obs.observe(sentinelRef.value);
return obs;
@ -181,7 +169,26 @@ function makeObserver(sentinelRef, loadFn) {
let observers = [];
// Scroll-hide title
const scrolled = ref(false);
let stopNavigateListener = null;
function onScroll() {
if (!scrolled.value && window.scrollY > 50) {
scrolled.value = true;
} else if (scrolled.value && window.scrollY < 10) {
scrolled.value = false;
}
}
onMounted(() => {
let trackedPath = window.location.pathname;
stopNavigateListener = router.on("navigate", (event) => {
const newPath = new URL(event.detail.page.url, window.location.origin).pathname;
if (newPath !== trackedPath) {
scrolled.value = false;
trackedPath = newPath;
}
});
window.addEventListener("scroll", onScroll, { passive: true });
observers.push(
makeObserver(pendingSentinel, () =>
loadMore(
@ -190,8 +197,8 @@ onMounted(() => {
pendingLastPage,
loadingPending,
"pendingJobs",
"pending",
),
"pending"
)
),
makeObserver(processedSentinel, () =>
loadMore(
@ -200,8 +207,8 @@ onMounted(() => {
processedLastPage,
loadingProcessed,
"processedJobs",
"processed",
),
"processed"
)
),
makeObserver(completedSentinel, () =>
loadMore(
@ -210,14 +217,16 @@ onMounted(() => {
completedLastPage,
loadingCompleted,
"completedJobs",
"completed",
),
),
"completed"
)
)
);
});
onUnmounted(() => {
if (stopNavigateListener) stopNavigateListener();
observers.forEach((o) => o.disconnect());
window.removeEventListener("scroll", onScroll);
});
// Counts
@ -237,10 +246,7 @@ function formatAmount(val) {
function getCaseUuid(job) {
return (
job?.contract?.client_case?.uuid ||
job?.client_case?.uuid ||
job?.case_uuid ||
null
job?.contract?.client_case?.uuid || job?.client_case?.uuid || job?.case_uuid || null
);
}
@ -266,8 +272,7 @@ const JobCard = defineComponent({
return () => {
const j = p.job;
const person = j.contract?.client_case?.person;
const clientName =
j.contract?.client_case?.client?.person?.full_name;
const clientName = j.contract?.client_case?.client?.person?.full_name;
const address = person?.address?.address;
const phone = person?.phones?.[0]?.nu;
const balance = j.contract?.account?.balance_amount;
@ -283,26 +288,26 @@ const JobCard = defineComponent({
h(
"p",
{
class: "font-bold text-base text-gray-900 dark:text-gray-100 truncate leading-tight",
class:
"font-bold text-base text-gray-900 dark:text-gray-100 truncate leading-tight",
},
person?.full_name || "—",
person?.full_name || "—"
),
h(
"p",
{
class: "text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate",
},
j.contract?.reference ||
j.contract?.uuid ||
"—",
j.contract?.reference || j.contract?.uuid || "—"
),
clientName
? h(
"p",
{
class: "text-xs text-indigo-600 dark:text-indigo-400 mt-0.5 truncate",
class:
"text-xs text-indigo-600 dark:text-indigo-400 mt-0.5 truncate",
},
clientName,
clientName
)
: null,
]),
@ -310,12 +315,13 @@ const JobCard = defineComponent({
? h(
"span",
{
class: "shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400 animate-pulse",
class:
"shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400 animate-pulse",
},
"Prioriteta",
"Prioriteta"
)
: null,
],
]
),
address || phone
? h("div", { class: "px-4 pb-3 space-y-1.5" }, [
@ -329,12 +335,8 @@ const JobCard = defineComponent({
h(MapPin, {
class: "w-3.5 h-3.5 shrink-0 text-gray-400",
}),
h(
"span",
{ class: "text-xs truncate" },
address,
),
],
h("span", { class: "text-xs truncate" }, address),
]
)
: null,
phone
@ -347,12 +349,8 @@ const JobCard = defineComponent({
h(Phone, {
class: "w-3.5 h-3.5 shrink-0 text-gray-400",
}),
h(
"span",
{ class: "text-xs font-medium" },
phone,
),
],
h("span", { class: "text-xs font-medium" }, phone),
]
)
: null,
])
@ -361,7 +359,8 @@ const JobCard = defineComponent({
? h(
"div",
{
class: "mx-4 mb-3 px-3 py-2 bg-red-50 dark:bg-red-950/20 rounded-lg flex items-center gap-2 border border-red-100 dark:border-red-900",
class:
"mx-4 mb-3 px-3 py-2 bg-red-50 dark:bg-red-950/20 rounded-lg flex items-center gap-2 border border-red-100 dark:border-red-900",
},
[
h(Wallet, {
@ -372,20 +371,17 @@ const JobCard = defineComponent({
{
class: "font-bold text-red-600 dark:text-red-400 text-sm",
},
`${formatAmount(balance)}`,
`${formatAmount(balance)}`
),
h(
"span",
{ class: "text-xs text-red-400" },
"odprto",
),
],
h("span", { class: "text-xs text-red-400" }, "odprto"),
]
)
: null,
h(
"div",
{
class: "px-4 py-3 border-t bg-gray-50/60 dark:bg-gray-900/40 flex items-center justify-between",
class:
"px-4 py-3 border-t bg-gray-50/60 dark:bg-gray-900/40 flex items-center justify-between",
},
[
h(
@ -400,9 +396,9 @@ const JobCard = defineComponent({
{},
p.showLastActivity && j.last_activity
? fmtDateDMY(j.last_activity)
: fmtDateDMY(j.assigned_at),
: fmtDateDMY(j.assigned_at)
),
],
]
),
p.href
? h(
@ -410,17 +406,10 @@ const JobCard = defineComponent({
{
class: "flex items-center gap-0.5 text-primary font-semibold text-sm",
},
[
"Odpri",
h(ChevronRight, { class: "w-4 h-4" }),
],
["Odpri", h(ChevronRight, { class: "w-4 h-4" })]
)
: h(
"span",
{ class: "text-xs text-gray-400 italic" },
"Manjka primer",
),
],
: h("span", { class: "text-xs text-gray-400 italic" }, "Manjka primer"),
]
),
]);
@ -429,16 +418,18 @@ const JobCard = defineComponent({
"a",
{
href: p.href,
class: "block rounded-xl border shadow-sm overflow-hidden bg-white dark:bg-card active:scale-[0.99] transition-transform duration-100",
class:
"block rounded-xl border shadow-sm overflow-hidden bg-white dark:bg-card active:scale-[0.99] transition-transform duration-100",
},
inner,
inner
)
: h(
"div",
{
class: "rounded-xl border shadow-sm overflow-hidden bg-white dark:bg-card opacity-60",
class:
"rounded-xl border shadow-sm overflow-hidden bg-white dark:bg-card opacity-60",
},
inner,
inner
);
};
},
@ -449,17 +440,14 @@ const JobCard = defineComponent({
<AppPhoneLayout title="Phone">
<template #header>
<h2
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight overflow-hidden transition-all duration-200 ease-in-out"
:class="scrolled ? 'max-h-0 opacity-0 mb-0' : 'max-h-12 opacity-100 mb-0'"
>
{{ isCompleted ? "Zaključeno danes" : "Terenska opravila" }}
</h2>
</template>
<div class="pb-8">
<!-- Filter bar -->
<div
class="sticky top-16 z-20 bg-white/95 dark:bg-gray-950/95 backdrop-blur border-b px-4 py-3 space-y-3"
>
<div class="pt-2 space-y-2">
<div class="flex items-center gap-2">
<div class="relative flex-1">
<Input
@ -478,9 +466,7 @@ const JobCard = defineComponent({
size="icon"
class="shrink-0"
:class="
showFilters || clientFilter !== 'all'
? 'bg-primary/10 border-primary'
: ''
showFilters || clientFilter !== 'all' ? 'bg-primary/10 border-primary' : ''
"
title="Filter po naročniku"
@click="showFilters = !showFilters"
@ -498,7 +484,7 @@ const JobCard = defineComponent({
</Button>
</div>
<div v-if="showFilters || clientFilter !== 'all'" class="pb-1">
<div v-if="showFilters || clientFilter !== 'all'">
<Select v-model="clientFilter">
<SelectTrigger class="w-full">
<SelectValue placeholder="Vsi naročniki" />
@ -516,32 +502,30 @@ const JobCard = defineComponent({
</Select>
</div>
</div>
</template>
<div class="pb-8">
<!-- Assigned mode: two tabs (Novo / Obdelano) -->
<div v-if="!isCompleted" class="px-4 pt-4">
<Tabs default-value="pending" class="w-full">
<TabsList class="w-full grid grid-cols-2 mb-4">
<TabsTrigger value="pending" class="gap-1.5">
<ClipboardList class="w-4 h-4" />
<TabsTrigger value="pending">
<span class="inline-flex flex-row items-center gap-1">
<ClipboardList class="w-3.5 h-3.5 shrink-0" />
Novo
<Badge
v-if="pendingCount"
variant="secondary"
class="ml-1 h-5 px-1.5 text-xs"
>
<Badge v-if="pendingCount" variant="secondary" class="h-4 px-1 text-xs">
{{ pendingCount }}
</Badge>
</span>
</TabsTrigger>
<TabsTrigger value="processed" class="gap-1.5">
<CheckCircle2 class="w-4 h-4" />
<TabsTrigger value="processed">
<span class="inline-flex flex-row items-center gap-1">
<CheckCircle2 class="w-3.5 h-3.5 shrink-0" />
Obdelano
<Badge
v-if="processedCount"
variant="secondary"
class="ml-1 h-5 px-1.5 text-xs"
>
<Badge v-if="processedCount" variant="secondary" class="h-4 px-1 text-xs">
{{ processedCount }}
</Badge>
</span>
</TabsTrigger>
</TabsList>
@ -560,25 +544,17 @@ const JobCard = defineComponent({
v-else-if="!loadingPending"
class="py-16 text-center text-gray-500 dark:text-gray-400 space-y-2"
>
<ClipboardList
class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto"
/>
<ClipboardList class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto" />
<p class="text-sm">
{{
search || clientFilter !== 'all'
? "Ni zadetkov"
: "Ni novih opravil"
search || clientFilter !== "all" ? "Ni zadetkov" : "Ni novih opravil"
}}
</p>
</div>
<!-- Sentinel for infinite scroll -->
<div ref="pendingSentinel" class="h-px" />
<div v-if="loadingPending" class="space-y-3">
<Skeleton
v-for="i in 3"
:key="i"
class="h-36 rounded-xl"
/>
<Skeleton v-for="i in 3" :key="i" class="h-36 rounded-xl" />
</div>
</TabsContent>
@ -598,12 +574,10 @@ const JobCard = defineComponent({
v-else-if="!loadingProcessed"
class="py-16 text-center text-gray-500 dark:text-gray-400 space-y-2"
>
<CheckCircle2
class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto"
/>
<CheckCircle2 class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto" />
<p class="text-sm">
{{
search || clientFilter !== 'all'
search || clientFilter !== "all"
? "Ni zadetkov"
: "Ni obdelanih opravil"
}}
@ -612,11 +586,7 @@ const JobCard = defineComponent({
<!-- Sentinel for infinite scroll -->
<div ref="processedSentinel" class="h-px" />
<div v-if="loadingProcessed" class="space-y-3">
<Skeleton
v-for="i in 3"
:key="i"
class="h-36 rounded-xl"
/>
<Skeleton v-for="i in 3" :key="i" class="h-36 rounded-xl" />
</div>
</TabsContent>
</Tabs>
@ -638,12 +608,10 @@ const JobCard = defineComponent({
v-else-if="!loadingCompleted"
class="py-16 text-center text-gray-500 dark:text-gray-400 space-y-2"
>
<CheckCircle2
class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto"
/>
<CheckCircle2 class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto" />
<p class="text-sm">
{{
search || clientFilter !== 'all'
search || clientFilter !== "all"
? "Ni zadetkov"
: "Danes ni zaključenih opravil"
}}
@ -652,14 +620,9 @@ const JobCard = defineComponent({
<!-- Sentinel for infinite scroll -->
<div ref="completedSentinel" class="h-px" />
<div v-if="loadingCompleted" class="space-y-3">
<Skeleton
v-for="i in 3"
:key="i"
class="h-36 rounded-xl"
/>
<Skeleton v-for="i in 3" :key="i" class="h-36 rounded-xl" />
</div>
</div>
</div>
</AppPhoneLayout>
</template>