|null */ public ?array $data = []; /** @var array */ public array $headers = []; public ?string $storedFilePath = null; /** @var array|null */ public ?array $importResult = null; public function mount(): void { $this->form->fill([ 'import_type' => ImportType::Employees->value, 'column_mapping' => [], ]); $this->refreshImportResult(); } public function defaultForm(Schema $schema): Schema { return $schema ->statePath('data') ->columns(1); } public function form(Schema $schema): Schema { return $schema->components([ Wizard::make([ Step::make('Upload') ->description('Select import type and upload an Excel file') ->schema([ Select::make('import_type') ->label('Import Type') ->options(ImportType::class) ->required() ->live() ->afterStateUpdated(fn () => $this->resetColumnMapping()), FileUpload::make('file') ->label('Excel File') ->disk(config('hr.file_uploads.disk')) ->directory(config('hr.file_uploads.directory') . '/imports') ->acceptedFileTypes([ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'text/csv', ]) ->required() ->afterStateUpdated(fn () => $this->loadHeaders()), ]), Step::make('Map Columns') ->description('Match spreadsheet columns to system fields') ->schema([ Section::make('Column Mapping') ->schema(fn (): array => $this->getColumnMappingFields()), ]), Step::make('Preview') ->description('Review the first 10 rows with validation') ->schema([ Placeholder::make('preview_table') ->label('Preview') ->content(fn (): HtmlString => new HtmlString($this->getPreviewHtml())), ]), Step::make('Import') ->description('Run the import and review results') ->schema([ Placeholder::make('import_results') ->label('Import Results') ->content(fn (): HtmlString => new HtmlString($this->getResultsHtml())), ]), ]) ->alpineSubmitHandler('$wire.runImport()') ->submitAction(new HtmlString('')) ->skippable(false), ]); } public function content(Schema $schema): Schema { return $schema->components([ Form::make([EmbeddedSchema::make('form')]) ->id('import-wizard-form') ->livewireSubmitHandler('runImport') ->footer([ Actions::make([]) ->alignment(Alignment::Start) ->key('import-wizard-actions'), ]), ]); } public function runImport(): void { $this->form->validate(); $filePath = $this->resolveStoredFilePath(); if ($filePath === null) { Notification::make() ->title('Upload required') ->body('Please upload an Excel file before importing.') ->danger() ->send(); return; } $importType = ImportType::from($this->data['import_type']); $columnMapping = $this->data['column_mapping'] ?? []; ProcessImportJob::dispatch( $importType, $filePath, $columnMapping, Auth::id(), ); Notification::make() ->title('Import queued') ->body('Your import has been queued and will be processed shortly.') ->success() ->send(); } public function refreshImportResult(): void { $result = ProcessImportJob::getCachedResult(Auth::id()); $this->importResult = $result?->toArray(); } public function loadHeaders(): void { $path = $this->resolveStoredFilePath(); if ($path === null) { $this->headers = []; return; } $this->headers = app(ImportService::class)->readHeaders($path); $this->resetColumnMapping(); } protected function resetColumnMapping(): void { $this->data['column_mapping'] = []; } /** * @return array */ protected function getColumnMappingFields(): array { if ($this->headers === []) { return [ Placeholder::make('mapping_hint') ->content('Upload a file in step 1 to load column headers.'), ]; } $importType = ImportType::from($this->data['import_type'] ?? ImportType::Employees->value); $headerOptions = array_combine($this->headers, $this->headers); return collect($importType->fields()) ->map(function (string $label, string $field) use ($headerOptions): Select { return Select::make("column_mapping.{$field}") ->label($label) ->options($headerOptions) ->searchable() ->nullable(); }) ->values() ->all(); } protected function getPreviewHtml(): string { $path = $this->resolveStoredFilePath(); if ($path === null) { return '

Upload a file to preview rows.

'; } $importType = ImportType::from($this->data['import_type'] ?? ImportType::Employees->value); $importService = app(ImportService::class); $rows = $importService->readRows($path, 10); $mappedRows = $importService->mapRows($rows, $this->data['column_mapping'] ?? []); $validation = $importService->validatePreview($importType, $mappedRows); if ($mappedRows === []) { return '

No rows found in the uploaded file.

'; } $fields = array_keys($importType->fields()); $html = '
'; $html .= ''; foreach ($fields as $field) { $html .= ''; } $html .= ''; foreach ($mappedRows as $index => $row) { $errors = $validation[$index]['errors'] ?? []; $html .= ''; $html .= ''; foreach ($fields as $field) { $html .= ''; } $html .= ''; } $html .= '
Row' . e($importType->fields()[$field]) . 'Validation
' . ($index + 2) . '' . e((string) ($row[$field] ?? '—')) . ''; if ($errors === []) { $html .= 'Valid'; } else { $html .= '' . e(implode(' ', $errors)) . ''; } $html .= '
'; return $html; } protected function getResultsHtml(): string { $this->refreshImportResult(); if ($this->importResult === null) { return '

Submit the import to see results here after processing completes.

'; } $html = '
'; $html .= '

Total rows: ' . $this->importResult['total'] . '

'; $html .= '

Imported: ' . $this->importResult['imported'] . '

'; $html .= '

Skipped (duplicates): ' . $this->importResult['skipped'] . '

'; $html .= '

Failed: ' . $this->importResult['failed'] . '

'; if (($this->importResult['errors'] ?? []) !== []) { $html .= '
Errors:
    '; foreach ($this->importResult['errors'] as $error) { $html .= '
  • ' . e($error) . '
  • '; } $html .= '
'; } $html .= '
'; return $html; } protected function resolveStoredFilePath(): ?string { $file = $this->data['file'] ?? null; if ($file === null || $file === []) { return null; } $relativePath = is_array($file) ? ($file[0] ?? null) : $file; if ($relativePath === null) { return null; } $disk = Storage::disk(config('hr.file_uploads.disk')); $this->storedFilePath = $disk->path($relativePath); return $this->storedFilePath; } public function getTitle(): string|Htmlable { return 'Import Wizard'; } }