changes 0230092025
This commit is contained in:
@@ -3,9 +3,13 @@ import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
clients: Array,
|
||||
segments: Array,
|
||||
decisions: Array,
|
||||
actions: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
@@ -16,6 +20,22 @@ const form = useForm({
|
||||
is_active: true,
|
||||
client_uuid: null,
|
||||
entities: [],
|
||||
meta: {
|
||||
segment_id: null,
|
||||
decision_id: null,
|
||||
action_id: null,
|
||||
delimiter: '',
|
||||
},
|
||||
});
|
||||
|
||||
const decisionsForSelectedAction = computed(() => {
|
||||
const act = (props.actions || []).find(a => a.id === form.meta.action_id);
|
||||
return act?.decisions || [];
|
||||
});
|
||||
|
||||
watch(() => form.meta.action_id, () => {
|
||||
// Clear decision when action changes to enforce valid pair
|
||||
form.meta.decision_id = null;
|
||||
});
|
||||
|
||||
function submit() {
|
||||
@@ -75,6 +95,32 @@ function submit() {
|
||||
<p class="text-xs text-gray-500 mt-1">Choose which tables this template targets. You can still define per-column mappings later.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Defaults: Segment / Decision / Action -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Default Segment</label>
|
||||
<select v-model="form.meta.segment_id" class="mt-1 block w-full border rounded p-2">
|
||||
<option :value="null">(none)</option>
|
||||
<option v-for="s in (props.segments || [])" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Default Decision</label>
|
||||
<select v-model="form.meta.decision_id" class="mt-1 block w-full border rounded p-2" :disabled="!form.meta.action_id">
|
||||
<option :value="null">(none)</option>
|
||||
<option v-for="d in decisionsForSelectedAction" :key="d.id" :value="d.id">{{ d.name }}</option>
|
||||
</select>
|
||||
<p v-if="!form.meta.action_id" class="text-xs text-gray-500 mt-1">Select an Action to see its Decisions.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Default Action (for Activity)</label>
|
||||
<select v-model="form.meta.action_id" class="mt-1 block w-full border rounded p-2">
|
||||
<option :value="null">(none)</option>
|
||||
<option v-for="a in (props.actions || [])" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input v-model="form.name" type="text" class="mt-1 block w-full border rounded p-2" />
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useForm, Link } from '@inertiajs/vue3';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import axios from 'axios';
|
||||
import { computed as vComputed, watch as vWatch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
template: Object,
|
||||
clients: Array,
|
||||
segments: Array,
|
||||
decisions: Array,
|
||||
actions: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
@@ -24,6 +29,15 @@ const form = useForm({
|
||||
},
|
||||
});
|
||||
|
||||
const decisionsForSelectedAction = vComputed(() => {
|
||||
const act = (props.actions || []).find(a => a.id === form.meta.action_id);
|
||||
return act?.decisions || [];
|
||||
});
|
||||
|
||||
vWatch(() => form.meta.action_id, () => {
|
||||
form.meta.decision_id = null;
|
||||
});
|
||||
|
||||
const entities = computed(() => (props.template.meta?.entities || []));
|
||||
const hasMappings = computed(() => (props.template.mappings?.length || 0) > 0);
|
||||
const canChangeClient = computed(() => !hasMappings.value); // guard reassignment when mappings exist (optional rule)
|
||||
@@ -42,6 +56,31 @@ const unassignedSourceColumns = computed(() => {
|
||||
});
|
||||
const unassignedState = ref({});
|
||||
|
||||
// Dynamic Import Entity definitions and field options from API
|
||||
const entityDefs = ref([]);
|
||||
const entityOptions = computed(() => entityDefs.value.map(e => ({ key: e.key, label: e.label || e.key })));
|
||||
const fieldOptions = computed(() => Object.fromEntries(entityDefs.value.map(e => [e.key, e.fields || []])));
|
||||
const ENTITY_ALIASES = computed(() => {
|
||||
const map = {};
|
||||
for (const e of entityDefs.value) {
|
||||
const aliases = Array.isArray(e.aliases) ? [...e.aliases] : [];
|
||||
if (!aliases.includes(e.key)) {
|
||||
aliases.push(e.key);
|
||||
}
|
||||
map[e.key] = aliases;
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
async function loadEntityDefs() {
|
||||
try {
|
||||
const { data } = await axios.get('/api/import-entities');
|
||||
entityDefs.value = data?.entities || [];
|
||||
} catch (e) {
|
||||
console.error('Failed to load import entity definitions', e);
|
||||
}
|
||||
}
|
||||
|
||||
function saveUnassigned(m) {
|
||||
const st = unassignedState.value[m.id] || {};
|
||||
if (st.entity && st.field) {
|
||||
@@ -52,25 +91,24 @@ function saveUnassigned(m) {
|
||||
updateMapping(m);
|
||||
}
|
||||
|
||||
const entityOptions = [
|
||||
{ key: 'person', label: 'Person' },
|
||||
{ key: 'person_addresses', label: 'Person Addresses' },
|
||||
{ key: 'person_phones', label: 'Person Phones' },
|
||||
{ key: 'emails', label: 'Emails' },
|
||||
{ key: 'accounts', label: 'Accounts' },
|
||||
{ key: 'contracts', label: 'Contracts' },
|
||||
];
|
||||
// Suggestions powered by backend API
|
||||
const suggestions = ref({}); // { [sourceColumn]: { entity, field } }
|
||||
async function refreshSuggestions(columns) {
|
||||
const cols = Array.isArray(columns) ? columns : unassignedSourceColumns.value;
|
||||
if (!cols || cols.length === 0) { return; }
|
||||
try {
|
||||
const { data } = await axios.post('/api/import-entities/suggest', { columns: cols });
|
||||
suggestions.value = { ...suggestions.value, ...(data?.suggestions || {}) };
|
||||
} catch (e) {
|
||||
console.error('Failed to load suggestions', e);
|
||||
}
|
||||
}
|
||||
|
||||
const fieldOptions = {
|
||||
person: [
|
||||
'first_name', 'last_name', 'full_name', 'gender', 'birthday', 'tax_number', 'social_security_number', 'description'
|
||||
],
|
||||
person_addresses: [ 'address', 'country', 'type_id', 'description' ],
|
||||
person_phones: [ 'nu', 'country_code', 'type_id', 'description' ],
|
||||
emails: [ 'email', 'is_primary' ],
|
||||
accounts: [ 'reference', 'balance_amount', 'contract_id', 'contract_reference' ],
|
||||
contracts: [ 'reference', 'start_date', 'end_date', 'description', 'type_id', 'client_case_id' ],
|
||||
};
|
||||
// entityOptions and fieldOptions now come from API (see computed above)
|
||||
|
||||
// ENTITY_ALIASES computed above from API definitions
|
||||
|
||||
// UI_PREFERRED no longer needed; UI keys are already the desired keys
|
||||
|
||||
function toggle(entity) {
|
||||
const el = document.getElementById(`acc-${entity}`);
|
||||
@@ -116,7 +154,11 @@ function deleteMapping(m) {
|
||||
function reorder(entity, direction, m) {
|
||||
// Build new order across all mappings, swapping positions for this entity scope
|
||||
const all = [...props.template.mappings];
|
||||
const entityMaps = all.filter(x => x.target_field?.startsWith(entity + '.'));
|
||||
const aliases = (ENTITY_ALIASES.value[entity] || [entity]).map(a => a + '.');
|
||||
const entityMaps = all.filter(x => {
|
||||
const tf = x.target_field || '';
|
||||
return aliases.some(prefix => tf.startsWith(prefix));
|
||||
});
|
||||
const idx = entityMaps.findIndex(x => x.id === m.id);
|
||||
if (idx < 0) return;
|
||||
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
|
||||
@@ -158,6 +200,18 @@ function performDelete() {
|
||||
onFinish: () => { deleteConfirmOpen.value = false; },
|
||||
});
|
||||
}
|
||||
|
||||
// Load entity definitions and initial suggestions
|
||||
onMounted(async () => {
|
||||
await loadEntityDefs();
|
||||
await refreshSuggestions();
|
||||
});
|
||||
|
||||
// Refresh suggestions when unassigned list changes
|
||||
watch(() => unassignedSourceColumns.value.join('|'), async () => {
|
||||
await refreshSuggestions();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -223,6 +277,28 @@ function performDelete() {
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Pusti prazno za samodejno zaznavo. Uporabi, ko zaznavanje ne deluje pravilno.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Privzeti Segment</label>
|
||||
<select v-model="form.meta.segment_id" class="mt-1 block w-full border rounded p-2">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="s in (props.segments || [])" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Privzeta Odločitev</label>
|
||||
<select v-model="form.meta.decision_id" class="mt-1 block w-full border rounded p-2" :disabled="!form.meta.action_id">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="d in decisionsForSelectedAction" :key="d.id" :value="d.id">{{ d.name }}</option>
|
||||
</select>
|
||||
<p v-if="!form.meta.action_id" class="text-xs text-gray-500 mt-1">Najprej izberi dejanje, nato odločitev.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Privzeto Dejanja (Activity)</label>
|
||||
<select v-model="form.meta.action_id" class="mt-1 block w-full border rounded p-2">
|
||||
<option :value="null">(brez)</option>
|
||||
<option v-for="a in (props.actions || [])" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="is_active" v-model="form.is_active" type="checkbox" class="rounded" />
|
||||
<label for="is_active" class="text-sm font-medium text-gray-700">Aktivna</label>
|
||||
@@ -313,6 +389,13 @@ function performDelete() {
|
||||
<div class="text-sm">
|
||||
<div class="text-gray-500 text-xs">Source</div>
|
||||
<div class="font-medium">{{ m.source_column }}</div>
|
||||
<div class="mt-1 text-xs" v-if="suggestions[m.source_column]">
|
||||
<span class="text-gray-500">Predlog:</span>
|
||||
<button
|
||||
class="ml-1 underline text-indigo-700 hover:text-indigo-900"
|
||||
@click.prevent="(() => { const s = suggestions[m.source_column]; if (!s) return; (unassignedState[m.id] ||= {}).entity = s.entity; (unassignedState[m.id] ||= {}).field = s.field; saveUnassigned(m); })()"
|
||||
>{{ suggestions[m.source_column].entity }}.{{ suggestions[m.source_column].field }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600">Entity</label>
|
||||
@@ -364,7 +447,10 @@ function performDelete() {
|
||||
<div class="mt-4 space-y-4">
|
||||
<!-- Existing mappings for this entity -->
|
||||
<div v-if="props.template.mappings && props.template.mappings.length" class="space-y-2">
|
||||
<div v-for="m in props.template.mappings.filter(m=>m.target_field?.startsWith(entity + '.'))" :key="m.id" class="flex items-center justify-between p-2 border rounded gap-3">
|
||||
<div v-for="m in props.template.mappings.filter(m => {
|
||||
const aliases = ENTITY_ALIASES[entity] || [entity];
|
||||
return aliases.some(a => m.target_field?.startsWith(a + '.'));
|
||||
})" :key="m.id" 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">
|
||||
<input v-model="m.source_column" class="border rounded p-2 text-sm" />
|
||||
<input v-model="m.target_field" class="border rounded p-2 text-sm" />
|
||||
@@ -470,9 +556,10 @@ function performDelete() {
|
||||
@click.prevent="(() => {
|
||||
const b = bulkRows[entity] ||= {};
|
||||
if (!b.sources || !b.sources.trim()) return;
|
||||
const eKey = entity;
|
||||
useForm({
|
||||
sources: b.sources,
|
||||
entity,
|
||||
entity: eKey,
|
||||
default_field: b.default_field || null,
|
||||
transform: b.transform || null,
|
||||
apply_mode: b.apply_mode || 'both',
|
||||
@@ -483,6 +570,35 @@ function performDelete() {
|
||||
})()"
|
||||
class="px-3 py-2 bg-indigo-600 text-white rounded"
|
||||
>Dodaj več</button>
|
||||
<button
|
||||
class="ml-2 px-3 py-2 bg-emerald-600 text-white rounded"
|
||||
@click.prevent="(async () => {
|
||||
const b = bulkRows[entity] ||= {};
|
||||
if (!b.sources || !b.sources.trim()) return;
|
||||
const list = b.sources.split(/[\n,]+/).map(s=>s.trim()).filter(Boolean);
|
||||
try {
|
||||
const { data } = await axios.post('/api/import-entities/suggest', { columns: list });
|
||||
const sugg = data?.suggestions || {};
|
||||
for (const src of list) {
|
||||
const s = sugg[src];
|
||||
if (!s) continue;
|
||||
const aliases = ENTITY_ALIASES.value[entity] || [entity];
|
||||
if (!aliases.includes(s.entity)) continue; // only apply for this entity
|
||||
const payload = {
|
||||
source_column: src,
|
||||
target_field: `${s.entity}.${s.field}`,
|
||||
transform: b.transform || null,
|
||||
apply_mode: b.apply_mode || 'both',
|
||||
position: (props.template.mappings?.length || 0) + 1,
|
||||
};
|
||||
useForm(payload).post(route('importTemplates.mappings.add', { template: props.template.uuid }), { preserveScroll: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to auto-add suggestions', e);
|
||||
}
|
||||
bulkRows[entity] = {};
|
||||
})()"
|
||||
>Auto iz predlog</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user