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">
|
<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">E‑pošta</div>
|
<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">
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user