Skip to content

Commit 37b2c0f

Browse files
Merge pull request #70 from Relaticle/refactor/simplify-company-matching
Refactor/simplify company matching
2 parents 3acc23e + 598e88e commit 37b2c0f

File tree

12 files changed

+295
-335
lines changed

12 files changed

+295
-335
lines changed

app-modules/ImportWizard/resources/views/livewire/partials/step-preview.blade.php

Lines changed: 36 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@
44
$hasMore = $totalRows > count($sampleRows);
55
$showCompanyMatch = in_array($entityType, ['people', 'opportunities']);
66
7-
// Calculate update method statistics
8-
$idBasedUpdates = collect($sampleRows)->where('_update_method', 'id')->count();
9-
$attributeBasedUpdates = collect($sampleRows)->where('_update_method', 'attribute')->count();
10-
117
// Calculate company match statistics
128
$companyMatchStats = [];
139
if ($showCompanyMatch) {
1410
$companyMatchStats = [
11+
'id' => collect($sampleRows)->where('_company_match_type', 'id')->count(),
1512
'domain' => collect($sampleRows)->where('_company_match_type', 'domain')->count(),
16-
'name' => collect($sampleRows)->where('_company_match_type', 'name')->count(),
17-
'ambiguous' => collect($sampleRows)->where('_company_match_type', 'ambiguous')->count(),
1813
'new' => collect($sampleRows)->where('_company_match_type', 'new')->count(),
14+
// 'none' is not counted - records with no company data
1915
];
2016
}
2117
@endphp
@@ -25,62 +21,32 @@
2521
<div>
2622
<div class="flex items-center gap-6 text-sm">
2723
<div class="flex items-center gap-1.5">
28-
<x-filament::icon icon="heroicon-o-document-text" class="h-4 w-4 text-gray-400" />
29-
<span class="font-medium text-gray-700 dark:text-gray-300">{{ number_format($totalRows) }}</span>
30-
<span class="text-gray-500 dark:text-gray-400">total rows</span>
31-
</div>
32-
<div class="flex items-center gap-1.5">
33-
<x-filament::icon icon="heroicon-o-plus-circle" class="h-4 w-4 text-success-500" />
34-
<span class="font-medium text-success-600 dark:text-success-400">~{{ number_format($this->getCreateCount()) }}</span>
35-
<span class="text-gray-500 dark:text-gray-400">new</span>
24+
<x-filament::icon icon="heroicon-m-plus-circle" class="h-5 w-5 text-success-500" />
25+
<span class="font-medium text-success-600 dark:text-success-400">{{ number_format($this->getCreateCount()) }}</span>
26+
<span class="text-gray-500 dark:text-gray-400">will be created</span>
3627
</div>
3728
<div class="flex items-center gap-1.5">
38-
<x-filament::icon icon="heroicon-o-arrow-path" class="h-4 w-4 text-info-500" />
39-
<span class="font-medium text-info-600 dark:text-info-400">~{{ number_format($this->getUpdateCount()) }}</span>
40-
<span class="text-gray-500 dark:text-gray-400">updates</span>
29+
<x-filament::icon icon="heroicon-m-arrow-path" class="h-5 w-5 text-info-500" />
30+
<span class="font-medium text-info-600 dark:text-info-400">{{ number_format($this->getUpdateCount()) }}</span>
31+
<span class="text-gray-500 dark:text-gray-400">will be updated</span>
4132
</div>
4233
</div>
4334
</div>
4435

