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:
Simon Pocrnjič
2025-10-09 22:28:48 +02:00
parent c8029c9eb0
commit 0598261cdc
27 changed files with 2517 additions and 375 deletions
+32 -1
View File
@@ -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>
+134 -2
View File
@@ -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(