Teren-app/resources/js/Components/PersonInfoGrid.vue
2025-11-06 22:30:17 +01:00

1108 lines
37 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 { FwbBadge } from "flowbite-vue";
import { EditIcon, PlusIcon, UserEditIcon, TrashBinIcon } from "@/Utilities/Icons";
import CusTab from "./CusTab.vue";
import CusTabs from "./CusTabs.vue";
import { provide, ref, watch, computed } from "vue";
import axios from "axios";
import PersonUpdateForm from "./PersonUpdateForm.vue";
import AddressCreateForm from "./AddressCreateForm.vue";
import AddressUpdateForm from "./AddressUpdateForm.vue";
import PhoneCreateForm from "./PhoneCreateForm.vue";
import EmailCreateForm from "./EmailCreateForm.vue";
import EmailUpdateForm from "./EmailUpdateForm.vue";
import TrrCreateForm from "./TrrCreateForm.vue";
import TrrUpdateForm from "./TrrUpdateForm.vue";
import ConfirmDialog from "./ConfirmDialog.vue";
import DialogModal from "@/Components/DialogModal.vue";
import { router, usePage } from "@inertiajs/vue3";
const props = defineProps({
person: Object,
personEdit: {
type: Boolean,
default: true,
},
edit: {
type: Boolean,
default: true,
},
tabColor: {
type: String,
default: "blue-600",
},
types: {
type: Object,
default: {
address_types: [],
phone_types: [],
},
},
// Enable sending SMS buttons (only pass true for ClientCase person context)
enableSms: { type: Boolean, default: false },
// Required when enableSms=true to scope route correctly
clientCaseUuid: { type: String, default: null },
// Optional overrides; if not provided, falls back to Inertia page props
smsProfiles: { type: Array, default: () => [] },
smsSenders: { type: Array, default: () => [] },
smsTemplates: { type: Array, default: () => [] },
});
const drawerUpdatePerson = ref(false);
const drawerAddAddress = ref(false);
const drawerAddPhone = ref(false);
const drawerAddEmail = ref(false);
const drawerAddTrr = ref(false);
const editAddress = ref(false);
const editAddressId = ref(0);
const editPhone = ref(false);
const editPhoneId = ref(0);
const editEmail = ref(false);
const editEmailId = ref(0);
const editTrr = ref(false);
const editTrrId = ref(0);
// Confirm dialog state
const confirm = ref({
show: false,
title: "Potrditev brisanja",
message: "",
type: "", // 'email' | 'trr' | 'address' | 'phone'
id: 0,
});
const openConfirm = (type, id, label = "") => {
confirm.value = {
show: true,
title: "Potrditev brisanja",
message: label
? `Ali res želite izbrisati “${label}”?`
: "Ali res želite izbrisati izbran element?",
type,
id,
};
};
const closeConfirm = () => {
confirm.value.show = false;
};
const getMainAddress = (adresses) => {
const addr = adresses.filter((a) => a.type.id === 1)[0] ?? "";
if (addr !== "") {
const tail = addr.post_code && addr.city ? `, ${addr.post_code} ${addr.city}` : "";
const country = addr.country !== "" ? ` - ${addr.country}` : "";
return addr.address !== "" ? addr.address + tail + country : "";
}
return "";
};
const getMainPhone = (phones) => {
const pho = phones.filter((a) => a.type.id === 1)[0] ?? "";
if (pho !== "") {
const countryCode = pho.country_code !== null ? `+${pho.country_code} ` : "";
return pho.nu !== "" ? countryCode + pho.nu : "";
}
return "";
};
const openDrawerUpdateClient = () => {
drawerUpdatePerson.value = true;
};
const openDrawerAddAddress = (edit = false, id = 0) => {
drawerAddAddress.value = true;
editAddress.value = edit;
editAddressId.value = id;
};
const operDrawerAddPhone = (edit = false, id = 0) => {
drawerAddPhone.value = true;
editPhone.value = edit;
editPhoneId.value = id;
};
const openDrawerAddEmail = (edit = false, id = 0) => {
drawerAddEmail.value = true;
editEmail.value = edit;
editEmailId.value = id;
};
const openDrawerAddTrr = (edit = false, id = 0) => {
drawerAddTrr.value = true;
editTrr.value = edit;
editTrrId.value = id;
};
// Delete handlers (expects routes: person.email.delete, person.trr.delete)
const deleteEmail = async (emailId, label = "") => {
if (!emailId) return;
openConfirm("email", emailId, label || "email");
};
const deleteTrr = async (trrId, label = "") => {
if (!trrId) return;
openConfirm("trr", trrId, label || "TRR");
};
const onConfirmDelete = async () => {
const { type, id } = confirm.value;
try {
if (type === "email") {
await axios.delete(
route("person.email.delete", { person: props.person, email_id: id })
);
const list = props.person.emails || [];
const idx = list.findIndex((e) => e.id === id);
if (idx !== -1) list.splice(idx, 1);
} else if (type === "trr") {
await axios.delete(
route("person.trr.delete", { person: props.person, trr_id: id })
);
let list =
props.person.trrs ||
props.person.bank_accounts ||
props.person.accounts ||
props.person.bankAccounts ||
[];
const idx = list.findIndex((a) => a.id === id);
if (idx !== -1) list.splice(idx, 1);
} else if (type === "address") {
await axios.delete(
route("person.address.delete", { person: props.person, address_id: id })
);
const list = props.person.addresses || [];
const idx = list.findIndex((a) => a.id === id);
if (idx !== -1) list.splice(idx, 1);
} else if (type === "phone") {
await axios.delete(
route("person.phone.delete", { person: props.person, phone_id: id })
);
const list = props.person.phones || [];
const idx = list.findIndex((p) => p.id === id);
if (idx !== -1) list.splice(idx, 1);
}
closeConfirm();
} catch (e) {
console.error("Delete failed", e?.response || e);
closeConfirm();
}
};
// Safe accessors for optional collections
const getEmails = (p) => (Array.isArray(p?.emails) ? p.emails : []);
const getTRRs = (p) => {
if (Array.isArray(p?.trrs)) return p.trrs;
if (Array.isArray(p?.bank_accounts)) return p.bank_accounts;
if (Array.isArray(p?.accounts)) return p.accounts;
if (Array.isArray(p?.bankAccounts)) return p.bankAccounts;
return [];
};
// SMS dialog state and actions (ClientCase person only)
const showSmsDialog = ref(false);
const smsTargetPhone = ref(null);
const smsMessage = ref("");
const smsSending = ref(false);
// Page-level props fallback for SMS metadata
const page = usePage();
// In Inertia Vue 3, page.props is already a reactive object (not a Ref),
// so do NOT access .value here; otherwise, you'll get undefined and empty lists.
const pageProps = computed(() => page?.props ?? {});
const pageSmsProfiles = computed(() => {
const fromProps =
Array.isArray(props.smsProfiles) && props.smsProfiles.length
? props.smsProfiles
: null;
return fromProps ?? pageProps.value?.sms_profiles ?? [];
});
const pageSmsSenders = computed(() => {
const fromProps =
Array.isArray(props.smsSenders) && props.smsSenders.length ? props.smsSenders : null;
return fromProps ?? pageProps.value?.sms_senders ?? [];
});
const pageSmsTemplates = computed(() => {
const fromProps =
Array.isArray(props.smsTemplates) && props.smsTemplates.length
? props.smsTemplates
: null;
return fromProps ?? pageProps.value?.sms_templates ?? [];
});
// Helpers: EU formatter and token renderer
const formatEu = (value, decimals = 2) => {
if (value === null || value === undefined || value === "") {
return new Intl.NumberFormat("de-DE", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(0);
}
const num =
typeof value === "number"
? value
: parseFloat(String(value).replace(/\./g, "").replace(",", "."));
return new Intl.NumberFormat("de-DE", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(isNaN(num) ? 0 : num);
};
// Flatten meta structure on the client side (mirrors backend logic)
const flattenMeta = (meta, prefix = "") => {
if (!meta || typeof meta !== "object") return {};
const result = {};
for (const [key, value] of Object.entries(meta)) {
const newKey = prefix === "" ? key : `${prefix}.${key}`;
if (value && typeof value === "object") {
// Check if it's a structured meta entry with 'value' field
if ("value" in value) {
result[newKey] = value.value;
// If parent key is numeric, also create direct alias
if (prefix !== "" && /^\d+$/.test(key)) {
result[key] = value.value;
}
} else {
// Recursively flatten nested objects
const nested = flattenMeta(value, newKey);
Object.assign(result, nested);
// If current key is numeric, also flatten without it
if (/^\d+$/.test(key)) {
const directNested = flattenMeta(value, prefix);
for (const [dk, dv] of Object.entries(directNested)) {
if (!(dk in result)) {
result[dk] = dv;
}
}
}
}
} else {
result[newKey] = value;
}
}
return result;
};
const renderTokens = (text, vars) => {
if (!text) return "";
const resolver = (obj, path) => {
if (!obj) return null;
if (Object.prototype.hasOwnProperty.call(obj, path)) return obj[path];
const segs = path.split(".");
let cur = obj;
for (const s of segs) {
if (cur && typeof cur === "object" && s in cur) {
cur = cur[s];
} else {
return null;
}
}
return cur;
};
return text.replace(/\{([a-zA-Z0-9_\.]+)\}/g, (_, key) => {
const val = resolver(vars, key);
return val !== null && val !== undefined ? String(val) : `{${key}}`;
});
};
// SMS length, encoding and credits
const GSM7_EXTENDED = new Set(["^", "{", "}", "\\", "[", "~", "]", "|"]);
const isGsm7 = (text) => {
for (const ch of text || "") {
if (ch === "€") continue; // Allowed via GSM 03.38 extension
const code = ch.charCodeAt(0);
if (code >= 0x80) return false; // Non-ASCII -> UCS-2
}
return true;
};
const gsm7Length = (text) => {
let len = 0;
for (const ch of text || "") {
if (ch === "€" || GSM7_EXTENDED.has(ch)) {
len += 2; // extension table uses ESC + char
} else {
len += 1;
}
}
return len;
};
const ucs2Length = (text) => (text ? text.length : 0); // UTF-16 units ~ UCS-2 cost
const smsEncoding = computed(() => (isGsm7(smsMessage.value) ? "GSM-7" : "UCS-2"));
const charCount = computed(() =>
smsEncoding.value === "GSM-7"
? gsm7Length(smsMessage.value)
: ucs2Length(smsMessage.value)
);
const perSegment = computed(() => {
const count = charCount.value;
if (smsEncoding.value === "GSM-7") {
return count <= 160 ? 160 : 153; // single vs concatenated
}
return count <= 70 ? 70 : 67;
});
const segments = computed(() => {
const count = charCount.value;
const size = perSegment.value || 1;
return count > 0 ? Math.ceil(count / size) : 0;
});
const creditsNeeded = computed(() => segments.value);
// Provider hard limit: max 4 SMS parts
// - GSM-7: 640 units total
// - Unicode (UCS-2): 320 units total
const maxAllowed = computed(() => (smsEncoding.value === "GSM-7" ? 640 : 320));
const remaining = computed(() => Math.max(0, maxAllowed.value - charCount.value));
// Truncate helper that respects GSM-7 extended char cost (2 units) and UCS-2 units (1 per code unit)
const truncateToLimit = (text, limit, encoding) => {
if (!text) return "";
if (limit <= 0) return "";
if (encoding === "UCS-2") {
// UCS-2: count per UTF-16 code unit
return text.slice(0, limit);
}
// GSM-7: iterate and sum per-char costs (extended table chars cost 2)
let acc = 0;
let out = "";
for (const ch of text) {
const cost = ch === "€" || GSM7_EXTENDED.has(ch) ? 2 : 1;
if (acc + cost > limit) break;
out += ch;
acc += cost;
}
return out;
};
// Enforce hard limits by truncating user input as they type/paste
watch(smsMessage, (val) => {
const limit = maxAllowed.value;
// If currently over limit, truncate
if (charCount.value > limit) {
smsMessage.value = truncateToLimit(val, limit, smsEncoding.value);
}
});
const buildVarsFromSelectedContract = () => {
const uuid = selectedContractUuid.value;
if (!uuid) return {};
const c = (contractsForCase.value || []).find((x) => x.uuid === uuid);
if (!c) return {};
const vars = {
contract: {
uuid: c.uuid,
reference: c.reference,
start_date: c.start_date || "",
end_date: c.end_date || "",
},
};
// Include contract.meta - flatten if needed (in case server returns nested structure)
if (c.meta && typeof c.meta === "object") {
// Check if already flattened (no nested objects with 'value' property)
const hasStructuredMeta = Object.values(c.meta).some(
(v) => v && typeof v === "object" && "value" in v
);
vars.contract.meta = hasStructuredMeta ? flattenMeta(c.meta) : c.meta;
}
if (c.account) {
vars.account = {
reference: c.account.reference,
type: c.account.type,
initial_amount:
c.account.initial_amount ??
(c.account.initial_amount_raw ? formatEu(c.account.initial_amount_raw) : null),
balance_amount:
c.account.balance_amount ??
(c.account.balance_amount_raw ? formatEu(c.account.balance_amount_raw) : null),
initial_amount_raw: c.account.initial_amount_raw ?? null,
balance_amount_raw: c.account.balance_amount_raw ?? null,
};
}
return vars;
};
const updateSmsFromSelection = async () => {
if (!selectedTemplateId.value) return;
// Try server preview first
try {
const url = route("clientCase.sms.preview", { client_case: props.clientCaseUuid });
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-TOKEN":
document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ||
"",
},
body: JSON.stringify({
template_id: selectedTemplateId.value,
contract_uuid: selectedContractUuid.value || null,
}),
credentials: "same-origin",
});
if (res.ok) {
const data = await res.json();
if (typeof data?.content === "string" && data.content.trim() !== "") {
smsMessage.value = data.content;
return;
}
}
} catch (e) {
// ignore and fallback
}
// Fallback: client-side render using template content and selected contract vars
const tpl = (pageSmsTemplates.value || []).find(
(t) => t.id === selectedTemplateId.value
);
if (tpl && typeof tpl.content === "string") {
smsMessage.value = renderTokens(tpl.content, buildVarsFromSelectedContract());
}
};
// Selections
const selectedProfileId = ref(null);
const selectedSenderId = ref(null);
const deliveryReport = ref(false);
const selectedTemplateId = ref(null);
// Contract selection for placeholder rendering
const contractsForCase = ref([]);
const selectedContractUuid = ref(null);
const sendersForSelectedProfile = computed(() => {
if (!selectedProfileId.value) return pageSmsSenders.value;
return (pageSmsSenders.value || []).filter(
(s) => s.profile_id === selectedProfileId.value
);
});
watch(selectedProfileId, () => {
// Clear sender selection if it doesn't belong to the chosen profile
if (!selectedSenderId.value) return;
const ok = sendersForSelectedProfile.value.some((s) => s.id === selectedSenderId.value);
if (!ok) selectedSenderId.value = null;
});
// When the available senders list changes, default to the first sender if none selected
watch(sendersForSelectedProfile, (list) => {
if (!Array.isArray(list)) return;
if (!selectedSenderId.value && list.length > 0) {
selectedSenderId.value = list[0].id;
}
});
const renderSmsPreview = async () => {
if (!selectedTemplateId.value) return;
try {
const url = route("clientCase.sms.preview", { client_case: props.clientCaseUuid });
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-TOKEN":
document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ||
"",
},
body: JSON.stringify({
template_id: selectedTemplateId.value,
contract_uuid: selectedContractUuid.value || null,
}),
credentials: "same-origin",
});
if (!res.ok) throw new Error(`Preview failed: ${res.status}`);
const data = await res.json();
if (typeof data?.content === "string") {
smsMessage.value = data.content;
}
} catch (e) {
// If preview fails and template has inline content, fallback
const tpl = (pageSmsTemplates.value || []).find(
(t) => t.id === selectedTemplateId.value
);
if (tpl && typeof tpl.content === "string") {
smsMessage.value = tpl.content;
}
}
};
watch(selectedTemplateId, () => {
if (!selectedTemplateId.value) return;
updateSmsFromSelection();
});
watch(selectedContractUuid, () => {
if (!selectedTemplateId.value) return;
updateSmsFromSelection();
});
// If templates array changes and none is chosen, pick the first by default
watch(pageSmsTemplates, (list) => {
if (!Array.isArray(list)) return;
if (!selectedTemplateId.value && list.length > 0) {
selectedTemplateId.value = list[0].id;
}
});
const openSmsDialog = (phone) => {
if (!props.enableSms || !props.clientCaseUuid) return;
smsTargetPhone.value = phone;
smsMessage.value = "";
showSmsDialog.value = true;
// Defaults
selectedProfileId.value =
(pageSmsProfiles.value && pageSmsProfiles.value[0]?.id) || null;
// If profile has default sender, try to preselect it
if (selectedProfileId.value) {
const prof = (pageSmsProfiles.value || []).find(
(p) => p.id === selectedProfileId.value
);
if (prof && prof.default_sender_id) {
// Use profile default sender if present
const inList = sendersForSelectedProfile.value.find(
(s) => s.id === prof.default_sender_id
);
selectedSenderId.value = inList ? prof.default_sender_id : null;
} else {
selectedSenderId.value = null;
}
} else {
selectedSenderId.value = null;
}
deliveryReport.value = false;
// Default template selection to first available
selectedTemplateId.value =
(pageSmsTemplates.value && pageSmsTemplates.value[0]?.id) || null;
// Load contracts for this case (for contract/account placeholders)
loadContractsForCase();
};
const loadContractsForCase = async () => {
try {
const url = route("clientCase.contracts.list", { client_case: props.clientCaseUuid });
const res = await fetch(url, {
headers: { "X-Requested-With": "XMLHttpRequest" },
credentials: "same-origin",
});
const json = await res.json();
contractsForCase.value = Array.isArray(json?.data) ? json.data : [];
// Do not auto-select a contract; let user pick explicitly
} catch (e) {
contractsForCase.value = [];
}
};
const closeSmsDialog = () => {
showSmsDialog.value = false;
smsTargetPhone.value = null;
smsMessage.value = "";
};
const submitSms = () => {
if (!smsTargetPhone.value || !smsMessage.value || !props.clientCaseUuid) {
return;
}
smsSending.value = true;
router.post(
route("clientCase.phone.sms", {
client_case: props.clientCaseUuid,
phone_id: smsTargetPhone.value.id,
}),
{
message: smsMessage.value,
template_id: selectedTemplateId.value,
contract_uuid: selectedContractUuid.value,
profile_id: selectedProfileId.value,
sender_id: selectedSenderId.value,
delivery_report: !!deliveryReport.value,
},
{
preserveScroll: true,
onFinish: () => {
smsSending.value = false;
closeSmsDialog();
},
}
);
};
</script>
<template>
<CusTabs :selected-color="tabColor">
<CusTab name="person" title="Oseba">
<div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800">
<button @click="openDrawerUpdateClient" v-if="edit && personEdit">
<UserEditIcon size="lg" css="text-gray-500 hover:text-gray-800" />
</button>
</span>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
<div class="rounded p-2 shadow">
<p class="text-xs leading-5 md:text-sm text-gray-500">Nu.</p>
<p class="text-sm md:text-base leading-7 text-gray-900">{{ person.nu }}</p>
</div>
<div class="rounded p-2 shadow">
<p class="text-sm leading-5 md:text-sm text-gray-500">Name.</p>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ person.full_name }}
</p>
</div>
<div class="rounded p-2 shadow">
<p class="text-sm leading-5 md:text-sm text-gray-500">Tax NU.</p>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ person.tax_number }}
</p>
</div>
<div class="rounded p-2 shadow">
<p class="text-sm leading-5 md:text-sm text-gray-500">Social security NU.</p>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ person.social_security_number }}
</p>
</div>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
<div class="rounded p-2 shadow">
<p class="text-sm leading-5 md:text-sm text-gray-500">Address</p>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ getMainAddress(person.addresses) }}
</p>
</div>
<div class="rounded p-2 shadow">
<p class="text-sm leading-5 md:text-sm text-gray-500">Phone</p>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ getMainPhone(person.phones) }}
</p>
</div>
<div class="md:col-span-full lg:col-span-1 rounded p-2 shadow">
<p class="text-sm leading-5 md:text-sm text-gray-500">Description</p>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ person.description }}
</p>
</div>
</div>
</CusTab>
<CusTab name="addresses" title="Naslovi">
<div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button>
<PlusIcon
@click="openDrawerAddAddress(false, 0)"
size="lg"
css="text-gray-500 hover:text-gray-800"
/>
</button>
</span>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
<div class="rounded p-2 shadow" v-for="address in person.addresses">
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between">
<div class="flex">
<FwbBadge type="yellow">{{ address.country }}</FwbBadge>
<FwbBadge>{{ address.type.name }}</FwbBadge>
</div>
<div class="flex items-center gap-2" v-if="edit">
<button>
<EditIcon
@click="openDrawerAddAddress(true, address.id)"
size="md"
css="text-gray-500 hover:text-gray-800"
/>
</button>
<button @click="openConfirm('address', address.id, address.address)">
<TrashBinIcon size="md" css="text-red-600 hover:text-red-700" />
</button>
</div>
</div>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{
address.post_code && address.city
? `${address.address}, ${address.post_code} ${address.city}`
: address.address
}}
</p>
</div>
</div>
</CusTab>
<CusTab name="phones" title="Telefonske">
<div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button>
<PlusIcon
@click="operDrawerAddPhone(false, 0)"
size="lg"
css="text-gray-500 hover:text-gray-800"
/>
</button>
</span>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
<div class="rounded p-2 shadow" v-for="phone in person.phones">
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between">
<div class="flex">
<FwbBadge title type="yellow">+{{ phone.country_code }}</FwbBadge>
<FwbBadge>{{
phone && phone.type && phone.type.name ? phone.type.name : "—"
}}</FwbBadge>
</div>
<div class="flex items-center gap-2">
<!-- Send SMS only in ClientCase person context -->
<button
v-if="enableSms && clientCaseUuid"
@click="openSmsDialog(phone)"
title="Pošlji SMS"
class="text-indigo-600 hover:text-indigo-800 text-xs border border-indigo-200 px-2 py-0.5 rounded"
>
SMS
</button>
<button v-if="edit">
<EditIcon
@click="operDrawerAddPhone(true, phone.id)"
size="md"
css="text-gray-500 hover:text-gray-800"
/>
</button>
<button @click="openConfirm('phone', phone.id, phone.nu)" v-if="edit">
<TrashBinIcon size="md" css="text-red-600 hover:text-red-700" />
</button>
</div>
</div>
<p class="text-sm md:text-base leading-7 text-gray-900">{{ phone.nu }}</p>
</div>
</div>
</CusTab>
<CusTab name="emails" title="Email">
<div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button>
<PlusIcon
@click="openDrawerAddEmail(false, 0)"
size="lg"
css="text-gray-500 hover:text-gray-800"
/>
</button>
</span>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
<template v-if="getEmails(person).length">
<div
class="rounded p-2 shadow"
v-for="(email, idx) in getEmails(person)"
:key="idx"
>
<div
class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between"
v-if="edit"
>
<div class="flex gap-2">
<FwbBadge v-if="email?.label">{{ email.label }}</FwbBadge>
<FwbBadge v-else type="indigo">Email</FwbBadge>
</div>
<div class="flex items-center gap-2">
<button>
<EditIcon
@click="openDrawerAddEmail(true, email.id)"
size="md"
css="text-gray-500 hover:text-gray-800"
/>
</button>
<button
@click="
deleteEmail(email.id, email?.value || email?.email || email?.address)
"
>
<TrashBinIcon size="md" css="text-red-600 hover:text-red-700" />
</button>
</div>
</div>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{ email?.value || email?.email || email?.address || "-" }}
</p>
<p v-if="email?.note" class="mt-1 text-xs text-gray-500 whitespace-pre-wrap">
{{ email.note }}
</p>
</div>
</template>
<p v-else class="p-2 text-sm text-gray-500">Ni e-poštnih naslovov.</p>
</div>
</CusTab>
<CusTab name="trr" title="TRR">
<div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button>
<PlusIcon
@click="openDrawerAddTrr(false, 0)"
size="lg"
css="text-gray-500 hover:text-gray-800"
/>
</button>
</span>
</div>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
<template v-if="getTRRs(person).length">
<div
class="rounded p-2 shadow"
v-for="(acc, idx) in getTRRs(person)"
:key="idx"
>
<div
class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between"
v-if="edit"
>
<div class="flex gap-2">
<FwbBadge v-if="acc?.bank_name">{{ acc.bank_name }}</FwbBadge>
<FwbBadge v-if="acc?.holder_name" type="indigo">{{
acc.holder_name
}}</FwbBadge>
<FwbBadge v-if="acc?.currency" type="yellow">{{ acc.currency }}</FwbBadge>
</div>
<div class="flex items-center gap-2">
<button>
<EditIcon
@click="openDrawerAddTrr(true, acc.id)"
size="md"
css="text-gray-500 hover:text-gray-800"
/>
</button>
<button @click="deleteTrr(acc.id, acc?.iban || acc?.account_number)">
<TrashBinIcon size="md" css="text-red-600 hover:text-red-700" />
</button>
</div>
</div>
<p class="text-sm md:text-base leading-7 text-gray-900">
{{
acc?.iban ||
acc?.account_number ||
acc?.account ||
acc?.nu ||
acc?.number ||
"-"
}}
</p>
<p v-if="acc?.notes" class="mt-1 text-xs text-gray-500 whitespace-pre-wrap">
{{ acc.notes }}
</p>
</div>
</template>
<p v-else class="p-2 text-sm text-gray-500">Ni TRR računov.</p>
</div>
</CusTab>
</CusTabs>
<PersonUpdateForm
:show="drawerUpdatePerson"
@close="drawerUpdatePerson = false"
:person="person"
/>
<AddressCreateForm
:show="drawerAddAddress"
@close="drawerAddAddress = false"
:person="person"
:types="types.address_types"
:id="editAddressId"
:edit="editAddress"
/>
<AddressUpdateForm
:show="drawerAddAddress && editAddress"
@close="drawerAddAddress = false"
:person="person"
:types="types.address_types"
:id="editAddressId"
/>
<PhoneCreateForm
:show="drawerAddPhone"
@close="drawerAddPhone = false"
:person="person"
:types="types.phone_types"
:id="editPhoneId"
:edit="editPhone"
/>
<!-- Email dialogs -->
<EmailCreateForm
:show="drawerAddEmail && !editEmail"
@close="drawerAddEmail = false"
:person="person"
:types="types.email_types ?? []"
:is-client-context="!!person?.client"
/>
<EmailUpdateForm
:show="drawerAddEmail && editEmail"
@close="drawerAddEmail = false"
:person="person"
:types="types.email_types ?? []"
:id="editEmailId"
:is-client-context="!!person?.client"
/>
<!-- TRR dialogs -->
<TrrCreateForm
:show="drawerAddTrr && !editTrr"
@close="drawerAddTrr = false"
:person="person"
:types="types.trr_types ?? []"
:banks="types.banks ?? []"
:currencies="types.currencies ?? ['EUR']"
/>
<TrrUpdateForm
:show="drawerAddTrr && editTrr"
@close="drawerAddTrr = false"
:person="person"
:types="types.trr_types ?? []"
:banks="types.banks ?? []"
:currencies="types.currencies ?? ['EUR']"
:id="editTrrId"
/>
<!-- Confirm deletion dialog -->
<ConfirmDialog
:show="confirm.show"
:title="confirm.title"
:message="confirm.message"
confirm-text="Izbriši"
cancel-text="Prekliči"
:danger="true"
@close="closeConfirm"
@confirm="onConfirmDelete"
/>
<!-- Send SMS dialog -->
<DialogModal :show="showSmsDialog" @close="closeSmsDialog">
<template #title>Pošlji SMS</template>
<template #content>
<div class="space-y-2">
<p class="text-sm text-gray-600">
Prejemnik: <span class="font-mono">{{ smsTargetPhone?.nu }}</span>
<span v-if="smsTargetPhone?.country_code" class="ml-2 text-xs text-gray-500"
>CC +{{ smsTargetPhone.country_code }}</span
>
</p>
<!-- Profile & Sender selectors -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div>
<label class="block text-sm font-medium text-gray-700">Profil</label>
<select
v-model="selectedProfileId"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null">—</option>
<option v-for="p in pageSmsProfiles" :key="p.id" :value="p.id">
{{ p.name || "Profil #" + p.id }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Pošiljatelj</label>
<select
v-model="selectedSenderId"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null">—</option>
<option v-for="s in sendersForSelectedProfile" :key="s.id" :value="s.id">
{{ s.name || s.phone || "Sender #" + s.id }}
</option>
</select>
</div>
</div>
<!-- Contract selector (for placeholders) -->
<div>
<label class="block text-sm font-medium text-gray-700">Pogodba</label>
<select
v-model="selectedContractUuid"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null">—</option>
<option v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid">
{{ c.reference || c.uuid }}
</option>
</select>
<p class="mt-1 text-xs text-gray-500">
Uporabi podatke pogodbe (in računa) za zapolnitev {contract.*} in {account.*}
mest.
</p>
</div>
<!-- Template selector -->
<div>
<label class="block text-sm font-medium text-gray-700">Predloga</label>
<select
v-model="selectedTemplateId"
class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null"></option>
<option v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
{{ t.name || "Predloga #" + t.id }}
</option>
</select>
</div>
<label class="block text-sm font-medium text-gray-700">Vsebina sporočila</label>
<textarea
v-model="smsMessage"
rows="4"
class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
placeholder="Vpišite SMS vsebino..."
></textarea>
<!-- Live counters -->
<div class="mt-1 text-xs text-gray-600 flex flex-col gap-1">
<div>
<span class="font-medium">Znakov:</span>
<span class="font-mono">{{ charCount }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Kodiranje:</span>
<span>{{ smsEncoding }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Deli SMS:</span>
<span class="font-mono">{{ segments }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Krediti:</span>
<span class="font-mono">{{ creditsNeeded }}</span>
</div>
<div>
<span class="font-medium">Omejitev:</span>
<span class="font-mono">{{ maxAllowed }}</span>
<span class="mx-2">|</span>
<span class="font-medium">Preostanek:</span>
<span class="font-mono" :class="{ 'text-red-600': remaining === 0 }">{{
remaining
}}</span>
</div>
<p class="text-[11px] text-gray-500 leading-snug">
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake, ki
ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS2). V tem
primeru je največja dolžina enega SMS sporočila 70 znakov (pri daljših
sporočilih 67 znakov na del), medtem ko je pri GSM7 160 znakov (pri daljših
sporočilih 153 znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in €)
štejejo dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
320 (UCS2) znakov.
</p>
</div>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 mt-1">
<input
type="checkbox"
v-model="deliveryReport"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
Zahtevaj poročilo o dostavi
</label>
</div>
</template>
<template #footer>
<button class="px-3 py-1 rounded border mr-2" @click="closeSmsDialog">
Prekliči
</button>
<button
class="px-3 py-1 rounded bg-indigo-600 text-white disabled:opacity-50"
:disabled="smsSending || !smsMessage"
@click="submitSms"
>
Pošlji
</button>
</template>
</DialogModal>
</template>