45-
{{-- Update Method Statistics --}}
46-
@if ($idBasedUpdates > 0 || $attributeBasedUpdates > 0)
47-
<div class="flex items-center gap-4 text-sm pt-2 border-t border-gray-100 dark:border-gray-800">
48-
<span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Update Method:</span>
49-
@if ($idBasedUpdates > 0)
50-
<div class="flex items-center gap-1.5">
51-
<x-filament::badge color="info" size="sm">{{ $idBasedUpdates }}</x-filament::badge>
52-
<span class="text-gray-500 dark:text-gray-400">by ID</span>
53-
</div>
54-
@endif
55-
@if ($attributeBasedUpdates > 0)
56-
<div class="flex items-center gap-1.5">
57-
<x-filament::badge color="warning" size="sm">{{ $attributeBasedUpdates }}</x-filament::badge>
58-
<span class="text-gray-500 dark:text-gray-400">by name/email</span>
59-
</div>
60-
@endif
61-
</div>
62-
@endif
63-
6436
{{-- Company Match Statistics (for People/Opportunities) --}}
6537
@if ($showCompanyMatch && array_sum($companyMatchStats) > 0)
6638
<div class="flex items-center gap-4 text-sm pt-2 border-t border-gray-100 dark:border-gray-800">
6739
<span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Company Matching:</span>
68-
@if ($companyMatchStats['domain'] > 0)
40+
@if ($companyMatchStats['id'] > 0)
6941
<div class="flex items-center gap-1.5">
70-
<x-filament::badge color="success" size="sm">{{ $companyMatchStats['domain'] }}</x-filament::badge>
71-
<span class="text-gray-500 dark:text-gray-400">domain</span>
72-
</div>
73-
@endif
74-
@if ($companyMatchStats['name'] > 0)
75-
<div class="flex items-center gap-1.5">
76-
<x-filament::badge color="info" size="sm">{{ $companyMatchStats['name'] }}</x-filament::badge>
77-
<span class="text-gray-500 dark:text-gray-400">name</span>
42+
<x-filament::badge color="purple" size="sm">{{ $companyMatchStats['id'] }}</x-filament::badge>
43+
<span class="text-gray-500 dark:text-gray-400">by ID</span>
7844
</div>
7945
@endif
80-
@if ($companyMatchStats['ambiguous'] > 0)
46+
@if ($companyMatchStats['domain'] > 0)
8147
<div class="flex items-center gap-1.5">
82-
<x-filament::badge color="warning" size="sm">{{ $companyMatchStats['ambiguous'] }}</x-filament::badge>
83-
<span class="text-gray-500 dark:text-gray-400">ambiguous</span>
48+
<x-filament::badge color="success" size="sm">{{ $companyMatchStats['domain'] }}</x-filament::badge>
49+
<span class="text-gray-500 dark:text-gray-400">by domain</span>
8450
</div>
8551
@endif
8652
@if ($companyMatchStats['new'] > 0)
@@ -106,7 +72,7 @@
10672
<table class="min-w-full text-sm">
10773
<thead class="bg-gray-50 dark:bg-gray-800/50 sticky top-0">
10874
<tr>
109-
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">#</th>
75+
<th class="px-3 py-2 w-10"></th>
11076
@foreach (array_keys($columnMap) as $fieldName)
11177
@if ($columnMap[$fieldName] !== '')
11278
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
@@ -117,14 +83,21 @@
11783
@if ($showCompanyMatch)
11884
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Company Match</th>
11985
@endif
120-
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Status</th>
12186
</tr>
12287
</thead>
12388
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
12489
@foreach ($sampleRows as $index => $row)
12590
<tr wire:key="preview-row-{{ $index }}">
126-
<td class="px-3 py-2 text-gray-500 dark:text-gray-400">
127-
{{ $row['_row_index'] ?? $index + 1 }}
91+
<td class="px-3 py-2">
92+
@php
93+
$isNew = $row['_is_new'] ?? true;
94+
$updateMethod = $row['_update_method'] ?? null;
95+
@endphp
96+
@if ($isNew)
97+
<x-filament::icon icon="heroicon-m-plus-circle" class="h-5 w-5 text-success-500" />
98+
@else
99+
<x-filament::icon icon="heroicon-m-arrow-path" class="h-5 w-5 text-info-500" />
100+
@endif
128101
</td>
129102
@foreach (array_keys($columnMap) as $fieldName)
130103
@if ($columnMap[$fieldName] !== '')
@@ -136,7 +109,7 @@
136109
@if ($showCompanyMatch)
137110
<td class="px-3 py-2">
138111
@php
139-
$matchType = $row['_company_match_type'] ?? 'new';
112+
$matchType = $row['_company_match_type'] ?? 'none';
140113
$matchCount = $row['_company_match_count'] ?? 0;
141114
$companyName = $row['_company_name'] ?? $row['company_name'] ?? '-';
142115
@endphp
@@ -145,48 +118,27 @@
145118
{{ $companyName ?: '-' }}
146119
</span>
147120
@switch($matchType)
121+
@case('id')
122+
<x-filament::badge color="purple" size="sm" icon="heroicon-m-key">
123+
ID
124+
</x-filament::badge>
125+
@break
148126
@case('domain')
149127
<x-filament::badge color="success" size="sm" icon="heroicon-m-check">
150128
Domain
151129
</x-filament::badge>
152130
@break
153-
@case('name')
154-
<x-filament::badge color="info" size="sm">
155-
Name
156-
</x-filament::badge>
157-
@break
158-
@case('ambiguous')
159-
<x-filament::badge color="warning" size="sm" icon="heroicon-m-exclamation-triangle">
160-
{{ $matchCount }} matches
161-
</x-filament::badge>
162-
@break
163-
@default
164-
<x-filament::badge color="gray" size="sm">
131+
@case('new')
132+
<x-filament::badge color="gray" size="sm" icon="heroicon-m-plus">
165133
New
166134
</x-filament::badge>
135+
@break
136+
@default {{-- none --}}
137+
<span class="text-xs text-gray-400">-</span>
167138
@endswitch
168139
</div>
169140
</td>
170141
@endif
171-
<td class="px-3 py-2">
172-
@php
173-
$isNew = $row['_is_new'] ?? true;
174-
$updateMethod = $row['_update_method'] ?? null;
175-
@endphp
176-
@if ($isNew)
177-
<x-filament::badge color="success" size="sm" icon="heroicon-m-plus">
178-
New
179-
</x-filament::badge>
180-
@elseif ($updateMethod === 'id')
181-
<x-filament::badge color="info" size="sm" icon="heroicon-m-arrow-path">
182-
Update by ID
183-
</x-filament::badge>
184-
@else
185-
<x-filament::badge color="warning" size="sm" icon="heroicon-m-arrow-path">
186-
Update
187-
</x-filament::badge>
188-
@endif
189-
</td>
190142
</tr>
191143
@endforeach
192144
</tbody>

