Files
Teren-app/resources/js/Components/PersonDetailPhone.vue
T
2026-06-21 19:49:04 +02:00

298 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">Epoš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&#8209;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>