Phone view update
This commit is contained in:
@@ -91,7 +91,7 @@ function maskIban(iban) {
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
<span v-if="primaryAddress" class="pill pill-slate" title="Naslov">
|
||||
<FontAwesomeIcon :icon="faLocationDot" class="w-4 h-4 mr-1" />
|
||||
<span class="truncate max-w-[9rem]">{{ primaryAddress.address }}</span>
|
||||
<span class="truncate max-w-36">{{ primaryAddress.address }}</span>
|
||||
</span>
|
||||
<span v-if="summaryPhones.length" class="pill pill-indigo" title="Telefon">
|
||||
<FontAwesomeIcon :icon="faPhone" class="w-4 h-4 mr-1" />
|
||||
@@ -109,11 +109,11 @@ function maskIban(iban) {
|
||||
</span>
|
||||
<span v-if="primaryEmail && showMore" class="pill pill-default" title="E-pošta">
|
||||
<FontAwesomeIcon :icon="faEnvelope" class="w-4 h-4 mr-1" />
|
||||
<span class="truncate max-w-[9rem]">{{ primaryEmail }}</span>
|
||||
<span class="truncate max-w-36">{{ primaryEmail }}</span>
|
||||
</span>
|
||||
<span v-if="bankIban" class="pill pill-emerald" title="TRR (zadnji dodan)">
|
||||
<FontAwesomeIcon :icon="faLandmark" class="w-4 h-4 mr-1" />
|
||||
{{ maskIban(bankIban) }}
|
||||
{{ bankIban }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,7 @@ function maskIban(iban) {
|
||||
</div>
|
||||
<div v-if="bankIban">
|
||||
<div class="label">TRR (zadnji)</div>
|
||||
<div class="value font-mono">{{ maskIban(bankIban) }}</div>
|
||||
<div class="value font-mono">{{ bankIban }}</div>
|
||||
</div>
|
||||
<div v-if="primaryEmail">
|
||||
<div class="label">E‑pošta</div>
|
||||
|
||||
@@ -224,7 +224,7 @@ const closeSearch = () => (searchOpen.value = false);
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Top bar -->
|
||||
<div
|
||||
class="h-16 bg-white border-b border-gray-200 px-4 flex items-center justify-between sticky top-0 z-30 backdrop-blur-sm bg-white/95 shadow-sm"
|
||||
class="h-16 border-b border-gray-200 px-4 flex items-center justify-between sticky top-0 z-30 backdrop-blur-sm bg-white/95 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Sidebar toggle -->
|
||||
@@ -308,7 +308,10 @@ const closeSearch = () => (searchOpen.value = false);
|
||||
</div>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<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">
|
||||
<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"
|
||||
@@ -319,7 +322,7 @@ const closeSearch = () => (searchOpen.value = false);
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 p-4 sm:p-6">
|
||||
<main class="flex-1 lg:p-4">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -13,27 +13,10 @@ import {
|
||||
import { Skeleton } from "@/Components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
|
||||
import { InfiniteScroll, router } from "@inertiajs/vue3";
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
h,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
ClipboardList,
|
||||
MapPin,
|
||||
Phone,
|
||||
SlidersHorizontal,
|
||||
Wallet,
|
||||
} from "lucide-vue-next";
|
||||
import { fmtDateDMY } from "@/Utilities/functions";
|
||||
import { CheckCircle2, ClipboardList, SlidersHorizontal } from "lucide-vue-next";
|
||||
import JobCard from "@/Pages/Phone/Partials/JobCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
pendingJobs: { type: Object, default: null },
|
||||
@@ -75,9 +58,7 @@ function performFilter() {
|
||||
preserveState: true,
|
||||
preserveScroll: false,
|
||||
only,
|
||||
reset: isCompleted.value
|
||||
? ["completedJobs"]
|
||||
: ["pendingJobs", "processedJobs"],
|
||||
reset: isCompleted.value ? ["completedJobs"] : ["pendingJobs", "processedJobs"],
|
||||
onSuccess: () => {
|
||||
isFiltering.value = false;
|
||||
},
|
||||
@@ -125,16 +106,6 @@ const pendingCount = computed(() => props.pendingJobs?.total ?? 0);
|
||||
const processedCount = computed(() => props.processedJobs?.total ?? 0);
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
function formatAmount(val) {
|
||||
if (val === null || val === undefined) return "0,00";
|
||||
const num = typeof val === "number" ? val : parseFloat(val);
|
||||
if (Number.isNaN(num)) return String(val);
|
||||
return num.toLocaleString("sl-SI", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
function getCaseUuid(job) {
|
||||
return (
|
||||
job?.contract?.client_case?.uuid || job?.client_case?.uuid || job?.case_uuid || null
|
||||
@@ -149,182 +120,6 @@ function jobHref(job) {
|
||||
completed: isCompleted.value ? 1 : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// ── JobCard component ────────────────────────────────────────────────────────
|
||||
const JobCard = defineComponent({
|
||||
name: "JobCard",
|
||||
props: {
|
||||
job: { type: Object, required: true },
|
||||
href: { type: String, default: null },
|
||||
accentClass: { type: String, default: "border-l-blue-500" },
|
||||
showLastActivity: { type: Boolean, default: false },
|
||||
},
|
||||
setup(p) {
|
||||
return () => {
|
||||
const j = p.job;
|
||||
const person = j.contract?.client_case?.person;
|
||||
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;
|
||||
|
||||
const inner = h("div", { class: `border-l-4 ${p.accentClass}` }, [
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
class: "px-4 pt-4 pb-2 flex items-start justify-between gap-3",
|
||||
},
|
||||
[
|
||||
h("div", { class: "flex-1 min-w-0" }, [
|
||||
h(
|
||||
"p",
|
||||
{
|
||||
class:
|
||||
"font-bold text-base text-gray-900 dark:text-gray-100 truncate leading-tight",
|
||||
},
|
||||
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 || "—"
|
||||
),
|
||||
clientName
|
||||
? h(
|
||||
"p",
|
||||
{
|
||||
class:
|
||||
"text-xs text-indigo-600 dark:text-indigo-400 mt-0.5 truncate",
|
||||
},
|
||||
clientName
|
||||
)
|
||||
: null,
|
||||
]),
|
||||
j.priority
|
||||
? 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",
|
||||
},
|
||||
"Prioriteta"
|
||||
)
|
||||
: null,
|
||||
]
|
||||
),
|
||||
address || phone
|
||||
? h("div", { class: "px-4 pb-3 space-y-1.5" }, [
|
||||
address
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "flex items-center gap-2 text-gray-500 dark:text-gray-400",
|
||||
},
|
||||
[
|
||||
h(MapPin, {
|
||||
class: "w-3.5 h-3.5 shrink-0 text-gray-400",
|
||||
}),
|
||||
h("span", { class: "text-xs truncate" }, address),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
phone
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "flex items-center gap-2 text-gray-500 dark:text-gray-400",
|
||||
},
|
||||
[
|
||||
h(Phone, {
|
||||
class: "w-3.5 h-3.5 shrink-0 text-gray-400",
|
||||
}),
|
||||
h("span", { class: "text-xs font-medium" }, phone),
|
||||
]
|
||||
)
|
||||
: null,
|
||||
])
|
||||
: null,
|
||||
balance != null
|
||||
? 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",
|
||||
},
|
||||
[
|
||||
h(Wallet, {
|
||||
class: "w-4 h-4 text-red-500 shrink-0",
|
||||
}),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
class: "font-bold text-red-600 dark:text-red-400 text-sm",
|
||||
},
|
||||
`${formatAmount(balance)} €`
|
||||
),
|
||||
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",
|
||||
},
|
||||
[
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
class: "flex items-center gap-1.5 text-xs text-gray-400",
|
||||
},
|
||||
[
|
||||
h(CalendarDays, { class: "w-3.5 h-3.5" }),
|
||||
h(
|
||||
"span",
|
||||
{},
|
||||
p.showLastActivity && j.last_activity
|
||||
? fmtDateDMY(j.last_activity)
|
||||
: fmtDateDMY(j.assigned_at)
|
||||
),
|
||||
]
|
||||
),
|
||||
p.href
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "flex items-center gap-0.5 text-primary font-semibold text-sm",
|
||||
},
|
||||
["Odpri", h(ChevronRight, { class: "w-4 h-4" })]
|
||||
)
|
||||
: h("span", { class: "text-xs text-gray-400 italic" }, "Manjka primer"),
|
||||
]
|
||||
),
|
||||
]);
|
||||
|
||||
return p.href
|
||||
? h(
|
||||
"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",
|
||||
},
|
||||
inner
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{
|
||||
class:
|
||||
"rounded-xl border shadow-sm overflow-hidden bg-white dark:bg-card opacity-60",
|
||||
},
|
||||
inner
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -399,30 +194,38 @@ const JobCard = defineComponent({
|
||||
<!-- 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">
|
||||
<TabsList class="w-full grid grid-cols-2 bg-zinc-200/50">
|
||||
<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="h-4 px-1 text-xs">
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="processed">
|
||||
<span class="inline-flex flex-row items-center gap-1">
|
||||
<CheckCircle2 class="w-3.5 h-3.5 shrink-0" />
|
||||
<div class="flex flex-row items-center gap-1 p-1">
|
||||
<CheckCircle2 :size="16" />
|
||||
Obdelano
|
||||
<Badge v-if="processedCount" variant="secondary" class="h-4 px-1 text-xs">
|
||||
<Badge
|
||||
v-if="processedCount"
|
||||
variant="outline"
|
||||
class="text-green-500 font-bold text-sm"
|
||||
>
|
||||
{{ processedCount }}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- Pending tab -->
|
||||
<TabsContent value="pending" class="space-y-3">
|
||||
<InfiniteScroll data="pendingJobs" only-next>
|
||||
<InfiniteScroll data="pendingJobs" class="space-y-2" only-next>
|
||||
<template #default="{ loading }">
|
||||
<template v-if="props.pendingJobs?.data?.length">
|
||||
<JobCard
|
||||
@@ -430,17 +233,21 @@ const JobCard = defineComponent({
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
:href="jobHref(job)"
|
||||
accent-class="border-l-blue-500"
|
||||
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" />
|
||||
<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>
|
||||
@@ -455,7 +262,7 @@ const JobCard = defineComponent({
|
||||
|
||||
<!-- Processed tab -->
|
||||
<TabsContent value="processed" class="space-y-3">
|
||||
<InfiniteScroll data="processedJobs" only-next>
|
||||
<InfiniteScroll data="processedJobs" class="space-y-2" only-next>
|
||||
<template #default="{ loading }">
|
||||
<template v-if="props.processedJobs?.data?.length">
|
||||
<JobCard
|
||||
@@ -463,7 +270,7 @@ const JobCard = defineComponent({
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
:href="jobHref(job)"
|
||||
accent-class="border-l-green-500"
|
||||
accent-class="border-green-500"
|
||||
:show-last-activity="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -471,7 +278,9 @@ const JobCard = defineComponent({
|
||||
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" />
|
||||
<CheckCircle2
|
||||
class="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto"
|
||||
/>
|
||||
<p class="text-sm">
|
||||
{{
|
||||
search || clientFilter !== "all"
|
||||
@@ -501,7 +310,7 @@ const JobCard = defineComponent({
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
:href="jobHref(job)"
|
||||
accent-class="border-l-purple-500"
|
||||
accent-class="border-secondary-500"
|
||||
:show-last-activity="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { CalendarDays, ChevronRight, MapPin, Phone, Wallet } from "lucide-vue-next";
|
||||
import { fmtDateDMY } from "@/Utilities/functions";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
job: { type: Object, required: true },
|
||||
href: { type: String, default: null },
|
||||
accentClass: { type: String, default: "border-blue-500" },
|
||||
showLastActivity: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const person = computed(() => props.job.contract?.client_case?.person);
|
||||
const clientName = computed(
|
||||
() => props.job.contract?.client_case?.client?.person?.full_name
|
||||
);
|
||||
const address = computed(() => person.value?.address?.address);
|
||||
const phone = computed(() => person.value?.phones?.[0]?.nu);
|
||||
const balance = computed(() => props.job.contract?.account?.balance_amount);
|
||||
|
||||
function formatAmount(val) {
|
||||
if (val === null || val === undefined) return "0,00";
|
||||
const num = typeof val === "number" ? val : parseFloat(val);
|
||||
if (Number.isNaN(num)) return String(val);
|
||||
return num.toLocaleString("sl-SI", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const dateLabel = computed(() =>
|
||||
props.showLastActivity && props.job.last_activity
|
||||
? fmtDateDMY(props.job.last_activity)
|
||||
: fmtDateDMY(props.job.assigned_at)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="href ? 'a' : 'div'"
|
||||
:href="href ?? undefined"
|
||||
:class="
|
||||
href ? 'block active:scale-[0.99] transition-transform duration-100' : 'opacity-60'
|
||||
"
|
||||
>
|
||||
<Card class="py-0! overflow-hidden gap-2">
|
||||
<CardHeader :class="cn('p-3 py-2! border-b-2', accentClass)">
|
||||
<CardTitle class="flex justify-between items-center">
|
||||
<span class="font-bold">{{ person?.full_name || "—" }}</span>
|
||||
|
||||
<Badge
|
||||
v-if="balance != null"
|
||||
class="bg-error-100 text-error-500 text-sm font-bold flex gap-1 items-center"
|
||||
>
|
||||
<Wallet :size="14" />
|
||||
<span>{{ formatAmount(balance) }} €</span>
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription class="flex gap-1 py-2">
|
||||
<Badge class="font-bold" variant="secondary">{{
|
||||
job.contract?.reference || job.contract?.uuid || "—"
|
||||
}}</Badge>
|
||||
<Badge v-if="clientName">{{ clientName }}</Badge>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="p-3 flex flex-row items-center justify-between gap-2">
|
||||
<div class="flex flex-auto items-center-safe gap-2">
|
||||
<p v-if="address" class="flex items-center gap-1 text-sm border p-1 rounded-md">
|
||||
<MapPin :size="14" class="text-gray-500" />
|
||||
{{ address }}
|
||||
</p>
|
||||
<p v-if="phone" class="flex items-center gap-2 text-sm border p-1 rounded-md">
|
||||
<Phone :size="14" class="text-gray-500" />
|
||||
{{ phone }}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</CardContent>
|
||||
<CardFooter class="bg-gray-50/60 border-t p-3 pt-3!">
|
||||
<div class="flex items-center gap-1 text-sm text-gray-500">
|
||||
<CalendarDays :size="12" />
|
||||
<span>{{ dateLabel }}</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</component>
|
||||
</template>
|
||||
Reference in New Issue
Block a user