app-modules/ImportWizard/src/Data/CompanyMatchResult.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
* Result of company matching during import preview.
1111
*
1212
* Match types:
13+
* - 'id': Matched via company ID (highest priority)
1314
* - 'domain': Matched via email domain → company domains custom field
14-
* - 'name': Matched via exact company name
15-
* - 'ambiguous': Multiple companies matched (by domain or name)
16-
* - 'new': No match found, will create new company
15+
* - 'new': Will create new company (company_name provided)
16+
* - 'none': No company data to link/create (company_name empty, no matches)
1717
*/
1818
final class CompanyMatchResult extends Data
1919
{
@@ -24,23 +24,39 @@ public function __construct(
2424
public ?string $companyId = null,
2525
) {}
2626

27-
public function isAmbiguous(): bool
27+
public function isIdMatch(): bool
2828
{
29-
return $this->matchType === 'ambiguous';
29+
return $this->matchType === 'id';
30+
}
31+
32+
public function isDomainMatch(): bool
33+
{
34+
return $this->matchType === 'domain';
3035
}
3136

3237
public function isNew(): bool
3338
{
3439
return $this->matchType === 'new';
3540
}
3641

37-
public function isDomainMatch(): bool
42+
public function isNone(): bool
3843
{
39-
return $this->matchType === 'domain';
44+
return $this->matchType === 'none';
4045
}
4146

47+
/**
48+
* @deprecated Name matching removed - use ID or domain matching only
49+
*/
4250
public function isNameMatch(): bool
4351
{
4452
return $this->matchType === 'name';
4553
}
54+
55+
/**
56+
* @deprecated Ambiguous handling simplified - returns 'new' instead
57+
*/
58+
public function isAmbiguous(): bool
59+
{
60+
return $this->matchType === 'ambiguous';
61+
}
4662
}

app-modules/ImportWizard/src/Filament/Imports/CompanyImporter.php

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ final class CompanyImporter extends BaseImporter
1616
{
1717
protected static ?string $model = Company::class;
1818

19-
protected static array $uniqueIdentifierColumns = ['id', 'name'];
19+
protected static array $uniqueIdentifierColumns = ['id'];
2020

21-
protected static string $missingUniqueIdentifiersMessage = 'For Companies, map a Company name or Record ID column';
21+
protected static string $missingUniqueIdentifiersMessage = 'For Companies, map a Record ID column';
2222

2323
public static function getColumns(): array
2424
{
@@ -68,8 +68,7 @@ public function resolveRecord(): Company
6868
return $record ?? new Company;
6969
}
7070

71-
// Step 1: Try domain-based duplicate detection (highest confidence for uniqueness)
72-
// Domain field is array type but imported as string or comma-separated
71+
// Try domain-based duplicate detection
7372
$domainsFieldKey = 'custom_fields_'.CompanyField::DOMAINS->value;
7473
$domainValue = $this->data[$domainsFieldKey] ?? null;
7574
$domain = $this->extractFirstDomain($domainValue);
@@ -82,29 +81,8 @@ public function resolveRecord(): Company
8281
}
8382
}
8483

85-
// Step 2: Fall back to name-based duplicate detection
86-
$name = $this->data['name'] ?? null;
87-
88-
if (blank($name)) {
89-
return new Company;
90-
}
91-
92-
// Fast path: Use pre-loaded resolver (preview mode)
93-
if ($this->hasRecordResolver()) {
94-
$existing = $this->getRecordResolver()->resolveCompanyByName(
95-
trim((string) $name),
96-
$this->import->team_id
97-
);
98-
} else {
99-
// Slow path: Query database (actual import execution)
100-
$existing = Company::query()
101-
->where('team_id', $this->import->team_id)
102-
->where('name', trim((string) $name))
103-
->first();
104-
}
105-
106-
/** @var Company */
107-
return $this->applyDuplicateStrategy($existing);
84+
// No match found - create new company
85+
return new Company;
10886
}
10987

11088
/**

app-modules/ImportWizard/src/Filament/Imports/NoteImporter.php

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,16 @@ public static function getColumns(): array
6666

6767
public function resolveRecord(): Note
6868
{
69-
// ID-based resolution takes absolute precedence
69+
// ID-based matching only
7070
if ($this->hasIdValue()) {
7171
/** @var Note|null $record */
7272
$record = $this->resolveById();
7373

7474
return $record ?? new Note;
7575
}
7676

77-
// Fall back to title-based duplicate detection
78-
$title = $this->data['title'] ?? null;
79-
80-
if (blank($title)) {
81-
return new Note;
82-
}
83-
84-
$existing = Note::query()
85-
->where('team_id', $this->import->team_id)
86-
->where('title', trim((string) $title))
87-
->first();
88-
89-
/** @var Note */
90-
return $this->applyDuplicateStrategy($existing);
77+
// No match found - create new note
78+
return new Note;
9179
}
9280

9381
/**

0 commit comments

Comments
 (0)