diff --git a/app/Filament/Imports/PointImporter.php b/app/Filament/Imports/PointImporter.php new file mode 100644 index 00000000..7627832d --- /dev/null +++ b/app/Filament/Imports/PointImporter.php @@ -0,0 +1,239 @@ +label(__('map_points.fields.latitude')) + ->example('27.9506') + ->requiredMapping(), + + ImportColumn::make('longitude') + ->label(__('map_points.fields.longitude')) + ->example('27.9506') + ->requiredMapping(), + + ImportColumn::make('pointType') + ->label(__('map_points.fields.point_type')) + ->example('Punct de colectare') + ->requiredMapping() + ->relationship('pointType', 'name') + ->rules( + [ + 'required', + 'string', + ] + ), + + ImportColumn::make('county') + ->requiredMapping() + ->example('București') + ->label(__('map_points.county')) + ->relationship(name:'county', resolveUsing: 'name') + ->rules( + [ + 'required', + 'string', + ] + ), + + ImportColumn::make('city_id') + ->label(__('map_points.city')) + ->example('Sector 2') + ->fillRecordUsing( + function (Point $record, string $state) { + $cityId = City::search($state)->where('county', $record->county->name)->first()?->id ?? 0; + if ($cityId !== 0) { + $record->city_id = $cityId; + } + } + ) + ->requiredMapping() + ->rules( + [ + 'required', + 'string', + ] + ), + + ImportColumn::make('address') + ->label(__('map_points.fields.address')) + ->example('Strada Ștefan cel Mare 1') + ->ignoreBlankState(), + + ImportColumn::make('notes') + ->label(__('map_points.fields.notes')) + ->ignoreBlankState(), + + ImportColumn::make('observations') + ->label(__('map_points.fields.observations')) + ->ignoreBlankState(), + + ImportColumn::make('administered_by') + ->label(__('map_points.fields.administered_by')) + ->ignoreBlankState(), + + ImportColumn::make('business_name') + ->label(__('map_points.fields.business_name')) + ->ignoreBlankState(), + + ImportColumn::make('offers_money') + ->label(__('map_points.fields.offers_money')) + ->ignoreBlankState(), + + ImportColumn::make('offers_transport') + ->label(__('map_points.fields.offers_transport')) + ->ignoreBlankState(), + + ImportColumn::make('schedule') + ->label(__('map_points.fields.schedule')) + ->ignoreBlankState(), + + ImportColumn::make('email') + ->label(__('map_points.fields.email')) + ->ignoreBlankState(), + + ImportColumn::make('website') + ->label(__('map_points.fields.website')) + ->ignoreBlankState(), + + ImportColumn::make('materials') + ->label(__('map_points.fields.materials')) + ->array(',') + ->ignoreBlankState(), + + ]; + } + + public function resolveRecord(): ?Point + { + $this->data['location'] = new PointObject((float) $this->data['latitude'], (float) $this->data['longitude']); + unset($this->data['latitude'], $this->data['longitude']); + + return new Point(); + } + + public static function getCompletedNotificationBody(Import $import): string + { + $body = 'Your point import has completed and ' . number_format($import->successful_rows) . ' ' . str('row')->plural($import->successful_rows) . ' imported.'; + + if ($failedRowsCount = $import->getFailedRowsCount()) { + $body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to import.'; + } + + return $body; + } + + public static function getOptionsFormComponents(): array + { + return [ + Select::make('service_type_id') + ->label(__('map_points.fields.service_type')) + ->live() + ->options( + ServiceType::all()->pluck('name', 'id')->toArray() + )->required(), + + Toggle::make('import_materials') + ->label(__('map_points.import_materials')) + ->hidden(fn (Get $get) => ! ServiceType::find($get('service_type_id'))?->can_collect_materials) + ->default(false), + + Section::make(__('map_points.use_default_values'))->columns(2)->schema( + [ + TextInput::make('default_administered_by') + ->label(__('map_points.fields.administered_by')), + TextInput::make('default_business_name') + ->label(__('map_points.fields.business_name')), + + Toggle::make('default_offers_money') + ->label(__('map_points.fields.offers_money')), + Toggle::make('default_offers_transport') + ->label(__('map_points.fields.offers_transport')), + + TextInput::make('default_schedule') + ->label(__('map_points.fields.schedule')), + + Select::make('point_group_id') + ->label(__('map_points.fields.group')) + ->options( + PointGroup::all()->pluck('name', 'id')->toArray() + ), + + ] + ), + + ]; + } + + public function saveRecord(): void + { + $materials = $this->data['materials'] ?? []; + unset($this->data['materials']); + unset($this->record->materials); + + $this->setMetadata(); + + $this->checkDefaultFields(); + + parent::saveRecord(); // TODO: Change the autogenerated stub + if ($this->options['import_materials']) { + collect($materials)->each(function ($material) { + $this->record->materials()->attach(Material::search($material)->first()); + }); + } + } + + private function checkDefaultFields(): void + { + if (blank($this->record->administered_by)) { + $this->record->administered_by = $this->options['default_administered_by']; + } + if (blank($this->record->business_name)) { + $this->record->business_name = $this->options['default_business_name']; + } + if (blank($this->record->offers_money)) { + $this->record->offers_money = $this->options['default_offers_money']; + } + if (blank($this->record->offers_transport)) { + $this->record->offers_transport = $this->options['default_offers_transport']; + } + if (blank($this->record->schedule)) { + $this->record->schedule = $this->options['default_schedule']; + } + } + + private function setMetadata(): void + { + $this->record->location = $this->data['location']; + $this->record->service_type_id = $this->options['service_type_id']; + $this->record->created_by = auth()->id(); + $this->record->source = Source::IMPORT; + $this->record->status = Status::VERIFIED; + } +} diff --git a/app/Filament/Resources/ImportResource.php b/app/Filament/Resources/ImportResource.php deleted file mode 100644 index a6acf052..00000000 --- a/app/Filament/Resources/ImportResource.php +++ /dev/null @@ -1,164 +0,0 @@ -schema([ - // - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - TextColumn::make('file') - ->label(__('import.columns.file')) - ->sortable() - ->searchable(), - TextColumn::make('createdBy.fullname') - ->label(__('import.columns.user')) - ->sortable() - ->searchable(), - TextColumn::make('created_at') - ->label(__('import.columns.created_at')) - ->sortable() - ->searchable(), - TextColumn::make('started_at') - ->label(__('import.columns.started_at')) - ->sortable() - ->searchable(), - TextColumn::make('finished_at') - ->label(__('import.columns.finished_at')) - ->sortable() - ->searchable(), - TextColumn::make('processed') - ->label(__('import.columns.processed')) - ->sortable() - ->searchable() - ->getStateUsing( - static function ($record): string { - try { - return \count($record->result['processed']); - } catch(\Exception $e) { - return 0; - } - } - ) - ->html(), - TextColumn::make('failed') - ->label(__('import.columns.failed')) - ->sortable() - ->searchable() - ->getStateUsing( - static function ($record): string { - try { - return \count($record->result['failed']); - } catch(\Exception $e) { - return 0; - } - } - ) - ->html(), - TextColumn::make('status') - ->label(__('import.columns.status')) - ->sortable() - ->searchable() - ->formatStateUsing(function ($state, $record) { - if ($state == 2 && \count($record->result['errors'])) { - $errors = ''; - foreach ($record->result['errors'] as $err) { - $errors .= __('import.' . $err); - } - - return $errors; - } - - return match ($state) { - 0 => __('import.status.pending'), - 1 => __('import.status.processing'), - 2 => '' . __('import.status.view') . '', - }; - }) - ->html(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\DeleteAction::make()->iconButton()->hidden(function ($record) { - return $record->status != 0; - }), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - - public static function getRelations(): array - { - return [ - // - ]; - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListImports::route('/'), - 'create' => Pages\CreateImport::route('/create'), - 'view_report' => Pages\ViewImport::route('/{record}'), - // 'edit' => Pages\EditImport::route('/{record}/edit'), - ]; - } - - public static function getLabel(): ?string - { - return 'Import'; - } - - public static function getPluralLabel(): ?string - { - return 'Import'; - } - - public static function getNavigationBadge(): ?string - { - return (string) static::getModel()::whereStatus(0)->count(); - } - - public static function getEloquentQuery(): Builder - { - return parent::getEloquentQuery(); - } - - public static function canAccess(): bool - { - return false; - } -} diff --git a/app/Filament/Resources/ImportResource/Pages/CreateImport.php b/app/Filament/Resources/ImportResource/Pages/CreateImport.php deleted file mode 100644 index 65265639..00000000 --- a/app/Filament/Resources/ImportResource/Pages/CreateImport.php +++ /dev/null @@ -1,55 +0,0 @@ -schema([ - FileUpload::make('file') - ->preserveFilenames() - ->acceptedFileTypes(['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']), - ]); - } - - public function getTitle(): string - { - return __('imports.import_map_points_label'); - } - - protected function handleRecordCreation(array $data): Model - { - $record = new ImportExportModel(); - $record->file = $data['file']; - $record->type = 'map_points'; - $record->status = 0; - $record->created_by = auth()->user()->id; - $record->save(); - - return $record; - } -} diff --git a/app/Filament/Resources/ImportResource/Pages/EditImport.php b/app/Filament/Resources/ImportResource/Pages/EditImport.php deleted file mode 100644 index 57a9861b..00000000 --- a/app/Filament/Resources/ImportResource/Pages/EditImport.php +++ /dev/null @@ -1,21 +0,0 @@ -view_type = app()->request->get('show', 'processed'); - $data = $this->getRecord()->result[$this->view_type]; - - return $data; - } - - public function getSubHeading(): string | Htmlable - { - return \sprintf(__('imports.sub_heading'), $this->getRecord()->createdBy->fullname, $this->getRecord()->created_at, $this->getRecord()->finished_at); - } - - protected function getHeaderActions(): array - { - $actions = []; - $record = $this->getRecord(); - $failed = (isset($record->result['failed'])) ? \count($record->result['failed']) : 0; - $processed = (isset($record->result['processed'])) ? \count($record->result['processed']) : 0; - $actions = array_merge($actions, [ - Action::make('failed') - ->label(\sprintf(__('imports.not_imported'), $failed)) - ->icon('heroicon-m-check') - ->hidden(function () use ($failed) { - return $this->view_type == 'failed' || $failed == 0; - }) - ->url(fn (ImportExportModel $record): string => ImportResource::getUrl('view_report', ['record' => $record->id]) . '?show=failed') - ->color('danger'), - Action::make('processed') - ->label(\sprintf(__('imports.imported'), $processed)) - ->icon('heroicon-m-check') - ->hidden(function () use ($processed) { - return $this->view_type == 'processed' || $processed == 0; - }) - ->url(fn (ImportExportModel $record): string => ImportResource::getUrl('view_report', ['record' => $record->id]) . '?show=processed') - ->color('success'), - ]); - - return $actions; - } - - protected function getPostFormSchema(): array - { - return [ - TextInput::make('title') - ->required(), - ]; - } - - public function getBreadcrumb(): string - { - return static::$breadcrumb ?? __('filament-panels::resources/pages/view-record.breadcrumb'); - } - - public function getContentTabLabel(): ?string - { - return __('filament-panels::resources/pages/view-record.content.tab.label'); - } - - public function getTitle(): string | Htmlable - { - $record = $this->getRecord(); - - $title = __('imports.import_record_label') . $this->getRecord()->id; - - return new HtmlString($title); - } - - public function table(Table $table): Table - { - switch($this->view_type) { - case 'processed': - return $this->getProccesedTable($table); - break; - case 'failed': - return $this->getFailedTable($table); - break; - } - } - - public function getProccesedTable(Table $table): Table - { - return $table - ->query(MapPointModel::query()->whereIn('id', array_values($this->data))) - ->columns([ - - TextColumn::make('id') - ->label(__('map_points.id')) - ->formatStateUsing(function ($state) { - return '' . $state . ''; - }) - ->sortable() - ->searchable() - ->html(), - TextColumn::make('type.display_name') - ->label(__('map_points.point_type')) - ->searchable(), - TextColumn::make('managed_by') - ->label(__('map_points.managed_by')) - ->sortable() - ->searchable() - ->wrap(), - TextColumn::make('materials.getParent.icon') - ->label(__('map_points.materials')) - ->sortable() - ->searchable() - ->formatStateUsing(function (string $state, $record) { - $icons = collect(explode(',', $state))->unique(); - $state = '
'; - foreach ($icons as $icon) { - $state .= __(""); - } - $state = rtrim($state) . '
'; - - return $state; - }) - ->html(), - TextColumn::make('county') - ->label(__('map_points.county')) - ->sortable() - ->searchable(), - TextColumn::make('city') - ->label(__('map_points.city')) - ->sortable() - ->searchable(), - TextColumn::make('address') - ->label(__('map_points.address')) - ->sortable() - ->searchable() - ->wrap(), - TextColumn::make('group.name') - ->label(__('map_points.group')) - ->sortable() - ->searchable() - ->wrap(), - - BadgeColumn::make('status') - ->color(static function ($state, $record): string { - if ($record->issues->count() > 0) { - return 'danger'; - } - if ((int) $state === 1) { - return 'success'; - } - - return 'warning'; - }) - ->formatStateUsing(function (string $state, $record) { - if ($record->issues->count() > 0) { - return __('map_points.issues_found'); - } - if ((int) $state === 1) { - return __('map_points.verified'); - } - - return __('map_points.requires_verification'); - })->html(), - ]); - } - - public function getFailedTable($table): Table - { - return $table - ->query(ImportExportModel::query()) - ->columns([ - - ]); - - return $table; - } -} diff --git a/app/Filament/Resources/ImportResource/Widgets/ImportExample.php b/app/Filament/Resources/ImportResource/Widgets/ImportExample.php deleted file mode 100644 index 504d7afa..00000000 --- a/app/Filament/Resources/ImportResource/Widgets/ImportExample.php +++ /dev/null @@ -1,12 +0,0 @@ -importer(PointImporter::class), + ] ) ->bulkActions([ diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 4ec6c878..957ff0c8 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -53,6 +53,6 @@ public function panel(Panel $panel): Panel ->authMiddleware([ Authenticate::class, ]) - ->databaseNotifications(false); + ->databaseNotifications(); } } diff --git a/database/migrations/2023_11_16_155454_create_imports_table.php b/database/migrations/2023_11_16_155454_create_imports_table.php deleted file mode 100644 index e3ab0487..00000000 --- a/database/migrations/2023_11_16_155454_create_imports_table.php +++ /dev/null @@ -1,41 +0,0 @@ -integer('id')->primary(); - $table->string('file')->nullable(); - $table->string('type')->default('map_point'); - $table->integer('status')->default(0); - $table->text('result')->nullable(); - $table->string('created_by', 100)->nullable(); - $table->timestamp('created_at')->nullable(); - $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate(); - $table->timestamp('started_at')->nullable(); - $table->timestamp('finished_at')->nullable(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('imports'); - } -} diff --git a/database/migrations/2024_09_10_204052_create_imports_table.php b/database/migrations/2024_09_10_204052_create_imports_table.php new file mode 100644 index 00000000..259c9be3 --- /dev/null +++ b/database/migrations/2024_09_10_204052_create_imports_table.php @@ -0,0 +1,37 @@ +id(); + $table->timestamp('completed_at')->nullable(); + $table->string('file_name'); + $table->string('file_path'); + $table->string('importer'); + $table->unsignedInteger('processed_rows')->default(0); + $table->unsignedInteger('total_rows'); + $table->unsignedInteger('successful_rows')->default(0); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('imports'); + } +}; diff --git a/database/migrations/2024_09_10_204053_create_exports_table.php b/database/migrations/2024_09_10_204053_create_exports_table.php new file mode 100644 index 00000000..bd951b50 --- /dev/null +++ b/database/migrations/2024_09_10_204053_create_exports_table.php @@ -0,0 +1,37 @@ +id(); + $table->timestamp('completed_at')->nullable(); + $table->string('file_disk'); + $table->string('file_name')->nullable(); + $table->string('exporter'); + $table->unsignedInteger('processed_rows')->default(0); + $table->unsignedInteger('total_rows'); + $table->unsignedInteger('successful_rows')->default(0); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('exports'); + } +}; diff --git a/database/migrations/2024_09_10_204054_create_failed_import_rows_table.php b/database/migrations/2024_09_10_204054_create_failed_import_rows_table.php new file mode 100644 index 00000000..b9ee3a86 --- /dev/null +++ b/database/migrations/2024_09_10_204054_create_failed_import_rows_table.php @@ -0,0 +1,32 @@ +id(); + $table->json('data'); + $table->foreignId('import_id')->constrained()->cascadeOnDelete(); + $table->text('validation_error')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('failed_import_rows'); + } +}; diff --git a/database/migrations/2024_09_10_224213_create_job_batches_table.php b/database/migrations/2024_09_10_224213_create_job_batches_table.php new file mode 100644 index 00000000..4d95053f --- /dev/null +++ b/database/migrations/2024_09_10_224213_create_job_batches_table.php @@ -0,0 +1,37 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_batches'); + } +}; diff --git a/database/migrations/2024_09_11_105821_create_jobs_table.php b/database/migrations/2024_09_11_105821_create_jobs_table.php new file mode 100644 index 00000000..53c22929 --- /dev/null +++ b/database/migrations/2024_09_11_105821_create_jobs_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index b381ff29..fd2fcef3 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -82,31 +82,31 @@ public function run(): void $materials = Material::all(); - foreach ($serviceTypes as $serviceType) { - $points = collect(); - - foreach ($serviceType->pointTypes as $pointType) { - $points->push( - ...Point::factory(500) - ->inCity($cities->random()) - ->withMaterials($materials->random(3)) - ->withType($serviceType, $pointType) - ->create() - ); - } - - foreach ($serviceType->issueTypes as $issueType) { - $point = $points->random(); - - $issue = Issue::factory() - ->create([ - 'service_type_id' => $point->service_type_id, - 'point_id' => $point->id, - ]); - - $issue->issueTypes()->attach($issueType->id, ['value' => ['test' => 'test']]); - } - } +// foreach ($serviceTypes as $serviceType) { +// $points = collect(); + +// foreach ($serviceType->pointTypes as $pointType) { +// $points->push( +// ...Point::factory(500) +// ->inCity($cities->random()) +// ->withMaterials($materials->random(3)) +// ->withType($serviceType, $pointType) +// ->create() +// ); +// } + +// foreach ($serviceType->issueTypes as $issueType) { +// $point = $points->random(); +// +// $issue = Issue::factory() +// ->create([ +// 'service_type_id' => $point->service_type_id, +// 'point_id' => $point->id, +// ]); +// +// $issue->issueTypes()->attach($issueType->id, ['value' => ['test' => 'test']]); +// } +// } Artisan::call('scout:rebuild'); } diff --git a/docker/s6-rc.d/worker/dependencies b/docker/s6-rc.d/worker/dependencies new file mode 100644 index 00000000..ff3a7516 --- /dev/null +++ b/docker/s6-rc.d/worker/dependencies @@ -0,0 +1 @@ +laravel diff --git a/docker/s6-rc.d/worker/run b/docker/s6-rc.d/worker/run new file mode 100644 index 00000000..f6e71996 --- /dev/null +++ b/docker/s6-rc.d/worker/run @@ -0,0 +1,9 @@ +#!/command/with-contenv sh + +php /var/www/artisan queue:work \ + --max-jobs $WORKER_MAX_JOBS \ + --sleep $WORKER_SLEEP \ + --rest $WORKER_REST \ + --timeout $WORKER_TIMEOUT \ + --tries $WORKER_TRIES \ + --force diff --git a/docker/s6-rc.d/worker/type b/docker/s6-rc.d/worker/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/docker/s6-rc.d/worker/type @@ -0,0 +1 @@ +longrun diff --git a/lang/ro/map_points.php b/lang/ro/map_points.php index 2afd3538..83532b35 100644 --- a/lang/ro/map_points.php +++ b/lang/ro/map_points.php @@ -7,8 +7,8 @@ 'point_type' => 'Tip', 'point_type_alt' => 'Tip punct', 'materials' => 'Materiale', - 'county' => 'Judet', - 'city' => 'Oras', + 'county' => 'Județul', + 'city' => 'Localitate', 'address' => 'Adresa', 'verified' => '* Verificat', 'requires_verification' => '* Necesita verificare', @@ -29,6 +29,8 @@ 'offers_money' => 'Ofera bani', 'suggest_new_point' => 'Sugereaza un nou punct pe harta', 'point_save_success' => 'Punct salvat cu succes', + 'import_materials' => 'Importa materiale', + 'use_default_values' => 'Foloseste valorile implicite', 'subheading' => ':serviceType :pointType administrat de :administeredBy alocat la grup :group', @@ -36,6 +38,7 @@ 'set_group' => 'Aloca la grup', 'location_type' => 'Tip locatie', 'create' => 'Adauga punct nou', + 'import' => 'Importǎ puncte', 'details' => 'Detalii punct', 'delete' => 'Sterge punct', 'view_on_map' => 'Vezi pe harta', @@ -72,8 +75,8 @@ 'closing_time' => 'Ora de inchidere', 'schedule' => 'Program', 'observations' => 'Observatii', - 'offers_transport' => 'Ofera transport', - 'offers_money' => 'Ofera bani', + 'offers_transport' => 'Oferǎ transport', + 'offers_money' => 'Oferǎ bani', 'offers_vouchers' => 'Ofera vouchere', 'free_of_charge' => 'Oferă servicii gratuite', 'business_name' => 'Nume business',