Teren-app/resources/js/Components/PersonDetailPhone.vue
2025-10-05 19:45:49 +02:00

324 lines
9.5 KiB
Vue
Raw Permalink 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 { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
faLocationDot,
faPhone,
faEnvelope,
faLandmark,
faChevronDown,
} from "@fortawesome/free-solid-svg-icons";
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;
}
}
);
function maskIban(iban) {
if (!iban || typeof iban !== "string") return null;
const clean = iban.replace(/\s+/g, "");
if (clean.length <= 8) return clean;
return `${clean.slice(0, 4)} **** **** ${clean.slice(-4)}`;
}
</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">
<FontAwesomeIcon :icon="faLocationDot" class="w-4 h-4 mr-1" />
<span class="truncate max-w-[9rem]">{{ 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" />
{{ 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">
<FontAwesomeIcon :icon="faEnvelope" class="w-4 h-4 mr-1" />
<span class="truncate max-w-[9rem]">{{ 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) }}
</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">{{ maskIban(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"
>
<FontAwesomeIcon
:icon="faChevronDown"
: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-5">
<div class="relative">
<div
class="flex w-full text-[11px] font-medium rounded-lg border bg-gray-50 overflow-hidden"
>
<button
type="button"
@click="activeTab = 'addresses'"
:class="['seg-btn', activeTab === 'addresses' && 'seg-active']"
>
<FontAwesomeIcon :icon="faLocationDot" class="w-3.5 h-3.5 mr-1 shrink-0" />
<span class="truncate">Naslovi ({{ allAddresses.length }})</span>
</button>
<button
type="button"
@click="activeTab = 'phones'"
:class="['seg-btn', activeTab === 'phones' && 'seg-active']"
>
<FontAwesomeIcon :icon="faPhone" class="w-3.5 h-3.5 mr-1 shrink-0" />
<span class="truncate">Telefoni ({{ allPhones.length }})</span>
</button>
<button
type="button"
@click="activeTab = 'emails'"
:class="['seg-btn', activeTab === 'emails' && 'seg-active']"
>
<FontAwesomeIcon :icon="faEnvelope" class="w-3.5 h-3.5 mr-1 shrink-0" />
<span class="truncate">Epošta ({{ allEmails.length }})</span>
</button>
</div>
</div>
<div class="mt-3 rounded-md border bg-white/60 p-2">
<!-- Addresses -->
<div v-if="activeTab === 'addresses'">
<div v-if="!allAddresses.length" class="empty">Ni naslovov.</div>
<div v-for="(a, idx) in allAddresses" :key="a.id || idx" class="item-row">
<div class="font-medium text-gray-800">{{ a.address }}</div>
<div v-if="a.country" class="sub">{{ a.country }}</div>
</div>
</div>
<!-- Phones -->
<div v-else-if="activeTab === 'phones'">
<div v-if="!allPhones.length" class="empty">Ni telefonov.</div>
<div v-for="(p, idx) in allPhones" :key="p.id || idx" class="item-row">
<div class="font-medium text-gray-800">
{{ p.nu }}
<span
v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name"
class="sub ml-1"
>({{ p.type?.name || phoneTypes[p.type_id] }})</span
>
</div>
</div>
</div>
<!-- Emails -->
<div v-else-if="activeTab === 'emails'">
<div v-if="!allEmails.length" class="empty">Ni e-poštnih naslovov.</div>
<div v-for="(e, idx) in allEmails" :key="e.id || idx" class="item-row">
<div class="font-medium text-gray-800">
{{ e.value }}<span v-if="e.label" class="sub ml-1">({{ e.label }})</span>
</div>
</div>
</div>
<!-- (TRR tab removed; last bank account surfaced in summary) -->
</div>
</div>
</template>
<style scoped>
/* Basic utility replacements (no Tailwind processor here) */
.pill {
display: inline-flex;
align-items: center;
max-width: 100%;
border-radius: 9999px;
padding: 0.35rem 0.75rem; /* slightly larger */
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;
}
.seg-btn {
flex: 1 1 0;
min-width: 0; /* allow flex item to shrink below intrinsic size */
white-space: nowrap;
padding: 0.5rem 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
border-right: 1px solid #e5e7eb;
font-size: 11px;
background: transparent;
color: #4b5563;
transition: background 0.15s, color 0.15s;
overflow: hidden;
}
.seg-btn:last-child {
border-right: none;
}
.seg-btn:hover {
background: #ffffffb3;
color: #1f2937;
}
.seg-active {
background: #fff;
color: #111827;
font-weight: 600;
box-shadow: inset 0 0 0 1px #e5e7eb;
}
.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>