88use Illuminate \Support \Str ;
99use Livewire \Attributes \Computed ;
1010use 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+ */
1319trait 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}
0 commit comments