Phone view update

This commit is contained in:
Simon Pocrnjič
2026-06-20 23:42:43 +02:00
parent 8ffc60aba5
commit ea9376c713
4 changed files with 144 additions and 234 deletions
@@ -91,7 +91,7 @@ function maskIban(iban) {
<div class="mt-2 flex flex-wrap gap-1.5"> <div class="mt-2 flex flex-wrap gap-1.5">
<span v-if="primaryAddress" class="pill pill-slate" title="Naslov"> <span v-if="primaryAddress" class="pill pill-slate" title="Naslov">
<FontAwesomeIcon :icon="faLocationDot" class="w-4 h-4 mr-1" /> <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>
<span v-if="summaryPhones.length" class="pill pill-indigo" title="Telefon"> <span v-if="summaryPhones.length" class="pill pill-indigo" title="Telefon">
<FontAwesomeIcon :icon="faPhone" class="w-4 h-4 mr-1" /> <FontAwesomeIcon :icon="faPhone" class="w-4 h-4 mr-1" />
@@ -109,11 +109,11 @@ function maskIban(iban) {
</span> </span>
<span v-if="primaryEmail && showMore" class="pill pill-default" title="E-pošta"> <span v-if="primaryEmail && showMore" class="pill pill-default" title="E-pošta">
<FontAwesomeIcon :icon="faEnvelope" class="w-4 h-4 mr-1" /> <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>
<span v-if="bankIban" class="pill pill-emerald" title="TRR (zadnji dodan)"> <span v-if="bankIban" class="pill pill-emerald" title="TRR (zadnji dodan)">
<FontAwesomeIcon :icon="faLandmark" class="w-4 h-4 mr-1" /> <FontAwesomeIcon :icon="faLandmark" class="w-4 h-4 mr-1" />
{{ maskIban(bankIban) }} {{ bankIban }}
</span> </span>
</div> </div>
@@ -129,7 +129,7 @@ function maskIban(iban) {
</div> </div>
<div v-if="bankIban"> <div v-if="bankIban">
<div class="label">TRR (zadnji)</div> <div class="label">TRR (zadnji)</div>
<div class="value font-mono">{{ maskIban(bankIban) }}</div> <div class="value font-mono">{{ bankIban }}</div>
</div> </div>
<div v-if="primaryEmail"> <div v-if="primaryEmail">
<div class="label">Epošta</div> <div class="label">Epošta</div>
+6 -3
View File
@@ -224,7 +224,7 @@ const closeSearch = () => (searchOpen.value = false);
<div class="flex-1 flex flex-col min-w-0"> <div class="flex-1 flex flex-col min-w-0">
<!-- Top bar --> <!-- Top bar -->
<div <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"> <div class="flex items-center gap-3">
<!-- Sidebar toggle --> <!-- Sidebar toggle -->
@@ -308,7 +308,10 @@ const closeSearch = () => (searchOpen.value = false);
</div> </div>
<!-- Page Heading --> <!-- 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"> <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"
@@ -319,7 +322,7 @@ const closeSearch = () => (searchOpen.value = false);
</header> </header>
<!-- Page Content --> <!-- Page Content -->
<main class="flex-1 p-4 sm:p-6"> <main class="flex-1 lg:p-4">
<slot /> <slot />
</main> </main>
</div> </div>
+36 -227
View File
@@ -13,27 +13,10 @@ import {
import { Skeleton } from "@/Components/ui/skeleton"; import { Skeleton } from "@/Components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
import { InfiniteScroll, router } from "@inertiajs/vue3"; import { InfiniteScroll, router } from "@inertiajs/vue3";
import { import { computed, 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 { CheckCircle2, ClipboardList, SlidersHorizontal } from "lucide-vue-next";
CalendarDays, import JobCard from "@/Pages/Phone/Partials/JobCard.vue";
CheckCircle2,
ChevronRight,
ClipboardList,
MapPin,
Phone,
SlidersHorizontal,
Wallet,
} from "lucide-vue-next";
import { fmtDateDMY } from "@/Utilities/functions";
const props = defineProps({ const props = defineProps({
pendingJobs: { type: Object, default: null }, pendingJobs: { type: Object, default: null },
@@ -75,9 +58,7 @@ function performFilter() {
preserveState: true, preserveState: true,
preserveScroll: false, preserveScroll: false,
only, only,
reset: isCompleted.value reset: isCompleted.value ? ["completedJobs"] : ["pendingJobs", "processedJobs"],
? ["completedJobs"]
: ["pendingJobs", "processedJobs"],
onSuccess: () => { onSuccess: () => {
isFiltering.value = false; isFiltering.value = false;
}, },
@@ -125,16 +106,6 @@ const pendingCount = computed(() => props.pendingJobs?.total ?? 0);
const processedCount = computed(() => props.processedJobs?.total ?? 0); const processedCount = computed(() => props.processedJobs?.total ?? 0);
// ── Helpers ────────────────────────────────────────────────────────────────── // ── 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) { function getCaseUuid(job) {
return ( return (
job?.contract?.client_case?.uuid || job?.client_case?.uuid || job?.case_uuid || null 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, 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> </script>
<template> <template>
@@ -399,30 +194,38 @@ const JobCard = defineComponent({
<!-- 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 bg-zinc-200/50">
<TabsTrigger value="pending"> <TabsTrigger value="pending">
<span class="inline-flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-1 p-1">
<ClipboardList class="w-3.5 h-3.5 shrink-0" /> <ClipboardList :size="16" />
Novo <span>Novo</span>
<Badge v-if="pendingCount" variant="secondary" class="h-4 px-1 text-xs"> <Badge
v-if="pendingCount"
variant="outline"
class="text-blue-500 font-bold text-sm"
>
{{ pendingCount }} {{ pendingCount }}
</Badge> </Badge>
</span> </div>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="processed"> <TabsTrigger value="processed">
<span class="inline-flex flex-row items-center gap-1"> <div class="flex flex-row items-center gap-1 p-1">
<CheckCircle2 class="w-3.5 h-3.5 shrink-0" /> <CheckCircle2 :size="16" />
Obdelano 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 }} {{ processedCount }}
</Badge> </Badge>
</span> </div>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<!-- Pending tab --> <!-- Pending tab -->
<TabsContent value="pending" class="space-y-3"> <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 #default="{ loading }">
<template v-if="props.pendingJobs?.data?.length"> <template v-if="props.pendingJobs?.data?.length">
<JobCard <JobCard
@@ -430,17 +233,21 @@ const JobCard = defineComponent({
:key="job.id" :key="job.id"
:job="job" :job="job"
:href="jobHref(job)" :href="jobHref(job)"
accent-class="border-l-blue-500" accent-class="border-blue-500"
/> />
</template> </template>
<div <div
v-else-if="!loading" v-else-if="!loading"
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 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"> <p class="text-sm">
{{ {{
search || clientFilter !== "all" ? "Ni zadetkov" : "Ni novih opravil" search || clientFilter !== "all"
? "Ni zadetkov"
: "Ni novih opravil"
}} }}
</p> </p>
</div> </div>
@@ -455,7 +262,7 @@ const JobCard = defineComponent({
<!-- Processed tab --> <!-- Processed tab -->
<TabsContent value="processed" class="space-y-3"> <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 #default="{ loading }">
<template v-if="props.processedJobs?.data?.length"> <template v-if="props.processedJobs?.data?.length">
<JobCard <JobCard
@@ -463,7 +270,7 @@ const JobCard = defineComponent({
:key="job.id" :key="job.id"
:job="job" :job="job"
:href="jobHref(job)" :href="jobHref(job)"
accent-class="border-l-green-500" accent-class="border-green-500"
:show-last-activity="true" :show-last-activity="true"
/> />
</template> </template>
@@ -471,7 +278,9 @@ const JobCard = defineComponent({
v-else-if="!loading" v-else-if="!loading"
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 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"> <p class="text-sm">
{{ {{
search || clientFilter !== "all" search || clientFilter !== "all"
@@ -501,7 +310,7 @@ const JobCard = defineComponent({
:key="job.id" :key="job.id"
:job="job" :job="job"
:href="jobHref(job)" :href="jobHref(job)"
accent-class="border-l-purple-500" accent-class="border-secondary-500"
:show-last-activity="true" :show-last-activity="true"
/> />
</template> </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>