production #1
|
|
@ -58,6 +58,13 @@ public function index()
|
||||||
'fields' => ['reference', 'balance_amount', 'contract_id', 'contract_reference', 'type_id', 'active', 'description'],
|
'fields' => ['reference', 'balance_amount', 'contract_id', 'contract_reference', 'type_id', 'active', 'description'],
|
||||||
'ui' => ['order' => 6],
|
'ui' => ['order' => 6],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'activities',
|
||||||
|
'canonical_root' => 'activity',
|
||||||
|
'label' => 'Activities',
|
||||||
|
'fields' => ['note', 'due_date', 'amount', 'action_id', 'decision_id', 'contract_id', 'client_case_id', 'user_id'],
|
||||||
|
'ui' => ['order' => 7],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Ensure fields are arrays for frontend consumption
|
// Ensure fields are arrays for frontend consumption
|
||||||
|
|
|
||||||
|
|
@ -111,10 +111,10 @@ public function store(Request $request)
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'reactivate' => 'boolean',
|
'reactivate' => 'boolean',
|
||||||
'entities' => 'nullable|array',
|
'entities' => 'nullable|array',
|
||||||
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'mappings' => 'array',
|
'mappings' => 'array',
|
||||||
'mappings.*.source_column' => 'required|string',
|
'mappings.*.source_column' => 'required|string',
|
||||||
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'mappings.*.target_field' => 'nullable|string',
|
'mappings.*.target_field' => 'nullable|string',
|
||||||
'mappings.*.transform' => 'nullable|string|max:50',
|
'mappings.*.transform' => 'nullable|string|max:50',
|
||||||
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
|
|
@ -124,7 +124,11 @@ public function store(Request $request)
|
||||||
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
||||||
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
|
'meta.activity_created_at' => 'nullable|date',
|
||||||
'meta.payments_import' => 'nullable|boolean',
|
'meta.payments_import' => 'nullable|boolean',
|
||||||
|
'meta.history_import' => 'nullable|boolean',
|
||||||
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
|
|
@ -142,10 +146,38 @@ public function store(Request $request)
|
||||||
$template = null;
|
$template = null;
|
||||||
DB::transaction(function () use (&$template, $request, $data) {
|
DB::transaction(function () use (&$template, $request, $data) {
|
||||||
$paymentsImport = (bool) (data_get($data, 'meta.payments_import') ?? false);
|
$paymentsImport = (bool) (data_get($data, 'meta.payments_import') ?? false);
|
||||||
|
$historyImport = (bool) (data_get($data, 'meta.history_import') ?? false);
|
||||||
$entities = $data['entities'] ?? [];
|
$entities = $data['entities'] ?? [];
|
||||||
|
if ($historyImport) {
|
||||||
|
$paymentsImport = false; // history import cannot be combined with payments mode
|
||||||
|
$allowedHistoryEntities = ['person', 'person_addresses', 'person_phones', 'contracts', 'activities', 'client_cases'];
|
||||||
|
$entities = array_values(array_intersect($entities, $allowedHistoryEntities));
|
||||||
|
// If contracts are present, ensure accounts are included implicitly for reference consistency
|
||||||
|
if (in_array('contracts', $entities, true) && ! in_array('accounts', $entities, true)) {
|
||||||
|
$entities[] = 'accounts';
|
||||||
|
}
|
||||||
|
// Reject mappings that target disallowed entities for history import
|
||||||
|
$disallowedMappings = collect($data['mappings'] ?? [])->filter(function ($m) use ($allowedHistoryEntities) {
|
||||||
|
if (empty($m['entity'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! in_array($m['entity'], $allowedHistoryEntities, true);
|
||||||
|
});
|
||||||
|
if ($disallowedMappings->isNotEmpty()) {
|
||||||
|
abort(422, 'History import only allows entities: person, person_addresses, person_phones, contracts, activities, client_cases. Remove other mapping entities.');
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($paymentsImport) {
|
if ($paymentsImport) {
|
||||||
$entities = ['contracts', 'accounts', 'payments'];
|
$entities = ['contracts', 'accounts', 'payments'];
|
||||||
}
|
}
|
||||||
|
if (in_array('activities', $entities, true)) {
|
||||||
|
$actionId = data_get($data, 'meta.activity_action_id');
|
||||||
|
$decisionId = data_get($data, 'meta.activity_decision_id');
|
||||||
|
if (! $actionId || ! $decisionId) {
|
||||||
|
abort(422, 'Activities import requires selecting both a default action and decision.');
|
||||||
|
}
|
||||||
|
}
|
||||||
$template = ImportTemplate::create([
|
$template = ImportTemplate::create([
|
||||||
'uuid' => (string) Str::uuid(),
|
'uuid' => (string) Str::uuid(),
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
|
|
@ -162,7 +194,11 @@ public function store(Request $request)
|
||||||
'segment_id' => data_get($data, 'meta.segment_id'),
|
'segment_id' => data_get($data, 'meta.segment_id'),
|
||||||
'decision_id' => data_get($data, 'meta.decision_id'),
|
'decision_id' => data_get($data, 'meta.decision_id'),
|
||||||
'action_id' => data_get($data, 'meta.action_id'),
|
'action_id' => data_get($data, 'meta.action_id'),
|
||||||
|
'activity_action_id' => data_get($data, 'meta.activity_action_id'),
|
||||||
|
'activity_decision_id' => data_get($data, 'meta.activity_decision_id'),
|
||||||
|
'activity_created_at' => data_get($data, 'meta.activity_created_at'),
|
||||||
'payments_import' => $paymentsImport ?: null,
|
'payments_import' => $paymentsImport ?: null,
|
||||||
|
'history_import' => $historyImport ?: null,
|
||||||
'contract_key_mode' => data_get($data, 'meta.contract_key_mode'),
|
'contract_key_mode' => data_get($data, 'meta.contract_key_mode'),
|
||||||
], fn ($v) => ! is_null($v) && $v !== ''),
|
], fn ($v) => ! is_null($v) && $v !== ''),
|
||||||
]);
|
]);
|
||||||
|
|
@ -244,7 +280,7 @@ public function addMapping(Request $request, ImportTemplate $template)
|
||||||
}
|
}
|
||||||
$data = validator($raw, [
|
$data = validator($raw, [
|
||||||
'source_column' => 'required|string',
|
'source_column' => 'required|string',
|
||||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'target_field' => 'nullable|string',
|
'target_field' => 'nullable|string',
|
||||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
|
|
@ -314,7 +350,11 @@ public function update(Request $request, ImportTemplate $template)
|
||||||
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
'meta.segment_id' => 'nullable|integer|exists:segments,id',
|
||||||
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
'meta.action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_action_id' => 'nullable|integer|exists:actions,id',
|
||||||
|
'meta.activity_decision_id' => 'nullable|integer|exists:decisions,id',
|
||||||
|
'meta.activity_created_at' => 'nullable|date',
|
||||||
'meta.payments_import' => 'nullable|boolean',
|
'meta.payments_import' => 'nullable|boolean',
|
||||||
|
'meta.history_import' => 'nullable|boolean',
|
||||||
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
'meta.contract_key_mode' => 'nullable|string|in:reference',
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
|
|
@ -342,6 +382,11 @@ public function update(Request $request, ImportTemplate $template)
|
||||||
unset($newMeta[$k]);
|
unset($newMeta[$k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
foreach (['activity_action_id', 'activity_decision_id', 'activity_created_at'] as $k) {
|
||||||
|
if (array_key_exists($k, $newMeta) && ($newMeta[$k] === '' || is_null($newMeta[$k]))) {
|
||||||
|
unset($newMeta[$k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize meta (ensure payments entities forced if enabled)
|
// Finalize meta (ensure payments entities forced if enabled)
|
||||||
|
|
@ -349,6 +394,20 @@ public function update(Request $request, ImportTemplate $template)
|
||||||
if (! empty($finalMeta['payments_import'])) {
|
if (! empty($finalMeta['payments_import'])) {
|
||||||
$finalMeta['entities'] = ['contracts', 'accounts', 'payments'];
|
$finalMeta['entities'] = ['contracts', 'accounts', 'payments'];
|
||||||
}
|
}
|
||||||
|
if (! empty($finalMeta['history_import'])) {
|
||||||
|
$finalMeta['payments_import'] = false;
|
||||||
|
$allowedHistoryEntities = ['person', 'person_addresses', 'person_phones', 'contracts', 'activities', 'client_cases'];
|
||||||
|
$finalMeta['entities'] = array_values(array_intersect($finalMeta['entities'] ?? [], $allowedHistoryEntities));
|
||||||
|
if (in_array('contracts', $finalMeta['entities'] ?? [], true) && ! in_array('accounts', $finalMeta['entities'] ?? [], true)) {
|
||||||
|
$finalMeta['entities'][] = 'accounts';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('activities', $finalMeta['entities'] ?? [], true)) {
|
||||||
|
if (empty($finalMeta['activity_action_id']) || empty($finalMeta['activity_decision_id'])) {
|
||||||
|
return back()->withErrors(['meta.activity_action_id' => 'Activities import requires selecting both a default action and decision.'])->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$update = [
|
$update = [
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
|
|
@ -381,7 +440,7 @@ public function bulkAddMappings(Request $request, ImportTemplate $template)
|
||||||
}
|
}
|
||||||
$data = validator($raw, [
|
$data = validator($raw, [
|
||||||
'sources' => 'required|string', // comma and/or newline separated
|
'sources' => 'required|string', // comma and/or newline separated
|
||||||
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments',
|
'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments,activities',
|
||||||
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
|
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
|
||||||
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||||
'transform' => 'nullable|string|in:trim,upper,lower',
|
'transform' => 'nullable|string|in:trim,upper,lower',
|
||||||
|
|
@ -583,6 +642,9 @@ public function applyToImport(Request $request, ImportTemplate $template, Import
|
||||||
'segment_id' => $tplMeta['segment_id'] ?? null,
|
'segment_id' => $tplMeta['segment_id'] ?? null,
|
||||||
'decision_id' => $tplMeta['decision_id'] ?? null,
|
'decision_id' => $tplMeta['decision_id'] ?? null,
|
||||||
'action_id' => $tplMeta['action_id'] ?? null,
|
'action_id' => $tplMeta['action_id'] ?? null,
|
||||||
|
'activity_action_id' => $tplMeta['activity_action_id'] ?? null,
|
||||||
|
'activity_decision_id' => $tplMeta['activity_decision_id'] ?? null,
|
||||||
|
'activity_created_at' => $tplMeta['activity_created_at'] ?? null,
|
||||||
'template_name' => $template->name,
|
'template_name' => $template->name,
|
||||||
], fn ($v) => ! is_null($v) && $v !== ''));
|
], fn ($v) => ! is_null($v) && $v !== ''));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,18 @@
|
||||||
use App\Models\Person\PersonType;
|
use App\Models\Person\PersonType;
|
||||||
use App\Models\Person\PhoneType;
|
use App\Models\Person\PhoneType;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class ImportProcessor
|
class ImportProcessor
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Track contracts that already existed and were matched during history imports.
|
||||||
|
* @var array<int,bool>
|
||||||
|
*/
|
||||||
|
private array $historyFoundContractIds = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process an import and apply basic dedup checks.
|
* Process an import and apply basic dedup checks.
|
||||||
* Returns summary counts.
|
* Returns summary counts.
|
||||||
|
|
@ -42,6 +49,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
$imported = 0;
|
$imported = 0;
|
||||||
$invalid = 0;
|
$invalid = 0;
|
||||||
$fh = null;
|
$fh = null;
|
||||||
|
$this->historyFoundContractIds = [];
|
||||||
|
|
||||||
// Only CSV/TSV supported in this pass
|
// Only CSV/TSV supported in this pass
|
||||||
if (! in_array($import->source_type, ['csv', 'txt'])) {
|
if (! in_array($import->source_type, ['csv', 'txt'])) {
|
||||||
|
|
@ -73,6 +81,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
// Template meta flags
|
// Template meta flags
|
||||||
$tplMeta = optional($import->template)->meta ?? [];
|
$tplMeta = optional($import->template)->meta ?? [];
|
||||||
$paymentsImport = (bool) ($tplMeta['payments_import'] ?? false);
|
$paymentsImport = (bool) ($tplMeta['payments_import'] ?? false);
|
||||||
|
$historyImport = (bool) ($tplMeta['history_import'] ?? false);
|
||||||
$contractKeyMode = $tplMeta['contract_key_mode'] ?? null;
|
$contractKeyMode = $tplMeta['contract_key_mode'] ?? null;
|
||||||
// Prefer explicitly chosen delimiter, then template meta, else detected
|
// Prefer explicitly chosen delimiter, then template meta, else detected
|
||||||
$delimiter = $import->meta['forced_delimiter']
|
$delimiter = $import->meta['forced_delimiter']
|
||||||
|
|
@ -299,9 +308,13 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
$contractResult = null;
|
$contractResult = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$contractResult = $this->upsertContractChain($import, $mapped, $mappings);
|
$contractResult = $this->upsertContractChain($import, $mapped, $mappings, $historyImport);
|
||||||
// If contract was resolved/updated/inserted and reactivation requested but not needed (already active), we just continue normal flow.
|
// If contract was resolved/updated/inserted and reactivation requested but not needed (already active), we just continue normal flow.
|
||||||
if ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
if ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||||
|
// Do not attempt reactivation on freshly inserted contracts
|
||||||
|
if (($contractResult['action'] ?? null) === 'inserted') {
|
||||||
|
// Newly created contracts are already active; skip reactivation path
|
||||||
|
} else {
|
||||||
$found = $contractResult['contract'];
|
$found = $contractResult['contract'];
|
||||||
if ($found->active == 0 || $found->deleted_at) {
|
if ($found->active == 0 || $found->deleted_at) {
|
||||||
$reactivationApplied = $this->attemptContractReactivation($found, $user);
|
$reactivationApplied = $this->attemptContractReactivation($found, $user);
|
||||||
|
|
@ -326,7 +339,20 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($contractResult['action'] === 'skipped') {
|
}
|
||||||
|
if ($contractResult['action'] === 'skipped_history') {
|
||||||
|
// History import: keep existing contract for downstream relations but do not update or attach segments/actions
|
||||||
|
$skipped++;
|
||||||
|
$importRow->update(['status' => 'skipped']);
|
||||||
|
ImportEvent::create([
|
||||||
|
'import_id' => $import->id,
|
||||||
|
'user_id' => $user?->getAuthIdentifier(),
|
||||||
|
'import_row_id' => $importRow->id,
|
||||||
|
'event' => 'row_skipped',
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => $contractResult['message'] ?? 'Existing contract reused (history import).',
|
||||||
|
]);
|
||||||
|
} elseif ($contractResult['action'] === 'skipped') {
|
||||||
// Even if no contract fields were updated, we may still need to apply template meta
|
// Even if no contract fields were updated, we may still need to apply template meta
|
||||||
// like attaching a segment or creating an activity. Do that if we have the contract.
|
// like attaching a segment or creating an activity. Do that if we have the contract.
|
||||||
if (isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
if (isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||||
|
|
@ -437,12 +463,18 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
$accountResult = null;
|
$accountResult = null;
|
||||||
|
if ($historyImport && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract && ! isset($mapped['account'])) {
|
||||||
|
$autoAcc = $this->ensureHistoryAccount($contractResult['contract'], $mapped);
|
||||||
|
if ($autoAcc) {
|
||||||
|
$accountResult = ['action' => 'inserted', 'account' => $autoAcc, 'contract' => $contractResult['contract'], 'contract_id' => $contractResult['contract']->id];
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isset($mapped['account'])) {
|
if (isset($mapped['account'])) {
|
||||||
// If a contract was just created or resolved above, pass its id to account mapping for this row
|
// If a contract was just created or resolved above, pass its id to account mapping for this row
|
||||||
if ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
if ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||||
$mapped['account']['contract_id'] = $contractResult['contract']->id;
|
$mapped['account']['contract_id'] = $contractResult['contract']->id;
|
||||||
}
|
}
|
||||||
$accountResult = $this->upsertAccount($import, $mapped, $mappings);
|
$accountResult = $this->upsertAccount($import, $mapped, $mappings, $historyImport);
|
||||||
if ($accountResult['action'] === 'skipped') {
|
if ($accountResult['action'] === 'skipped') {
|
||||||
$skipped++;
|
$skipped++;
|
||||||
$importRow->update(['status' => 'skipped']);
|
$importRow->update(['status' => 'skipped']);
|
||||||
|
|
@ -555,6 +587,55 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activities: create or update activities linked to contracts/cases
|
||||||
|
if (isset($mapped['activity'])) {
|
||||||
|
$activityResult = $this->upsertActivity($import, $mapped, $mappings, $contractResult ?? null, $accountResult ?? null);
|
||||||
|
if ($activityResult['action'] === 'skipped') {
|
||||||
|
$skipped++;
|
||||||
|
$importRow->update(['status' => 'skipped']);
|
||||||
|
ImportEvent::create([
|
||||||
|
'import_id' => $import->id,
|
||||||
|
'user_id' => $user?->getAuthIdentifier(),
|
||||||
|
'import_row_id' => $importRow->id,
|
||||||
|
'event' => 'row_skipped',
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => $activityResult['message'] ?? 'Skipped (no changes).',
|
||||||
|
'context' => $activityResult['context'] ?? null,
|
||||||
|
]);
|
||||||
|
} elseif (in_array($activityResult['action'], ['inserted', 'updated'], true)) {
|
||||||
|
$imported++;
|
||||||
|
$importRow->update([
|
||||||
|
'status' => 'imported',
|
||||||
|
'entity_type' => Activity::class,
|
||||||
|
'entity_id' => $activityResult['activity']->id,
|
||||||
|
]);
|
||||||
|
$activityFieldsStr = '';
|
||||||
|
if (! empty($activityResult['applied_fields'] ?? [])) {
|
||||||
|
$activityFieldsStr = $this->formatAppliedFieldMessage('activity', $activityResult['applied_fields']);
|
||||||
|
}
|
||||||
|
ImportEvent::create([
|
||||||
|
'import_id' => $import->id,
|
||||||
|
'user_id' => $user?->getAuthIdentifier(),
|
||||||
|
'import_row_id' => $importRow->id,
|
||||||
|
'event' => 'row_imported',
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => ucfirst($activityResult['action']).' activity'.($activityFieldsStr ? ' '.$activityFieldsStr : ''),
|
||||||
|
'context' => ['id' => $activityResult['activity']->id, 'fields' => $activityResult['applied_fields'] ?? []],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$invalid++;
|
||||||
|
$importRow->update(['status' => 'invalid', 'errors' => [$activityResult['message'] ?? 'Activity processing failed']]);
|
||||||
|
ImportEvent::create([
|
||||||
|
'import_id' => $import->id,
|
||||||
|
'user_id' => $user?->getAuthIdentifier(),
|
||||||
|
'import_row_id' => $importRow->id,
|
||||||
|
'event' => 'row_invalid',
|
||||||
|
'level' => 'error',
|
||||||
|
'message' => $activityResult['message'] ?? 'Activity processing failed',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Contacts: resolve person via Contract/Account chain, client_case.client_ref, contacts, or identifiers
|
// Contacts: resolve person via Contract/Account chain, client_case.client_ref, contacts, or identifiers
|
||||||
$personIdForRow = null;
|
$personIdForRow = null;
|
||||||
// Prefer person from contract created/updated above
|
// Prefer person from contract created/updated above
|
||||||
|
|
@ -1060,6 +1141,29 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$meta = $import->meta ?? [];
|
||||||
|
if ($historyImport) {
|
||||||
|
if (! empty($this->historyFoundContractIds)) {
|
||||||
|
$found = Contract::query()
|
||||||
|
->with(['clientCase.person'])
|
||||||
|
->whereIn('id', array_keys($this->historyFoundContractIds))
|
||||||
|
->get()
|
||||||
|
->map(function (Contract $c) {
|
||||||
|
return [
|
||||||
|
'contract_uuid' => $c->uuid ?? null,
|
||||||
|
'reference' => $c->reference,
|
||||||
|
'case_uuid' => $c->clientCase?->uuid,
|
||||||
|
'full_name' => $c->clientCase?->person?->full_name,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
$meta['history_found_contracts'] = $found;
|
||||||
|
} else {
|
||||||
|
$meta['history_found_contracts'] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$import->update([
|
$import->update([
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
'finished_at' => now(),
|
'finished_at' => now(),
|
||||||
|
|
@ -1067,6 +1171,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
|
||||||
'imported_rows' => $imported,
|
'imported_rows' => $imported,
|
||||||
'invalid_rows' => $invalid,
|
'invalid_rows' => $invalid,
|
||||||
'valid_rows' => $total - $invalid,
|
'valid_rows' => $total - $invalid,
|
||||||
|
'meta' => $meta,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
|
@ -1119,6 +1224,21 @@ protected function applyMappings(array $raw, $mappings, array $supportsMultiple)
|
||||||
{
|
{
|
||||||
$recordType = null;
|
$recordType = null;
|
||||||
$mapped = [];
|
$mapped = [];
|
||||||
|
$appendValue = function ($existing, $label, $value) {
|
||||||
|
// Skip empty new values
|
||||||
|
if (is_null($value) || (is_string($value) && trim($value) === '')) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
$stringVal = is_scalar($value) ? (string) $value : json_encode($value);
|
||||||
|
$existingStr = is_null($existing) ? '' : (is_scalar($existing) ? (string) $existing : json_encode($existing));
|
||||||
|
if ($existingStr === '') {
|
||||||
|
return $label.': '.$stringVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $existingStr.', '.$label.': '.$stringVal;
|
||||||
|
};
|
||||||
|
$fieldFirstLabel = [];
|
||||||
|
$rootFirstLabel = [];
|
||||||
foreach ($mappings as $map) {
|
foreach ($mappings as $map) {
|
||||||
$src = $map->source_column;
|
$src = $map->source_column;
|
||||||
$target = $map->target_field;
|
$target = $map->target_field;
|
||||||
|
|
@ -1278,9 +1398,34 @@ protected function applyMappings(array $raw, $mappings, array $supportsMultiple)
|
||||||
if (! isset($mapped[$root]) || ! is_array($mapped[$root])) {
|
if (! isset($mapped[$root]) || ! is_array($mapped[$root])) {
|
||||||
$mapped[$root] = [];
|
$mapped[$root] = [];
|
||||||
}
|
}
|
||||||
|
$canConcat = $this->fieldAllowsConcatenation($root, $field);
|
||||||
|
if ($canConcat) {
|
||||||
|
$key = $root.'.'.$field;
|
||||||
|
if (! array_key_exists($field, $mapped[$root]) || is_null($mapped[$root][$field]) || $mapped[$root][$field] === '') {
|
||||||
$mapped[$root][$field] = $value;
|
$mapped[$root][$field] = $value;
|
||||||
|
$fieldFirstLabel[$key] = (string) $src;
|
||||||
} else {
|
} else {
|
||||||
|
$existing = $mapped[$root][$field];
|
||||||
|
$firstLabel = $fieldFirstLabel[$key] ?? null;
|
||||||
|
$existingStr = $firstLabel ? ($firstLabel.': '.(is_scalar($existing) ? (string) $existing : json_encode($existing))) : (is_scalar($existing) ? (string) $existing : json_encode($existing));
|
||||||
|
$mapped[$root][$field] = $appendValue($existingStr, (string) $src, $value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For typed fields (dates/numbers), keep the first non-empty value to avoid coercion errors
|
||||||
|
if (! array_key_exists($field, $mapped[$root]) || is_null($mapped[$root][$field]) || $mapped[$root][$field] === '') {
|
||||||
|
$mapped[$root][$field] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (! array_key_exists($root, $mapped) || is_null($mapped[$root]) || $mapped[$root] === '') {
|
||||||
$mapped[$root] = $value;
|
$mapped[$root] = $value;
|
||||||
|
$rootFirstLabel[$root] = (string) $src;
|
||||||
|
} else {
|
||||||
|
$firstLabel = $rootFirstLabel[$root] ?? null;
|
||||||
|
$existing = $mapped[$root];
|
||||||
|
$existingStr = $firstLabel ? ($firstLabel.': '.(is_scalar($existing) ? (string) $existing : json_encode($existing))) : (is_scalar($existing) ? (string) $existing : json_encode($existing));
|
||||||
|
$mapped[$root] = $appendValue($existingStr, (string) $src, $value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1288,6 +1433,28 @@ protected function applyMappings(array $raw, $mappings, array $supportsMultiple)
|
||||||
return [$recordType, $mapped];
|
return [$recordType, $mapped];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether multiple mapped source columns should be concatenated for a given target field.
|
||||||
|
* For date/time and numeric-like fields we avoid concatenation to prevent invalid type coercion.
|
||||||
|
*/
|
||||||
|
private function fieldAllowsConcatenation(?string $root, ?string $field): bool
|
||||||
|
{
|
||||||
|
if ($field === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$f = strtolower($field);
|
||||||
|
// Date / datetime indicators
|
||||||
|
if ($f === 'birthday' || str_contains($f, 'date') || str_ends_with($f, '_at')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Common numeric fields
|
||||||
|
if (in_array($f, ['amount', 'amount_cents', 'quantity', 'balance_amount'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private function arraySetDot(array &$arr, string $path, $value): void
|
private function arraySetDot(array &$arr, string $path, $value): void
|
||||||
{
|
{
|
||||||
$keys = explode('.', $path);
|
$keys = explode('.', $path);
|
||||||
|
|
@ -1301,7 +1468,7 @@ private function arraySetDot(array &$arr, string $path, $value): void
|
||||||
$ref = $value;
|
$ref = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function upsertAccount(Import $import, array $mapped, $mappings): array
|
private function upsertAccount(Import $import, array $mapped, $mappings, bool $historyImport = false): array
|
||||||
{
|
{
|
||||||
$clientId = $import->client_id; // may be null, used for contract lookup/creation
|
$clientId = $import->client_id; // may be null, used for contract lookup/creation
|
||||||
$acc = $mapped['account'] ?? [];
|
$acc = $mapped['account'] ?? [];
|
||||||
|
|
@ -1447,6 +1614,10 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($historyImport && $existing) {
|
||||||
|
return ['action' => 'skipped', 'account' => $existing, 'contract_id' => $contractId, 'message' => 'History import does not update accounts'];
|
||||||
|
}
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
// Build non-null changes for account fields
|
// Build non-null changes for account fields
|
||||||
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
||||||
|
|
@ -1505,6 +1676,11 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||||
&& ($initMode === null || in_array($initMode, ['insert', 'both'], true))) {
|
&& ($initMode === null || in_array($initMode, ['insert', 'both'], true))) {
|
||||||
$applyInsert['initial_amount'] = $applyInsert['balance_amount'];
|
$applyInsert['initial_amount'] = $applyInsert['balance_amount'];
|
||||||
}
|
}
|
||||||
|
if ($historyImport) {
|
||||||
|
// Force zero amounts for history imports regardless of mapped amounts
|
||||||
|
$applyInsert['balance_amount'] = 0;
|
||||||
|
$applyInsert['initial_amount'] = 0;
|
||||||
|
}
|
||||||
if (empty($applyInsert)) {
|
if (empty($applyInsert)) {
|
||||||
return ['action' => 'skipped', 'message' => 'No fields marked for insert'];
|
return ['action' => 'skipped', 'message' => 'No fields marked for insert'];
|
||||||
}
|
}
|
||||||
|
|
@ -1516,12 +1692,44 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
|
||||||
if (! array_key_exists('active', $data)) {
|
if (! array_key_exists('active', $data)) {
|
||||||
$data['active'] = 1;
|
$data['active'] = 1;
|
||||||
}
|
}
|
||||||
|
if ($historyImport) {
|
||||||
|
$data['balance_amount'] = 0;
|
||||||
|
$data['initial_amount'] = 0;
|
||||||
|
}
|
||||||
$created = Account::create($data);
|
$created = Account::create($data);
|
||||||
|
|
||||||
return ['action' => 'inserted', 'account' => $created, 'contract_id' => $contractId, 'applied_fields' => $data];
|
return ['action' => 'inserted', 'account' => $created, 'contract_id' => $contractId, 'applied_fields' => $data];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureHistoryAccount(Contract $contract, array $mapped): ?Account
|
||||||
|
{
|
||||||
|
$existing = Account::query()
|
||||||
|
->where('contract_id', $contract->id)
|
||||||
|
->where('active', 1)
|
||||||
|
->first();
|
||||||
|
if ($existing) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reference = $mapped['account']['reference'] ?? $mapped['contract']['reference'] ?? null;
|
||||||
|
if (is_string($reference)) {
|
||||||
|
$reference = preg_replace('/\s+/', '', trim($reference));
|
||||||
|
}
|
||||||
|
if (! $reference || $reference === '') {
|
||||||
|
$reference = 'HIST-'.$contract->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Account::create([
|
||||||
|
'contract_id' => $contract->id,
|
||||||
|
'reference' => $reference,
|
||||||
|
'type_id' => $this->getDefaultAccountTypeId(),
|
||||||
|
'active' => 1,
|
||||||
|
'balance_amount' => 0,
|
||||||
|
'initial_amount' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function upsertCaseObject(Import $import, array $mapped, $mappings, int $contractId): array
|
private function upsertCaseObject(Import $import, array $mapped, $mappings, int $contractId): array
|
||||||
{
|
{
|
||||||
// Support both 'case_object' and 'case_objects' keys (template may use plural)
|
// Support both 'case_object' and 'case_objects' keys (template may use plural)
|
||||||
|
|
@ -1625,6 +1833,208 @@ private function upsertCaseObject(Import $import, array $mapped, $mappings, int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function upsertActivity(Import $import, array $mapped, $mappings, ?array $contractResult, ?array $accountResult): array
|
||||||
|
{
|
||||||
|
$activity = $mapped['activity'] ?? [];
|
||||||
|
|
||||||
|
// Default contract/client_case from freshly created or updated contract when present
|
||||||
|
if ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||||
|
$activity['contract_id'] = $activity['contract_id'] ?? $contractResult['contract']->id;
|
||||||
|
$activity['client_case_id'] = $activity['client_case_id'] ?? $contractResult['contract']->client_case_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contractId = $activity['contract_id'] ?? null;
|
||||||
|
if (! $contractId && $contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||||
|
$contractId = $contractResult['contract']->id;
|
||||||
|
} elseif (! $contractId && $accountResult && isset($accountResult['contract_id'])) {
|
||||||
|
$contractId = $accountResult['contract_id'];
|
||||||
|
} elseif (! $contractId && $accountResult && isset($accountResult['contract']) && $accountResult['contract'] instanceof Contract) {
|
||||||
|
$contractId = $accountResult['contract']->id;
|
||||||
|
}
|
||||||
|
if (! $contractId && $import->client_id && ! empty($mapped['contract']['reference'] ?? null)) {
|
||||||
|
$ref = preg_replace('/\s+/', '', trim((string) $mapped['contract']['reference']));
|
||||||
|
if ($ref !== '') {
|
||||||
|
$contractId = Contract::query()
|
||||||
|
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
|
||||||
|
->where('client_cases.client_id', $import->client_id)
|
||||||
|
->where('contracts.reference', $ref)
|
||||||
|
->value('contracts.id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientCaseId = $activity['client_case_id'] ?? null;
|
||||||
|
if (! $clientCaseId && $contractId) {
|
||||||
|
$clientCaseId = Contract::where('id', $contractId)->value('client_case_id');
|
||||||
|
}
|
||||||
|
if (! $clientCaseId && $contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) {
|
||||||
|
$clientCaseId = $contractResult['contract']->client_case_id;
|
||||||
|
}
|
||||||
|
if (! $clientCaseId && $accountResult && isset($accountResult['contract']) && $accountResult['contract'] instanceof Contract) {
|
||||||
|
$clientCaseId = $accountResult['contract']->client_case_id;
|
||||||
|
}
|
||||||
|
if (! $clientCaseId && $accountResult && isset($accountResult['contract_id'])) {
|
||||||
|
$clientCaseId = Contract::where('id', $accountResult['contract_id'])->value('client_case_id');
|
||||||
|
}
|
||||||
|
if (! $clientCaseId && $import->client_id && ! empty($mapped['client_case']['client_ref'] ?? null)) {
|
||||||
|
$clientCaseId = ClientCase::where('client_id', $import->client_id)
|
||||||
|
->where('client_ref', $mapped['client_case']['client_ref'])
|
||||||
|
->value('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $clientCaseId) {
|
||||||
|
return [
|
||||||
|
'action' => 'invalid',
|
||||||
|
'message' => 'Activity requires a client_case_id or resolvable contract.',
|
||||||
|
'context' => [
|
||||||
|
'contract_id' => $contractId,
|
||||||
|
'contract_reference' => $mapped['contract']['reference'] ?? null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect apply_mode settings for activity fields
|
||||||
|
$applyModeByField = [];
|
||||||
|
foreach ($mappings as $map) {
|
||||||
|
$target = (string) ($map->target_field ?? '');
|
||||||
|
if (! str_starts_with($target, 'activity.')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$field = substr($target, strlen('activity.'));
|
||||||
|
$applyModeByField[$field] = (string) ($map->apply_mode ?? 'both');
|
||||||
|
}
|
||||||
|
|
||||||
|
$applyInsert = [];
|
||||||
|
$applyUpdate = [];
|
||||||
|
foreach ($activity as $field => $value) {
|
||||||
|
$applyMode = $applyModeByField[$field] ?? 'both';
|
||||||
|
$normalized = $value;
|
||||||
|
if ($field === 'due_date') {
|
||||||
|
$normalized = is_scalar($value) ? $this->normalizeDate((string) $value) : null;
|
||||||
|
} elseif ($field === 'created_at') {
|
||||||
|
$normalized = is_scalar($value) ? $this->normalizeDateTime((string) $value) : null;
|
||||||
|
} elseif ($field === 'amount') {
|
||||||
|
if (is_string($value)) {
|
||||||
|
$normalized = $this->normalizeDecimal($value);
|
||||||
|
}
|
||||||
|
$normalized = is_null($normalized) ? null : (float) $normalized;
|
||||||
|
} elseif (in_array($field, ['action_id', 'decision_id', 'user_id'], true)) {
|
||||||
|
$normalized = is_null($value) ? null : (int) $value;
|
||||||
|
} elseif (is_string($normalized)) {
|
||||||
|
$normalized = trim($normalized);
|
||||||
|
}
|
||||||
|
if (in_array($applyMode, ['both', 'insert'], true)) {
|
||||||
|
$applyInsert[$field] = $normalized;
|
||||||
|
}
|
||||||
|
if (in_array($applyMode, ['both', 'update'], true)) {
|
||||||
|
$applyUpdate[$field] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = \App\Models\PaymentSetting::query()->first();
|
||||||
|
$tplMeta = optional($import->template)->meta ?? [];
|
||||||
|
$defaultActionId = $applyInsert['action_id']
|
||||||
|
?? $applyUpdate['action_id']
|
||||||
|
?? ($import->meta['activity_action_id'] ?? null)
|
||||||
|
?? ($tplMeta['activity_action_id'] ?? null)
|
||||||
|
?? ($import->meta['action_id'] ?? null)
|
||||||
|
?? ($tplMeta['action_id'] ?? null)
|
||||||
|
?? ($settings?->default_action_id ?? null);
|
||||||
|
$defaultDecisionId = $applyInsert['decision_id']
|
||||||
|
?? $applyUpdate['decision_id']
|
||||||
|
?? ($import->meta['activity_decision_id'] ?? null)
|
||||||
|
?? ($tplMeta['activity_decision_id'] ?? null)
|
||||||
|
?? ($import->meta['decision_id'] ?? null)
|
||||||
|
?? ($tplMeta['decision_id'] ?? null);
|
||||||
|
|
||||||
|
if (! $defaultActionId) {
|
||||||
|
return [
|
||||||
|
'action' => 'invalid',
|
||||||
|
'message' => 'Activity requires action_id (provide via mapping or import meta).',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$applyInsert['action_id'] = $applyInsert['action_id'] ?? $defaultActionId;
|
||||||
|
$applyUpdate['action_id'] = $applyUpdate['action_id'] ?? $defaultActionId;
|
||||||
|
if ($defaultDecisionId) {
|
||||||
|
$applyInsert['decision_id'] = $applyInsert['decision_id'] ?? $defaultDecisionId;
|
||||||
|
$applyUpdate['decision_id'] = $applyUpdate['decision_id'] ?? $defaultDecisionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default created_at for inserted activities (from mapped field or template/import meta)
|
||||||
|
if (empty($applyInsert['created_at'])) {
|
||||||
|
$defaultCreated = $this->normalizeDateTime($applyInsert['created_at'] ?? $import->meta['activity_created_at'] ?? $tplMeta['activity_created_at'] ?? null);
|
||||||
|
if ($defaultCreated) {
|
||||||
|
$applyInsert['created_at'] = $defaultCreated;
|
||||||
|
if (empty($applyInsert['updated_at'])) {
|
||||||
|
$applyInsert['updated_at'] = $defaultCreated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$applyInsert['client_case_id'] = $clientCaseId;
|
||||||
|
$applyUpdate['client_case_id'] = $applyUpdate['client_case_id'] ?? $clientCaseId;
|
||||||
|
if ($contractId) {
|
||||||
|
$applyInsert['contract_id'] = $contractId;
|
||||||
|
$applyUpdate['contract_id'] = $applyUpdate['contract_id'] ?? $contractId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotency: if note + due_date + contract match, treat as existing
|
||||||
|
$existing = null;
|
||||||
|
$noteKey = $applyInsert['note'] ?? null;
|
||||||
|
$dueKey = $applyInsert['due_date'] ?? null;
|
||||||
|
if ($contractId && $noteKey && $dueKey) {
|
||||||
|
$existing = Activity::query()
|
||||||
|
->where('contract_id', $contractId)
|
||||||
|
->where('note', $noteKey)
|
||||||
|
->whereDate('due_date', $dueKey)
|
||||||
|
->first();
|
||||||
|
} elseif ($clientCaseId && $noteKey && $dueKey) {
|
||||||
|
$existing = Activity::query()
|
||||||
|
->where('client_case_id', $clientCaseId)
|
||||||
|
->where('note', $noteKey)
|
||||||
|
->whereDate('due_date', $dueKey)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
||||||
|
if (! empty($changes)) {
|
||||||
|
$existing->fill($changes);
|
||||||
|
$existing->save();
|
||||||
|
|
||||||
|
return ['action' => 'updated', 'activity' => $existing, 'applied_fields' => $changes];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['action' => 'skipped', 'activity' => $existing, 'message' => 'No changes needed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array_filter($applyInsert, fn ($v) => ! is_null($v));
|
||||||
|
$activityModel = new Activity();
|
||||||
|
$activityModel->forceFill($data);
|
||||||
|
if (array_key_exists('created_at', $data)) {
|
||||||
|
// Preserve provided timestamps by disabling automatic timestamps for this save
|
||||||
|
$activityModel->timestamps = false;
|
||||||
|
$activityModel->save();
|
||||||
|
$activityModel->timestamps = true;
|
||||||
|
} else {
|
||||||
|
$activityModel->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['action' => 'inserted', 'activity' => $activityModel, 'applied_fields' => $data];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDateTime(?string $raw): ?string
|
||||||
|
{
|
||||||
|
if ($raw === null || trim($raw) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Carbon::parse($raw)->format('Y-m-d H:i:s');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function mappingsContainRoot($mappings, string $root): bool
|
private function mappingsContainRoot($mappings, string $root): bool
|
||||||
{
|
{
|
||||||
foreach ($mappings as $map) {
|
foreach ($mappings as $map) {
|
||||||
|
|
@ -1657,7 +2067,7 @@ private function findPersonIdByIdentifiers(array $p): ?int
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function upsertContractChain(Import $import, array $mapped, $mappings): array
|
private function upsertContractChain(Import $import, array $mapped, $mappings, bool $historyImport = false): array
|
||||||
{
|
{
|
||||||
$contractData = $mapped['contract'] ?? [];
|
$contractData = $mapped['contract'] ?? [];
|
||||||
$reference = $contractData['reference'] ?? null;
|
$reference = $contractData['reference'] ?? null;
|
||||||
|
|
@ -1821,6 +2231,10 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
|
if ($historyImport) {
|
||||||
|
$this->historyFoundContractIds[$existing->id] = true;
|
||||||
|
return ['action' => 'skipped_history', 'contract' => $existing, 'message' => 'Existing contract left unchanged (history import)'];
|
||||||
|
}
|
||||||
// 1) Prepare contract field changes (non-null)
|
// 1) Prepare contract field changes (non-null)
|
||||||
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
||||||
|
|
||||||
|
|
@ -2529,6 +2943,12 @@ private function upsertEmail(int $personId, array $emailData, $mappings): array
|
||||||
if ($value === '') {
|
if ($value === '') {
|
||||||
return ['action' => 'skipped', 'message' => 'No email value'];
|
return ['action' => 'skipped', 'message' => 'No email value'];
|
||||||
}
|
}
|
||||||
|
$normalizedEmail = filter_var($value, FILTER_VALIDATE_EMAIL);
|
||||||
|
if (! $normalizedEmail) {
|
||||||
|
return ['action' => 'skipped', 'message' => 'Invalid email format'];
|
||||||
|
}
|
||||||
|
$value = $normalizedEmail;
|
||||||
|
$emailData['value'] = $normalizedEmail;
|
||||||
$existing = Email::where('person_id', $personId)->where('value', $value)->first();
|
$existing = Email::where('person_id', $personId)->where('value', $value)->first();
|
||||||
$applyInsert = [];
|
$applyInsert = [];
|
||||||
$applyUpdate = [];
|
$applyUpdate = [];
|
||||||
|
|
@ -2580,9 +3000,19 @@ private function upsertEmail(int $personId, array $emailData, $mappings): array
|
||||||
private function upsertAddress(int $personId, array $addrData, $mappings): array
|
private function upsertAddress(int $personId, array $addrData, $mappings): array
|
||||||
{
|
{
|
||||||
$addressLine = trim((string) ($addrData['address'] ?? ''));
|
$addressLine = trim((string) ($addrData['address'] ?? ''));
|
||||||
if ($addressLine === '') {
|
// Normalize whitespace
|
||||||
|
$addressLine = preg_replace('/\s+/', ' ', $addressLine);
|
||||||
|
// Skip common placeholders or missing values
|
||||||
|
if ($addressLine === '' || $addressLine === '0' || strcasecmp($addressLine, '#N/A') === 0 || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) {
|
||||||
return ['action' => 'skipped', 'message' => 'No address value'];
|
return ['action' => 'skipped', 'message' => 'No address value'];
|
||||||
}
|
}
|
||||||
|
if (mb_strlen($addressLine) < 3) {
|
||||||
|
return ['action' => 'skipped', 'message' => 'Invalid address value'];
|
||||||
|
}
|
||||||
|
// Allow only basic address characters to avoid noisy special chars
|
||||||
|
if (! preg_match('/^[A-Za-z0-9\\s\\.,\\-\\/\\#\\\'"\\(\\)&]+$/', $addressLine)) {
|
||||||
|
return ['action' => 'skipped', 'message' => 'Invalid address value'];
|
||||||
|
}
|
||||||
// Default country SLO if not provided
|
// Default country SLO if not provided
|
||||||
if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') {
|
if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') {
|
||||||
$addrData['country'] = 'SLO';
|
$addrData['country'] = 'SLO';
|
||||||
|
|
@ -2644,6 +3074,10 @@ private function upsertPhone(int $personId, array $phoneData, $mappings): array
|
||||||
if ($nu === '') {
|
if ($nu === '') {
|
||||||
return ['action' => 'skipped', 'message' => 'No phone value'];
|
return ['action' => 'skipped', 'message' => 'No phone value'];
|
||||||
}
|
}
|
||||||
|
if (! preg_match('/^[0-9]{6,20}$/', $nu)) {
|
||||||
|
return ['action' => 'skipped', 'message' => 'Invalid phone value'];
|
||||||
|
}
|
||||||
|
$phoneData['nu'] = $nu;
|
||||||
|
|
||||||
// Find existing phone by normalized number (strip non-numeric from DB values too)
|
// Find existing phone by normalized number (strip non-numeric from DB values too)
|
||||||
$existing = PersonPhone::where('person_id', $personId)
|
$existing = PersonPhone::where('person_id', $personId)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ class ImportSimulationService
|
||||||
*/
|
*/
|
||||||
private ?int $clientId = null;
|
private ?int $clientId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History import mode flag (from template meta).
|
||||||
|
*/
|
||||||
|
private bool $historyImport = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public entry: simulate import applying mappings to first $limit rows.
|
* Public entry: simulate import applying mappings to first $limit rows.
|
||||||
* Keeps existing machine keys for backward compatibility, but adds Slovenian
|
* Keeps existing machine keys for backward compatibility, but adds Slovenian
|
||||||
|
|
@ -79,6 +84,7 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||||
$simRows = [];
|
$simRows = [];
|
||||||
// Determine keyref behavior for contract.reference from mappings/template
|
// Determine keyref behavior for contract.reference from mappings/template
|
||||||
$tplMeta = optional($import->template)->meta ?? [];
|
$tplMeta = optional($import->template)->meta ?? [];
|
||||||
|
$this->historyImport = (bool) ($tplMeta['history_import'] ?? false);
|
||||||
$contractKeyModeTpl = $tplMeta['contract_key_mode'] ?? null; // e.g. 'reference'
|
$contractKeyModeTpl = $tplMeta['contract_key_mode'] ?? null; // e.g. 'reference'
|
||||||
$contractRefMode = $this->mappingModeForImport($import, 'contract.reference'); // e.g. 'keyref'
|
$contractRefMode = $this->mappingModeForImport($import, 'contract.reference'); // e.g. 'keyref'
|
||||||
foreach ($rows as $idx => $rawValues) {
|
foreach ($rows as $idx => $rawValues) {
|
||||||
|
|
@ -489,6 +495,38 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// History import: auto-ensure account placeholder when contract exists but no account mapping
|
||||||
|
if ($this->historyImport && $existingContract && isset($rowEntities['contract']['id']) && ! isset($rowEntities['account'])) {
|
||||||
|
if (! isset($summaries['account'])) {
|
||||||
|
$summaries['account'] = [
|
||||||
|
'root' => 'account',
|
||||||
|
'total_rows' => 0,
|
||||||
|
'create' => 0,
|
||||||
|
'update' => 0,
|
||||||
|
'missing_ref' => 0,
|
||||||
|
'invalid' => 0,
|
||||||
|
'duplicate' => 0,
|
||||||
|
'duplicate_db' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$summaries['account']['total_rows']++;
|
||||||
|
$summaries['account']['update']++;
|
||||||
|
$ref = $rowEntities['contract']['reference'] ?? null;
|
||||||
|
if ($ref === null || $ref === '') {
|
||||||
|
$ref = 'HIST-'.$rowEntities['contract']['id'];
|
||||||
|
}
|
||||||
|
$rowEntities['account'] = [
|
||||||
|
'reference' => $ref,
|
||||||
|
'exists' => true,
|
||||||
|
'id' => null,
|
||||||
|
'balance_before' => 0,
|
||||||
|
'balance_after' => 0,
|
||||||
|
'action' => 'implicit_history',
|
||||||
|
'action_label' => $translatedActions['implicit'] ?? 'posredno',
|
||||||
|
'history_zeroed' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Payment (affects account balance; may create implicit account)
|
// Payment (affects account balance; may create implicit account)
|
||||||
if (isset($entityRoots['payment'])) {
|
if (isset($entityRoots['payment'])) {
|
||||||
// Inject inferred account if none mapped explicitly
|
// Inject inferred account if none mapped explicitly
|
||||||
|
|
@ -891,7 +929,7 @@ private function simulateContract(callable $val, array $summaries, array $cache,
|
||||||
'client_case_id' => $contract?->client_case_id,
|
'client_case_id' => $contract?->client_case_id,
|
||||||
'active' => $contract?->active,
|
'active' => $contract?->active,
|
||||||
'deleted_at' => $contract?->deleted_at,
|
'deleted_at' => $contract?->deleted_at,
|
||||||
'action' => $contract ? 'update' : ($reference ? 'create' : 'skip'),
|
'action' => $contract ? ($this->historyImport ? 'skipped_history' : 'update') : ($reference ? 'create' : 'skip'),
|
||||||
];
|
];
|
||||||
$summaries['contract']['total_rows']++;
|
$summaries['contract']['total_rows']++;
|
||||||
if (! $reference) {
|
if (! $reference) {
|
||||||
|
|
@ -902,6 +940,11 @@ private function simulateContract(callable $val, array $summaries, array $cache,
|
||||||
$summaries['contract']['create']++;
|
$summaries['contract']['create']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->historyImport && $contract) {
|
||||||
|
$entity['history_reuse'] = true;
|
||||||
|
$entity['message'] = 'Existing contract reused (history import)';
|
||||||
|
}
|
||||||
|
|
||||||
return [$entity, $summaries, $cache];
|
return [$entity, $summaries, $cache];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -931,7 +974,7 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
||||||
'exists' => (bool) $account,
|
'exists' => (bool) $account,
|
||||||
'balance_before' => $account?->balance_amount,
|
'balance_before' => $account?->balance_amount,
|
||||||
'balance_after' => $account?->balance_amount,
|
'balance_after' => $account?->balance_amount,
|
||||||
'action' => $account ? 'update' : ($reference ? 'create' : 'skip'),
|
'action' => $account ? ($this->historyImport ? 'skipped_history' : 'update') : ($reference ? 'create' : 'skip'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Direct balance override support.
|
// Direct balance override support.
|
||||||
|
|
@ -940,7 +983,7 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
||||||
$rawIncoming = $val('account.balance_amount')
|
$rawIncoming = $val('account.balance_amount')
|
||||||
?? $val('accounts.balance_amount')
|
?? $val('accounts.balance_amount')
|
||||||
?? $val('account.balance');
|
?? $val('account.balance');
|
||||||
if ($rawIncoming !== null && $rawIncoming !== '') {
|
if (! $this->historyImport && $rawIncoming !== null && $rawIncoming !== '') {
|
||||||
$rawStr = (string) $rawIncoming;
|
$rawStr = (string) $rawIncoming;
|
||||||
// Remove currency symbols and non numeric punctuation except , . -
|
// Remove currency symbols and non numeric punctuation except , . -
|
||||||
$clean = preg_replace('/[^0-9,\.\-]+/', '', $rawStr) ?? '';
|
$clean = preg_replace('/[^0-9,\.\-]+/', '', $rawStr) ?? '';
|
||||||
|
|
@ -974,6 +1017,19 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
|
||||||
$summaries['account']['create']++;
|
$summaries['account']['create']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->historyImport) {
|
||||||
|
// History imports keep balances unchanged and do not update accounts
|
||||||
|
$entity['balance_after'] = $account?->balance_amount ?? 0;
|
||||||
|
$entity['balance_before'] = $account?->balance_amount ?? 0;
|
||||||
|
if ($account) {
|
||||||
|
$entity['message'] = 'Existing account left unchanged (history import)';
|
||||||
|
} else {
|
||||||
|
$entity['balance_after'] = 0;
|
||||||
|
$entity['balance_before'] = 0;
|
||||||
|
$entity['history_zeroed'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [$entity, $summaries, $cache];
|
return [$entity, $summaries, $cache];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1210,6 +1266,10 @@ private function simulateGenericRoot(
|
||||||
$reference = $val('phone.nu');
|
$reference = $val('phone.nu');
|
||||||
} elseif ($root === 'email') {
|
} elseif ($root === 'email') {
|
||||||
$reference = $val('email.value');
|
$reference = $val('email.value');
|
||||||
|
} elseif ($root === 'activity') {
|
||||||
|
$noteRef = $val('activity.note');
|
||||||
|
$dueRef = $val('activity.due_date');
|
||||||
|
$reference = $noteRef || $dueRef ? trim((string) ($dueRef ?? '')).($noteRef ? ' | '.$noteRef : '') : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1253,6 +1313,13 @@ private function simulateGenericRoot(
|
||||||
$entity['description'] = $val('case_object.description') ?? null;
|
$entity['description'] = $val('case_object.description') ?? null;
|
||||||
$entity['type'] = $val('case_object.type') ?? null;
|
$entity['type'] = $val('case_object.type') ?? null;
|
||||||
break;
|
break;
|
||||||
|
case 'activity':
|
||||||
|
$entity['note'] = $val('activity.note') ?? null;
|
||||||
|
$entity['due_date'] = $val('activity.due_date') ?? null;
|
||||||
|
$entity['amount'] = $val('activity.amount') ?? null;
|
||||||
|
$entity['action_id'] = $val('activity.action_id') ?? null;
|
||||||
|
$entity['decision_id'] = $val('activity.decision_id') ?? null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($verbose) {
|
if ($verbose) {
|
||||||
|
|
@ -1367,6 +1434,16 @@ private function genericIdentityCandidates(string $root, callable $val): array
|
||||||
$ids[] = 'name:'.mb_strtolower(trim((string) $name));
|
$ids[] = 'name:'.mb_strtolower(trim((string) $name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
case 'activity':
|
||||||
|
$note = $val('activity.note');
|
||||||
|
$due = $val('activity.due_date');
|
||||||
|
$contractRef = $val('contract.reference');
|
||||||
|
$ids = [];
|
||||||
|
if ($note || $due) {
|
||||||
|
$ids[] = 'activity:'.mb_strtolower(trim((string) ($note ?? ''))).'|'.mb_strtolower(trim((string) ($due ?? ''))).'|'.mb_strtolower(trim((string) ($contractRef ?? '')));
|
||||||
|
}
|
||||||
|
|
||||||
return $ids;
|
return $ids;
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -1426,6 +1503,20 @@ private function loadExistingGenericIdentities(string $root): array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'activity':
|
||||||
|
foreach (\App\Models\Activity::query()->get(['note', 'due_date', 'contract_id', 'client_case_id']) as $rec) {
|
||||||
|
$note = mb_strtolower(trim((string) ($rec->note ?? '')));
|
||||||
|
$due = $rec->due_date ? mb_strtolower(trim((string) $rec->due_date)) : '';
|
||||||
|
$contractRef = null;
|
||||||
|
if ($rec->contract_id) {
|
||||||
|
$contractRef = Contract::where('id', $rec->contract_id)->value('reference');
|
||||||
|
}
|
||||||
|
$key = 'activity:'.$note.'|'.$due.'|'.mb_strtolower(trim((string) ($contractRef ?? '')));
|
||||||
|
if (trim($key, 'activity:|') !== '') {
|
||||||
|
$set[$key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
// swallow and return what we have
|
// swallow and return what we have
|
||||||
|
|
@ -1730,6 +1821,8 @@ private function actionTranslations(): array
|
||||||
'skip' => 'preskoči',
|
'skip' => 'preskoči',
|
||||||
'implicit' => 'posredno',
|
'implicit' => 'posredno',
|
||||||
'reactivate' => 'reaktiviraj',
|
'reactivate' => 'reaktiviraj',
|
||||||
|
'skipped_history' => 'preskoči (zgodovina)',
|
||||||
|
'implicit_history' => 'posredno (zgodovina)',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,29 @@ public function run(): void
|
||||||
],
|
],
|
||||||
'ui' => ['order' => 9],
|
'ui' => ['order' => 9],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'activities',
|
||||||
|
'canonical_root' => 'activity',
|
||||||
|
'label' => 'Activities',
|
||||||
|
'fields' => ['note', 'due_date', 'amount', 'action_id', 'decision_id', 'contract_id', 'client_case_id', 'user_id'],
|
||||||
|
'field_aliases' => [
|
||||||
|
'opis' => 'note',
|
||||||
|
'datum' => 'due_date',
|
||||||
|
'rok' => 'due_date',
|
||||||
|
'znesek' => 'amount',
|
||||||
|
],
|
||||||
|
'aliases' => ['activity', 'activities', 'opravilo', 'opravila'],
|
||||||
|
'rules' => [
|
||||||
|
['pattern' => '/^(aktivnost|activity|note|opis)\b/i', 'field' => 'note'],
|
||||||
|
['pattern' => '/^(rok|due|datum|date)\b/i', 'field' => 'due_date'],
|
||||||
|
['pattern' => '/^(znesek|amount|vrednost|value)\b/i', 'field' => 'amount'],
|
||||||
|
['pattern' => '/^(akcija|action)\b/i', 'field' => 'action_id'],
|
||||||
|
['pattern' => '/^(odlocitev|odločitev|decision)\b/i', 'field' => 'decision_id'],
|
||||||
|
['pattern' => '/^(pogodba|contract)\b/i', 'field' => 'contract_id'],
|
||||||
|
['pattern' => '/^(primer|case)\b/i', 'field' => 'client_case_id'],
|
||||||
|
],
|
||||||
|
'ui' => ['order' => 10],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($defs as $d) {
|
foreach ($defs as $d) {
|
||||||
|
|
|
||||||
|
|
@ -155,5 +155,42 @@ public function run(): void
|
||||||
'options' => $map['options'] ?? null,
|
'options' => $map['options'] ?? null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activities linked to contracts demo
|
||||||
|
$activities = ImportTemplate::query()->firstOrCreate([
|
||||||
|
'name' => 'Activities CSV (contract linked)',
|
||||||
|
], [
|
||||||
|
'uuid' => (string) Str::uuid(),
|
||||||
|
'description' => 'Activities import linked to existing contracts via reference.',
|
||||||
|
'source_type' => 'csv',
|
||||||
|
'default_record_type' => 'activity',
|
||||||
|
'sample_headers' => ['contract_reference', 'note', 'due_date', 'amount', 'action', 'decision', 'user_email'],
|
||||||
|
'is_active' => true,
|
||||||
|
'meta' => [
|
||||||
|
'delimiter' => ',',
|
||||||
|
'enclosure' => '"',
|
||||||
|
'escape' => '\\',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$activityMappings = [
|
||||||
|
['source_column' => 'contract_reference', 'target_field' => 'contract.reference', 'position' => 1],
|
||||||
|
['source_column' => 'note', 'target_field' => 'activity.note', 'position' => 2],
|
||||||
|
['source_column' => 'due_date', 'target_field' => 'activity.due_date', 'position' => 3],
|
||||||
|
['source_column' => 'amount', 'target_field' => 'activity.amount', 'position' => 4],
|
||||||
|
['source_column' => 'action', 'target_field' => 'activity.action_id', 'position' => 5],
|
||||||
|
['source_column' => 'decision', 'target_field' => 'activity.decision_id', 'position' => 6],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($activityMappings as $map) {
|
||||||
|
ImportTemplateMapping::firstOrCreate([
|
||||||
|
'import_template_id' => $activities->id,
|
||||||
|
'source_column' => $map['source_column'],
|
||||||
|
], [
|
||||||
|
'target_field' => $map['target_field'],
|
||||||
|
'position' => $map['position'],
|
||||||
|
'options' => $map['options'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import Modal from "@/Components/Modal.vue"; // still potentially used elsewhere
|
||||||
import CsvPreviewModal from "./Partials/CsvPreviewModal.vue";
|
import CsvPreviewModal from "./Partials/CsvPreviewModal.vue";
|
||||||
import SimulationModal from "./Partials/SimulationModal.vue";
|
import SimulationModal from "./Partials/SimulationModal.vue";
|
||||||
import { useCurrencyFormat } from "./useCurrencyFormat.js";
|
import { useCurrencyFormat } from "./useCurrencyFormat.js";
|
||||||
|
import DialogModal from "@/Components/DialogModal.vue";
|
||||||
|
|
||||||
// Reintroduce props definition lost during earlier edits
|
// Reintroduce props definition lost during earlier edits
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -185,6 +186,23 @@ function downloadUnresolvedCsv() {
|
||||||
window.location.href = route("imports.missing-keyref-csv", { import: importId.value });
|
window.location.href = route("imports.missing-keyref-csv", { import: importId.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// History import: list of contracts that already existed in DB and were matched
|
||||||
|
const isHistoryImport = computed(() => {
|
||||||
|
const foundList = props.import?.meta?.history_found_contracts;
|
||||||
|
const hasFound = Array.isArray(foundList) && foundList.length > 0;
|
||||||
|
return Boolean(
|
||||||
|
props.import?.template?.meta?.history_import ??
|
||||||
|
props.import?.import_template?.meta?.history_import ??
|
||||||
|
props.import?.meta?.history_import ??
|
||||||
|
hasFound
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const historyFoundContracts = computed(() => {
|
||||||
|
const list = props.import?.meta?.history_found_contracts;
|
||||||
|
return Array.isArray(list) ? list : [];
|
||||||
|
});
|
||||||
|
const showFoundContracts = ref(false);
|
||||||
|
|
||||||
// Determine if all detected columns are mapped with entity+field
|
// Determine if all detected columns are mapped with entity+field
|
||||||
function evaluateMappingSaved() {
|
function evaluateMappingSaved() {
|
||||||
console.log("here the evaluation happen of mapping save!");
|
console.log("here the evaluation happen of mapping save!");
|
||||||
|
|
@ -1145,6 +1163,21 @@ async function fetchSimulation() {
|
||||||
<div class="py-6">
|
<div class="py-6">
|
||||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="bg-white shadow sm:rounded-lg p-6 space-y-6">
|
<div class="bg-white shadow sm:rounded-lg p-6 space-y-6">
|
||||||
|
<div
|
||||||
|
v-if="isHistoryImport || historyFoundContracts.length"
|
||||||
|
class="flex flex-wrap items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 bg-emerald-700 text-white text-xs rounded"
|
||||||
|
@click.prevent="showFoundContracts = true"
|
||||||
|
title="Prikaži pogodbe, ki so bile najdene in že obstajajo v bazi"
|
||||||
|
>
|
||||||
|
Najdene pogodbe
|
||||||
|
</button>
|
||||||
|
<span v-if="historyFoundContracts.length" class="text-xs text-gray-600">
|
||||||
|
{{ historyFoundContracts.length }} že obstoječih
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div v-if="isCompleted" class="p-3 border rounded bg-gray-50 text-sm">
|
<div v-if="isCompleted" class="p-3 border rounded bg-gray-50 text-sm">
|
||||||
<div class="flex flex-wrap gap-x-6 gap-y-1">
|
<div class="flex flex-wrap gap-x-6 gap-y-1">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1378,6 +1411,45 @@ async function fetchSimulation() {
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- History import: existing contracts found -->
|
||||||
|
<DialogModal :show="showFoundContracts" max-width="3xl" @close="showFoundContracts = false">
|
||||||
|
<template #title>Obstoječe pogodbe najdene v zgodovinskem uvozu</template>
|
||||||
|
<template #content>
|
||||||
|
<div v-if="!historyFoundContracts.length" class="text-sm text-gray-600">Ni zadetkov.</div>
|
||||||
|
<ul v-else class="divide-y divide-gray-200 max-h-[70vh] overflow-auto">
|
||||||
|
<li
|
||||||
|
v-for="item in historyFoundContracts"
|
||||||
|
:key="item.contract_uuid || item.reference"
|
||||||
|
class="py-3 flex items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="font-mono text-sm text-gray-900">{{ item.reference }}</div>
|
||||||
|
<div class="text-xs text-gray-600 truncate">
|
||||||
|
<span>{{ item.full_name || "—" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<a
|
||||||
|
v-if="item.case_uuid"
|
||||||
|
:href="route('clientCase.show', { client_case: item.case_uuid })"
|
||||||
|
class="text-blue-600 hover:underline text-xs"
|
||||||
|
>
|
||||||
|
Odpri primer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 bg-gray-700 text-white text-xs rounded"
|
||||||
|
@click.prevent="showFoundContracts = false"
|
||||||
|
>
|
||||||
|
Zapri
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
|
||||||
<!-- Unresolved keyref rows modal -->
|
<!-- Unresolved keyref rows modal -->
|
||||||
<Modal :show="showUnresolved" max-width="5xl" @close="showUnresolved = false">
|
<Modal :show="showUnresolved" max-width="5xl" @close="showUnresolved = false">
|
||||||
<div class="p-4 max-h-[75vh] overflow-auto">
|
<div class="p-4 max-h-[75vh] overflow-auto">
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ const form = useForm({
|
||||||
delimiter: "",
|
delimiter: "",
|
||||||
// Payments import mode
|
// Payments import mode
|
||||||
payments_import: false,
|
payments_import: false,
|
||||||
|
// History import mode
|
||||||
|
history_import: false,
|
||||||
// For payments mode: how to locate Contract - use single key 'reference'
|
// For payments mode: how to locate Contract - use single key 'reference'
|
||||||
contract_key_mode: null,
|
contract_key_mode: null,
|
||||||
},
|
},
|
||||||
|
|
@ -59,6 +61,9 @@ const prevEntities = ref([]);
|
||||||
watch(
|
watch(
|
||||||
() => form.meta.payments_import,
|
() => form.meta.payments_import,
|
||||||
(enabled) => {
|
(enabled) => {
|
||||||
|
if (enabled && form.meta.history_import) {
|
||||||
|
form.meta.history_import = false;
|
||||||
|
}
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// Save current selection and lock to the required chain
|
// Save current selection and lock to the required chain
|
||||||
prevEntities.value = Array.isArray(form.entities) ? [...form.entities] : [];
|
prevEntities.value = Array.isArray(form.entities) ? [...form.entities] : [];
|
||||||
|
|
@ -74,6 +79,35 @@ watch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// History import: restrict entities and auto-add accounts when contracts selected
|
||||||
|
watch(
|
||||||
|
() => form.meta.history_import,
|
||||||
|
(enabled) => {
|
||||||
|
if (enabled && form.meta.payments_import) {
|
||||||
|
form.meta.payments_import = false;
|
||||||
|
form.meta.contract_key_mode = null;
|
||||||
|
}
|
||||||
|
const allowed = ["person", "person_addresses", "person_phones", "contracts", "activities", "client_cases"];
|
||||||
|
if (enabled) {
|
||||||
|
const current = Array.isArray(form.entities) ? [...form.entities] : [];
|
||||||
|
let filtered = current.filter((e) => allowed.includes(e));
|
||||||
|
if (filtered.includes("contracts") && !filtered.includes("accounts")) {
|
||||||
|
filtered = [...filtered, "accounts"];
|
||||||
|
}
|
||||||
|
form.entities = filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.entities,
|
||||||
|
(vals) => {
|
||||||
|
if (form.meta.history_import && Array.isArray(vals) && vals.includes("contracts") && ! vals.includes("accounts")) {
|
||||||
|
form.entities = [...vals, "accounts"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -112,7 +146,16 @@ watch(
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
>Entities (tables)</label
|
>Entities (tables)</label
|
||||||
>
|
>
|
||||||
<label class="inline-flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="form.meta.history_import"
|
||||||
|
class="rounded"
|
||||||
|
/>
|
||||||
|
<span>History import</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="form.meta.payments_import"
|
v-model="form.meta.payments_import"
|
||||||
|
|
@ -121,6 +164,7 @@ watch(
|
||||||
<span>Payments import</span>
|
<span>Payments import</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<template v-if="!form.meta.payments_import">
|
<template v-if="!form.meta.payments_import">
|
||||||
<Multiselect
|
<Multiselect
|
||||||
v-model="form.entities"
|
v-model="form.entities"
|
||||||
|
|
@ -128,11 +172,13 @@ watch(
|
||||||
{ value: 'person', label: 'Person' },
|
{ value: 'person', label: 'Person' },
|
||||||
{ value: 'person_addresses', label: 'Person Addresses' },
|
{ value: 'person_addresses', label: 'Person Addresses' },
|
||||||
{ value: 'person_phones', label: 'Person Phones' },
|
{ value: 'person_phones', label: 'Person Phones' },
|
||||||
|
{ value: 'client_cases', label: 'Client Cases' },
|
||||||
{ value: 'emails', label: 'Emails' },
|
{ value: 'emails', label: 'Emails' },
|
||||||
{ value: 'accounts', label: 'Accounts' },
|
{ value: 'accounts', label: 'Accounts' },
|
||||||
{ value: 'contracts', label: 'Contracts' },
|
{ value: 'contracts', label: 'Contracts' },
|
||||||
{ value: 'case_objects', label: 'Case Objects' },
|
{ value: 'case_objects', label: 'Case Objects' },
|
||||||
{ value: 'payments', label: 'Payments' },
|
{ value: 'payments', label: 'Payments' },
|
||||||
|
{ value: 'activities', label: 'Activities' },
|
||||||
]"
|
]"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
track-by="value"
|
track-by="value"
|
||||||
|
|
@ -156,6 +202,9 @@ watch(
|
||||||
Choose which tables this template targets. You can still define per-column
|
Choose which tables this template targets. You can still define per-column
|
||||||
mappings later.
|
mappings later.
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="form.meta.history_import" class="mt-2 text-xs text-gray-600">
|
||||||
|
History mode allows only person/address/phone/contracts/activities/client cases. Accounts are auto-added when contracts are present and balances stay unchanged.
|
||||||
|
</div>
|
||||||
<div v-if="form.meta.payments_import" class="mt-2 text-xs text-gray-600">
|
<div v-if="form.meta.payments_import" class="mt-2 text-xs text-gray-600">
|
||||||
Payments mode locks entities to:
|
Payments mode locks entities to:
|
||||||
<span class="font-medium">Contracts → Accounts → Payments</span> and
|
<span class="font-medium">Contracts → Accounts → Payments</span> and
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ const form = useForm({
|
||||||
// Add meta with default delimiter support
|
// Add meta with default delimiter support
|
||||||
meta: {
|
meta: {
|
||||||
...(props.template.meta || {}),
|
...(props.template.meta || {}),
|
||||||
|
payments_import: props.template.meta?.payments_import ?? false,
|
||||||
|
history_import: props.template.meta?.history_import ?? false,
|
||||||
|
activity_action_id: props.template.meta?.activity_action_id ?? props.template.meta?.action_id ?? null,
|
||||||
|
activity_decision_id: props.template.meta?.activity_decision_id ?? props.template.meta?.decision_id ?? null,
|
||||||
delimiter: (props.template.meta && props.template.meta.delimiter) || "",
|
delimiter: (props.template.meta && props.template.meta.delimiter) || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -35,6 +39,21 @@ const decisionsForSelectedAction = vComputed(() => {
|
||||||
return act?.decisions || [];
|
return act?.decisions || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const decisionsForActivitiesAction = vComputed(() => {
|
||||||
|
const act = (props.actions || []).find((a) => a.id === form.meta.activity_action_id);
|
||||||
|
return act?.decisions || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const activityCreatedAtInput = computed({
|
||||||
|
get() {
|
||||||
|
if (!form.meta.activity_created_at) return "";
|
||||||
|
return String(form.meta.activity_created_at).replace(" ", "T");
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
form.meta.activity_created_at = v ? String(v).replace("T", " ") : null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
vWatch(
|
vWatch(
|
||||||
() => form.meta.action_id,
|
() => form.meta.action_id,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -42,6 +61,13 @@ vWatch(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
vWatch(
|
||||||
|
() => form.meta.activity_action_id,
|
||||||
|
() => {
|
||||||
|
form.meta.activity_decision_id = null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const entities = computed(() => props.template.meta?.entities || []);
|
const entities = computed(() => props.template.meta?.entities || []);
|
||||||
const hasMappings = computed(() => (props.template.mappings?.length || 0) > 0);
|
const hasMappings = computed(() => (props.template.mappings?.length || 0) > 0);
|
||||||
const canChangeClient = computed(() => !hasMappings.value); // guard reassignment when mappings exist (optional rule)
|
const canChangeClient = computed(() => !hasMappings.value); // guard reassignment when mappings exist (optional rule)
|
||||||
|
|
@ -67,6 +93,15 @@ const unassignedSourceColumns = computed(() => {
|
||||||
}
|
}
|
||||||
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
||||||
});
|
});
|
||||||
|
const allSourceColumns = computed(() => {
|
||||||
|
const set = new Set();
|
||||||
|
(props.template.sample_headers || []).forEach((h) => set.add(h));
|
||||||
|
(props.template.mappings || []).forEach((m) => {
|
||||||
|
if (m.source_column) set.add(m.source_column);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
||||||
|
});
|
||||||
const unassignedState = ref({});
|
const unassignedState = ref({});
|
||||||
|
|
||||||
// Dynamic Import Entity definitions and field options from API
|
// Dynamic Import Entity definitions and field options from API
|
||||||
|
|
@ -252,6 +287,11 @@ const save = () => {
|
||||||
// drop client change when blocked
|
// drop client change when blocked
|
||||||
delete payload.client_uuid;
|
delete payload.client_uuid;
|
||||||
}
|
}
|
||||||
|
const hasActivities = Array.isArray(payload.meta?.entities) && payload.meta.entities.includes('activities');
|
||||||
|
if (hasActivities && (!payload.meta?.activity_action_id || !payload.meta?.activity_decision_id)) {
|
||||||
|
alert('Activity imports require selecting an Action and Decision (Activities section).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Normalize empty delimiter: remove from meta to allow auto-detect
|
// Normalize empty delimiter: remove from meta to allow auto-detect
|
||||||
if (
|
if (
|
||||||
payload.meta &&
|
payload.meta &&
|
||||||
|
|
@ -300,11 +340,27 @@ watch(
|
||||||
watch(
|
watch(
|
||||||
() => form.meta.payments_import,
|
() => form.meta.payments_import,
|
||||||
(enabled) => {
|
(enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
if (form.meta.history_import) {
|
||||||
|
form.meta.history_import = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (enabled && !form.meta.contract_key_mode) {
|
if (enabled && !form.meta.contract_key_mode) {
|
||||||
form.meta.contract_key_mode = "reference";
|
form.meta.contract_key_mode = "reference";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// History mode is mutually exclusive with payments mode
|
||||||
|
watch(
|
||||||
|
() => form.meta.history_import,
|
||||||
|
(enabled) => {
|
||||||
|
if (enabled && form.meta.payments_import) {
|
||||||
|
form.meta.payments_import = false;
|
||||||
|
form.meta.contract_key_mode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -423,7 +479,7 @@ watch(
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
>Privzeto Dejanja (Activity)</label
|
>Privzeto Dejanja (post-contract activity)</label
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
v-model="form.meta.action_id"
|
v-model="form.meta.action_id"
|
||||||
|
|
@ -437,7 +493,7 @@ watch(
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
>Privzeta Odločitev</label
|
>Privzeta Odločitev (post-contract)</label
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
v-model="form.meta.decision_id"
|
v-model="form.meta.decision_id"
|
||||||
|
|
@ -484,22 +540,31 @@ watch(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payments import toggle and settings -->
|
<!-- History / Payments import toggles and settings -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center flex-wrap gap-4">
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="history_import"
|
||||||
|
v-model="form.meta.history_import"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700">History import</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
id="payments_import"
|
id="payments_import"
|
||||||
v-model="form.meta.payments_import"
|
v-model="form.meta.payments_import"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
/>
|
||||||
<label for="payments_import" class="text-sm font-medium text-gray-700"
|
<span class="text-sm font-medium text-gray-700">Payments import</span>
|
||||||
>Payments import</label
|
</label>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<p class="text-xs text-gray-500">
|
||||||
When enabled, entities are locked to Contracts → Accounts → Payments.
|
History allows person/address/phone/contracts/activities/client cases; accounts are auto-added with contracts. Payments locks entities to Contracts → Accounts → Payments.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="form.meta.payments_import">
|
<div v-if="form.meta.payments_import">
|
||||||
|
|
@ -856,6 +921,41 @@ watch(
|
||||||
<span class="text-xs text-gray-500">Klikni za razširitev</span>
|
<span class="text-xs text-gray-500">Klikni za razširitev</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
|
<div v-if="entity === 'activities'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 p-3 bg-gray-50 rounded border">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Activity action *</label>
|
||||||
|
<select
|
||||||
|
v-model="form.meta.activity_action_id"
|
||||||
|
class="mt-1 block w-full border rounded p-2"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option :value="null">(Select action)</option>
|
||||||
|
<option v-for="a in props.actions" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Activity decision *</label>
|
||||||
|
<select
|
||||||
|
v-model="form.meta.activity_decision_id"
|
||||||
|
class="mt-1 block w-full border rounded p-2"
|
||||||
|
:disabled="!form.meta.activity_action_id"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option :value="null">(Select decision)</option>
|
||||||
|
<option v-for="d in decisionsForActivitiesAction" :key="d.id" :value="d.id">{{ d.name }}</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="!form.meta.activity_action_id" class="text-xs text-gray-500 mt-1">Choose an action first.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Activity created at (override)</label>
|
||||||
|
<input
|
||||||
|
v-model="activityCreatedAtInput"
|
||||||
|
type="datetime-local"
|
||||||
|
class="mt-1 block w-full border rounded p-2"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Optional: set a fixed timestamp for inserted activities (history imports).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Existing mappings for this entity -->
|
<!-- Existing mappings for this entity -->
|
||||||
<div
|
<div
|
||||||
v-if="props.template.mappings && props.template.mappings.length"
|
v-if="props.template.mappings && props.template.mappings.length"
|
||||||
|
|
@ -954,22 +1054,19 @@ watch(
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-6 gap-2 items-end">
|
<div class="grid grid-cols-1 sm:grid-cols-6 gap-2 items-end">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-600"
|
<label class="block text-xs text-gray-600"
|
||||||
>Source column (ne-dodeljene)</label
|
>Source column (lahko uporabiš večkrat)</label
|
||||||
>
|
>
|
||||||
<select
|
<input
|
||||||
v-model="(newRows[entity] ||= {}).source"
|
v-model="(newRows[entity] ||= {}).source"
|
||||||
class="mt-1 w-full border rounded p-2"
|
class="mt-1 w-full border rounded p-2"
|
||||||
>
|
list="src-opts-{{ entity }}"
|
||||||
<option value="" disabled>(izberi)</option>
|
placeholder="npr.: note, description"
|
||||||
<option v-for="s in unassignedSourceColumns" :key="s" :value="s">
|
/>
|
||||||
{{ s }}
|
<datalist :id="`src-opts-${entity}`">
|
||||||
</option>
|
<option v-for="s in allSourceColumns" :key="s" :value="s">{{ s }}</option>
|
||||||
</select>
|
</datalist>
|
||||||
<p
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
v-if="!unassignedSourceColumns.length"
|
Več stolpcev lahko povežeš na isto polje (npr. activity.note).
|
||||||
class="text-xs text-gray-500 mt-1"
|
|
||||||
>
|
|
||||||
Ni nedodeljenih virov. Uporabi Bulk ali najprej dodaj vire.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user