341 lines
12 KiB
Vue
341 lines
12 KiB
Vue
<script setup>
|
|
import AppPhoneLayout from "@/Layouts/AppPhoneLayout.vue";
|
|
import { Badge } from "@/Components/ui/badge";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Input } from "@/Components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/Components/ui/select";
|
|
import { Skeleton } from "@/Components/ui/skeleton";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
|
|
import { InfiniteScroll, router } from "@inertiajs/vue3";
|
|
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
import { useDebounceFn } from "@vueuse/core";
|
|
import { CheckCircle2, ClipboardList, SlidersHorizontal } from "lucide-vue-next";
|
|
import JobCard from "@/Pages/Phone/Partials/JobCard.vue";
|
|
|
|
const props = defineProps({
|
|
pendingJobs: { type: Object, default: null },
|
|
processedJobs: { type: Object, default: null },
|
|
completedJobs: { type: Object, default: null },
|
|
clients: { type: Array, default: () => [] },
|
|
view_mode: { type: String, default: "assigned" },
|
|
filters: { type: Object, default: () => ({ search: "", client: "" }) },
|
|
});
|
|
|
|
const isCompleted = computed(() => props.view_mode === "completed-today");
|
|
|
|
// ── Filters ──────────────────────────────────────────────────────────────────
|
|
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 debouncedFilter = useDebounceFn(() => performFilter(), 500);
|
|
watch(search, () => debouncedFilter());
|
|
watch(clientFilter, () => performFilter());
|
|
|
|
function performFilter() {
|
|
isFiltering.value = true;
|
|
const targetRoute = isCompleted.value ? "phone.completed" : "phone.index";
|
|
const only = isCompleted.value
|
|
? ["completedJobs", "filters"]
|
|
: ["pendingJobs", "processedJobs", "filters"];
|
|
|
|
router.get(
|
|
route(targetRoute),
|
|
{
|
|
search: search.value || undefined,
|
|
client: clientFilter.value !== "all" ? clientFilter.value : undefined,
|
|
},
|
|
{
|
|
preserveState: true,
|
|
preserveScroll: false,
|
|
only,
|
|
reset: isCompleted.value ? ["completedJobs"] : ["pendingJobs", "processedJobs"],
|
|
onSuccess: () => {
|
|
isFiltering.value = false;
|
|
},
|
|
onError: () => {
|
|
isFiltering.value = false;
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
function clearFilters() {
|
|
search.value = "";
|
|
clientFilter.value = "all";
|
|
}
|
|
|
|
// ── 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 });
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (stopNavigateListener) stopNavigateListener();
|
|
window.removeEventListener("scroll", onScroll);
|
|
});
|
|
|
|
// ── Counts ───────────────────────────────────────────────────────────────────
|
|
const pendingCount = computed(() => props.pendingJobs?.total ?? 0);
|
|
const processedCount = computed(() => props.processedJobs?.total ?? 0);
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
function getCaseUuid(job) {
|
|
return (
|
|
job?.contract?.client_case?.uuid || job?.client_case?.uuid || job?.case_uuid || null
|
|
);
|
|
}
|
|
|
|
function jobHref(job) {
|
|
const uuid = getCaseUuid(job);
|
|
if (!uuid) return null;
|
|
return route("phone.case", {
|
|
client_case: uuid,
|
|
completed: isCompleted.value ? 1 : undefined,
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AppPhoneLayout title="Phone">
|
|
<template #header>
|
|
<h2
|
|
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>
|
|
|
|
<!-- Filter bar -->
|
|
<div class="pt-2 space-y-2">
|
|
<div class="flex items-center gap-2">
|
|
<div class="relative flex-1">
|
|
<Input
|
|
v-model="search"
|
|
type="text"
|
|
placeholder="Išči po referenci ali imenu..."
|
|
class="pr-10"
|
|
/>
|
|
<span
|
|
v-if="isFiltering"
|
|
class="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin"
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
class="shrink-0"
|
|
:class="
|
|
showFilters || clientFilter !== 'all' ? 'bg-primary/10 border-primary' : ''
|
|
"
|
|
title="Filter po naročniku"
|
|
@click="showFilters = !showFilters"
|
|
>
|
|
<SlidersHorizontal class="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
v-if="search || clientFilter !== 'all'"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="shrink-0 text-muted-foreground"
|
|
@click="clearFilters"
|
|
>
|
|
Počisti
|
|
</Button>
|
|
</div>
|
|
|
|
<div v-if="showFilters || clientFilter !== 'all'">
|
|
<Select v-model="clientFilter">
|
|
<SelectTrigger class="w-full">
|
|
<SelectValue placeholder="Vsi naročniki" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Vsi naročniki</SelectItem>
|
|
<SelectItem
|
|
v-for="client in props.clients"
|
|
:key="client.uuid"
|
|
:value="client.uuid"
|
|
>
|
|
{{ client.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</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 bg-zinc-200/50">
|
|
<TabsTrigger value="pending">
|
|
<div class="flex flex-row items-center gap-1 p-1">
|
|
<ClipboardList :size="16" />
|
|
<span>Novo</span>
|
|
<Badge
|
|
v-if="pendingCount"
|
|
variant="outline"
|
|
class="text-blue-500 font-bold text-sm"
|
|
>
|
|
{{ pendingCount }}
|
|
</Badge>
|
|
</div>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="processed">
|
|
<div class="flex flex-row items-center gap-1 p-1">
|
|
<CheckCircle2 :size="16" />
|
|
Obdelano
|
|
<Badge
|
|
v-if="processedCount"
|
|
variant="outline"
|
|
class="text-green-500 font-bold text-sm"
|
|
>
|
|
{{ processedCount }}
|
|
</Badge>
|
|
</div>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<!-- Pending tab -->
|
|
<TabsContent value="pending" class="space-y-3">
|
|
<InfiniteScroll data="pendingJobs" class="space-y-2" 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-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">
|
|
<InfiniteScroll data="processedJobs" class="space-y-2" 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-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 props.completedJobs.data"
|
|
:key="job.id"
|
|
:job="job"
|
|
:href="jobHref(job)"
|
|
accent-class="border-secondary-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"
|
|
: "Danes ni zaključenih 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>
|
|
</div>
|
|
</div>
|
|
</AppPhoneLayout>
|
|
</template>
|