diff --git a/app/Console/Commands/UpdateDepartedFlights.php b/app/Console/Commands/UpdateDepartedFlights.php index 864f287..382b945 100644 --- a/app/Console/Commands/UpdateDepartedFlights.php +++ b/app/Console/Commands/UpdateDepartedFlights.php @@ -7,6 +7,7 @@ use App\Models\Aircraft; use App\Models\IataEquipmentCode; use App\Models\Notification; use App\Models\UserFlight; +use App\Services\FlightStatsService; use Carbon\Carbon; use Carbon\CarbonImmutable; use Illuminate\Console\Attributes\Description; @@ -19,80 +20,10 @@ use Illuminate\Support\Facades\Log; #[Description('Command description')] class UpdateDepartedFlights extends Command { - /** - * Fetch live flight data from FlightStats. - * Returns null if the request fails or no data is found. - */ - protected function fetchFlightData(string $airlineCode, string $flightNumber, CarbonImmutable $date): ?FlightStatData + + public function __construct(protected FlightStatsService $flightStats) { - $url = sprintf( - 'https://www.flightstats.com/v2/api-next/flight-tracker/%s/%s/%d/%d/%d', - $airlineCode, - $flightNumber, - $date->year, - $date->month, - $date->day, - ); - - $response = Http::withOptions([ - 'verify' => config('app.verify_ssl'), - ])->get($url); - - if (!$response->successful()) { - Log::warning("FlightStats request failed for {$airlineCode}{$flightNumber}: HTTP {$response->status()}"); - return null; - } - - $flightData = $response->json('data'); - - if (empty($flightData)) { - return null; - } - - return FlightStatData::fromApiResponse($flightData); - } - /** - * Attempt to resolve the best matching Aircraft record for a given IATA equipment code. - * Prefers passenger variants over freighters/BBJs where multiple matches exist. - */ - protected function guessAircraftFromIata(string $iataCode): ?Aircraft - { - $equipment = IataEquipmentCode::where('iata_code', $iataCode)->first(); - - if (!$equipment) { - Log::info("Unknown IATA equipment code: {$iataCode}"); - return null; - } - - $candidates = Aircraft::where('designator', $equipment->icao_code)->get(); - - if ($candidates->isEmpty()) { - Log::info("No aircraft found for ICAO: {$equipment->icao_code} (IATA: {$iataCode})"); - return null; - } - - if ($candidates->count() === 1) { - return $candidates->first(); - } - - // Prefer passenger variants — deprioritise freighters, BBJs, and convertibles - $deprioritised = ['freighter', 'bbj', 'combi', 'mixed', 'cargo', 'prestige', 'winglet', 'sharklet', 'freight']; - - $pattern = implode('|', $deprioritised); - - $passengerVariants = $candidates->filter( - fn(Aircraft $a) => !preg_match("/({$pattern})/i", $a->display_name_short) - ); - - if ($passengerVariants->count() === 1) { - return $passengerVariants->first(); - } - - if ($passengerVariants->isNotEmpty()) { - return $passengerVariants->first(); - } - - return $candidates->first(); + parent::__construct(); } protected function notifyDataError(UserFlight $flight): void @@ -110,7 +41,7 @@ class UpdateDepartedFlights extends Command /** * Execute the console command. */ - public function handle() + public function handle(): void { $now = now()->utc(); $oneHourAgo = $now->copy()->subHours(1); @@ -136,7 +67,7 @@ class UpdateDepartedFlights extends Command $arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone); - $data = $this->fetchFlightData($airlineCode, $flightNumber, $arrivalDate); + $data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate); if (!$data) { $this->warn("No flight data returned for {$airlineCode}{$flightNumber}"); @@ -170,7 +101,7 @@ class UpdateDepartedFlights extends Command $currentAircraft = $flight->aircraft; if ($currentAircraft?->iata_code !== $data->equipment_iata) { - $match = $this->guessAircraftFromIata($data->equipment_iata); + $match = $this->flightStats->guessAircraftFromIata($data->equipment_iata); if ($match) { $updates['aircraft_id'] = $match->id; diff --git a/app/DTOs/FlightStatData.php b/app/DTOs/FlightStatData.php index f5c924d..4435a52 100644 --- a/app/DTOs/FlightStatData.php +++ b/app/DTOs/FlightStatData.php @@ -13,21 +13,35 @@ readonly class FlightStatData public ?string $equipment_iata, public ?string $departure_iata, public ?string $arrival_iata, + public array $airline_fs_codes, + public ?string $operating_fs, ) {} public static function fromApiResponse(array $flightData): self { + $primaryFs = $flightData['resultHeader']['carrier']['fs'] ?? null; + $operatingFs = $flightData['positional']['flexTrack']['carrierFsCode'] ?? null; + $codeshares = array_column($flightData['codeshares'] ?? [], 'fs'); + + $fsCodes = array_values(array_unique(array_filter([ + $primaryFs, + $operatingFs, + ...$codeshares, + ]))); + return new self( - aircraft_registration: $flightData['positional']['flexTrack']['tailNumber'] ?? null, - estimated_departure_utc: isset($flightData['schedule']['estimatedActualDepartureUTC']) + aircraft_registration: $flightData['positional']['flexTrack']['tailNumber'] ?? null, + estimated_departure_utc: isset($flightData['schedule']['estimatedActualDepartureUTC']) ? Carbon::parse($flightData['schedule']['estimatedActualDepartureUTC']) : null, - estimated_arrival_utc: isset($flightData['schedule']['estimatedActualArrivalUTC']) + estimated_arrival_utc: isset($flightData['schedule']['estimatedActualArrivalUTC']) ? Carbon::parse($flightData['schedule']['estimatedActualArrivalUTC']) : null, - equipment_iata: $flightData['additionalFlightInfo']['equipment']['iata'] ?? null, - departure_iata: $flightData['departureAirport']['iata'] ?? null, - arrival_iata: $flightData['arrivalAirport']['iata'] ?? null, + equipment_iata: $flightData['additionalFlightInfo']['equipment']['iata'] ?? null, + departure_iata: $flightData['departureAirport']['iata'] ?? null, + arrival_iata: $flightData['arrivalAirport']['iata'] ?? null, + airline_fs_codes: $fsCodes, + operating_fs: $flightData['positional']['flexTrack']['carrierFsCode'] ?? null, ); } } diff --git a/app/Http/Controllers/AchievementController.php b/app/Http/Controllers/AchievementController.php index 7671809..2904358 100644 --- a/app/Http/Controllers/AchievementController.php +++ b/app/Http/Controllers/AchievementController.php @@ -22,10 +22,17 @@ class AchievementController extends Controller $userAchievements = $user->achievements() ->with('achievement') + ->select(['achievement_id', 'progress']) ->orderBy('achievement_id') ->get() ->keyBy('achievement_id'); + $unlockedByCategory = $achievements->map(fn($group) => + $group->filter(fn($a) => $userAchievements->get($a->id)?->unlocked)->count() + ); + + $unlockedCount = $userAchievements->filter(fn($ua) => $ua->unlocked)->count(); + return Inertia::render('UserAchievements', [ 'user' => $user, 'canEdit' => auth()->id() === $user->id, @@ -33,6 +40,9 @@ class AchievementController extends Controller 'achievements' => $achievements, 'userAchievements' => $userAchievements, 'loggedInUser' => auth()->user(), + 'unlockedCount' => $unlockedCount, + 'unlockedByCategory' => $unlockedByCategory, + 'totalAchievements' => $achievements->flatten()->count(), ]); } @@ -98,6 +108,7 @@ class AchievementController extends Controller 'airlines' => $airlines, 'continents' => $continents, 'aircraft_families' => $aircraftFamilies, + 'achievementCount' => $user->unlockedAchievements()->count(), ]); } diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index 68ebb1d..67d28b9 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -8,9 +8,11 @@ use App\Models\Airport; use App\Models\CrewType; use App\Models\FlightClass; use App\Models\FlightReason; +use App\Models\IataEquipmentCode; use App\Models\SeatType; use App\Models\UserAction; use App\Models\UserFlight; +use App\Services\FlightStatsService; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -24,7 +26,24 @@ class FlightController extends Controller return [ 'flight_number' => ['nullable', 'string', 'max:10'], 'departure_date' => ['required', 'date'], - 'arrival_date' => ['required', 'date'], + 'arrival_date' => ['required', 'date', function ($attribute, $value, $fail) { + $from = request()->input('from_id'); + $to = request()->input('to_id'); + + if (!$from || !$to) return; + + $departureAirport = Airport::find($from); + $arrivalAirport = Airport::find($to); + + if (!$departureAirport || !$arrivalAirport) return; + + $departureUtc = Carbon::createFromFormat('Y-m-d\TH:i', request()->input('departure_date'), $departureAirport->timezone)->utc(); + $arrivalUtc = Carbon::createFromFormat('Y-m-d\TH:i', $value, $arrivalAirport->timezone)->utc(); + + if ($arrivalUtc->lessThanOrEqualTo($departureUtc)) { + $fail('The arrival time must be after the departure time, accounting for time zones.'); + } + }], 'from_id' => ['required', 'integer', 'exists:airports,id'], 'to_id' => ['required', 'integer', 'exists:airports,id'], 'airline_id' => ['nullable', 'integer', 'exists:airlines,id'], @@ -44,24 +63,74 @@ class FlightController extends Controller { $number = strtoupper(trim($request->query('number', ''))); - // Extract the airline code prefix — letters at the start e.g. "QF" from "QF1" - preg_match('/^([A-Z]{2,3})/', $number, $matches); - $code = $matches[1] ?? null; - $isIata = strlen($code) === 2; - $codeColumn = $isIata ? 'IATA_code' : 'ICAO_code'; + preg_match('/^([A-Z]{2,3})(\d+)/', $number, $matches); + $code = $matches[1] ?? null; + $flightNumber = $matches[2] ?? null; + $isIata = strlen($code) === 2; + $codeColumn = $isIata ? 'IATA_code' : 'ICAO_code'; + + $apiAirlineCodes = []; + $fromOptions = []; + $toOptions = []; + $aircraftOptions = []; + + if (strlen($number) >= 3 && $isIata) { + $flightStatsApi = new FlightStatsService(); + $flightData = $flightStatsApi->fetchFlightData($code, $flightNumber); + + if ($flightData) { + $apiAirlineCodes = $flightData->airline_fs_codes; + + $fromOptions = Airport::where('iata_code', $flightData->departure_iata) + ->get() + ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name, 'country_code' => strtolower($a->region->country->code)]) + ->values() + ->toArray(); + + $toOptions = Airport::where('iata_code', $flightData->arrival_iata) + ->get() + ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name, 'country_code' => strtolower($a->region->country->code)]) + ->values() + ->toArray(); + + if($flightData->equipment_iata){ + $equipment = IataEquipmentCode::where('iata_code', $flightData->equipment_iata)->first(); + if ($equipment) { + $bestGuess = $flightStatsApi->guessAircraftFromIata($flightData->equipment_iata); + + $aircraftOptions = Aircraft::where('designator', $equipment->icao_code) + ->get() + ->sortBy(fn($a) => $a->id === $bestGuess?->id ? 0 : 1) + ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name]) + ->values() + ->toArray(); + } + } + } + } + + // Airlines from the typed code + any additional codes from the API, merged and deduped by id + $allCodes = array_unique(array_filter([$code, ...$apiAirlineCodes])); + + $airlines = Airline::where(function ($q) use ($codeColumn, $code, $allCodes) { + $q->whereIn($codeColumn, $allCodes); + }) + ->get() + ->unique('id') + ->sortBy(function ($a) use ($code, $flightData) { + if ($a->IATA_code === $flightData?->operating_fs) return 0; + if ($a->IATA_code === $code) return 1; + return 2; + }) + ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name, 'logo_url' => $a->logo_url]) + ->values() + ->toArray(); - $airlines = $code - ? Airline::where($codeColumn, $code) - ->get() - ->map(fn ($airline) => ['value' => $airline->id, 'title' => $airline->display_name, 'logo_url' => $airline->logo_url]) - ->values() - ->toArray() - : collect()->toArray(); return response()->json([ 'airline_options' => $airlines, - 'from_options' => [], - 'to_options' => [], - 'aircraft_options' => [], + 'from_options' => $fromOptions, + 'to_options' => $toOptions, + 'aircraft_options' => $aircraftOptions, ]); } diff --git a/app/Http/Controllers/FlightImportController.php b/app/Http/Controllers/FlightImportController.php index d1e36fb..b0c37d5 100644 --- a/app/Http/Controllers/FlightImportController.php +++ b/app/Http/Controllers/FlightImportController.php @@ -35,37 +35,21 @@ class FlightImportController extends Controller ->toArray(); } - public function getPossibleAircraft(string $aircraftQuery) { + public function getPossibleAircraft(string $aircraftQuery): array + { preg_match('/\((\w+)\)/', $aircraftQuery, $matches); $designator = $matches[1] ?? null; - $sortOverrides = [ - 'B788' => "CASE WHEN model_full_name ILIKE '%BBJ%' THEN 1 ELSE 0 END", - 'B789' => "CASE WHEN model_full_name ILIKE '%BBJ%' THEN 1 ELSE 0 END", - ]; + if (!$designator) return []; - if(!$designator){ - $aircraft = []; - } else { - - $aircraft = Aircraft::when($designator, fn($query) => $query->where('designator', 'ilike', $designator)) - ->when( - isset($sortOverrides[$designator]), - fn($q) => $q->orderByRaw($sortOverrides[$designator]) - ) - ->orderBy('model_full_name') - ->limit(10) - ->get(['id', 'manufacturer_code', 'model_full_name', 'designator']) - ->map(fn($aircraft) => [ - 'value' => $aircraft->id, - 'title' => $aircraft->display_name, - ]) - ->values() - ->toArray(); - - } - - return $aircraft; + return Aircraft::where('designator', 'ilike', $designator) + ->orderByDesc('preferred') + ->orderBy('model_full_name') + ->limit(10) + ->get(['id', 'manufacturer_code', 'model_full_name', 'designator', 'preferred']) + ->map(fn($a) => ['value' => $a->id, 'title' => $a->display_name]) + ->values() + ->toArray(); } public function getPossibleAirports(string $airportQuery) { diff --git a/app/Models/Aircraft.php b/app/Models/Aircraft.php index a314892..6fd4048 100644 --- a/app/Models/Aircraft.php +++ b/app/Models/Aircraft.php @@ -15,10 +15,12 @@ class Aircraft extends Model 'engine_type', 'engine_count', 'wtc', + 'preferred' ]; protected $casts = [ 'engine_count' => 'integer', + 'preferred' => 'boolean' ]; protected $appends = [ @@ -51,6 +53,7 @@ class Aircraft extends Model 'A380' => ['A380', 'A388'], ]; + protected function displayName() : Attribute{ return Attribute::make( get: function () { diff --git a/app/Models/User.php b/app/Models/User.php index a75ba02..5fb6855 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -41,6 +41,26 @@ class User extends Authenticatable return $this->hasMany(UserAchievement::class); } + public function unlockedAchievements(): HasMany + { + return $this->achievements() + ->join('achievements', 'achievements.id', '=', 'user_achievements.achievement_id') + ->where(function ($query) { + $query + // Non-progressive achievements: always count + ->where(function ($q) { + $q->where('achievements.progressive', false) + ->orWhereNull('achievements.progressive'); + }) + // Progressive achievements: only if progress >= threshold + ->orWhere(function ($q) { + $q->where('achievements.progressive', true) + ->whereNotNull('achievements.threshold') + ->whereColumn('user_achievements.progress', '>=', 'achievements.threshold'); + }); + }); + } + public function resolveRouteBinding($value, $field = null): ?User { return $this->where('name', 'ilike', $value)->firstOrFail(); diff --git a/app/Models/UserAchievement.php b/app/Models/UserAchievement.php index e06e3da..c36ac58 100644 --- a/app/Models/UserAchievement.php +++ b/app/Models/UserAchievement.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -26,6 +27,21 @@ class UserAchievement extends Model 'progress' => 'integer', ]; + protected $appends = [ + 'unlocked', + ]; + + protected function unlocked(): Attribute + { + return Attribute::make( + get: function () { + if (!$this->achievement) return false; + if (!$this->achievement->progressive || !$this->achievement->threshold) return true; + return ($this->progress ?? 0) >= $this->achievement->threshold; + } + ); + } + // --------------------------------------------------------------- // Relationships // --------------------------------------------------------------- diff --git a/app/Services/Achievements/Checkers/GeneralFlyingChecker.php b/app/Services/Achievements/Checkers/GeneralFlyingChecker.php index 8a2e140..e17fa0f 100644 --- a/app/Services/Achievements/Checkers/GeneralFlyingChecker.php +++ b/app/Services/Achievements/Checkers/GeneralFlyingChecker.php @@ -74,6 +74,7 @@ class GeneralFlyingChecker extends BaseChecker $this->awardProgress((int) $totalDistance, 'general_flying.circumference_of_the_earth'); $this->awardProgress((int) $totalDistance, 'general_flying.to_the_moon'); + $this->awardProgress((int) $totalDistance, 'general_flying.gigametre'); $this->awardProgress($count,'general_flying.10_flights'); $this->awardProgress($count,'general_flying.50_flights'); diff --git a/app/Services/FlightStatsService.php b/app/Services/FlightStatsService.php new file mode 100644 index 0000000..12b5c9e --- /dev/null +++ b/app/Services/FlightStatsService.php @@ -0,0 +1,111 @@ + config('app.verify_ssl'), + ])->get($url); + + if (!$response->successful()) { + return []; + } + + $data = $response->json('data'); + + if (!is_array($data)) { + return []; + } + + return $data; + } + + public function fetchFlightData(string $airlineCode, string $flightNumber, ?CarbonImmutable $date = null): ?FlightStatData + { + $specificDate = $date !== null; + $date ??= now()->utc()->toImmutable(); + + $data = $this->fetchForDate($airlineCode, $flightNumber, $date); + + if ($data || $specificDate) return $data; + + $otherDays = $this->fetchOtherDays($airlineCode, $flightNumber); + + $pastDays = collect($otherDays) + ->filter(fn($day) => !empty($day['flights'])) + ->sortByDesc(fn($day) => $day['year'] . $day['date1']); + + foreach ($pastDays as $day) { + $pastDate = CarbonImmutable::createFromFormat('Y-d-M', $day['year'] . '-' . $day['date1']); + $result = $this->fetchForDate($airlineCode, $flightNumber, $pastDate); + if ($result) return $result; + } + + return null; + } + + protected function fetchForDate(string $airlineCode, string $flightNumber, CarbonImmutable $date): ?FlightStatData + { + $url = sprintf( + 'https://www.flightstats.com/v2/api-next/flight-tracker/%s/%s/%d/%d/%d', + $airlineCode, + $flightNumber, + $date->year, + $date->month, + $date->day, + ); + + $response = Http::withOptions([ + 'verify' => config('app.verify_ssl'), + ])->get($url); + + if (!$response->successful()) { + Log::warning("FlightStats request failed for {$airlineCode}{$flightNumber}: HTTP {$response->status()}"); + return null; + } + + $flightData = $response->json('data'); + + if (empty($flightData)) return null; + + return FlightStatData::fromApiResponse($flightData); + } + + public function guessAircraftFromIata(string $iataCode): ?Aircraft + { + $equipment = IataEquipmentCode::where('iata_code', $iataCode)->first(); + + if (!$equipment) { + Log::info("Unknown IATA equipment code: {$iataCode}"); + return null; + } + + $aircraft = Aircraft::where('designator', $equipment->icao_code) + ->orderByDesc('preferred') + ->orderBy('id') + ->first(); + + if (!$aircraft) { + Log::info("No aircraft found for ICAO: {$equipment->icao_code} (IATA: {$iataCode})"); + } + + return $aircraft; + } +} diff --git a/database/migrations/2026_05_17_123304_production_fixes.php b/database/migrations/2026_05_17_123304_production_fixes.php new file mode 100644 index 0000000..9abb612 --- /dev/null +++ b/database/migrations/2026_05_17_123304_production_fixes.php @@ -0,0 +1,163 @@ + $manufacturer_code] : []; + + $count = Aircraft::where([ + 'designator' => $designator, + 'model_full_name' => $model_name, + ...$manufacturerSearchTerm, + ])->update(['preferred' => true]); + + echo $designator . ' ' . $model_name . ' ' . $manufacturer_code . ' ' . $count . PHP_EOL; + + return $this; + } + + function setDesignatorDefaults(): void + { + $this + ->setDesignatorDefault('A19N', 'A-319neo') + ->setDesignatorDefault('A306', 'A-300B4-600') + ->setDesignatorDefault('A30B', 'A-300B4-200') + ->setDesignatorDefault('A310', 'A-310') + ->setDesignatorDefault('A318', 'A-318') + ->setDesignatorDefault('A319', 'A-319') + ->setDesignatorDefault('A320', 'A-320') + ->setDesignatorDefault('A332', 'A-330-200') + ->setDesignatorDefault('A338', 'A-330-800') + ->setDesignatorDefault('A339', 'A-330-900') + ->setDesignatorDefault('A342', 'A-340-200') + ->setDesignatorDefault('A343', 'A-340-300') + ->setDesignatorDefault('A345', 'A-340-500') + ->setDesignatorDefault('A346', 'A-340-600') + ->setDesignatorDefault('A359', 'A-350-900 XWB') + ->setDesignatorDefault('A35K', 'A-350-1000 XWB') + ->setDesignatorDefault('A388', 'A-380-800') + ->setDesignatorDefault('AN24', 'An-24') + ->setDesignatorDefault('AN26', 'An-26') + ->setDesignatorDefault('AT43', 'ATR-42-300') + ->setDesignatorDefault('AT44', 'ATR-42-400') + ->setDesignatorDefault('AT72', 'ATR-72-201') + ->setDesignatorDefault('AT73', 'ATR-72-211') + ->setDesignatorDefault('AT75', 'ATR-72-500') + ->setDesignatorDefault('AT76', 'ATR-72-600') + ->setDesignatorDefault('B37M', '737 MAX 7') + ->setDesignatorDefault('B38M', '737 MAX 8') + ->setDesignatorDefault('B39M', '737 MAX 9') + ->setDesignatorDefault('B3XM', '737 MAX 10') + ->setDesignatorDefault('B461', 'BAe-146-100') + ->setDesignatorDefault('B462', 'BAe-146-200') + ->setDesignatorDefault('B703', '707-300') + ->setDesignatorDefault('B712', '717-200') + ->setDesignatorDefault('B721', '727-100') + ->setDesignatorDefault('B732', '737-200') + ->setDesignatorDefault('B737', '737-700') + ->setDesignatorDefault('B738', '737-800') + ->setDesignatorDefault('B739', '737-900') + ->setDesignatorDefault('B742', '747-200') + ->setDesignatorDefault('B748', '747-8') + ->setDesignatorDefault('B752', '757-200') + ->setDesignatorDefault('B772', '777-200') + ->setDesignatorDefault('B778', '777-8') + ->setDesignatorDefault('B77L', '777-200LR') + ->setDesignatorDefault('B77W', '777-300ER') + ->setDesignatorDefault('B788', '787-8 Dreamliner') + ->setDesignatorDefault('B789', '787-9 Dreamliner') + ->setDesignatorDefault('BCS1', 'A-220-100') + ->setDesignatorDefault('BCS3', 'A-220-300') + ->setDesignatorDefault('CRJ1', 'CL-600 Regional Jet CRJ-100') + ->setDesignatorDefault('CRJ2', 'CL-600 Regional Jet CRJ-200') + ->setDesignatorDefault('CRJ7', 'CL-600 Regional Jet CRJ-700') + ->setDesignatorDefault('CRJ9', 'CL-600 Regional Jet CRJ-900') + ->setDesignatorDefault('DC10', 'DC-10') + ->setDesignatorDefault('DC3', 'DC-3') + ->setDesignatorDefault('DH8B', 'DHC-8-200 Dash 8' , 'DE HAVILLAND CANADA') + ->setDesignatorDefault('DH8A', 'DHC-8-100 Dash 8' , 'DE HAVILLAND CANADA') + ->setDesignatorDefault('DH8C', 'DHC-8-300 Dash 8' , 'DE HAVILLAND CANADA') + ->setDesignatorDefault('DH8D', 'DHC-8-400 Dash 8' , 'DE HAVILLAND CANADA') + ->setDesignatorDefault('DHC6', 'DHC-6 Twin Otter','DE HAVILLAND CANADA') + ->setDesignatorDefault('E120', 'EMB-120 Brasilia', 'EMBRAER') + ->setDesignatorDefault('E135', 'ERJ-135') + ->setDesignatorDefault('E145', 'ERJ-145ER') + ->setDesignatorDefault('E170', '170') + ->setDesignatorDefault('E190', '190') + ->setDesignatorDefault('E195', '195') + ->setDesignatorDefault('E275', 'E175-E2') + ->setDesignatorDefault('E290', 'E190-E2') + ->setDesignatorDefault('E295', 'E195-E2') + ->setDesignatorDefault('F27', 'F-27 Friendship', 'FOKKER') + ->setDesignatorDefault('JS32', 'BAe-3200 Jetstream Super 31', 'BRITISH AEROSPACE') + ->setDesignatorDefault('JS41', 'BAe-4100 Jetstream 41', 'BRITISH AEROSPACE') + ->setDesignatorDefault('MD81', 'MD-81', 'MCDONNELL DOUGLAS') + ->setDesignatorDefault('MD82', 'MD-82', 'MCDONNELL DOUGLAS') + ->setDesignatorDefault('MD83', 'MD-83', 'MCDONNELL DOUGLAS') + ->setDesignatorDefault('MD87', 'MD-87', 'MCDONNELL DOUGLAS') + ->setDesignatorDefault('MD88', 'MD-88', 'MCDONNELL DOUGLAS') + ->setDesignatorDefault('MD90', 'MD-90', 'MCDONNELL DOUGLAS') + ->setDesignatorDefault('AJ27', 'C-909') + ; + } + + public function up(): void + { + Schema::table('aircraft', function (Blueprint $table) { + $table->boolean('preferred')->default(false)->after('designator'); + }); + + $this->setDesignatorDefaults(); + + IataEquipmentCode::create([ + 'iata_code' => '388', + 'icao_code' => 'A388', + 'description' => 'Airbus A380' + ]); + + Achievement::create([ + 'name' => 'Gone a Gigametre', + 'internal_name' => 'general_flying.gigametre', + 'short_description' => 'Fly 1 million kilometres.', + 'long_description' => '', + 'achievement_difficulty_id' => AchievementDifficulty::whereInternalName('near_impossible')->first()->id, + 'achievement_category_id' => AchievementCategory::whereInternalName('general_flying')->first()->id, + 'icon' => 'standard_achievement.png', + 'has_page' => false, + 'sort_order' => 18, + 'progressive' => true, + 'threshold' => 1000000, + ]); + + Achievement::whereInternalName('aircraft.smaller_manufacturer')->update([ + 'difficulty_description' => 'General Aviation flights do not count. Only flights with an airline and flight number can earn this achievement', + ]); + + foreach(User::all() as $user) { + $user->calculateAchievements(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/resources/js/Components/FlightsGoneBy/AchievementCard.vue b/resources/js/Components/FlightsGoneBy/AchievementCard.vue index c088df9..af4a8ba 100644 --- a/resources/js/Components/FlightsGoneBy/AchievementCard.vue +++ b/resources/js/Components/FlightsGoneBy/AchievementCard.vue @@ -11,6 +11,7 @@ import Distance from "@/Components/Distance.vue"; const distanceAchievements = [ 'general_flying.circumference_of_the_earth', 'general_flying.to_the_moon', + 'general_flying.gigametre' ]; const props = defineProps<{ @@ -29,6 +30,10 @@ const progress = computed(() => { } }) +const inProgress = computed(() => + !unlocked.value && (props.userAchievement?.progress ?? 0) > 0 +) + const unlocked = computed(() => { if (!props.userAchievement) return false if (props.achievement.progressive) return (progress.value?.percentage ?? 0) >= 100 @@ -52,11 +57,11 @@ const difficultyVariant = computed(() => {