base commit

This commit is contained in:
Mekan1206
2026-07-30 17:24:40 +05:00
commit a794dacd39
345 changed files with 29597 additions and 0 deletions

View File

@@ -0,0 +1,332 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Enums\ImportType;
use App\Jobs\ProcessImportJob;
use App\Services\Import\ImportService;
use BackedEnum;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\HtmlString;
use UnitEnum;
class ImportWizard extends Page
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedArrowUpTray;
protected static string|UnitEnum|null $navigationGroup = 'System';
protected static ?string $navigationLabel = 'Import Wizard';
protected static ?int $navigationSort = 2;
protected static ?string $title = 'Import Wizard';
protected static ?string $slug = 'import-wizard';
/**
* @var array<string, mixed>|null
*/
public ?array $data = [];
/** @var array<int, string> */
public array $headers = [];
public ?string $storedFilePath = null;
/** @var array<string, mixed>|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('<button type="submit" class="fi-btn fi-btn-primary">Start Import</button>'))
->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<int, Select>
*/
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 '<p class="text-sm text-gray-500">Upload a file to preview rows.</p>';
}
$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 '<p class="text-sm text-gray-500">No rows found in the uploaded file.</p>';
}
$fields = array_keys($importType->fields());
$html = '<div class="overflow-x-auto"><table class="fi-ta-table w-full text-sm"><thead><tr>';
$html .= '<th class="px-3 py-2 text-left">Row</th>';
foreach ($fields as $field) {
$html .= '<th class="px-3 py-2 text-left">' . e($importType->fields()[$field]) . '</th>';
}
$html .= '<th class="px-3 py-2 text-left">Validation</th></tr></thead><tbody>';
foreach ($mappedRows as $index => $row) {
$errors = $validation[$index]['errors'] ?? [];
$html .= '<tr class="border-t border-gray-200 dark:border-gray-700">';
$html .= '<td class="px-3 py-2">' . ($index + 2) . '</td>';
foreach ($fields as $field) {
$html .= '<td class="px-3 py-2">' . e((string) ($row[$field] ?? '—')) . '</td>';
}
$html .= '<td class="px-3 py-2">';
if ($errors === []) {
$html .= '<span class="text-success-600">Valid</span>';
} else {
$html .= '<span class="text-danger-600">' . e(implode(' ', $errors)) . '</span>';
}
$html .= '</td></tr>';
}
$html .= '</tbody></table></div>';
return $html;
}
protected function getResultsHtml(): string
{
$this->refreshImportResult();
if ($this->importResult === null) {
return '<p class="text-sm text-gray-500">Submit the import to see results here after processing completes.</p>';
}
$html = '<div class="space-y-2 text-sm">';
$html .= '<p><strong>Total rows:</strong> ' . $this->importResult['total'] . '</p>';
$html .= '<p><strong>Imported:</strong> ' . $this->importResult['imported'] . '</p>';
$html .= '<p><strong>Skipped (duplicates):</strong> ' . $this->importResult['skipped'] . '</p>';
$html .= '<p><strong>Failed:</strong> ' . $this->importResult['failed'] . '</p>';
if (($this->importResult['errors'] ?? []) !== []) {
$html .= '<div class="mt-3"><strong>Errors:</strong><ul class="list-disc ps-5">';
foreach ($this->importResult['errors'] as $error) {
$html .= '<li>' . e($error) . '</li>';
}
$html .= '</ul></div>';
}
$html .= '</div>';
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';
}
}