Importer update add support for meta data and multiple inserts for some entities like addresses and phones, updated other things

This commit is contained in:
Simon Pocrnjič
2025-10-09 22:28:48 +02:00
parent c8029c9eb0
commit 0598261cdc
27 changed files with 2517 additions and 375 deletions
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('contracts', function (Blueprint $table) {
if (! Schema::hasColumn('contracts', 'meta')) {
$table->json('meta')->nullable()->after('description');
}
});
}
public function down(): void
{
Schema::table('contracts', function (Blueprint $table) {
if (Schema::hasColumn('contracts', 'meta')) {
$table->dropColumn('meta');
}
});
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('import_entities', function (Blueprint $table): void {
if (! Schema::hasColumn('import_entities', 'supports_multiple')) {
$table->boolean('supports_multiple')->default(false)->after('aliases');
}
});
}
public function down(): void
{
Schema::table('import_entities', function (Blueprint $table): void {
if (Schema::hasColumn('import_entities', 'supports_multiple')) {
$table->dropColumn('supports_multiple');
}
});
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('import_entities', function (Blueprint $table) {
if (! Schema::hasColumn('import_entities', 'meta')) {
$table->boolean('meta')->default(false)->after('supports_multiple');
}
});
}
public function down(): void
{
Schema::table('import_entities', function (Blueprint $table) {
if (Schema::hasColumn('import_entities', 'meta')) {
$table->dropColumn('meta');
}
});
}
};
@@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$driver = DB::getDriverName();
if ($driver === 'pgsql' || $driver === 'sqlite') {
// Partial unique indexes for non-deleted rows
DB::statement('CREATE UNIQUE INDEX IF NOT EXISTS unique_emails_person_value_active ON emails(person_id, value) WHERE deleted_at IS NULL');
DB::statement('CREATE UNIQUE INDEX IF NOT EXISTS unique_person_phones_person_nu_active ON person_phones(person_id, nu) WHERE deleted_at IS NULL');
DB::statement('CREATE UNIQUE INDEX IF NOT EXISTS unique_person_addresses_person_address_active ON person_addresses(person_id, address) WHERE deleted_at IS NULL');
} elseif ($driver === 'mysql') {
// MySQL does not support partial indexes; use a generated stored column to represent active rows
foreach ([
['table' => 'emails', 'col' => 'value', 'idx' => 'unique_emails_person_value_active'],
['table' => 'person_phones', 'col' => 'nu', 'idx' => 'unique_person_phones_person_nu_active'],
['table' => 'person_addresses', 'col' => 'address', 'idx' => 'unique_person_addresses_person_address_active'],
] as $cfg) {
$table = $cfg['table'];
$indexName = $cfg['idx'];
if (! Schema::hasColumn($table, 'active_flag')) {
// Use STORED generated column so it can be indexed
DB::statement("ALTER TABLE `{$table}` ADD COLUMN `active_flag` TINYINT(1) GENERATED ALWAYS AS (CASE WHEN `deleted_at` IS NULL THEN 1 ELSE 0 END) STORED");
}
// Create unique index on (person_id, key, active_flag)
Schema::table($table, function (Blueprint $t) use ($cfg, $indexName): void {
$t->unique(['person_id', $cfg['col'], 'active_flag'], $indexName);
});
}
}
}
public function down(): void
{
$driver = DB::getDriverName();
if ($driver === 'pgsql' || $driver === 'sqlite') {
DB::statement('DROP INDEX IF EXISTS unique_emails_person_value_active');
DB::statement('DROP INDEX IF EXISTS unique_person_phones_person_nu_active');
DB::statement('DROP INDEX IF EXISTS unique_person_addresses_person_address_active');
} elseif ($driver === 'mysql') {
foreach ([
['table' => 'emails', 'col' => 'value', 'idx' => 'unique_emails_person_value_active'],
['table' => 'person_phones', 'col' => 'nu', 'idx' => 'unique_person_phones_person_nu_active'],
['table' => 'person_addresses', 'col' => 'address', 'idx' => 'unique_person_addresses_person_address_active'],
] as $cfg) {
Schema::table($cfg['table'], function (Blueprint $t) use ($cfg): void {
$t->dropUnique($cfg['idx']);
});
}
// Drop generated column
foreach (['emails', 'person_phones', 'person_addresses'] as $table) {
if (Schema::hasColumn($table, 'active_flag')) {
DB::statement("ALTER TABLE `{$table}` DROP COLUMN `active_flag`");
}
}
}
}
};
+6 -1
View File
@@ -38,6 +38,7 @@ public function run(): void
'canonical_root' => 'address',
'label' => 'Person Addresses',
'fields' => ['address', 'city', 'postal_code', 'country', 'type_id', 'description'],
'supports_multiple' => true,
'field_aliases' => [
'ulica' => 'address',
'naslov' => 'address',
@@ -64,6 +65,7 @@ public function run(): void
'canonical_root' => 'phone',
'label' => 'Person Phones',
'fields' => ['nu', 'country_code', 'type_id', 'description'],
'supports_multiple' => true,
'field_aliases' => ['number' => 'nu'],
'aliases' => ['phone', 'person_phones'],
'rules' => [
@@ -76,6 +78,7 @@ public function run(): void
'canonical_root' => 'email',
'label' => 'Emails',
'fields' => ['value', 'is_primary', 'label'],
'supports_multiple' => true,
'field_aliases' => ['email' => 'value'],
'aliases' => ['email', 'emails'],
'rules' => [
@@ -87,8 +90,10 @@ public function run(): void
'key' => 'contracts',
'canonical_root' => 'contract',
'label' => 'Contracts',
'fields' => ['reference', 'start_date', 'end_date', 'description', 'type_id', 'client_case_id'],
// Include 'meta' so the UI can select contract.meta as a field target
'fields' => ['reference', 'start_date', 'end_date', 'description', 'type_id', 'client_case_id', 'meta'],
'aliases' => ['contract', 'contracts', 'contracs'],
'meta' => true,
'rules' => [
['pattern' => '/^(sklic|reference|ref)\b/i', 'field' => 'reference'],
['pattern' => '/^(od|from|start|zacetek|začetek)\b/i', 'field' => 'start_date'],
+109 -1
View File
@@ -18,7 +18,7 @@ public function run(): void
'description' => 'Basic person import: name, email, phone, address',
'source_type' => 'csv',
'default_record_type' => 'person',
'sample_headers' => ['first_name','last_name','email','phone','address','city','postal_code','country'],
'sample_headers' => ['first_name', 'last_name', 'email', 'phone', 'address', 'city', 'postal_code', 'country'],
'is_active' => true,
'meta' => [
'delimiter' => ',',
@@ -47,5 +47,113 @@ public function run(): void
'position' => $map['position'],
]);
}
// Multi-contacts template demonstrating group support for emails/phones/addresses
$multi = ImportTemplate::query()->firstOrCreate([
'name' => 'Person CSV (multi contacts)',
], [
'uuid' => (string) Str::uuid(),
'description' => 'Person import with multiple emails/phones/addresses per row using group 1 and 2',
'source_type' => 'csv',
'default_record_type' => 'person',
'sample_headers' => [
'first_name', 'last_name', 'Email 1', 'Email 2', 'Phone 1', 'Phone 2',
'Address 1', 'City 1', 'Postal 1', 'Country 1',
'Address 2', 'City 2', 'Postal 2', 'Country 2',
],
'is_active' => true,
'meta' => [
'delimiter' => ',',
'enclosure' => '"',
'escape' => '\\',
],
]);
$multiMappings = [
// Person identity
['source_column' => 'first_name', 'target_field' => 'person.first_name', 'position' => 1],
['source_column' => 'last_name', 'target_field' => 'person.last_name', 'position' => 2],
// Emails (groups 1, 2)
['source_column' => 'Email 1', 'target_field' => 'email.value', 'position' => 3, 'options' => ['group' => '1']],
['source_column' => 'Email 2', 'target_field' => 'email.value', 'position' => 4, 'options' => ['group' => '2']],
// Phones (groups 1, 2)
['source_column' => 'Phone 1', 'target_field' => 'phone.nu', 'position' => 5, 'options' => ['group' => '1']],
['source_column' => 'Phone 2', 'target_field' => 'phone.nu', 'position' => 6, 'options' => ['group' => '2']],
// Address group 1
['source_column' => 'Address 1', 'target_field' => 'address.address', 'position' => 7, 'options' => ['group' => '1']],
['source_column' => 'City 1', 'target_field' => 'address.city', 'position' => 8, 'options' => ['group' => '1']],
['source_column' => 'Postal 1', 'target_field' => 'address.postal_code', 'position' => 9, 'options' => ['group' => '1']],
['source_column' => 'Country 1', 'target_field' => 'address.country', 'position' => 10, 'options' => ['group' => '1']],
// Address group 2
['source_column' => 'Address 2', 'target_field' => 'address.address', 'position' => 11, 'options' => ['group' => '2']],
['source_column' => 'City 2', 'target_field' => 'address.city', 'position' => 12, 'options' => ['group' => '2']],
['source_column' => 'Postal 2', 'target_field' => 'address.postal_code', 'position' => 13, 'options' => ['group' => '2']],
['source_column' => 'Country 2', 'target_field' => 'address.country', 'position' => 14, 'options' => ['group' => '2']],
];
foreach ($multiMappings as $map) {
ImportTemplateMapping::firstOrCreate([
'import_template_id' => $multi->id,
'source_column' => $map['source_column'],
], [
'target_field' => $map['target_field'],
'position' => $map['position'],
'options' => $map['options'] ?? null,
]);
}
// Contracts with Meta (groups + keys) demo
$contractsMeta = ImportTemplate::query()->firstOrCreate([
'name' => 'Contracts CSV (meta groups demo)',
], [
'uuid' => (string) Str::uuid(),
'description' => 'Contracts import demonstrating contract.meta mappings using groups and keys.',
'source_type' => 'csv',
'default_record_type' => 'contract',
'sample_headers' => [
'Reference', 'Start', 'End',
'Note', 'Category', 'Custom ID',
'Legal Note', 'Legal ID',
],
'is_active' => true,
'meta' => [
'delimiter' => ',',
'enclosure' => '"',
'escape' => '\\',
],
]);
$contractsMetaMappings = [
// Core contract fields
['source_column' => 'Reference', 'target_field' => 'contract.reference', 'position' => 1],
['source_column' => 'Start', 'target_field' => 'contract.start_date', 'position' => 2],
['source_column' => 'End', 'target_field' => 'contract.end_date', 'position' => 3],
// Meta group 1 using explicit options.key
['source_column' => 'Note', 'target_field' => 'contract.meta', 'position' => 4, 'options' => ['group' => '1', 'key' => 'note']],
['source_column' => 'Category', 'target_field' => 'contract.meta', 'position' => 5, 'options' => ['group' => '1', 'key' => 'category']],
// Meta group 1 using bracket key syntax (no options.key needed)
['source_column' => 'Custom ID', 'target_field' => 'contract.meta[custom_id]', 'position' => 6, 'options' => ['group' => '1']],
// Meta group 2 examples
['source_column' => 'Legal Note', 'target_field' => 'contract.meta', 'position' => 7, 'options' => ['group' => '2', 'key' => 'note']],
['source_column' => 'Legal ID', 'target_field' => 'contract.meta[legal_id]', 'position' => 8, 'options' => ['group' => '2']],
];
foreach ($contractsMetaMappings as $map) {
ImportTemplateMapping::firstOrCreate([
'import_template_id' => $contractsMeta->id,
'source_column' => $map['source_column'],
], [
'target_field' => $map['target_field'],
'position' => $map['position'],
'options' => $map['options'] ?? null,
]);
}
}
}