with(['client:id,uuid,person_id', 'client.person:id,full_name']) ->orderBy('name') ->get(); return Inertia::render('Imports/Templates/Index', [ 'templates' => $templates->map(fn ($t) => [ 'uuid' => $t->uuid, 'name' => $t->name, 'description' => $t->description, 'source_type' => $t->source_type, 'is_active' => $t->is_active, 'client' => $t->client ? [ 'uuid' => $t->client->uuid, 'name' => $t->client->person?->full_name, ] : null, ]), ]); } // Show the template creation page public function create() { // Preload clients for optional association (global when null) $clients = Client::query() ->join('person', 'person.id', '=', 'clients.person_id') ->orderBy('person.full_name') ->get([ 'clients.id', // kept for compatibility, UI will use uuid 'clients.uuid', DB::raw('person.full_name as name'), ]); $segments = Segment::query()->orderBy('name')->get(['id', 'name']); $decisions = Decision::query()->orderBy('name')->get(['id', 'name']); $actions = Action::with(['decisions:id,name']) ->orderBy('name') ->get(['id', 'name']) ->map(fn ($a) => [ 'id' => $a->id, 'name' => $a->name, 'decisions' => $a->decisions->map(fn ($d) => ['id' => $d->id, 'name' => $d->name])->values(), ]); return Inertia::render('Imports/Templates/Create', [ 'clients' => $clients, 'segments' => $segments, 'decisions' => $decisions, 'actions' => $actions, ]); } public function store(Request $request) { // Normalize payload to be resilient to UI variations $raw = $request->all(); // Allow passing default segment/decision either inside meta or as top-level if (isset($raw['segment_id']) && ! isset($raw['meta']['segment_id'])) { $raw['meta']['segment_id'] = $raw['segment_id']; } if (isset($raw['decision_id']) && ! isset($raw['meta']['decision_id'])) { $raw['meta']['decision_id'] = $raw['decision_id']; } // Resolve client by uuid if provided, or cast string numeric to int if (! empty($raw['client_uuid'] ?? null)) { $raw['client_id'] = Client::where('uuid', $raw['client_uuid'])->value('id'); } elseif (isset($raw['client_id']) && is_string($raw['client_id']) && ctype_digit($raw['client_id'])) { $raw['client_id'] = (int) $raw['client_id']; } // Normalize entities to array of strings if (isset($raw['entities']) && is_array($raw['entities'])) { $raw['entities'] = array_values(array_filter(array_map(function ($e) { if (is_string($e)) { return $e; } if (is_array($e) && array_key_exists('value', $e)) { return (string) $e['value']; } return null; }, $raw['entities']))); } $data = validator($raw, [ 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', 'source_type' => 'required|string|in:csv,xml,xls,xlsx,json', 'default_record_type' => 'nullable|string|max:50', 'sample_headers' => 'nullable|array', 'client_id' => 'nullable|integer|exists:clients,id', 'is_active' => 'boolean', 'reactivate' => 'boolean', 'entities' => 'nullable|array', 'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', 'mappings' => 'array', 'mappings.*.source_column' => 'required|string', 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', 'mappings.*.target_field' => 'nullable|string', 'mappings.*.transform' => 'nullable|string|max:50', 'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref', 'mappings.*.options' => 'nullable|array', 'mappings.*.position' => 'nullable|integer', 'meta' => 'nullable|array', 'meta.segment_id' => 'nullable|integer|exists:segments,id', 'meta.decision_id' => 'nullable|integer|exists:decisions,id', 'meta.action_id' => 'nullable|integer|exists:actions,id', 'meta.payments_import' => 'nullable|boolean', 'meta.contract_key_mode' => 'nullable|string|in:reference', ])->validate(); // Ensure decision belongs to action if both provided in meta $meta = $data['meta'] ?? []; if (! empty($meta['action_id']) && ! empty($meta['decision_id'])) { $belongs = \DB::table('action_decision') ->where('action_id', $meta['action_id']) ->where('decision_id', $meta['decision_id']) ->exists(); if (! $belongs) { return back()->withErrors(['meta.decision_id' => 'Selected decision is not associated with the chosen action.'])->withInput(); } } $template = null; DB::transaction(function () use (&$template, $request, $data) { $paymentsImport = (bool) (data_get($data, 'meta.payments_import') ?? false); $entities = $data['entities'] ?? []; if ($paymentsImport) { $entities = ['contracts', 'accounts', 'payments']; } $template = ImportTemplate::create([ 'uuid' => (string) Str::uuid(), 'name' => $data['name'], 'description' => $data['description'] ?? null, 'source_type' => $data['source_type'], 'default_record_type' => $data['default_record_type'] ?? null, 'sample_headers' => $data['sample_headers'] ?? null, 'user_id' => $request->user()?->id, 'client_id' => $data['client_id'] ?? null, 'is_active' => $data['is_active'] ?? true, 'reactivate' => $data['reactivate'] ?? false, 'meta' => array_filter([ 'entities' => $entities, 'segment_id' => data_get($data, 'meta.segment_id'), 'decision_id' => data_get($data, 'meta.decision_id'), 'action_id' => data_get($data, 'meta.action_id'), 'payments_import' => $paymentsImport ?: null, 'contract_key_mode' => data_get($data, 'meta.contract_key_mode'), ], fn ($v) => ! is_null($v) && $v !== ''), ]); foreach (($data['mappings'] ?? []) as $m) { ImportTemplateMapping::create([ 'import_template_id' => $template->id, 'entity' => $m['entity'] ?? null, 'source_column' => $m['source_column'], 'target_field' => $m['target_field'] ?? null, 'transform' => $m['transform'] ?? null, 'apply_mode' => $m['apply_mode'] ?? 'both', 'options' => $m['options'] ?? null, 'position' => $m['position'] ?? null, ]); } }); // Redirect to edit page for the newly created template return redirect() ->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', 'Template created successfully.'); } // Edit template UI (by uuid) public function edit(ImportTemplate $template) { // Eager-load mappings $template->load(['mappings']); // Preload clients list (uuid + name) for possible reassignment $clients = Client::query() ->join('person', 'person.id', '=', 'clients.person_id') ->orderBy('person.full_name') ->get([ 'clients.uuid', DB::raw('person.full_name as name'), ]); $segments = Segment::query()->orderBy('name')->get(['id', 'name']); $decisions = Decision::query()->orderBy('name')->get(['id', 'name']); $actions = Action::with(['decisions:id,name']) ->orderBy('name') ->get(['id', 'name']) ->map(fn ($a) => [ 'id' => $a->id, 'name' => $a->name, 'decisions' => $a->decisions->map(fn ($d) => ['id' => $d->id, 'name' => $d->name])->values(), ]); return Inertia::render('Imports/Templates/Edit', [ 'template' => [ 'uuid' => $template->uuid, 'name' => $template->name, 'description' => $template->description, 'source_type' => $template->source_type, 'default_record_type' => $template->default_record_type, 'is_active' => $template->is_active, 'reactivate' => $template->reactivate, 'client_uuid' => $template->client?->uuid, 'sample_headers' => $template->sample_headers, 'meta' => $template->meta, 'mappings' => $template->mappings()->orderBy('position')->get(['id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position']), ], 'clients' => $clients, 'segments' => $segments, 'decisions' => $decisions, 'actions' => $actions, ]); } // Add a new mapping to a template (by uuid) public function addMapping(Request $request, ImportTemplate $template) { // Normalize empty transform to null $raw = $request->all(); if (array_key_exists('transform', $raw) && $raw['transform'] === '') { $raw['transform'] = null; } $data = validator($raw, [ 'source_column' => 'required|string', 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', 'target_field' => 'nullable|string', 'transform' => 'nullable|string|in:trim,upper,lower', 'apply_mode' => 'nullable|string|in:insert,update,both,keyref', 'options' => 'nullable|array', 'position' => 'nullable|integer', ])->validate(); // Avoid duplicates by source_column within the same template: update if exists $existing = ImportTemplateMapping::where('import_template_id', $template->id) ->where('source_column', $data['source_column']) ->first(); if ($existing) { $existing->update([ 'target_field' => $data['target_field'] ?? $existing->target_field, 'entity' => $data['entity'] ?? $existing->entity, 'transform' => $data['transform'] ?? $existing->transform, 'apply_mode' => $data['apply_mode'] ?? $existing->apply_mode ?? 'both', 'options' => $data['options'] ?? $existing->options, 'position' => $data['position'] ?? $existing->position, ]); return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('info', 'Mapping already existed. Updated existing mapping.'); } else { $position = $data['position'] ?? (int) (($template->mappings()->max('position') ?? 0) + 1); ImportTemplateMapping::create([ 'import_template_id' => $template->id, 'entity' => $data['entity'] ?? null, 'source_column' => $data['source_column'], 'target_field' => $data['target_field'] ?? null, 'transform' => $data['transform'] ?? null, 'apply_mode' => $data['apply_mode'] ?? 'both', 'options' => $data['options'] ?? null, 'position' => $position, ]); return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', 'Mapping added'); } } // Update template basic fields public function update(Request $request, ImportTemplate $template) { $raw = $request->all(); if (! empty($raw['client_uuid'] ?? null)) { $raw['client_id'] = Client::where('uuid', $raw['client_uuid'])->value('id'); } // If template already has mappings, lock client assignment on backend as well // to prevent accidental clearing when client_uuid/client_id not sent. $hasMappings = $template->mappings()->exists(); if ($hasMappings) { unset($raw['client_id'], $raw['client_uuid']); } $data = validator($raw, [ 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', 'source_type' => 'required|string|in:csv,xml,xls,xlsx,json', 'default_record_type' => 'nullable|string|max:50', 'client_id' => 'nullable|integer|exists:clients,id', 'is_active' => 'boolean', 'reactivate' => 'boolean', 'sample_headers' => 'nullable|array', 'meta' => 'nullable|array', 'meta.delimiter' => 'nullable|string|max:4', 'meta.segment_id' => 'nullable|integer|exists:segments,id', 'meta.decision_id' => 'nullable|integer|exists:decisions,id', 'meta.action_id' => 'nullable|integer|exists:actions,id', 'meta.payments_import' => 'nullable|boolean', 'meta.contract_key_mode' => 'nullable|string|in:reference', ])->validate(); // Validate decision/action consistency on update as well $meta = $data['meta'] ?? []; if (! empty($meta['action_id']) && ! empty($meta['decision_id'])) { $belongs = \DB::table('action_decision') ->where('action_id', $meta['action_id']) ->where('decision_id', $meta['decision_id']) ->exists(); if (! $belongs) { return back()->withErrors(['meta.decision_id' => 'Selected decision is not associated with the chosen action.'])->withInput(); } } // Merge meta safely, preserving existing keys when not provided $newMeta = $template->meta ?? []; if (array_key_exists('meta', $data) && is_array($data['meta'])) { $newMeta = array_merge($newMeta, $data['meta']); // Drop empty delimiter to allow auto-detect if (array_key_exists('delimiter', $newMeta) && (! is_string($newMeta['delimiter']) || trim((string) $newMeta['delimiter']) === '')) { unset($newMeta['delimiter']); } foreach (['segment_id', 'decision_id', 'action_id', 'payments_import', 'contract_key_mode'] 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) $finalMeta = $newMeta; if (! empty($finalMeta['payments_import'])) { $finalMeta['entities'] = ['contracts', 'accounts', 'payments']; } $update = [ 'name' => $data['name'], 'description' => $data['description'] ?? null, 'source_type' => $data['source_type'], 'default_record_type' => $data['default_record_type'] ?? null, // Only set client_id if explicitly present and not locked, otherwise keep existing 'is_active' => $data['is_active'] ?? $template->is_active, 'reactivate' => $data['reactivate'] ?? $template->reactivate, 'sample_headers' => $data['sample_headers'] ?? $template->sample_headers, 'meta' => $finalMeta, ]; if (! $hasMappings && array_key_exists('client_id', $data)) { $update['client_id'] = $data['client_id']; } // When locked, do not touch client_id (prevents clearing to null) $template->update($update); return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', 'Template updated'); } // Bulk add multiple mappings from a textarea input public function bulkAddMappings(Request $request, ImportTemplate $template) { // Accept either commas or newlines as separators $raw = $request->all(); if (array_key_exists('transform', $raw) && $raw['transform'] === '') { $raw['transform'] = null; } $data = validator($raw, [ 'sources' => 'required|string', // comma and/or newline separated 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', 'default_field' => 'nullable|string', // if provided, used as the field name for all entries 'apply_mode' => 'nullable|string|in:insert,update,both,keyref', 'transform' => 'nullable|string|in:trim,upper,lower', 'options' => 'nullable|array', 'group' => 'nullable|string|max:50', // convenience: will be wrapped into options.group ])->validate(); // Accept commas, semicolons, and newlines; strip surrounding quotes/apostrophes and whitespace $list = preg_split('/[\r\n,;]+/', $data['sources']); $list = array_values(array_filter(array_map(function ($s) { $s = trim((string) $s); // remove surrounding double/single quotes if present $s = preg_replace('/^([\"\'])|([\"\'])$/u', '', $s) ?? $s; return $s; }, $list), fn ($s) => $s !== '')); if (empty($list)) { return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('warning', 'No valid source columns provided.'); } $basePosition = (int) (($template->mappings()->max('position') ?? 0)); $apply = $data['apply_mode'] ?? 'both'; $transform = $data['transform'] ?? null; $entity = $data['entity'] ?? null; $defaultField = $data['default_field'] ?? null; // allows forcing a specific field for all // Build options payload once $opts = []; if (isset($data['options']) && is_array($data['options'])) { $opts = $data['options']; } if (! empty($data['group'])) { $opts['group'] = (string) $data['group']; } $created = 0; $updated = 0; DB::transaction(function () use ($template, $list, $apply, $transform, $entity, $defaultField, $basePosition, $opts, &$created, &$updated) { foreach ($list as $idx => $source) { $targetField = null; if ($defaultField) { $targetField = $entity ? ($entity.'.'.$defaultField) : $defaultField; } elseif ($entity) { $targetField = $entity.'.'.$source; } $existing = ImportTemplateMapping::where('import_template_id', $template->id) ->where('source_column', $source) ->first(); if ($existing) { $existing->update([ 'target_field' => $targetField ?? $existing->target_field, 'entity' => $entity ?? $existing->entity, 'transform' => $transform ?? $existing->transform, 'apply_mode' => $apply ?? $existing->apply_mode ?? 'both', 'options' => empty($opts) ? $existing->options : $opts, // keep existing position ]); $updated++; } else { ImportTemplateMapping::create([ 'import_template_id' => $template->id, 'entity' => $entity, 'source_column' => $source, 'target_field' => $targetField, 'transform' => $transform, 'apply_mode' => $apply, 'options' => empty($opts) ? null : $opts, 'position' => $basePosition + $idx + 1, ]); $created++; } } }); $msg = []; if ($created) { $msg[] = "$created created"; } if ($updated) { $msg[] = "$updated updated"; } $text = 'Mappings processed'; if (! empty($msg)) { $text .= ': '.implode(', ', $msg); } return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', $text); } // Update an existing mapping public function updateMapping(Request $request, ImportTemplate $template, ImportTemplateMapping $mapping) { if ($mapping->import_template_id !== $template->id) { abort(404); } $raw = $request->all(); if (array_key_exists('transform', $raw) && $raw['transform'] === '') { $raw['transform'] = null; } $data = validator($raw, [ 'source_column' => 'required|string', 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', 'target_field' => 'nullable|string', 'transform' => 'nullable|string|in:trim,upper,lower', 'apply_mode' => 'nullable|string|in:insert,update,both,keyref', 'options' => 'nullable|array', 'position' => 'nullable|integer', ])->validate(); $mapping->update([ 'source_column' => $data['source_column'], 'entity' => $data['entity'] ?? null, 'target_field' => $data['target_field'] ?? null, 'transform' => $data['transform'] ?? null, 'apply_mode' => $data['apply_mode'] ?? 'both', 'options' => $data['options'] ?? null, 'position' => $data['position'] ?? $mapping->position, ]); return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', 'Mapping updated'); } // Delete a mapping public function deleteMapping(ImportTemplate $template, ImportTemplateMapping $mapping) { if ($mapping->import_template_id !== $template->id) { abort(404); } $mapping->delete(); return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', 'Mapping deleted'); } // Reorder mappings in bulk public function reorderMappings(Request $request, ImportTemplate $template) { $data = $request->validate([ 'order' => 'required|array', 'order.*' => 'integer', ]); $ids = $data['order']; // Ensure all ids belong to template $validIds = ImportTemplateMapping::where('import_template_id', $template->id) ->whereIn('id', $ids)->pluck('id')->all(); if (count($validIds) !== count($ids)) { abort(422, 'Invalid mapping ids'); } // Apply new positions foreach ($ids as $idx => $id) { ImportTemplateMapping::where('id', $id)->update(['position' => $idx]); } return redirect()->route('importTemplates.edit', ['template' => $template->uuid]) ->with('success', 'Mappings reordered'); } // Apply a template’s mappings to a specific import (copy into import_mappings) public function applyToImport(Request $request, ImportTemplate $template, Import $import) { // optional: clear previous mappings $clear = $request->boolean('clear', true); $copied = 0; DB::transaction(function () use ($clear, $template, $import, &$copied) { if ($clear) { \DB::table('import_mappings')->where('import_id', $import->id)->delete(); } $rows = $template->mappings()->orderBy('position')->get(); foreach ($rows as $row) { $options = $row->options; if (is_array($options) || $options instanceof \JsonSerializable || $options instanceof \stdClass) { $options = json_encode($options); } \DB::table('import_mappings')->insert([ 'import_id' => $import->id, 'entity' => $row->entity, 'source_column' => $row->source_column, 'target_field' => $row->target_field, 'transform' => $row->transform, 'apply_mode' => $row->apply_mode ?? 'both', 'options' => $options, 'position' => $row->position ?? null, 'created_at' => now(), 'updated_at' => now(), ]); $copied++; } // Merge default actions (segment/decision) into import meta for processing $importMeta = $import->meta ?? []; $tplMeta = $template->meta ?? []; $merged = array_merge($importMeta, array_filter([ 'segment_id' => $tplMeta['segment_id'] ?? null, 'decision_id' => $tplMeta['decision_id'] ?? null, 'action_id' => $tplMeta['action_id'] ?? null, 'template_name' => $template->name, ], fn ($v) => ! is_null($v) && $v !== '')); $import->update([ 'import_template_id' => $template->id, 'meta' => $merged, ]); }); return response()->json(['ok' => true, 'copied' => $copied, 'cleared' => $clear]); } // Delete a template and cascade delete its mappings; detach from imports public function destroy(ImportTemplate $template) { DB::transaction(function () use ($template) { // Nullify references from imports to this template \DB::table('imports')->where('import_template_id', $template->id)->update(['import_template_id' => null]); // Delete mappings first (if FK cascade not set) \DB::table('import_template_mappings')->where('import_template_id', $template->id)->delete(); // Delete the template $template->delete(); }); return redirect()->route('importTemplates.index')->with('success', 'Template deleted'); } }