changes 0230092025
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ref, watch, computed, onMounted } from 'vue';
|
||||
import { useForm, router } from '@inertiajs/vue3';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import axios from 'axios';
|
||||
@@ -23,6 +23,53 @@ const selectedMappingsCount = computed(() => mappingRows.value.filter(r => !r.sk
|
||||
const mappingError = ref('');
|
||||
const savingMappings = ref(false);
|
||||
|
||||
// Dynamic entity definitions and suggestions from API
|
||||
const entityDefs = ref([]);
|
||||
const entityOptions = computed(() => entityDefs.value.map(e => ({ value: e.key, label: e.label || e.key })));
|
||||
const fieldOptionsByEntity = computed(() => Object.fromEntries(entityDefs.value.map(e => [e.key, (e.fields || []).map(f => ({ value: f, label: f }))])));
|
||||
const canonicalRootByKey = computed(() => Object.fromEntries(entityDefs.value.map(e => [e.key, e.canonical_root || e.key])));
|
||||
const keyByCanonicalRoot = computed(() => {
|
||||
const m = {};
|
||||
for (const e of entityDefs.value) {
|
||||
if (e.canonical_root) {
|
||||
m[e.canonical_root] = e.key;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
});
|
||||
const suggestions = ref({});
|
||||
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);
|
||||
}
|
||||
}
|
||||
async function refreshSuggestions(columns) {
|
||||
const cols = Array.isArray(columns) ? columns : (detected.value.columns || []);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function applySuggestionToRow(row) {
|
||||
const s = suggestions.value[row.source_column];
|
||||
if (!s) return false;
|
||||
if (!fieldOptionsByEntity.value[s.entity]) return false;
|
||||
row.entity = s.entity;
|
||||
row.field = s.field;
|
||||
// default transform on if missing
|
||||
if (!row.transform) { row.transform = 'trim'; }
|
||||
if (!row.apply_mode) { row.apply_mode = 'both'; }
|
||||
row.skip = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
client_uuid: null,
|
||||
import_template_id: null,
|
||||
@@ -120,6 +167,7 @@ async function fetchColumns() {
|
||||
position: idx,
|
||||
}));
|
||||
}
|
||||
await refreshSuggestions(detected.value.columns);
|
||||
// If there are mappings already (template applied or saved), load them to auto-assign
|
||||
await loadImportMappings();
|
||||
}
|
||||
@@ -203,9 +251,9 @@ async function loadImportMappings() {
|
||||
mappingRows.value = (mappingRows.value || []).map((r, idx) => {
|
||||
const m = bySource.get(r.source_column);
|
||||
if (!m) return r;
|
||||
// Parse target_field like 'person.first_name' into UI entity/field
|
||||
const [record, field] = String(m.target_field || '').split('.', 2);
|
||||
const entity = recordToEntityKey(record);
|
||||
// Parse target_field like 'person.first_name' into UI entity/field
|
||||
const [record, field] = String(m.target_field || '').split('.', 2);
|
||||
const entity = keyByCanonicalRoot.value[record] || record;
|
||||
return {
|
||||
...r,
|
||||
entity,
|
||||
@@ -244,61 +292,7 @@ async function processImport() {
|
||||
}
|
||||
}
|
||||
|
||||
const entityOptions = [
|
||||
{ value: 'person', label: 'Person' },
|
||||
{ value: 'person_addresses', label: 'Person Address' },
|
||||
{ value: 'person_phones', label: 'Person Phone' },
|
||||
{ value: 'emails', label: 'Email' },
|
||||
{ value: 'accounts', label: 'Account' },
|
||||
{ value: 'contracts', label: 'Contract' },
|
||||
];
|
||||
|
||||
const fieldOptionsByEntity = {
|
||||
person: [
|
||||
{ value: 'first_name', label: 'First name' },
|
||||
{ value: 'last_name', label: 'Last name' },
|
||||
{ value: 'full_name', label: 'Full name' },
|
||||
{ value: 'tax_number', label: 'Tax number' },
|
||||
{ value: 'social_security_number', label: 'SSN' },
|
||||
{ value: 'birthday', label: 'Birthday' },
|
||||
{ value: 'gender', label: 'Gender' },
|
||||
{ value: 'description', label: 'Description' },
|
||||
],
|
||||
person_addresses: [
|
||||
{ value: 'address', label: 'Address' },
|
||||
{ value: 'country', label: 'Country' },
|
||||
{ value: 'type_id', label: 'Address Type Id' },
|
||||
{ value: 'description', label: 'Description' },
|
||||
],
|
||||
person_phones: [
|
||||
{ value: 'nu', label: 'Phone number' },
|
||||
{ value: 'country_code', label: 'Country code' },
|
||||
{ value: 'type_id', label: 'Phone Type Id' },
|
||||
{ value: 'description', label: 'Description' },
|
||||
],
|
||||
emails: [
|
||||
{ value: 'value', label: 'Email address' },
|
||||
{ value: 'label', label: 'Label' },
|
||||
{ value: 'is_primary', label: 'Is primary' },
|
||||
],
|
||||
accounts: [
|
||||
{ value: 'reference', label: 'Reference' },
|
||||
{ value: 'description', label: 'Description' },
|
||||
{ value: 'contract_id', label: 'Contract Id' },
|
||||
{ value: 'contract_reference', label: 'Contract Reference' },
|
||||
{ value: 'type_id', label: 'Account Type Id' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'balance_amount', label: 'Balance Amount' },
|
||||
],
|
||||
contracts: [
|
||||
{ value: 'reference', label: 'Reference' },
|
||||
{ value: 'start_date', label: 'Start Date' },
|
||||
{ value: 'end_date', label: 'End Date' },
|
||||
{ value: 'description', label: 'Description' },
|
||||
{ value: 'client_case_id', label: 'Client Case Id' },
|
||||
{ value: 'type_id', label: 'Contract Type Id' },
|
||||
],
|
||||
};
|
||||
// entity options and fields are dynamic from API
|
||||
|
||||
async function saveMappings() {
|
||||
if (!importId.value) return;
|
||||
@@ -307,7 +301,7 @@ async function saveMappings() {
|
||||
.filter(r => !r.skip && r.entity && r.field)
|
||||
.map(r => ({
|
||||
source_column: r.source_column,
|
||||
target_field: `${entityKeyToRecord(r.entity)}.${r.field}`,
|
||||
target_field: `${(canonicalRootByKey.value[r.entity] || r.entity)}.${r.field}`,
|
||||
transform: r.transform || null,
|
||||
apply_mode: r.apply_mode || 'both',
|
||||
options: null,
|
||||
@@ -350,24 +344,9 @@ watch(mappingRows, () => {
|
||||
mappingError.value = '';
|
||||
}, { deep: true });
|
||||
|
||||
function entityKeyToRecord(key) {
|
||||
// Map UI entities to record_type nouns used by processor
|
||||
if (key === 'person_addresses') return 'address';
|
||||
if (key === 'person_phones') return 'phone';
|
||||
if (key === 'emails') return 'email';
|
||||
if (key === 'accounts') return 'account';
|
||||
if (key === 'contracts') return 'contract';
|
||||
return 'person';
|
||||
}
|
||||
|
||||
function recordToEntityKey(record) {
|
||||
if (record === 'address') return 'person_addresses';
|
||||
if (record === 'phone') return 'person_phones';
|
||||
if (record === 'email') return 'emails';
|
||||
if (record === 'account') return 'accounts';
|
||||
if (record === 'contract') return 'contracts';
|
||||
return 'person';
|
||||
}
|
||||
onMounted(async () => {
|
||||
await loadEntityDefs();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -472,7 +451,13 @@ function recordToEntityKey(record) {
|
||||
</div>
|
||||
|
||||
<div v-if="detected.columns.length" class="pt-4">
|
||||
<h3 class="font-semibold mb-2">Detected Columns ({{ detected.has_header ? 'header' : 'positional' }})</h3>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-semibold">Detected Columns ({{ detected.has_header ? 'header' : 'positional' }})</h3>
|
||||
<button
|
||||
class="px-3 py-1.5 border rounded text-sm"
|
||||
@click.prevent="(async () => { await refreshSuggestions(detected.columns); mappingRows.forEach(r => applySuggestionToRow(r)); })()"
|
||||
>Auto map suggestions</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full border bg-white">
|
||||
<thead>
|
||||
@@ -487,7 +472,15 @@ function recordToEntityKey(record) {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in mappingRows" :key="idx" class="border-t">
|
||||
<td class="p-2 border text-sm">{{ row.source_column }}</td>
|
||||
<td class="p-2 border text-sm">
|
||||
<div>{{ row.source_column }}</div>
|
||||
<div class="text-xs mt-1" v-if="suggestions[row.source_column]">
|
||||
<span class="text-gray-500">Suggest:</span>
|
||||
<button class="ml-1 underline text-indigo-700 hover:text-indigo-900" @click.prevent="applySuggestionToRow(row)">
|
||||
{{ suggestions[row.source_column].entity }}.{{ suggestions[row.source_column].field }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-2 border">
|
||||
<select v-model="row.entity" class="border rounded p-1 w-full">
|
||||
<option value="">—</option>
|
||||
|
||||
Reference in New Issue
Block a user