Importer update add support for meta data and multiple inserts for some entities like addresses and phones, updated other things
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
faBoxArchive,
|
||||
faFileWord,
|
||||
faSpinner,
|
||||
faTags,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -36,7 +37,10 @@ const props = defineProps({
|
||||
|
||||
// Debug: log incoming contract balances (remove after fix)
|
||||
try {
|
||||
console.debug('Contracts received (balances):', props.contracts.map(c => ({ ref: c.reference, bal: c?.account?.balance_amount })));
|
||||
console.debug(
|
||||
"Contracts received (balances):",
|
||||
props.contracts.map((c) => ({ ref: c.reference, bal: c?.account?.balance_amount }))
|
||||
);
|
||||
} catch (e) {}
|
||||
|
||||
const emit = defineEmits(["edit", "delete", "add-activity"]);
|
||||
@@ -52,6 +56,107 @@ const hasDesc = (c) => {
|
||||
return typeof d === "string" && d.trim().length > 0;
|
||||
};
|
||||
|
||||
// Meta helpers
|
||||
const formatMetaDate = (v) => {
|
||||
if (!v) {
|
||||
return "-";
|
||||
}
|
||||
const d = new Date(v);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(v);
|
||||
}
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = d.getFullYear();
|
||||
return `${dd}.${mm}.${yyyy}`;
|
||||
};
|
||||
|
||||
const formatMetaNumber = (v) => {
|
||||
if (v === null || v === undefined || v === "") {
|
||||
return "0";
|
||||
}
|
||||
let n = typeof v === "number" ? v : parseFloat(String(v).replace(",", "."));
|
||||
if (isNaN(n)) {
|
||||
return String(v);
|
||||
}
|
||||
const hasDecimal = Math.abs(n % 1) > 0;
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
minimumFractionDigits: hasDecimal ? 2 : 0,
|
||||
maximumFractionDigits: hasDecimal ? 2 : 0,
|
||||
}).format(n);
|
||||
};
|
||||
|
||||
const formatMetaValue = (entry) => {
|
||||
const value = entry?.value;
|
||||
const type = entry?.type;
|
||||
if (value === null || value === undefined || String(value).trim() === "") {
|
||||
return "-";
|
||||
}
|
||||
if (type === "date") {
|
||||
return formatMetaDate(value);
|
||||
}
|
||||
if (type === "number") {
|
||||
return formatMetaNumber(value);
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return formatMetaNumber(value);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
// Try number
|
||||
const n = parseFloat(value.replace(",", "."));
|
||||
if (!isNaN(n)) {
|
||||
return formatMetaNumber(n);
|
||||
}
|
||||
// Try date
|
||||
const d = new Date(value);
|
||||
if (!isNaN(d.getTime())) {
|
||||
return formatMetaDate(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const getMetaEntries = (c) => {
|
||||
const meta = c?.meta;
|
||||
const results = [];
|
||||
const visit = (node, keyName) => {
|
||||
if (node === null || node === undefined) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((el) => visit(el));
|
||||
return;
|
||||
}
|
||||
if (typeof node === "object") {
|
||||
const hasValue = Object.prototype.hasOwnProperty.call(node, "value");
|
||||
const hasTitle = Object.prototype.hasOwnProperty.call(node, "title");
|
||||
if (hasValue || hasTitle) {
|
||||
const title =
|
||||
(node.title || keyName || "").toString().trim() || keyName || "Meta";
|
||||
results.push({ title, value: node.value, type: node.type });
|
||||
return;
|
||||
}
|
||||
for (const [k, v] of Object.entries(node)) {
|
||||
visit(v, k);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (keyName) {
|
||||
results.push({ title: keyName, value: node });
|
||||
}
|
||||
};
|
||||
visit(meta, undefined);
|
||||
return results.filter(
|
||||
(e) =>
|
||||
e.title &&
|
||||
e.value !== null &&
|
||||
e.value !== undefined &&
|
||||
String(e.value).trim() !== ""
|
||||
);
|
||||
};
|
||||
|
||||
const hasMeta = (c) => getMetaEntries(c).length > 0;
|
||||
|
||||
const onEdit = (c) => emit("edit", c);
|
||||
const onDelete = (c) => emit("delete", c);
|
||||
const onAddActivity = (c) => emit("add-activity", c);
|
||||
@@ -385,7 +490,7 @@ const closePaymentsDialog = () => {
|
||||
}}</FwbTableCell>
|
||||
<FwbTableCell class="text-center">
|
||||
<div class="inline-flex items-center justify-center gap-0.5">
|
||||
<Dropdown width="64" align="left">
|
||||
<Dropdown align="right">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
@@ -414,8 +519,50 @@ const closePaymentsDialog = () => {
|
||||
</template>
|
||||
</Dropdown>
|
||||
|
||||
<!-- Meta data dropdown -->
|
||||
<Dropdown align="right">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center h-5 w-5 rounded-full"
|
||||
:title="'Pokaži meta'"
|
||||
:disabled="!hasMeta(c)"
|
||||
:class="
|
||||
hasMeta(c)
|
||||
? 'hover:bg-gray-100 focus:outline-none'
|
||||
: 'text-gray-400'
|
||||
"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="faTags"
|
||||
class="h-4 w-4"
|
||||
:class="hasMeta(c) ? 'text-gray-700' : 'text-gray-400'"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="max-w-sm px-3 py-2 text-sm text-gray-700">
|
||||
<template v-if="hasMeta(c)">
|
||||
<div
|
||||
v-for="(m, idx) in getMetaEntries(c)"
|
||||
:key="idx"
|
||||
class="flex items-start gap-2 py-0.5"
|
||||
>
|
||||
<span class="text-gray-500 whitespace-nowrap"
|
||||
>{{ m.title }}:</span
|
||||
>
|
||||
<span class="text-gray-800">{{ formatMetaValue(m) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-gray-500">Ni meta podatkov.</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
|
||||
<!-- Promise date indicator -->
|
||||
<Dropdown width="64" align="left">
|
||||
<Dropdown align="right">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { ref } from "vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import Pagination from "@/Components/Pagination.vue";
|
||||
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
|
||||
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
|
||||
const props = defineProps({
|
||||
client: Object,
|
||||
contracts: Object,
|
||||
filters: Object,
|
||||
types: Object,
|
||||
});
|
||||
|
||||
const fromDate = ref(props.filters?.from || "");
|
||||
const toDate = ref(props.filters?.to || "");
|
||||
const search = ref(props.filters?.search || "");
|
||||
function applyDateFilter() {
|
||||
const params = Object.fromEntries(
|
||||
new URLSearchParams(window.location.search).entries()
|
||||
);
|
||||
if (fromDate.value) {
|
||||
params.from = fromDate.value;
|
||||
} else {
|
||||
delete params.from;
|
||||
}
|
||||
if (toDate.value) {
|
||||
params.to = toDate.value;
|
||||
} else {
|
||||
delete params.to;
|
||||
}
|
||||
if (search.value && search.value.trim() !== "") {
|
||||
params.search = search.value.trim();
|
||||
} else {
|
||||
delete params.search;
|
||||
}
|
||||
delete params.page;
|
||||
router.get(route("client.contracts", { uuid: props.client.uuid }), params, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applySearch() {
|
||||
const params = Object.fromEntries(
|
||||
new URLSearchParams(window.location.search).entries()
|
||||
);
|
||||
if (fromDate.value) {
|
||||
params.from = fromDate.value;
|
||||
} else {
|
||||
delete params.from;
|
||||
}
|
||||
if (toDate.value) {
|
||||
params.to = toDate.value;
|
||||
} else {
|
||||
delete params.to;
|
||||
}
|
||||
if (search.value && search.value.trim() !== "") {
|
||||
params.search = search.value.trim();
|
||||
} else {
|
||||
delete params.search;
|
||||
}
|
||||
delete params.page;
|
||||
router.get(route("client.contracts", { uuid: props.client.uuid }), params, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Build params for navigating to client case show, including active segment if available
|
||||
function caseShowParams(contract) {
|
||||
const params = { client_case: contract?.client_case?.uuid };
|
||||
const segId = contract?.segments?.[0]?.id;
|
||||
if (segId) {
|
||||
params.segment = segId;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// Format YYYY-MM-DD (or ISO date) to dd.mm.yyyy
|
||||
function formatDate(value) {
|
||||
if (!value) return "-";
|
||||
try {
|
||||
const iso = String(value).split("T")[0];
|
||||
const parts = iso.split("-");
|
||||
if (parts.length !== 3) return value;
|
||||
const [y, m, d] = parts;
|
||||
return `${d.padStart(2, "0")}.${m.padStart(2, "0")}.${y}`;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Pogodbe">
|
||||
<template #header></template>
|
||||
<!-- Header card (matches Client/Show header style) -->
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400"
|
||||
>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
{{ client.person.full_name }}
|
||||
</template>
|
||||
</SectionTitle>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3 text-sm">
|
||||
<Link
|
||||
:href="route('client.show', { uuid: client.uuid })"
|
||||
class="px-2 py-1 rounded hover:underline"
|
||||
>Primeri</Link
|
||||
>
|
||||
<span class="text-gray-300">|</span>
|
||||
<Link
|
||||
:href="route('client.contracts', { uuid: client.uuid })"
|
||||
class="px-2 py-1 rounded text-indigo-600 hover:underline"
|
||||
>Pogodbe</Link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client details card (separate container) -->
|
||||
<div class="pt-1">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400"
|
||||
>
|
||||
<div class="mx-auto max-w-4x1 px-2">
|
||||
<PersonInfoGrid :types="types" :person="client.person" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contracts list card -->
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="mx-auto max-w-4x1 py-3">
|
||||
<div
|
||||
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<label class="font-medium mr-2">Filter po datumu:</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-600">Od</span>
|
||||
<input
|
||||
type="date"
|
||||
v-model="fromDate"
|
||||
@change="applyDateFilter"
|
||||
class="rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-600">Do</span>
|
||||
<input
|
||||
type="date"
|
||||
v-model="toDate"
|
||||
@change="applyDateFilter"
|
||||
class="rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
v-model="search"
|
||||
@keyup.enter="applySearch"
|
||||
placeholder="Išči po referenci ali imenu"
|
||||
class="rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2 text-sm w-64"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="applySearch"
|
||||
class="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded"
|
||||
>
|
||||
Išči
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto mt-3">
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="py-2 pr-4">Referenca</th>
|
||||
<th class="py-2 pr-4">Stranka</th>
|
||||
<th class="py-2 pr-4">Začetek</th>
|
||||
<th class="py-2 pr-4">Segment</th>
|
||||
<th class="py-2 pr-4 text-right">Stanje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="contract in contracts.data"
|
||||
:key="contract.uuid"
|
||||
class="border-b last:border-0"
|
||||
>
|
||||
<td class="py-2 pr-4">
|
||||
<Link
|
||||
:href="route('clientCase.show', caseShowParams(contract))"
|
||||
class="text-indigo-600 hover:underline"
|
||||
>
|
||||
{{ contract.reference }}
|
||||
</Link>
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
{{ contract.client_case?.person?.full_name || "-" }}
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
{{ formatDate(contract.start_date) }}
|
||||
</td>
|
||||
<td class="py-2 pr-4">{{ contract.segments?.[0]?.name || "-" }}</td>
|
||||
<td class="py-2 pr-4 text-right">
|
||||
{{
|
||||
new Intl.NumberFormat("sl-SI", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(Number(contract.account?.balance_amount ?? 0))
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!contracts.data || contracts.data.length === 0">
|
||||
<td colspan="5" class="py-4 text-gray-500">Ni zadetkov.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
:links="contracts.links"
|
||||
:from="contracts.from"
|
||||
:to="contracts.to"
|
||||
:total="contracts.total"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -52,12 +52,27 @@ const openDrawerCreateCase = () => {
|
||||
<div
|
||||
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400"
|
||||
>
|
||||
<div class="mx-auto max-w-4x1 p-3 flex justify-between">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
{{ client.person.full_name }}
|
||||
</template>
|
||||
</SectionTitle>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
{{ client.person.full_name }}
|
||||
</template>
|
||||
</SectionTitle>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3 text-sm">
|
||||
<Link
|
||||
:href="route('client.show', { uuid: client.uuid })"
|
||||
class="px-2 py-1 rounded hover:underline"
|
||||
>Primeri</Link
|
||||
>
|
||||
<span class="text-gray-300">|</span>
|
||||
<Link
|
||||
:href="route('client.contracts', { uuid: client.uuid })"
|
||||
class="px-2 py-1 rounded text-indigo-600 hover:underline"
|
||||
>Pogodbe</Link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +130,6 @@ const openDrawerCreateCase = () => {
|
||||
{{ c.person?.full_name || "-" }}
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
<td class="py-2 pr-4">{{ c.person?.tax_number || "-" }}</td>
|
||||
<td class="py-2 pr-4 text-right">
|
||||
{{ c.active_contracts_count ?? 0 }}
|
||||
|
||||
@@ -130,6 +130,24 @@ function evaluateMappingSaved() {
|
||||
persistedSignature.value = computeMappingSignature(mappingRows.value);
|
||||
}
|
||||
|
||||
function normalizeOptions(val) {
|
||||
if (!val) {
|
||||
return {};
|
||||
}
|
||||
if (typeof val === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
return parsed && typeof parsed === "object" ? parsed : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (typeof val === "object") {
|
||||
return val;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function computeMappingSignature(rows) {
|
||||
return rows
|
||||
.filter((r) => r && r.source_column)
|
||||
@@ -270,6 +288,7 @@ function defaultEntityDefs() {
|
||||
"description",
|
||||
"type_id",
|
||||
"client_case_id",
|
||||
"meta",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -359,6 +378,7 @@ const displayRows = computed(() => {
|
||||
skip: false,
|
||||
transform: "trim",
|
||||
apply_mode: "both",
|
||||
options: {},
|
||||
position: idx,
|
||||
}));
|
||||
});
|
||||
@@ -570,6 +590,7 @@ async function fetchColumns() {
|
||||
skip: false,
|
||||
transform: "trim",
|
||||
apply_mode: "both",
|
||||
options: {},
|
||||
position: idx,
|
||||
}));
|
||||
suppressMappingWatch = false;
|
||||
@@ -592,6 +613,7 @@ async function fetchColumns() {
|
||||
skip: false,
|
||||
transform: m.transform || "trim",
|
||||
apply_mode: m.apply_mode || "both",
|
||||
options: normalizeOptions(m.options),
|
||||
position: idx,
|
||||
};
|
||||
});
|
||||
@@ -685,6 +707,7 @@ async function loadImportMappings() {
|
||||
field,
|
||||
transform: m.transform || "",
|
||||
apply_mode: m.apply_mode || "both",
|
||||
options: normalizeOptions(m.options) || r.options || {},
|
||||
skip: false,
|
||||
position: idx,
|
||||
};
|
||||
@@ -738,7 +761,13 @@ async function saveMappings() {
|
||||
target_field: `${entityKeyToRecord(r.entity)}.${r.field}`,
|
||||
transform: r.transform || null,
|
||||
apply_mode: r.apply_mode || "both",
|
||||
options: null,
|
||||
options:
|
||||
r.field === "meta"
|
||||
? {
|
||||
key: r.options?.key ?? null,
|
||||
type: r.options?.type ?? null,
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
if (!mappings.length) {
|
||||
mappingSaved.value = false;
|
||||
@@ -820,6 +849,7 @@ onMounted(async () => {
|
||||
skip: false,
|
||||
transform: "trim",
|
||||
apply_mode: "both",
|
||||
options: {},
|
||||
position: idx,
|
||||
};
|
||||
});
|
||||
@@ -877,6 +907,7 @@ watch(
|
||||
skip: false,
|
||||
transform: "trim",
|
||||
apply_mode: "both",
|
||||
options: {},
|
||||
position: idx,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,6 +35,8 @@ function duplicateTarget(row){
|
||||
<th class="p-2 border">Source column</th>
|
||||
<th class="p-2 border">Entity</th>
|
||||
<th class="p-2 border">Field</th>
|
||||
<th class="p-2 border">Meta key</th>
|
||||
<th class="p-2 border">Meta type</th>
|
||||
<th class="p-2 border">Transform</th>
|
||||
<th class="p-2 border">Apply mode</th>
|
||||
<th class="p-2 border">Skip</th>
|
||||
@@ -55,6 +57,32 @@ function duplicateTarget(row){
|
||||
<option v-for="f in fieldsForEntity(row.entity)" :key="f" :value="f">{{ f }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="p-2 border">
|
||||
<input
|
||||
v-if="row.field === 'meta'"
|
||||
v-model="(row.options ||= {}).key"
|
||||
type="text"
|
||||
class="border rounded p-1 w-full"
|
||||
placeholder="e.g. monthly_rent"
|
||||
:disabled="isCompleted"
|
||||
/>
|
||||
<span v-else class="text-gray-400 text-xs">—</span>
|
||||
</td>
|
||||
<td class="p-2 border">
|
||||
<select
|
||||
v-if="row.field === 'meta'"
|
||||
v-model="(row.options ||= {}).type"
|
||||
class="border rounded p-1 w-full"
|
||||
:disabled="isCompleted"
|
||||
>
|
||||
<option :value="null">Default (string)</option>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="date">date</option>
|
||||
<option value="boolean">boolean</option>
|
||||
</select>
|
||||
<span v-else class="text-gray-400 text-xs">—</span>
|
||||
</td>
|
||||
<td class="p-2 border">
|
||||
<select v-model="row.transform" class="border rounded p-1 w-full" :disabled="isCompleted">
|
||||
<option value="">None</option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
const props = defineProps({ mappings: Array })
|
||||
const props = defineProps({ mappings: Array });
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="mappings?.length" class="pt-4">
|
||||
@@ -12,14 +12,30 @@ const props = defineProps({ mappings: Array })
|
||||
<th class="p-2 border">Target field</th>
|
||||
<th class="p-2 border">Transform</th>
|
||||
<th class="p-2 border">Mode</th>
|
||||
<th class="p-2 border">Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in mappings" :key="m.id || (m.source_column + m.target_field)" class="border-t">
|
||||
<tr
|
||||
v-for="m in mappings"
|
||||
:key="m.id || m.source_column + m.target_field"
|
||||
class="border-t"
|
||||
>
|
||||
<td class="p-2 border">{{ m.source_column }}</td>
|
||||
<td class="p-2 border">{{ m.target_field }}</td>
|
||||
<td class="p-2 border">{{ m.transform || '—' }}</td>
|
||||
<td class="p-2 border">{{ m.apply_mode || 'both' }}</td>
|
||||
<td class="p-2 border">{{ m.transform || "—" }}</td>
|
||||
<td class="p-2 border">{{ m.apply_mode || "both" }}</td>
|
||||
<td class="p-2 border">
|
||||
<template v-if="m.options">
|
||||
<span v-if="m.options.key" class="inline-block mr-2"
|
||||
>key: <strong>{{ m.options.key }}</strong></span
|
||||
>
|
||||
<span v-if="m.options.type" class="inline-block"
|
||||
>type: <strong>{{ m.options.type }}</strong></span
|
||||
>
|
||||
</template>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -99,23 +99,45 @@ const entityStats = computed(() => {
|
||||
if (!r.entities) continue;
|
||||
for (const [k, ent] of Object.entries(r.entities)) {
|
||||
if (!stats[k]) continue;
|
||||
// Count one row per entity root
|
||||
stats[k].total_rows++;
|
||||
switch (ent.action) {
|
||||
case "create":
|
||||
stats[k].create++;
|
||||
break;
|
||||
case "update":
|
||||
stats[k].update++;
|
||||
break;
|
||||
case "missing_ref":
|
||||
stats[k].missing_ref++;
|
||||
break;
|
||||
case "invalid":
|
||||
stats[k].invalid++;
|
||||
break;
|
||||
if (Array.isArray(ent)) {
|
||||
for (const item of ent) {
|
||||
switch (item.action) {
|
||||
case "create":
|
||||
stats[k].create++;
|
||||
break;
|
||||
case "update":
|
||||
stats[k].update++;
|
||||
break;
|
||||
case "missing_ref":
|
||||
stats[k].missing_ref++;
|
||||
break;
|
||||
case "invalid":
|
||||
stats[k].invalid++;
|
||||
break;
|
||||
}
|
||||
if (item.duplicate) stats[k].duplicate++;
|
||||
if (item.duplicate_db) stats[k].duplicate_db++;
|
||||
}
|
||||
} else {
|
||||
switch (ent.action) {
|
||||
case "create":
|
||||
stats[k].create++;
|
||||
break;
|
||||
case "update":
|
||||
stats[k].update++;
|
||||
break;
|
||||
case "missing_ref":
|
||||
stats[k].missing_ref++;
|
||||
break;
|
||||
case "invalid":
|
||||
stats[k].invalid++;
|
||||
break;
|
||||
}
|
||||
if (ent.duplicate) stats[k].duplicate++;
|
||||
if (ent.duplicate_db) stats[k].duplicate_db++;
|
||||
}
|
||||
if (ent.duplicate) stats[k].duplicate++;
|
||||
if (ent.duplicate_db) stats[k].duplicate_db++;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
@@ -134,7 +156,9 @@ const visibleRows = computed(() => {
|
||||
.filter((r) => {
|
||||
if (!r.entities || !r.entities[activeEntity.value]) return false;
|
||||
const ent = r.entities[activeEntity.value];
|
||||
if (hideChain.value && ent.existing_chain) return false;
|
||||
if (!Array.isArray(ent)) {
|
||||
if (hideChain.value && ent.existing_chain) return false;
|
||||
}
|
||||
if (showOnlyChanged.value) {
|
||||
// Define change criteria per entity
|
||||
if (activeEntity.value === "account") {
|
||||
@@ -148,6 +172,9 @@ const visibleRows = computed(() => {
|
||||
return ent.amount !== null && ent.amount !== undefined;
|
||||
}
|
||||
// Generic entities: any create/update considered change
|
||||
if (Array.isArray(ent)) {
|
||||
return ent.some((i) => i && (i.action === "create" || i.action === "update"));
|
||||
}
|
||||
if (ent.action === "create" || ent.action === "update") return true;
|
||||
return false;
|
||||
}
|
||||
@@ -371,36 +398,45 @@ function referenceOf(entityName, ent) {
|
||||
class="font-semibold uppercase tracking-wide text-gray-600 mb-1 flex items-center justify-between"
|
||||
>
|
||||
<span>{{ activeEntity }}</span>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].action_label"
|
||||
:class="[
|
||||
'text-[10px] px-1 py-0.5 rounded',
|
||||
r.entities[activeEntity].action === 'create' && 'bg-emerald-100 text-emerald-700',
|
||||
r.entities[activeEntity].action === 'update' && 'bg-blue-100 text-blue-700',
|
||||
r.entities[activeEntity].action === 'reactivate' && 'bg-purple-100 text-purple-700 font-semibold',
|
||||
r.entities[activeEntity].action === 'skip' && 'bg-gray-100 text-gray-600',
|
||||
r.entities[activeEntity].action === 'implicit' && 'bg-teal-100 text-teal-700'
|
||||
].filter(Boolean)"
|
||||
>{{ r.entities[activeEntity].action_label }}</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].existing_chain"
|
||||
class="ml-1 text-[9px] px-1 py-0.5 rounded bg-indigo-100 text-indigo-700"
|
||||
title="Iz obstoječe verige (contract → client_case → person)"
|
||||
>chain</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].inherited_reference"
|
||||
class="ml-1 text-[9px] px-1 py-0.5 rounded bg-amber-100 text-amber-700"
|
||||
title="Referenca podedovana"
|
||||
>inh</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].action === 'implicit'"
|
||||
class="ml-1 text-[9px] px-1 py-0.5 rounded bg-teal-100 text-teal-700"
|
||||
title="Implicitno"
|
||||
>impl</span
|
||||
>
|
||||
<template v-if="!Array.isArray(r.entities[activeEntity])">
|
||||
<span
|
||||
v-if="r.entities[activeEntity].action_label"
|
||||
:class="
|
||||
[
|
||||
'text-[10px] px-1 py-0.5 rounded',
|
||||
r.entities[activeEntity].action === 'create' &&
|
||||
'bg-emerald-100 text-emerald-700',
|
||||
r.entities[activeEntity].action === 'update' &&
|
||||
'bg-blue-100 text-blue-700',
|
||||
r.entities[activeEntity].action === 'reactivate' &&
|
||||
'bg-purple-100 text-purple-700 font-semibold',
|
||||
r.entities[activeEntity].action === 'skip' &&
|
||||
'bg-gray-100 text-gray-600',
|
||||
r.entities[activeEntity].action === 'implicit' &&
|
||||
'bg-teal-100 text-teal-700',
|
||||
].filter(Boolean)
|
||||
"
|
||||
>{{ r.entities[activeEntity].action_label }}</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].existing_chain"
|
||||
class="ml-1 text-[9px] px-1 py-0.5 rounded bg-indigo-100 text-indigo-700"
|
||||
title="Iz obstoječe verige (contract → client_case → person)"
|
||||
>chain</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].inherited_reference"
|
||||
class="ml-1 text-[9px] px-1 py-0.5 rounded bg-amber-100 text-amber-700"
|
||||
title="Referenca podedovana"
|
||||
>inh</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].action === 'implicit'"
|
||||
class="ml-1 text-[9px] px-1 py-0.5 rounded bg-teal-100 text-teal-700"
|
||||
title="Implicitno"
|
||||
>impl</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="activeEntity === 'account'">
|
||||
@@ -510,10 +546,13 @@ function referenceOf(entityName, ent) {
|
||||
<div>
|
||||
Akcija:
|
||||
<span
|
||||
:class="[
|
||||
'font-medium inline-flex items-center gap-1',
|
||||
r.entities[activeEntity].action === 'reactivate' && 'text-purple-700'
|
||||
].filter(Boolean)"
|
||||
:class="
|
||||
[
|
||||
'font-medium inline-flex items-center gap-1',
|
||||
r.entities[activeEntity].action === 'reactivate' &&
|
||||
'text-purple-700',
|
||||
].filter(Boolean)
|
||||
"
|
||||
>{{
|
||||
r.entities[activeEntity].action_label ||
|
||||
r.entities[activeEntity].action
|
||||
@@ -526,179 +565,319 @@ function referenceOf(entityName, ent) {
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].original_action === 'update' && r.entities[activeEntity].action === 'reactivate'" class="text-[10px] text-purple-600 mt-0.5">
|
||||
<div
|
||||
v-if="
|
||||
r.entities[activeEntity].original_action === 'update' &&
|
||||
r.entities[activeEntity].action === 'reactivate'
|
||||
"
|
||||
class="text-[10px] text-purple-600 mt-0.5"
|
||||
>
|
||||
(iz neaktivnega → aktivno)
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].meta"
|
||||
class="mt-1 text-[10px] text-gray-700"
|
||||
>
|
||||
<div class="font-semibold text-gray-600">Meta</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="(entries, grp) in r.entities[activeEntity].meta"
|
||||
:key="grp"
|
||||
class="border rounded p-1 bg-white"
|
||||
>
|
||||
<div class="text-[9px] text-gray-500 mb-0.5">
|
||||
skupina: {{ grp }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(entry, key) in entries"
|
||||
:key="key"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span class="text-gray-500">{{ key }}:</span>
|
||||
<span class="text-gray-800">{{ entry?.value ?? "—" }}</span>
|
||||
<span class="text-gray-400">(iz: {{ entry?.title }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap gap-1 mb-1">
|
||||
<span
|
||||
v-if="r.entities[activeEntity].identity_used"
|
||||
class="px-1 py-0.5 rounded bg-indigo-50 text-indigo-700 text-[10px]"
|
||||
title="Uporabljena identiteta"
|
||||
>{{ r.entities[activeEntity].identity_used }}</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].duplicate"
|
||||
class="px-1 py-0.5 rounded bg-amber-100 text-amber-700 text-[10px]"
|
||||
title="Podvojen v tej seriji"
|
||||
>duplikat</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].duplicate_db"
|
||||
class="px-1 py-0.5 rounded bg-amber-200 text-amber-800 text-[10px]"
|
||||
title="Že obstaja v bazi"
|
||||
>obstaja v bazi</span
|
||||
>
|
||||
</div>
|
||||
<template v-if="activeEntity === 'person'">
|
||||
<div class="grid grid-cols-1 gap-0.5">
|
||||
<!-- Multi-item rendering for grouped roots (email/phone/address) -->
|
||||
<template v-if="Array.isArray(r.entities[activeEntity])">
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-if="
|
||||
referenceOf(activeEntity, r.entities[activeEntity]) !== '—'
|
||||
"
|
||||
class="text-[10px] text-gray-600"
|
||||
v-for="(item, idx) in r.entities[activeEntity]"
|
||||
:key="idx"
|
||||
class="border rounded p-2 bg-white"
|
||||
>
|
||||
Ref:
|
||||
<span class="font-medium text-gray-800">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].full_name"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Ime:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].full_name
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
r.entities[activeEntity].first_name ||
|
||||
r.entities[activeEntity].last_name
|
||||
"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Ime:
|
||||
<span class="font-medium">{{
|
||||
[
|
||||
r.entities[activeEntity].first_name,
|
||||
r.entities[activeEntity].last_name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].birthday"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Rojstvo:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].birthday
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].description"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Opis:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].description
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].identity_candidates?.length"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Identitete:
|
||||
{{ r.entities[activeEntity].identity_candidates.join(", ") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeEntity === 'email'"
|
||||
><div class="text-[10px] text-gray-600">
|
||||
Email:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div></template
|
||||
>
|
||||
<template v-else-if="activeEntity === 'phone'"
|
||||
><div class="text-[10px] text-gray-600">
|
||||
Telefon:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div></template
|
||||
>
|
||||
<template v-else-if="activeEntity === 'address'">
|
||||
<div class="text-[10px] text-gray-600 space-y-0.5">
|
||||
<div
|
||||
v-if="
|
||||
referenceOf(activeEntity, r.entities[activeEntity]) !== '—'
|
||||
"
|
||||
>
|
||||
Ref:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].address">
|
||||
Naslov:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].address
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
r.entities[activeEntity].postal_code ||
|
||||
r.entities[activeEntity].country
|
||||
"
|
||||
>
|
||||
Lokacija:
|
||||
<span class="font-medium">{{
|
||||
[
|
||||
r.entities[activeEntity].postal_code,
|
||||
r.entities[activeEntity].country,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeEntity === 'client_case'">
|
||||
<div class="text-[10px] text-gray-600 space-y-0.5">
|
||||
<div
|
||||
v-if="
|
||||
referenceOf(activeEntity, r.entities[activeEntity]) !== '—'
|
||||
"
|
||||
>
|
||||
Ref:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].title">
|
||||
Naslov:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].title
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].status">
|
||||
Status:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].status
|
||||
}}</span>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="
|
||||
item.group !== undefined &&
|
||||
item.group !== null &&
|
||||
String(item.group) !== ''
|
||||
"
|
||||
class="text-[10px] px-1 py-0.5 rounded bg-gray-100 text-gray-700"
|
||||
>skupina: {{ item.group }}</span
|
||||
>
|
||||
<span
|
||||
v-if="item.identity_used"
|
||||
class="px-1 py-0.5 rounded bg-indigo-50 text-indigo-700 text-[10px]"
|
||||
title="Uporabljena identiteta"
|
||||
>{{ item.identity_used }}</span
|
||||
>
|
||||
<span
|
||||
v-if="item.duplicate"
|
||||
class="px-1 py-0.5 rounded bg-amber-100 text-amber-700 text-[10px]"
|
||||
title="Podvojen v tej seriji"
|
||||
>duplikat</span
|
||||
>
|
||||
<span
|
||||
v-if="item.duplicate_db"
|
||||
class="px-1 py-0.5 rounded bg-amber-200 text-amber-800 text-[10px]"
|
||||
title="Že obstaja v bazi"
|
||||
>obstaja v bazi</span
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
v-if="item.action_label"
|
||||
:class="
|
||||
[
|
||||
'text-[10px] px-1 py-0.5 rounded',
|
||||
item.action === 'create' &&
|
||||
'bg-emerald-100 text-emerald-700',
|
||||
item.action === 'update' &&
|
||||
'bg-blue-100 text-blue-700',
|
||||
item.action === 'skip' && 'bg-gray-100 text-gray-600',
|
||||
].filter(Boolean)
|
||||
"
|
||||
>{{ item.action_label || item.action }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<template v-if="activeEntity === 'email'">
|
||||
<div class="text-[10px] text-gray-600">
|
||||
Email:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, item)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeEntity === 'phone'">
|
||||
<div class="text-[10px] text-gray-600">
|
||||
Telefon:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, item)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeEntity === 'address'">
|
||||
<div class="text-[10px] text-gray-600 space-y-0.5">
|
||||
<div v-if="referenceOf(activeEntity, item) !== '—'">
|
||||
Ref:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, item)
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="item.address">
|
||||
Naslov:
|
||||
<span class="font-medium">{{ item.address }}</span>
|
||||
</div>
|
||||
<div v-if="item.postal_code || item.country">
|
||||
Lokacija:
|
||||
<span class="font-medium">{{
|
||||
[item.postal_code, item.country]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<pre class="text-[10px] whitespace-pre-wrap">{{
|
||||
item
|
||||
}}</pre>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Single-item generic rendering (existing) -->
|
||||
<template v-else>
|
||||
<pre class="text-[10px] whitespace-pre-wrap">{{
|
||||
r.entities[activeEntity]
|
||||
}}</pre>
|
||||
<div class="flex flex-wrap gap-1 mb-1">
|
||||
<span
|
||||
v-if="r.entities[activeEntity].identity_used"
|
||||
class="px-1 py-0.5 rounded bg-indigo-50 text-indigo-700 text-[10px]"
|
||||
title="Uporabljena identiteta"
|
||||
>{{ r.entities[activeEntity].identity_used }}</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].duplicate"
|
||||
class="px-1 py-0.5 rounded bg-amber-100 text-amber-700 text-[10px]"
|
||||
title="Podvojen v tej seriji"
|
||||
>duplikat</span
|
||||
>
|
||||
<span
|
||||
v-if="r.entities[activeEntity].duplicate_db"
|
||||
class="px-1 py-0.5 rounded bg-amber-200 text-amber-800 text-[10px]"
|
||||
title="Že obstaja v bazi"
|
||||
>obstaja v bazi</span
|
||||
>
|
||||
</div>
|
||||
<template v-if="activeEntity === 'person'">
|
||||
<div class="grid grid-cols-1 gap-0.5">
|
||||
<div
|
||||
v-if="
|
||||
referenceOf(activeEntity, r.entities[activeEntity]) !==
|
||||
'—'
|
||||
"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Ref:
|
||||
<span class="font-medium text-gray-800">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].full_name"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Ime:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].full_name
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
r.entities[activeEntity].first_name ||
|
||||
r.entities[activeEntity].last_name
|
||||
"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Ime:
|
||||
<span class="font-medium">{{
|
||||
[
|
||||
r.entities[activeEntity].first_name,
|
||||
r.entities[activeEntity].last_name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].birthday"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Rojstvo:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].birthday
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].description"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Opis:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].description
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="r.entities[activeEntity].identity_candidates?.length"
|
||||
class="text-[10px] text-gray-600"
|
||||
>
|
||||
Identitete:
|
||||
{{
|
||||
r.entities[activeEntity].identity_candidates.join(", ")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeEntity === 'email'"
|
||||
><div class="text-[10px] text-gray-600">
|
||||
Email:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div></template
|
||||
>
|
||||
<template v-else-if="activeEntity === 'phone'"
|
||||
><div class="text-[10px] text-gray-600">
|
||||
Telefon:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div></template
|
||||
>
|
||||
<template v-else-if="activeEntity === 'address'">
|
||||
<div class="text-[10px] text-gray-600 space-y-0.5">
|
||||
<div
|
||||
v-if="
|
||||
referenceOf(activeEntity, r.entities[activeEntity]) !==
|
||||
'—'
|
||||
"
|
||||
>
|
||||
Ref:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].address">
|
||||
Naslov:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].address
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
r.entities[activeEntity].postal_code ||
|
||||
r.entities[activeEntity].country
|
||||
"
|
||||
>
|
||||
Lokacija:
|
||||
<span class="font-medium">{{
|
||||
[
|
||||
r.entities[activeEntity].postal_code,
|
||||
r.entities[activeEntity].country,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeEntity === 'client_case'">
|
||||
<div class="text-[10px] text-gray-600 space-y-0.5">
|
||||
<div
|
||||
v-if="
|
||||
referenceOf(activeEntity, r.entities[activeEntity]) !==
|
||||
'—'
|
||||
"
|
||||
>
|
||||
Ref:
|
||||
<span class="font-medium">{{
|
||||
referenceOf(activeEntity, r.entities[activeEntity])
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].title">
|
||||
Naslov:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].title
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="r.entities[activeEntity].status">
|
||||
Status:
|
||||
<span class="font-medium">{{
|
||||
r.entities[activeEntity].status
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<pre class="text-[10px] whitespace-pre-wrap">{{
|
||||
r.entities[activeEntity]
|
||||
}}</pre>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@ const bulkGlobal = ref({
|
||||
default_field: "",
|
||||
transform: "",
|
||||
apply_mode: "both",
|
||||
group: "",
|
||||
});
|
||||
const unassigned = computed(() =>
|
||||
(props.template.mappings || []).filter((m) => !m.target_field)
|
||||
@@ -104,6 +105,21 @@ function saveUnassigned(m) {
|
||||
} else {
|
||||
m.target_field = null;
|
||||
}
|
||||
if (st.group) {
|
||||
m.options = m.options && typeof m.options === "object" ? m.options : {};
|
||||
m.options.group = st.group;
|
||||
}
|
||||
// If targeting any .meta field, allow setting options.key via UI
|
||||
if (st.field === "meta") {
|
||||
if (st.metaKey && String(st.metaKey).trim() !== "") {
|
||||
m.options = m.options && typeof m.options === "object" ? m.options : {};
|
||||
m.options.key = String(st.metaKey).trim();
|
||||
}
|
||||
if (st.metaType && String(st.metaType).trim() !== "") {
|
||||
m.options = m.options && typeof m.options === "object" ? m.options : {};
|
||||
m.options.type = String(st.metaType).trim();
|
||||
}
|
||||
}
|
||||
updateMapping(m);
|
||||
}
|
||||
|
||||
@@ -141,13 +157,22 @@ function addRow(entity) {
|
||||
const row = newRows.value[entity];
|
||||
if (!row || !row.source || !row.field) return;
|
||||
const target_field = `${entity}.${row.field}`;
|
||||
const opts = {};
|
||||
if (row.group) opts.group = row.group;
|
||||
if (entity === "contract" && row.field === "meta" && row.metaKey) {
|
||||
opts.key = String(row.metaKey).trim();
|
||||
}
|
||||
const payload = {
|
||||
source_column: row.source,
|
||||
target_field,
|
||||
transform: row.transform || null,
|
||||
apply_mode: row.apply_mode || "both",
|
||||
options: Object.keys(opts).length ? opts : null,
|
||||
position: (props.template.mappings?.length || 0) + 1,
|
||||
};
|
||||
if (row.field === "meta" && row.metaType) {
|
||||
opts.type = String(row.metaType).trim();
|
||||
}
|
||||
useForm(payload).post(
|
||||
route("importTemplates.mappings.add", { template: props.template.uuid }),
|
||||
{
|
||||
@@ -165,6 +190,7 @@ function updateMapping(m) {
|
||||
target_field: m.target_field,
|
||||
transform: m.transform,
|
||||
apply_mode: m.apply_mode,
|
||||
options: m.options || null,
|
||||
position: m.position,
|
||||
};
|
||||
useForm(payload).put(
|
||||
@@ -602,6 +628,15 @@ watch(
|
||||
<option value="update">update</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-indigo-900">Group (za vse)</label>
|
||||
<input
|
||||
v-model="bulkGlobal.group"
|
||||
type="text"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
placeholder="1, 2, home, work"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button
|
||||
@@ -614,6 +649,7 @@ watch(
|
||||
default_field: bulkGlobal.default_field || null,
|
||||
transform: bulkGlobal.transform || null,
|
||||
apply_mode: bulkGlobal.apply_mode || 'both',
|
||||
group: bulkGlobal.group || '',
|
||||
}).post(
|
||||
route('importTemplates.mappings.bulk', {
|
||||
template: props.template.uuid,
|
||||
@@ -626,6 +662,7 @@ watch(
|
||||
bulkGlobal.default_field = '';
|
||||
bulkGlobal.transform = '';
|
||||
bulkGlobal.apply_mode = 'both';
|
||||
bulkGlobal.group = '';
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -710,6 +747,39 @@ watch(
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600">Group</label>
|
||||
<input
|
||||
v-model="(unassignedState[m.id] ||= {}).group"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
type="text"
|
||||
placeholder="1, 2, home, work"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="(unassignedState[m.id] || {}).field === 'meta'">
|
||||
<label class="block text-xs text-gray-600">Meta key</label>
|
||||
<input
|
||||
v-model="(unassignedState[m.id] ||= {}).metaKey"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
type="text"
|
||||
placeholder="npr.: note, category"
|
||||
/>
|
||||
<label class="block text-xs text-gray-600 mt-2">Meta type</label>
|
||||
<select
|
||||
v-model="(unassignedState[m.id] ||= {}).metaType"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
>
|
||||
<option value="">(auto/string)</option>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="date">date</option>
|
||||
<option value="boolean">boolean</option>
|
||||
</select>
|
||||
<p class="text-[11px] text-gray-500 mt-1">
|
||||
Če ne določiš, lahko uporabiš tudi zapis cilja kot
|
||||
<code>contract.meta[key]</code>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600">Transform</label>
|
||||
<select v-model="m.transform" class="mt-1 w-full border rounded p-2">
|
||||
@@ -800,7 +870,7 @@ watch(
|
||||
class="flex items-center justify-between p-2 border rounded gap-3"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-5 gap-2 flex-1 items-center"
|
||||
class="grid grid-cols-1 sm:grid-cols-6 gap-2 flex-1 items-center"
|
||||
>
|
||||
<input
|
||||
v-model="m.source_column"
|
||||
@@ -822,6 +892,28 @@ watch(
|
||||
<option value="update">update</option>
|
||||
<option value="keyref">keyref (use as lookup key)</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="(m.options ||= {}).group"
|
||||
class="border rounded p-2 text-sm"
|
||||
placeholder="Group"
|
||||
/>
|
||||
<input
|
||||
v-if="/^(contracts?\.meta)(\.|\[|$)/.test(m.target_field || '')"
|
||||
v-model="(m.options ||= {}).key"
|
||||
class="border rounded p-2 text-sm"
|
||||
placeholder="Meta key"
|
||||
/>
|
||||
<select
|
||||
v-if="/^(contracts?\.meta)(\.|\[|$)/.test(m.target_field || '')"
|
||||
v-model="(m.options ||= {}).type"
|
||||
class="border rounded p-2 text-sm"
|
||||
>
|
||||
<option value="">(auto/string)</option>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="date">date</option>
|
||||
<option value="boolean">boolean</option>
|
||||
</select>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="px-2 py-1 text-xs border rounded"
|
||||
@@ -859,7 +951,7 @@ watch(
|
||||
|
||||
<!-- Add new mapping row -->
|
||||
<div class="p-3 bg-gray-50 rounded border">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-5 gap-2 items-end">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-6 gap-2 items-end">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600"
|
||||
>Source column (ne-dodeljene)</label
|
||||
@@ -919,6 +1011,35 @@ watch(
|
||||
<option value="keyref">keyref (use as lookup key)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600">Group</label>
|
||||
<input
|
||||
v-model="(newRows[entity] ||= {}).group"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
type="text"
|
||||
placeholder="1, 2, home, work"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="(newRows[entity] || {}).field === 'meta'">
|
||||
<label class="block text-xs text-gray-600">Meta key</label>
|
||||
<input
|
||||
v-model="(newRows[entity] ||= {}).metaKey"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
type="text"
|
||||
placeholder="npr.: note, category"
|
||||
/>
|
||||
<label class="block text-xs text-gray-600 mt-2">Meta type</label>
|
||||
<select
|
||||
v-model="(newRows[entity] ||= {}).metaType"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
>
|
||||
<option value="">(auto/string)</option>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="date">date</option>
|
||||
<option value="boolean">boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-1">
|
||||
<button
|
||||
@click.prevent="addRow(entity)"
|
||||
@@ -992,6 +1113,15 @@ watch(
|
||||
<option value="keyref">keyref (use as lookup key)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600">Group (za vse)</label>
|
||||
<input
|
||||
v-model="(bulkRows[entity] ||= {}).group"
|
||||
class="mt-1 w-full border rounded p-2"
|
||||
type="text"
|
||||
placeholder="1, 2, home, work"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button
|
||||
@@ -1006,6 +1136,7 @@ watch(
|
||||
default_field: b.default_field || null,
|
||||
transform: b.transform || null,
|
||||
apply_mode: b.apply_mode || 'both',
|
||||
group: b.group || '',
|
||||
}).post(
|
||||
route('importTemplates.mappings.bulk', {
|
||||
template: props.template.uuid,
|
||||
@@ -1051,6 +1182,7 @@ watch(
|
||||
target_field: `${s.entity}.${s.field}`,
|
||||
transform: b.transform || null,
|
||||
apply_mode: b.apply_mode || 'both',
|
||||
options: b.group ? { group: b.group } : null,
|
||||
position: (props.template.mappings?.length || 0) + 1,
|
||||
};
|
||||
useForm(payload).post(
|
||||
|
||||
@@ -24,6 +24,7 @@ const props = defineProps({
|
||||
types: Object,
|
||||
actions: Array,
|
||||
activities: Array,
|
||||
completed_mode: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const viewer = reactive({ open: false, src: "", title: "" });
|
||||
@@ -206,7 +207,14 @@ const clientSummary = computed(() => {
|
||||
</h2>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<span
|
||||
v-if="props.completed_mode"
|
||||
class="inline-flex items-center px-2 py-1 rounded-full bg-emerald-100 text-emerald-700 text-xs font-medium"
|
||||
>
|
||||
Zaključeno danes
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="px-3 py-2 rounded bg-green-600 text-white hover:bg-green-700"
|
||||
@click="confirmComplete = true"
|
||||
@@ -247,6 +255,12 @@ const clientSummary = computed(() => {
|
||||
<span class="truncate">{{ client_case.person.full_name }}</span>
|
||||
<span class="chip-base chip-indigo">Primer</span>
|
||||
</h3>
|
||||
<div
|
||||
v-if="client_case?.person?.description"
|
||||
class="mt-2 text-sm text-gray-700 whitespace-pre-line"
|
||||
>
|
||||
{{ client_case.person.description }}
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-dashed">
|
||||
<PersonDetailPhone
|
||||
:types="types"
|
||||
|
||||
@@ -1,103 +1,215 @@
|
||||
<script setup>
|
||||
import AppPhoneLayout from '@/Layouts/AppPhoneLayout.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import AppPhoneLayout from "@/Layouts/AppPhoneLayout.vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
jobs: { type: Array, default: () => [] },
|
||||
view_mode: { type: String, default: "assigned" }, // 'assigned' | 'completed-today'
|
||||
});
|
||||
|
||||
const items = computed(() => props.jobs || []);
|
||||
|
||||
// Client filter options derived from jobs
|
||||
const clientFilter = ref("");
|
||||
const clientOptions = computed(() => {
|
||||
const map = new Map();
|
||||
for (const job of items.value) {
|
||||
const client = job?.contract?.client_case?.client;
|
||||
const uuid = client?.uuid;
|
||||
const name = client?.person?.full_name;
|
||||
if (uuid && name && !map.has(uuid)) {
|
||||
map.set(uuid, { uuid, name });
|
||||
}
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// Search filter (contract reference or person full name)
|
||||
const search = ref('');
|
||||
const search = ref("");
|
||||
const filteredJobs = computed(() => {
|
||||
const term = search.value.trim().toLowerCase();
|
||||
if (!term) return items.value;
|
||||
return items.value.filter(job => {
|
||||
const refStr = (job.contract?.reference || job.contract?.uuid || '').toString().toLowerCase();
|
||||
const nameStr = (job.contract?.client_case?.person?.full_name || '').toLowerCase();
|
||||
return refStr.includes(term) || nameStr.includes(term);
|
||||
return items.value.filter((job) => {
|
||||
// Filter by selected client (if any)
|
||||
if (clientFilter.value) {
|
||||
const juuid = job?.contract?.client_case?.client?.uuid;
|
||||
if (juuid !== clientFilter.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Text search
|
||||
if (!term) return true;
|
||||
const refStr = (job.contract?.reference || job.contract?.uuid || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
const nameStr = (job.contract?.client_case?.person?.full_name || "").toLowerCase();
|
||||
const clientNameStr = (
|
||||
job.contract?.client_case?.client?.person?.full_name || ""
|
||||
).toLowerCase();
|
||||
return (
|
||||
refStr.includes(term) || nameStr.includes(term) || clientNameStr.includes(term)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function formatDateDMY(d) {
|
||||
if (!d) return '-';
|
||||
if (!d) return "-";
|
||||
// Handle date-only strings from Laravel JSON casts (YYYY-MM-DD...)
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(d)) {
|
||||
const [y, m, rest] = d.split('-');
|
||||
const day = (rest || '').slice(0, 2) || '01';
|
||||
const [y, m, rest] = d.split("-");
|
||||
const day = (rest || "").slice(0, 2) || "01";
|
||||
return `${day}.${m}.${y}`;
|
||||
}
|
||||
const dt = new Date(d);
|
||||
if (Number.isNaN(dt.getTime())) return String(d);
|
||||
const dd = String(dt.getDate()).padStart(2, '0');
|
||||
const mm = String(dt.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(dt.getDate()).padStart(2, "0");
|
||||
const mm = String(dt.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = dt.getFullYear();
|
||||
return `${dd}.${mm}.${yyyy}`;
|
||||
}
|
||||
|
||||
function formatAmount(val) {
|
||||
if (val === null || val === undefined) return '0,00';
|
||||
const num = typeof val === 'number' ? val : parseFloat(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 });
|
||||
return num.toLocaleString("sl-SI", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Safely resolve a client case UUID for a job
|
||||
function getCaseUuid(job) {
|
||||
return (
|
||||
job?.contract?.client_case?.uuid || job?.client_case?.uuid || job?.case_uuid || null
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppPhoneLayout title="Phone">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Moja terenska opravila</h2>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{
|
||||
props.view_mode === "completed-today"
|
||||
? "Zaključena opravila danes"
|
||||
: "Moja terenska opravila"
|
||||
}}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-4 sm:py-8">
|
||||
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<input v-model="search" type="text" placeholder="Išči po referenci ali imenu..."
|
||||
class="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||
<button v-if="search" type="button" @click="search = ''"
|
||||
class="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200 text-gray-600">Počisti</button>
|
||||
<div class="mb-4 flex items-center gap-2 flex-wrap">
|
||||
<select
|
||||
v-model="clientFilter"
|
||||
class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="">Vsi naročniki</option>
|
||||
<option v-for="c in clientOptions" :key="c.uuid" :value="c.uuid">
|
||||
{{ c.name }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
placeholder="Išči po referenci ali imenu..."
|
||||
class="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
<button
|
||||
v-if="search"
|
||||
type="button"
|
||||
@click="search = ''"
|
||||
class="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200 text-gray-600"
|
||||
>
|
||||
Počisti
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<template v-if="filteredJobs.length">
|
||||
<div v-for="job in filteredJobs" :key="job.id" class="bg-white rounded-lg shadow border p-3 sm:p-4">
|
||||
<div
|
||||
v-for="job in filteredJobs"
|
||||
:key="job.id"
|
||||
class="bg-white rounded-lg shadow border p-3 sm:p-4"
|
||||
>
|
||||
<div class="mb-4 flex gap-2">
|
||||
<a :href="route('phone.case', { client_case: job.contract?.client_case?.uuid })"
|
||||
class="inline-flex-1 flex-1 text-center px-3 py-2 rounded-md bg-blue-600 text-white text-sm hover:bg-blue-700">Odpri
|
||||
primer</a>
|
||||
<a
|
||||
v-if="getCaseUuid(job)"
|
||||
:href="
|
||||
route('phone.case', {
|
||||
client_case: getCaseUuid(job),
|
||||
completed: props.view_mode === 'completed-today' ? 1 : undefined,
|
||||
})
|
||||
"
|
||||
class="inline-flex-1 flex-1 text-center px-3 py-2 rounded-md bg-blue-600 text-white text-sm hover:bg-blue-700"
|
||||
>
|
||||
Odpri primer
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
disabled
|
||||
class="inline-flex-1 flex-1 text-center px-3 py-2 rounded-md bg-gray-300 text-gray-600 text-sm cursor-not-allowed"
|
||||
>
|
||||
Manjka primer
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-gray-500">Dodeljeno: <span class="font-medium text-gray-700">{{
|
||||
formatDateDMY(job.assigned_at) }}</span></p>
|
||||
<span v-if="job.priority"
|
||||
class="inline-block text-xs px-2 py-0.5 rounded bg-amber-100 text-amber-700">Prioriteta</span>
|
||||
<p class="text-sm text-gray-500">
|
||||
Dodeljeno:
|
||||
<span class="font-medium text-gray-700">{{
|
||||
formatDateDMY(job.assigned_at)
|
||||
}}</span>
|
||||
</p>
|
||||
<span
|
||||
v-if="job.priority"
|
||||
class="inline-block text-xs px-2 py-0.5 rounded bg-amber-100 text-amber-700"
|
||||
>Prioriteta</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<p class="text-base sm:text-lg font-semibold text-gray-800">
|
||||
{{ job.contract?.client_case?.person?.full_name || '—' }}
|
||||
{{ job.contract?.client_case?.person?.full_name || "—" }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 truncate">Kontrakt: {{ job.contract?.reference || job.contract?.uuid }}
|
||||
<p class="text-sm text-gray-600">
|
||||
Naročnik:
|
||||
<span class="font-semibold text-gray-800">
|
||||
{{ job.contract?.client_case?.client?.person?.full_name || "—" }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">Tip: {{ job.contract?.type?.name || '—' }}</p>
|
||||
<p class="text-sm text-gray-600"
|
||||
v-if="job.contract?.account && job.contract.account.balance_amount !== null && job.contract.account.balance_amount !== undefined">
|
||||
<p class="text-sm text-gray-600 truncate">
|
||||
Kontrakt: {{ job.contract?.reference || job.contract?.uuid }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
Tip: {{ job.contract?.type?.name || "—" }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm text-gray-600"
|
||||
v-if="
|
||||
job.contract?.account &&
|
||||
job.contract.account.balance_amount !== null &&
|
||||
job.contract.account.balance_amount !== undefined
|
||||
"
|
||||
>
|
||||
Odprto: {{ formatAmount(job.contract.account.balance_amount) }} €
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-600">
|
||||
<p>
|
||||
<span class="font-medium">Naslov:</span>
|
||||
{{ job.contract?.client_case?.person?.addresses?.[0]?.address || '—' }}
|
||||
{{ job.contract?.client_case?.person?.addresses?.[0]?.address || "—" }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-medium">Telefon:</span>
|
||||
{{ job.contract?.client_case?.person?.phones?.[0]?.nu || '—' }}
|
||||
{{ job.contract?.client_case?.person?.phones?.[0]?.nu || "—" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="col-span-full bg-white rounded-lg shadow border p-6 text-center text-gray-600">
|
||||
<div
|
||||
v-else
|
||||
class="col-span-full bg-white rounded-lg shadow border p-6 text-center text-gray-600"
|
||||
>
|
||||
<span v-if="search">Ni zadetkov za podani filter.</span>
|
||||
<span v-else>Trenutno nimate dodeljenih terenskih opravil.</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user