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">
<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">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">
<!-- 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>
+36 -227
View File
@@ -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>