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'); $search = $request->input('search');
$clientFilter = $request->input('client'); $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 = [ $eagerLoad = [
'contract' => function ($q) { 'contract' => function ($q) {
$q->with([ $q->with([
@ -97,6 +102,11 @@ public function completedToday(Request $request): \Inertia\Response
$search = $request->input('search'); $search = $request->input('search');
$clientFilter = $request->input('client'); $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(); $start = now()->startOfDay();
$end = now()->endOfDay(); $end = now()->endOfDay();

View File

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

View File

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

View File

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