Major change update laravel, inertia v2 -> v3, other changes

This commit is contained in:
Simon Pocrnjič
2026-04-19 13:47:30 +02:00
parent 92f54f7103
commit 054202dc32
15 changed files with 1280 additions and 1167 deletions
+92 -189
View File
@@ -12,8 +12,16 @@ import {
} from "@/Components/ui/select";
import { Skeleton } from "@/Components/ui/skeleton";
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 { InfiniteScroll, router } from "@inertiajs/vue3";
import {
computed,
defineComponent,
h,
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import { useDebounceFn } from "@vueuse/core";
import {
CalendarDays,
@@ -67,8 +75,10 @@ function performFilter() {
preserveState: true,
preserveScroll: false,
only,
reset: isCompleted.value
? ["completedJobs"]
: ["pendingJobs", "processedJobs"],
onSuccess: () => {
resetLists();
isFiltering.value = false;
},
onError: () => {
@@ -83,92 +93,6 @@ function clearFilters() {
clientFilter.value = "all";
}
// ── Infinite scroll lists ────────────────────────────────────────────────────
const pendingList = ref(props.pendingJobs?.data ?? []);
const processedList = ref(props.processedJobs?.data ?? []);
const completedList = ref(props.completedJobs?.data ?? []);
const pendingPage = ref(props.pendingJobs?.current_page ?? 1);
const processedPage = ref(props.processedJobs?.current_page ?? 1);
const completedPage = ref(props.completedJobs?.current_page ?? 1);
const pendingLastPage = ref(props.pendingJobs?.last_page ?? 1);
const processedLastPage = ref(props.processedJobs?.last_page ?? 1);
const completedLastPage = ref(props.completedJobs?.last_page ?? 1);
const loadingPending = ref(false);
const loadingProcessed = ref(false);
const loadingCompleted = ref(false);
const pendingSentinel = ref(null);
const processedSentinel = ref(null);
const completedSentinel = ref(null);
function resetLists() {
pendingList.value = props.pendingJobs?.data ?? [];
processedList.value = props.processedJobs?.data ?? [];
completedList.value = props.completedJobs?.data ?? [];
pendingPage.value = props.pendingJobs?.current_page ?? 1;
processedPage.value = props.processedJobs?.current_page ?? 1;
completedPage.value = props.completedJobs?.current_page ?? 1;
pendingLastPage.value = props.pendingJobs?.last_page ?? 1;
processedLastPage.value = props.processedJobs?.last_page ?? 1;
completedLastPage.value = props.completedJobs?.last_page ?? 1;
clientFilter.value = props.filters.client || "all";
}
function appendUnique(list, newItems) {
const ids = new Set(list.value.map((i) => i.id));
list.value.push(...newItems.filter((i) => !ids.has(i.id)));
}
function buildPageUrl(pageParam, pageNum) {
const params = new URLSearchParams(window.location.search);
params.set(pageParam, pageNum);
return `${window.location.pathname}?${params.toString()}`;
}
function loadMore(listRef, pageRef, lastPageRef, loadingRef, propKey, pageParam) {
if (loadingRef.value) return;
if (pageRef.value >= lastPageRef.value) return;
const nextPage = pageRef.value + 1;
loadingRef.value = true;
router.get(
buildPageUrl(pageParam, nextPage),
{},
{
preserveState: true,
preserveScroll: true,
only: [propKey],
onSuccess: () => {
const newData = props[propKey]?.data ?? [];
appendUnique(listRef, newData);
pageRef.value = nextPage;
lastPageRef.value = props[propKey]?.last_page ?? lastPageRef.value;
loadingRef.value = false;
},
onError: () => {
loadingRef.value = false;
},
}
);
}
function makeObserver(sentinelRef, loadFn) {
const obs = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) loadFn();
},
{ rootMargin: "200px" }
);
if (sentinelRef.value) obs.observe(sentinelRef.value);
return obs;
}
let observers = [];
// ── Scroll-hide title ────────────────────────────────────────────────────────
const scrolled = ref(false);
let stopNavigateListener = null;
@@ -189,43 +113,10 @@ onMounted(() => {
}
});
window.addEventListener("scroll", onScroll, { passive: true });
observers.push(
makeObserver(pendingSentinel, () =>
loadMore(
pendingList,
pendingPage,
pendingLastPage,
loadingPending,
"pendingJobs",
"pending"
)
),
makeObserver(processedSentinel, () =>
loadMore(
processedList,
processedPage,
processedLastPage,
loadingProcessed,
"processedJobs",
"processed"
)
),
makeObserver(completedSentinel, () =>
loadMore(
completedList,
completedPage,
completedLastPage,
loadingCompleted,
"completedJobs",
"completed"
)
)
);
});
onUnmounted(() => {
if (stopNavigateListener) stopNavigateListener();
observers.forEach((o) => o.disconnect());
window.removeEventListener("scroll", onScroll);
});
@@ -531,47 +422,91 @@ const JobCard = defineComponent({
<!-- Pending tab -->
<TabsContent value="pending" class="space-y-3">
<template v-if="pendingList.length">
<JobCard
v-for="job in pendingList"
:key="job.id"
:job="job"
:href="jobHref(job)"
accent-class="border-l-blue-500"
/>
</template>
<div
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" />
<p class="text-sm">
{{
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" />
</div>
<InfiniteScroll data="pendingJobs" only-next>
<template #default="{ loading }">
<template v-if="props.pendingJobs?.data?.length">
<JobCard
v-for="job in props.pendingJobs.data"
:key="job.id"
:job="job"
:href="jobHref(job)"
accent-class="border-l-blue-500"
/>
</template>
<div
v-else-if="!loading"
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" />
<p class="text-sm">
{{
search || clientFilter !== "all" ? "Ni zadetkov" : "Ni novih opravil"
}}
</p>
</div>
</template>
<template #loading>
<div class="space-y-3">
<Skeleton v-for="i in 3" :key="i" class="h-36 rounded-xl" />
</div>
</template>
</InfiniteScroll>
</TabsContent>
<!-- Processed tab -->
<TabsContent value="processed" class="space-y-3">
<template v-if="processedList.length">
<InfiniteScroll data="processedJobs" only-next>
<template #default="{ loading }">
<template v-if="props.processedJobs?.data?.length">
<JobCard
v-for="job in props.processedJobs.data"
:key="job.id"
:job="job"
:href="jobHref(job)"
accent-class="border-l-green-500"
:show-last-activity="true"
/>
</template>
<div
v-else-if="!loading"
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" />
<p class="text-sm">
{{
search || clientFilter !== "all"
? "Ni zadetkov"
: "Ni obdelanih opravil"
}}
</p>
</div>
</template>
<template #loading>
<div class="space-y-3">
<Skeleton v-for="i in 3" :key="i" class="h-36 rounded-xl" />
</div>
</template>
</InfiniteScroll>
</TabsContent>
</Tabs>
</div>
<!-- Completed-today mode: single scroll list -->
<div v-else class="px-4 pt-4 space-y-3">
<InfiniteScroll data="completedJobs" only-next>
<template #default="{ loading }">
<template v-if="props.completedJobs?.data?.length">
<JobCard
v-for="job in processedList"
v-for="job in props.completedJobs.data"
:key="job.id"
:job="job"
:href="jobHref(job)"
accent-class="border-l-green-500"
accent-class="border-l-purple-500"
:show-last-activity="true"
/>
</template>
<div
v-else-if="!loadingProcessed"
v-else-if="!loading"
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" />
@@ -579,49 +514,17 @@ const JobCard = defineComponent({
{{
search || clientFilter !== "all"
? "Ni zadetkov"
: "Ni obdelanih opravil"
: "Danes ni zaključenih opravil"
}}
</p>
</div>
<!-- Sentinel for infinite scroll -->
<div ref="processedSentinel" class="h-px" />
<div v-if="loadingProcessed" class="space-y-3">
</template>
<template #loading>
<div class="space-y-3">
<Skeleton v-for="i in 3" :key="i" class="h-36 rounded-xl" />
</div>
</TabsContent>
</Tabs>
</div>
<!-- Completed-today mode: single scroll list -->
<div v-else class="px-4 pt-4 space-y-3">
<template v-if="completedList.length">
<JobCard
v-for="job in completedList"
:key="job.id"
:job="job"
:href="jobHref(job)"
accent-class="border-l-purple-500"
:show-last-activity="true"
/>
</template>
<div
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" />
<p class="text-sm">
{{
search || clientFilter !== "all"
? "Ni zadetkov"
: "Danes ni zaključenih opravil"
}}
</p>
</div>
<!-- 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" />
</div>
</template>
</InfiniteScroll>
</div>
</div>
</AppPhoneLayout>