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(() => {
-
+
-
+
{