Changes to phone view, fixed infinity scroll issues with page refresh, updated design a bit
This commit is contained in:
parent
8f8c5c5a12
commit
92f54f7103
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 (UCS‑2). V tem
|
7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS‑2). 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 GSM‑7 160 znakov (pri daljših
|
sporočilih 67 znakov na del), medtem ko je pri GSM‑7 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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user