380 lines
12 KiB
PHP
380 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Imports;
|
|
|
|
use App\Models\Airport;
|
|
use App\Models\Fare;
|
|
use App\Models\Flight;
|
|
use App\Models\FlightBundle;
|
|
use App\Models\Subfleet;
|
|
use App\Services\AirportService;
|
|
use App\Services\FareService;
|
|
use App\Services\FlightService;
|
|
use App\Support\Days;
|
|
use App\Support\FlightTimeParser;
|
|
use App\Support\Utils;
|
|
use Filament\Actions\Imports\Exceptions\RowImportFailedException;
|
|
use Filament\Actions\Imports\ImportColumn;
|
|
use Filament\Actions\Imports\Importer;
|
|
use Filament\Actions\Imports\Models\Import;
|
|
use Filament\Forms\Components\Select;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Number;
|
|
|
|
/**
|
|
* @property Flight $record
|
|
*/
|
|
class FlightImporter extends Importer
|
|
{
|
|
protected static ?string $model = Flight::class;
|
|
|
|
public static function getColumns(): array
|
|
{
|
|
return [
|
|
ImportColumn::make('airline')
|
|
->requiredMapping()
|
|
->relationship(resolveUsing: 'icao')
|
|
->rules(['required']),
|
|
|
|
ImportColumn::make('flight_number')
|
|
->requiredMapping()
|
|
->numeric()
|
|
->rules(['required', 'integer']),
|
|
|
|
ImportColumn::make('callsign')
|
|
->rules(['max:10']),
|
|
|
|
ImportColumn::make('route_code')
|
|
->rules(['max:5']),
|
|
|
|
ImportColumn::make('route_leg')
|
|
->numeric()
|
|
->rules(['nullable', 'integer']),
|
|
|
|
ImportColumn::make('dpt_airport')
|
|
->requiredMapping()
|
|
->fillRecordUsing(function (Flight $record, string $state): void {
|
|
$record->dpt_airport_id = self::processAirport($state)->id;
|
|
})
|
|
->rules(['required']),
|
|
|
|
ImportColumn::make('arr_airport')
|
|
->requiredMapping()
|
|
->fillRecordUsing(function (Flight $record, string $state): void {
|
|
$record->arr_airport_id = self::processAirport($state)->id;
|
|
})
|
|
->rules(['required']),
|
|
|
|
ImportColumn::make('bundle_id')
|
|
->numeric()
|
|
->rules(['nullable', 'integer', 'exists:flight_bundles,id'])
|
|
->fillRecordUsing(function (Flight $record, ?string $state, FlightImporter $importer): void {
|
|
if ($state !== null && $state !== '') {
|
|
$record->bundle_id = (int) $state;
|
|
|
|
return;
|
|
}
|
|
|
|
$fallback = $importer->getImportBundleId();
|
|
|
|
if ($fallback === null) {
|
|
throw new RowImportFailedException('No flight bundle resolved for row; ensure a bundle named "Default" exists or select one in the import options.');
|
|
}
|
|
|
|
$record->bundle_id = $fallback;
|
|
}),
|
|
|
|
ImportColumn::make('alt_airport')
|
|
->fillRecordUsing(function (Flight $record, ?string $state): void {
|
|
if ($state) {
|
|
$record->alt_airport_id = self::processAirport($state)->id;
|
|
}
|
|
}),
|
|
|
|
ImportColumn::make('dpt_time')
|
|
->rules(['max:10'])
|
|
->fillRecordUsing(function (Flight $record, ?string $state): void {
|
|
if ($state !== null && $state !== '') {
|
|
$record->setAttribute('departure_time', FlightTimeParser::parse($state));
|
|
}
|
|
}),
|
|
|
|
ImportColumn::make('arr_time')
|
|
->rules(['max:10'])
|
|
->fillRecordUsing(function (Flight $record, ?string $state): void {
|
|
if ($state !== null && $state !== '') {
|
|
$record->setAttribute('arrival_time', FlightTimeParser::parse($state));
|
|
}
|
|
}),
|
|
|
|
ImportColumn::make('level')
|
|
->numeric()
|
|
->rules(['nullable', 'integer']),
|
|
|
|
ImportColumn::make('distance')
|
|
->numeric()
|
|
->rules(['nullable', 'integer']),
|
|
|
|
ImportColumn::make('flight_time')
|
|
->numeric()
|
|
->rules(['nullable', 'integer']),
|
|
|
|
ImportColumn::make('flight_type')
|
|
->requiredMapping()
|
|
->rules(['required', 'max:1']),
|
|
|
|
ImportColumn::make('load_factor')
|
|
->numeric()
|
|
->rules(['nullable', 'numeric', 'min:0', 'max:100']),
|
|
|
|
ImportColumn::make('load_factor_variance')
|
|
->numeric()
|
|
->rules(['nullable', 'numeric', 'min:0', 'max:100']),
|
|
|
|
ImportColumn::make('route')
|
|
->fillRecordUsing(function (Flight $record, ?string $state): void {
|
|
$record->route = strtoupper($state);
|
|
}),
|
|
|
|
ImportColumn::make('pilot_pay')
|
|
->numeric()
|
|
->rules(['nullable', 'integer']),
|
|
|
|
ImportColumn::make('notes'),
|
|
|
|
ImportColumn::make('days')
|
|
->fillRecordUsing(function (Flight $record, ?string $state): void {
|
|
$record->days = self::setDays($state);
|
|
}),
|
|
|
|
ImportColumn::make('start_date')
|
|
->rules(['nullable', 'date']),
|
|
|
|
ImportColumn::make('end_date')
|
|
->rules(['nullable', 'date']),
|
|
|
|
ImportColumn::make('enabled')
|
|
->requiredMapping()
|
|
->boolean()
|
|
->rules(['required', 'boolean']),
|
|
|
|
ImportColumn::make('event')
|
|
->relationship(),
|
|
|
|
ImportColumn::make('user')
|
|
->relationship(),
|
|
|
|
ImportColumn::make('owner_type')
|
|
->rules(['max:191']),
|
|
|
|
ImportColumn::make('owner_id')
|
|
->rules(['max:36']),
|
|
|
|
ImportColumn::make('subfleets')
|
|
->fillRecordUsing(function (): void {}),
|
|
|
|
ImportColumn::make('fares')
|
|
->fillRecordUsing(function (): void {}),
|
|
|
|
ImportColumn::make('fields')
|
|
->fillRecordUsing(function (): void {}),
|
|
];
|
|
}
|
|
|
|
#[\Override]
|
|
public function resolveRecord(): Flight
|
|
{
|
|
return Flight::withTrashed()->firstOrNew([
|
|
'flight_number' => $this->data['flight_number'],
|
|
'dpt_airport_id' => $this->data['dpt_airport'],
|
|
'arr_airport_id' => $this->data['arr_airport'],
|
|
'route_code' => $this->data['route_code'],
|
|
'route_leg' => $this->data['route_leg'],
|
|
'days' => self::setDays($this->data['days']),
|
|
], [
|
|
'id' => Utils::generateNewId(),
|
|
]);
|
|
}
|
|
|
|
public static function getCompletedNotificationBody(Import $import): string
|
|
{
|
|
$body = 'Your flight import has completed and '.Number::format($import->successful_rows).' '.str('row')->plural($import->successful_rows).' imported.';
|
|
|
|
if (($failedRowsCount = $import->getFailedRowsCount()) !== 0) {
|
|
$body .= ' '.Number::format($failedRowsCount).' '.str('row')->plural($failedRowsCount).' failed to import.';
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
protected function beforeUpdate(): void
|
|
{
|
|
if ($this->record->trashed()) {
|
|
$this->record->restore();
|
|
}
|
|
}
|
|
|
|
protected function afterSave(): void
|
|
{
|
|
if (array_key_exists('subfleets', $this->data) && $this->data['subfleets'] != '') {
|
|
$this->processSubfleets($this->record, $this->data['subfleets']);
|
|
}
|
|
|
|
if (array_key_exists('fares', $this->data) && $this->data['fares'] != '') {
|
|
$this->processFares($this->record, $this->data['fares']);
|
|
}
|
|
|
|
if (array_key_exists('fields', $this->data) && $this->data['fields'] != '') {
|
|
$this->processFields($this->record, $this->data['fields']);
|
|
}
|
|
}
|
|
|
|
private static function setDays(?string $state): int
|
|
{
|
|
if (in_array($state, [null, '', '0'], true)) {
|
|
return 0;
|
|
}
|
|
|
|
$days = [];
|
|
if (str_contains($state, '1')) {
|
|
$days[] = Days::MONDAY;
|
|
}
|
|
|
|
if (str_contains($state, '2')) {
|
|
$days[] = Days::TUESDAY;
|
|
}
|
|
|
|
if (str_contains($state, '3')) {
|
|
$days[] = Days::WEDNESDAY;
|
|
}
|
|
|
|
if (str_contains($state, '4')) {
|
|
$days[] = Days::THURSDAY;
|
|
}
|
|
|
|
if (str_contains($state, '5')) {
|
|
$days[] = Days::FRIDAY;
|
|
}
|
|
|
|
if (str_contains($state, '6')) {
|
|
$days[] = Days::SATURDAY;
|
|
}
|
|
|
|
if (str_contains($state, '7')) {
|
|
$days[] = Days::SUNDAY;
|
|
}
|
|
|
|
return Days::getDaysMask($days);
|
|
}
|
|
|
|
private function processSubfleets(Flight $flight, string $col): void
|
|
{
|
|
$count = 0;
|
|
$subfleets = Utils::parseMultiColumnValues($col);
|
|
foreach ($subfleets as $subfleet_type) {
|
|
$subfleet_type = trim((string) $subfleet_type);
|
|
if ($subfleet_type === '') {
|
|
continue;
|
|
}
|
|
|
|
if ($subfleet_type === '0') {
|
|
continue;
|
|
}
|
|
|
|
$subfleet = Subfleet::firstOrCreate(
|
|
['type' => $subfleet_type],
|
|
[
|
|
'name' => $subfleet_type,
|
|
'airline_id' => $flight->airline_id,
|
|
]
|
|
);
|
|
|
|
$subfleet->save();
|
|
|
|
// sync
|
|
$flight->subfleets()->syncWithoutDetaching([$subfleet->id]);
|
|
$count++;
|
|
}
|
|
|
|
Log::info('Subfleets added/processed: '.$count);
|
|
}
|
|
|
|
private function processFares(Flight $flight, ?string $state): void
|
|
{
|
|
$fares = Utils::parseMultiColumnValues($state);
|
|
foreach ($fares as $fare_code => $fare_attributes) {
|
|
if (\is_int($fare_code)) {
|
|
$fare_code = $fare_attributes;
|
|
$fare_attributes = [];
|
|
}
|
|
|
|
$fare = Fare::firstOrCreate(['code' => $fare_code], ['name' => $fare_code]);
|
|
app(FareService::class)->setForFlight($flight, $fare, $fare_attributes);
|
|
$fare->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse all of the subfields
|
|
*/
|
|
private function processFields(Flight $flight, ?string $col): void
|
|
{
|
|
$pass_fields = [];
|
|
$fields = Utils::parseMultiColumnValues($col);
|
|
|
|
foreach ($fields as $field_name => $field_value) {
|
|
$pass_fields[] = [
|
|
'name' => $field_name,
|
|
'value' => $field_value,
|
|
];
|
|
}
|
|
|
|
app(FlightService::class)->updateCustomFields($flight, $pass_fields);
|
|
}
|
|
|
|
private static function processAirport(string $id): Airport
|
|
{
|
|
$airport = app(AirportService::class)->lookupAirportIfNotFound($id);
|
|
|
|
if (!$airport) {
|
|
throw new RowImportFailedException('Could not find airport '.$id);
|
|
}
|
|
|
|
return $airport;
|
|
}
|
|
|
|
/**
|
|
* Filament import-options form: lets the admin pick a target bundle for rows
|
|
* whose `bundle_id` column is empty. Defaults to the seeded "Default" bundle.
|
|
*/
|
|
#[\Override]
|
|
public static function getOptionsFormComponents(): array
|
|
{
|
|
return [
|
|
Select::make('bundle_id')
|
|
->label(__('filament.flights.fields.bundle'))
|
|
->relationship('bundle', 'name', modifyQueryUsing: fn ($query) => $query->orderBy('name'))
|
|
->default(fn (): ?int => FlightBundle::query()->where('name', 'Default')->value('id'))
|
|
->preload()
|
|
->required(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Resolve the bundle id to use for a row whose CSV `bundle_id` is empty.
|
|
* Reads from the import-options form first, falls back to the "Default" bundle.
|
|
*/
|
|
public function getImportBundleId(): ?int
|
|
{
|
|
$optionId = $this->getOptions()['bundle_id'] ?? null;
|
|
|
|
if ($optionId !== null && $optionId !== '') {
|
|
return (int) $optionId;
|
|
}
|
|
|
|
$id = FlightBundle::query()->where('name', 'Default')->value('id');
|
|
|
|
return $id === null ? null : (int) $id;
|
|
}
|
|
}
|