Skip to content

Commit 8bb8059

Browse files
committed
fix(import-wizard): correct company ID field and add company_name warning
- Fix bug: enrichRowWithCompanyMatch was reading $row['id'] (person/opportunity ID) instead of $row['company_id'] (actual company ID) - Add hasMappingWarnings() to detect multiple warning conditions - Add hasCompanyNameWithoutId() to detect company_name without company_id mapping - Add getMappingWarningsHtml() to generate dynamic modal content - Extend existing warning modal to show both unique identifier and company warnings
1 parent 19c93a0 commit 8bb8059

File tree

4 files changed

+120
-268
lines changed

4 files changed

+120
-268
lines changed

app-modules/ImportWizard/src/Livewire/Concerns/HasColumnMapping.php

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
use Illuminate\Support\Str;
99
use Livewire\Attributes\Computed;
1010
use Relaticle\ImportWizard\Filament\Imports\BaseImporter;
11-
12-
/** @property array<\Filament\Actions\Imports\ImportColumn> $importerColumns */
11+
use Relaticle\ImportWizard\Services\ColumnMatcher;
12+
use Relaticle\ImportWizard\Services\DataTypeInferencer;
13+
14+
/**
15+
* @property array<\Filament\Actions\Imports\ImportColumn> $importerColumns
16+
*
17+
* @phpstan-type InferredMapping array{field: string, confidence: float, type: string}
18+
*/
1319
trait HasColumnMapping
1420
{
21+
/** @var array<string, array{field: string, confidence: float, type: string}> */
22+
public array $inferredMappings = [];
23+
1524
/** @return array<ImportColumn> */
1625
#[Computed]
1726
public function importerColumns(): array
@@ -30,18 +39,55 @@ protected function autoMapColumns(): void
3039
return;
3140
}
3241

33-
$csvHeadersLower = collect($this->csvHeaders)->mapWithKeys(
34-
fn (string $header): array => [Str::lower($header) => $header]
35-
);
42+
$matcher = app(ColumnMatcher::class);
3643

44+
// Phase 1: Header matching
3745
$this->columnMap = collect($this->importerColumns)
38-
->mapWithKeys(function (ImportColumn $column) use ($csvHeadersLower): array {
39-
$guesses = collect($column->getGuesses())->map(fn (string $g): string => Str::lower($g));
40-
$match = $guesses->first(fn (string $guess): bool => $csvHeadersLower->has($guess));
46+
->mapWithKeys(function (ImportColumn $column) use ($matcher): array {
47+
$match = $matcher->findMatchingHeader($this->csvHeaders, $column->getGuesses());
4148

42-
return [$column->getName() => $match !== null ? $csvHeadersLower->get($match) : ''];
49+
return [$column->getName() => $match ?? ''];
4350
})
4451
->toArray();
52+
53+
// Phase 2: Data type inference for unmapped CSV columns
54+
$this->applyDataTypeInference();
55+
}
56+
57+
/**
58+
* Apply data type inference for CSV columns that weren't matched by headers.
59+
*/
60+
protected function applyDataTypeInference(): void
61+
{
62+
$inferencer = app(DataTypeInferencer::class);
63+
$this->inferredMappings = [];
64+
65+
$mappedHeaders = array_filter($this->columnMap);
66+
$unmappedHeaders = array_diff($this->csvHeaders, $mappedHeaders);
67+
68+
foreach ($unmappedHeaders as $header) {
69+
$sampleValues = $this->getColumnPreviewValues($header, 20);
70+
$inference = $inferencer->inferType($sampleValues);
71+
72+
if ($inference['type'] === null) {
73+
continue;
74+
}
75+
76+
// Find first unmapped field that matches suggested fields
77+
foreach ($inference['suggestedFields'] as $suggestedField) {
78+
if (isset($this->columnMap[$suggestedField]) && $this->columnMap[$suggestedField] === '') {
79+
// Auto-apply the inference
80+
$this->columnMap[$suggestedField] = $header;
81+
$this->inferredMappings[$header] = [
82+
'field' => $suggestedField,
83+
'confidence' => $inference['confidence'],
84+
'type' => $inference['type'],
85+
];
86+
87+
break;
88+
}
89+
}
90+
}
4591
}
4692

4793
/** @return class-string<BaseImporter>|null */
@@ -123,4 +169,43 @@ protected function getMissingUniqueIdentifiersMessage(): string
123169

124170
return $importerClass::getMissingUniqueIdentifiersMessage();
125171
}
172+
173+
protected function hasMappingWarnings(): bool
174+
{
175+
return ! $this->hasUniqueIdentifierMapped() || $this->hasCompanyNameWithoutId();
176+
}
177+
178+
protected function hasCompanyNameWithoutId(): bool
179+
{
180+
$columns = collect($this->importerColumns)->keyBy(fn (ImportColumn $col): string => $col->getName());
181+
182+
if (! $columns->has('company_name') || ! $columns->has('company_id')) {
183+
return false;
184+
}
185+
186+
$hasCompanyName = isset($this->columnMap['company_name']) && $this->columnMap['company_name'] !== '';
187+
$hasCompanyId = isset($this->columnMap['company_id']) && $this->columnMap['company_id'] !== '';
188+
189+
return $hasCompanyName && ! $hasCompanyId;
190+
}
191+
192+
protected function getMappingWarningsHtml(): string
193+
{
194+
$warnings = [];
195+
196+
if (! $this->hasUniqueIdentifierMapped()) {
197+
$warnings[] = '<strong>'.$this->getMissingUniqueIdentifiersMessage().'</strong>';
198+
}
199+
200+
if ($this->hasCompanyNameWithoutId()) {
201+
$warnings[] = '<strong>Company Name</strong> is mapped without <strong>Company Record ID</strong>. '.
202+
'New companies will be created for each unique name in your CSV.';
203+
}
204+
205+
$docsUrl = route('documentation.show', 'import').'#unique-identifiers';
206+
207+
return 'Please review the following before continuing:<br><br>'.
208+
implode('<br><br>', $warnings).'<br><br>'.
209+
'<a href="'.$docsUrl.'" target="_blank" class="text-primary-600 hover:underline">Learn more about imports</a>';
210+
}
126211
}

