298 lines
8.8 KiB
Vue
298 lines
8.8 KiB
Vue
<script setup>
|
||
import { computed, ref, watch } from "vue";
|
||
import {
|
||
MapPin,
|
||
Phone,
|
||
Mail,
|
||
Landmark,
|
||
ChevronDown,
|
||
CheckIcon,
|
||
CircleCheckIcon,
|
||
CircleCheckBigIcon,
|
||
} from "lucide-vue-next";
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
|
||
import Badge from "./ui/badge/Badge.vue";
|
||
|
||
const props = defineProps({
|
||
person: { type: Object, required: true },
|
||
types: { type: Object, default: () => ({}) },
|
||
// Allow overriding the default active tab: 'addresses' | 'phones' | 'emails' | 'bank'
|
||
defaultTab: { type: String, default: "addresses" },
|
||
});
|
||
|
||
const phoneTypes = computed(() => {
|
||
const arr = props.types?.phone_types || [];
|
||
const map = {};
|
||
for (const t of arr) {
|
||
map[t.id] = t.name;
|
||
}
|
||
return map;
|
||
});
|
||
|
||
const displayName = computed(() => {
|
||
const p = props.person || {};
|
||
const full = p.full_name?.trim();
|
||
if (full) {
|
||
return full;
|
||
}
|
||
const first = p.first_name?.trim() || "";
|
||
const last = p.last_name?.trim() || "";
|
||
return `${first} ${last}`.trim();
|
||
});
|
||
|
||
const primaryAddress = computed(() => props.person?.addresses?.[0] || null);
|
||
const primaryEmail = computed(() => props.person?.emails?.[0]?.value || null);
|
||
// Backend phone model uses `nu` as the number
|
||
const allPhones = computed(() => props.person?.phones || []);
|
||
const allAddresses = computed(() => props.person?.addresses || []);
|
||
const allEmails = computed(() => props.person?.emails || []);
|
||
// Laravel serializes relation names to snake_case, so prefer bank_accounts, fallback to bankAccounts
|
||
const allBankAccounts = computed(
|
||
() => props.person?.bank_accounts || props.person?.bankAccounts || []
|
||
);
|
||
// Use the LAST added bank account (assumes incoming order oldest -> newest)
|
||
const bankIban = computed(() => {
|
||
const list = allBankAccounts.value || [];
|
||
if (!list.length) {
|
||
return null;
|
||
}
|
||
return list[list.length - 1]?.iban || null;
|
||
});
|
||
const taxNumber = computed(() => props.person?.tax_number || null);
|
||
const ssn = computed(() => props.person?.social_security_number || null);
|
||
|
||
// Summary sizing
|
||
const showMore = ref(false);
|
||
const summaryPhones = computed(() => allPhones.value.slice(0, showMore.value ? 3 : 1));
|
||
|
||
// Tabs
|
||
// Limit tabs to addresses | phones | emails (TRR tab removed)
|
||
const allowedTabs = ["addresses", "phones", "emails"];
|
||
const initialTab = allowedTabs.includes(props.defaultTab)
|
||
? props.defaultTab
|
||
: "addresses";
|
||
const activeTab = ref(initialTab);
|
||
watch(
|
||
() => props.defaultTab,
|
||
(val) => {
|
||
if (val && allowedTabs.includes(val)) {
|
||
activeTab.value = val;
|
||
}
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<!-- Summary -->
|
||
<div class="text-sm">
|
||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||
<span v-if="primaryAddress" class="pill pill-slate" title="Naslov">
|
||
<MapPin class="w-4 h-4 mr-1" />
|
||
<span class="truncate max-w-36">{{ primaryAddress.address }}</span>
|
||
</span>
|
||
<span v-if="summaryPhones.length" class="pill pill-indigo" title="Telefon">
|
||
<Phone class="w-4 h-4 mr-1" />
|
||
{{ summaryPhones[0].nu
|
||
}}<span
|
||
v-if="
|
||
(summaryPhones[0].type_id && phoneTypes[summaryPhones[0].type_id]) ||
|
||
summaryPhones[0].type?.name
|
||
"
|
||
class="ml-1 text-[10px] opacity-80"
|
||
>({{
|
||
summaryPhones[0].type?.name || phoneTypes[summaryPhones[0].type_id]
|
||
}})</span
|
||
>
|
||
</span>
|
||
<span v-if="primaryEmail && showMore" class="pill pill-default" title="E-pošta">
|
||
<Mail class="w-4 h-4 mr-1" />
|
||
<span class="truncate max-w-36">{{ primaryEmail }}</span>
|
||
</span>
|
||
<span v-if="bankIban" class="pill pill-emerald" title="TRR (zadnji dodan)">
|
||
<Landmark class="w-4 h-4 mr-1" />
|
||
{{ bankIban }}
|
||
</span>
|
||
</div>
|
||
|
||
<transition name="fade">
|
||
<div v-if="showMore" class="mt-3 grid grid-cols-2 gap-x-2 gap-y-2 text-[14px]">
|
||
<div v-if="taxNumber">
|
||
<div class="label">Davčna</div>
|
||
<div class="value font-mono">{{ taxNumber }}</div>
|
||
</div>
|
||
<div v-if="ssn">
|
||
<div class="label">EMŠO</div>
|
||
<div class="value font-mono">{{ ssn }}</div>
|
||
</div>
|
||
<div v-if="bankIban">
|
||
<div class="label">TRR (zadnji)</div>
|
||
<div class="value font-mono">{{ bankIban }}</div>
|
||
</div>
|
||
<div v-if="primaryEmail">
|
||
<div class="label">E‑pošta</div>
|
||
<div class="value truncate">{{ primaryEmail }}</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
<button
|
||
type="button"
|
||
class="mt-3 inline-flex items-center text-[11px] font-medium text-indigo-600 hover:text-indigo-700 focus:outline-none"
|
||
@click="showMore = !showMore"
|
||
>
|
||
<ChevronDown
|
||
:class="[
|
||
'w-3 h-3 mr-1 transition-transform',
|
||
showMore ? 'rotate-180' : 'rotate-0',
|
||
]"
|
||
/>
|
||
{{ showMore ? "Manj podrobnosti" : "Več podrobnosti" }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Segmented Tabs -->
|
||
<div class="mt-4 text-sm">
|
||
<Tabs :default-value="activeTab" @update:model-value="activeTab = $event">
|
||
<TabsList class="w-full">
|
||
<TabsTrigger value="addresses" class="flex-1">
|
||
<div class="flex flex-row items-center gap-1">
|
||
<MapPin class="w-3.5 h-3.5 shrink-0" />
|
||
<span class="truncate">Naslovi ({{ allAddresses.length }})</span>
|
||
</div>
|
||
</TabsTrigger>
|
||
<TabsTrigger value="phones" class="flex-1">
|
||
<div class="flex flex-row items-center gap-1">
|
||
<Phone class="w-3.5 h-3.5 shrink-0" />
|
||
<span class="truncate">Telefoni ({{ allPhones.length }})</span>
|
||
</div>
|
||
</TabsTrigger>
|
||
<TabsTrigger value="emails" class="flex-1">
|
||
<div class="flex flex-row items-center gap-1">
|
||
<Mail class="w-3.5 h-3.5 shrink-0" />
|
||
<span class="truncate">E‑pošta ({{ allEmails.length }})</span>
|
||
</div>
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="addresses" class="mt-2 rounded-md border">
|
||
<div v-if="!allAddresses.length" class="p-2 text-center">Ni naslovov.</div>
|
||
<div
|
||
v-for="(a, idx) in allAddresses"
|
||
:key="a.id || idx"
|
||
class="flex flex-row gap-1 items-center p-2 border-b last:border-0"
|
||
>
|
||
<p class="font-bold wrap-break-word max-w-60">{{ a.address }}</p>
|
||
<Badge v-if="a.country" variant="outline" class="text-xs">{{
|
||
a.country
|
||
}}</Badge>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="phones" class="mt-2 rounded-md border">
|
||
<div v-if="!allPhones.length" class="p-2 text-center">Ni telefonov.</div>
|
||
<div
|
||
v-for="(p, idx) in allPhones"
|
||
:key="p.id || idx"
|
||
class="flex flex-row gap-1 items-center p-2 border-b last:border-0"
|
||
>
|
||
<p class="font-bold wrap-break-word max-w-60">{{ p.nu }}</p>
|
||
|
||
<CircleCheckBigIcon v-if="p.validated" class="text-green-500" :size="16" />
|
||
<Badge variant="outline" v-if="p.label" class="text-xs font-medium">{{
|
||
p.label
|
||
}}</Badge>
|
||
|
||
<Badge
|
||
variant="outline"
|
||
v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name"
|
||
>
|
||
{{ p.type?.name || phoneTypes[p.type_id] }}
|
||
</Badge>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="emails" class="mt-2 rounded-md border">
|
||
<div v-if="!allEmails.length" class="p-2 text-center">Ni e-poštnih naslovov.</div>
|
||
<div
|
||
v-for="(e, idx) in allEmails"
|
||
:key="e.id || idx"
|
||
class="flex flex-row gap-1 items-center p-2 border-b last:border-0"
|
||
>
|
||
<p class="font-bold wrap-break-word max-w-60">
|
||
{{ e.value }}
|
||
</p>
|
||
<CircleCheckBigIcon v-if="e.valid" class="text-green-500" :size="16" />
|
||
<Badge v-if="e.label" variant="outline">({{ e.label }})</Badge>
|
||
</div>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
max-width: 100%;
|
||
border-radius: 9999px;
|
||
padding: 0.35rem 0.75rem;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 1.15;
|
||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
|
||
}
|
||
.pill-slate {
|
||
background: #f1f5f9;
|
||
color: #334155;
|
||
}
|
||
.pill-indigo {
|
||
background: #e0e7ff;
|
||
color: #3730a3;
|
||
}
|
||
.pill-default {
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
}
|
||
.pill-emerald {
|
||
background: #d1fae5;
|
||
color: #047857;
|
||
}
|
||
|
||
.item-row {
|
||
padding: 0.375rem 0;
|
||
border-bottom: 1px dashed #e5e7eb;
|
||
}
|
||
.item-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.sub {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
font-weight: 400;
|
||
}
|
||
.empty {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
font-style: italic;
|
||
}
|
||
|
||
.label {
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
font-size: 10px;
|
||
color: #9ca3af;
|
||
}
|
||
.value {
|
||
margin-top: 0.125rem;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.18s ease;
|
||
}
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
</style>
|