app-modules/ImportWizard/src/Livewire/ImportWizard.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Illuminate\Support\HtmlString;
1818
use Illuminate\Support\Str;
1919
use Illuminate\View\View;
20+
use Livewire\Attributes\Computed;
2021
use Livewire\Attributes\Locked;
2122
use Livewire\Attributes\Validate;
2223
use Livewire\Component;
@@ -140,8 +141,8 @@ public function nextStep(): void
140141
return;
141142
}
142143

143-
// Check for unique identifier mapping when leaving MAP step
144-
if ($this->currentStep === self::STEP_MAP && ! $this->hasUniqueIdentifierMapped()) {
144+
// Check for mapping warnings when leaving MAP step
145+
if ($this->currentStep === self::STEP_MAP && $this->hasMappingWarnings()) {
145146
$this->mountAction('proceedWithoutUniqueIdentifiers');
146147

147148
return;
@@ -531,6 +532,20 @@ public function getEntityLabel(): string
531532
return $entities[$this->entityType]['label'] ?? str($this->entityType)->title()->toString();
532533
}
533534

535+
/**
536+
* Get human-readable labels for all mapped fields.
537+
*
538+
* @return array<string, string>
539+
*/
540+
#[Computed]
541+
public function fieldLabels(): array
542+
{
543+
return collect($this->columnMap)
544+
->filter()
545+
->mapWithKeys(fn ($_, string $field): array => [$field => $this->getFieldLabel($field)])
546+
->all();
547+
}
548+
534549
/**
535550
* Toggle column expansion in review step.
536551
*/
@@ -577,20 +592,14 @@ public function proceedWithErrorsAction(): Action
577592

578593
public function proceedWithoutUniqueIdentifiersAction(): Action
579594
{
580-
$docsUrl = route('documentation.show', 'import').'#unique-identifiers';
581-
582595
return Action::make('proceedWithoutUniqueIdentifiers')
583-
->label('Continue without mapping')
596+
->label('Continue anyway')
584597
->icon(Heroicon::OutlinedExclamationTriangle)
585598
->color('warning')
586599
->requiresConfirmation()
587-
->modalHeading('Avoid creating duplicate records')
588-
->modalDescription(fn (): HtmlString => new HtmlString(
589-
'To avoid creating duplicate records, make sure you include and map the following columns:<br><br>'.
590-
'<strong>'.$this->getMissingUniqueIdentifiersMessage().'</strong><br><br>'.
591-
'<a href="'.$docsUrl.'" target="_blank" class="text-primary-600 hover:underline">Learn more about unique identifiers</a>'
592-
))
593-
->modalSubmitActionLabel('Continue without mapping')
600+
->modalHeading('Review mapping before continuing')
601+
->modalDescription(fn (): HtmlString => new HtmlString($this->getMappingWarningsHtml()))
602+
->modalSubmitActionLabel('Continue anyway')
594603
->modalCancelActionLabel('Go back')
595604
->action(function (): void {
596605
$this->advanceToNextStep();

app-modules/ImportWizard/src/Services/PreviewChunkService.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,10 @@ private function applyCorrections(array $record, array $columnMap, array $correc
229229
{
230230
foreach ($corrections as $fieldName => $valueMappings) {
231231
$csvColumn = $columnMap[$fieldName] ?? null;
232-
if ($csvColumn === null || ! isset($record[$csvColumn])) {
232+
if ($csvColumn === null) {
233+
continue;
234+
}
235+
if (! isset($record[$csvColumn])) {
233236
continue;
234237
}
235238

@@ -275,7 +278,7 @@ private function shouldEnrichWithCompanyMatch(string $importerClass): bool
275278
*/
276279
private function enrichRowWithCompanyMatch(array $row, string $teamId): array
277280
{
278-
$companyId = (string) ($row['id'] ?? '');
281+
$companyId = (string) ($row['company_id'] ?? '');
279282
$companyName = (string) ($row['company_name'] ?? '');
280283
$emails = $this->extractEmailsFromRow($row);
281284

0 commit comments

Comments
 (0)