diff --git a/app/Models/Achievement.php b/app/Models/Achievement.php new file mode 100644 index 0000000..fa6b645 --- /dev/null +++ b/app/Models/Achievement.php @@ -0,0 +1,72 @@ + $userAchievements + * @property-read Collection $users + */ +class Achievement extends Model +{ + protected $fillable = [ + 'name', + 'internal_name', + 'short_description', + 'long_description', + 'icon', + 'progressive', + 'difficulty_description', + 'achievement_category_id', + 'achievement_difficulty_id', + ]; + + protected $casts = [ + 'progressive' => 'boolean', + ]; + + // --------------------------------------------------------------- + // Relationships + // --------------------------------------------------------------- + + public function category(): BelongsTo + { + return $this->belongsTo(AchievementCategory::class, 'achievement_category_id'); + } + + public function difficulty(): BelongsTo + { + return $this->belongsTo(AchievementDifficulty::class, 'achievement_difficulty_id'); + } + + public function userAchievements(): HasMany + { + return $this->hasMany(UserAchievement::class, 'achievement_id'); + } + + /** Users who have unlocked (or are progressing toward) this achievement. */ + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'user_achievements') + ->withPivot('progress') + ->withTimestamps(); + } +} diff --git a/app/Models/AchievementCategory.php b/app/Models/AchievementCategory.php new file mode 100644 index 0000000..81ee181 --- /dev/null +++ b/app/Models/AchievementCategory.php @@ -0,0 +1,33 @@ + $achievements + */ +class AchievementCategory extends Model +{ + protected $fillable = [ + 'internal_name', + 'name', + 'description', + ]; + + // --------------------------------------------------------------- + // Relationships + // --------------------------------------------------------------- + + public function achievements(): HasMany + { + return $this->hasMany(Achievement::class, 'achievement_category_id'); + } +} diff --git a/app/Models/AchievementDifficulty.php b/app/Models/AchievementDifficulty.php new file mode 100644 index 0000000..be4368e --- /dev/null +++ b/app/Models/AchievementDifficulty.php @@ -0,0 +1,33 @@ + $achievements + */ +class AchievementDifficulty extends Model +{ + protected $fillable = [ + 'internal_name', + 'name', + 'description', + ]; + + // --------------------------------------------------------------- + // Relationships + // --------------------------------------------------------------- + + public function achievements(): HasMany + { + return $this->hasMany(Achievement::class, 'achievement_difficulty_id'); + } +} diff --git a/app/Models/Notification.php b/app/Models/Notification.php new file mode 100644 index 0000000..4ad6353 --- /dev/null +++ b/app/Models/Notification.php @@ -0,0 +1,102 @@ + 'boolean', + 'read_at' => 'datetime', + 'expires_at' => 'datetime', + ]; + + // --------------------------------------------------------------- + // Relationships + // --------------------------------------------------------------- + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function achievement(): BelongsTo + { + return $this->belongsTo(Achievement::class); + } + + // --------------------------------------------------------------- + // Scopes + // --------------------------------------------------------------- + + /** Notifications the user hasn't opened yet. */ + public function scopeUnread(Builder $query): void + { + $query->whereNull('read_at'); + } + + /** Not expired (no expiry set, or expiry is in the future). */ + public function scopeActive(Builder $query): void + { + $query->where(function (Builder $q) { + $q->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }); + } + + /** Only achievement notifications. */ + public function scopeForAchievements(Builder $query): void + { + $query->where('is_achievement', true); + } + + // --------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------- + + public function markAsRead(): void + { + if (! $this->read_at) { + $this->update(['read_at' => now()]); + } + } + + public function isExpired(): bool + { + return $this->expires_at !== null && $this->expires_at->isPast(); + } +} diff --git a/app/Models/UserAchievement.php b/app/Models/UserAchievement.php new file mode 100644 index 0000000..e06e3da --- /dev/null +++ b/app/Models/UserAchievement.php @@ -0,0 +1,42 @@ + 'integer', + ]; + + // --------------------------------------------------------------- + // Relationships + // --------------------------------------------------------------- + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function achievement(): BelongsTo + { + return $this->belongsTo(Achievement::class); + } +} diff --git a/database/migrations/2026_04_26_030253_build_achievement_system.php b/database/migrations/2026_04_26_030253_build_achievement_system.php new file mode 100644 index 0000000..dbc6b76 --- /dev/null +++ b/database/migrations/2026_04_26_030253_build_achievement_system.php @@ -0,0 +1,740 @@ +id(); + $table->string('internal_name')->unique(); + $table->string('name'); + $table->text('description'); + $table->timestamps(); + }); + + DB::table('achievement_difficulties')->insert([ + [ + 'internal_name' => 'easy', + 'name' => 'Easy', + 'description' => "As easy as booking a flight somewhere new!", + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'moderate', + 'name' => 'Moderate', + 'description' => 'You might have to reroute your flights a little just to get the achievement!', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'hard', + 'name' => 'Hard', + 'description' => "You'll need to go quite a bit out of the way to get this achievement!", + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'expensive', + 'name' => 'Expensive', + 'description' => 'It might be hard, but it will be easier if you have a lot of money!', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'near_impossible', + 'name' => 'Near-Impossible', + 'description' => 'You will actively have to try to get this achievement and they may be very few ways to go about it.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'impossible', + 'name' => 'Impossible', + 'description' => 'This achievement is impossible to get if you start today, but was previously possible.', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + // --------------------------------------------------------------- + // 2. achievement_categories + // --------------------------------------------------------------- + Schema::create('achievement_categories', function (Blueprint $table) { + $table->id(); + $table->string('internal_name')->unique(); + $table->string('name'); + $table->text('description'); + $table->timestamps(); + }); + + DB::table('achievement_categories')->insert([ + [ + 'internal_name' => 'general_flying', + 'name' => 'General Flying', + 'description' => 'Achievements earned through everyday flying activity.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'countries_and_continents', + 'name' => 'Countries & Continents', + 'description' => 'Achievements for visiting countries, regions, and continents.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'aircraft', + 'name' => 'Aircraft', + 'description' => 'Achievements related to specific aircraft types and manufacturers.', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'airlines_and_alliances', + 'name' => 'Airlines and Alliances', + 'description' => 'Achievements focused on airlines and alliances', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'internal_name' => 'fun_challenges', + 'name' => 'Fun Challenges', + 'description' => 'Quirky and creative challenges for the adventurous traveller.', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + // --------------------------------------------------------------- + // 3. Alter achievements — add new columns & foreign keys + // --------------------------------------------------------------- + Schema::table('achievements', function (Blueprint $table) { + // Replace description with split fields + $table->string('short_description')->after('internal_name'); + $table->text('long_description')->after('short_description'); + + // Progressive tracking + $table->boolean('progressive')->default(false)->after('long_description'); + + // Difficulty flavour text specific to this achievement + $table->text('difficulty_description')->nullable()->after('progressive'); + + // Foreign keys + $table->foreignId('achievement_category_id') + ->after('difficulty_description') + ->constrained('achievement_categories') + ->restrictOnDelete(); + + $table->foreignId('achievement_difficulty_id') + ->after('achievement_category_id') + ->constrained('achievement_difficulties') + ->restrictOnDelete(); + + // Drop the old monolithic description column + $table->dropColumn('description'); + }); + + // --------------------------------------------------------------- + // 4. Alter user_achievements — add nullable progress column + // --------------------------------------------------------------- + Schema::table('user_achievements', function (Blueprint $table) { + $table->unsignedInteger('progress')->nullable()->after('achievement_id'); + }); + + Schema::create('notifications', function (Blueprint $table) { + $table->id(); + + $table->foreignId('user_id') + ->constrained('users') + ->cascadeOnDelete(); + + // Content + $table->string('title'); + $table->text('body'); + $table->string('url')->nullable(); + + // Achievement flag & optional link + $table->boolean('is_achievement')->default(false); + $table->foreignId('achievement_id') + ->nullable() + ->constrained('achievements') + ->nullOnDelete(); + + // State + $table->timestamp('read_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + + $table->timestamps(); + + // Common query patterns + $table->index(['user_id', 'read_at']); + $table->index('expires_at'); + }); + + + $this->seedAchievements(); + } + + private function seedAchievements(): void + { + $difficulties = DB::table('achievement_difficulties')->pluck('id', 'internal_name'); + $categories = DB::table('achievement_categories')->pluck('id', 'internal_name'); + + $easy = $difficulties['easy']; + $moderate = $difficulties['moderate']; + $hard = $difficulties['hard']; + $expensive = $difficulties['expensive']; + $nearImposs = $difficulties['near_impossible']; + $impossible = $difficulties['impossible']; + + $generalFlying = $categories['general_flying']; + $countriesAndContinents = $categories['countries_and_continents']; + $aircraft = $categories['aircraft']; + $airlinesAndAlliances = $categories['airlines_and_alliances']; + $funChallenges = $categories['fun_challenges']; + + $icon = 'standard_achievement.png'; + $now = now(); + + $achievements = [ + + // ----------------------------------------------------------- + // General Flying + // ----------------------------------------------------------- + [ + 'internal_name' => 'general_flying.first_flight', + 'name' => 'Off the Ground', + 'short_description' => 'Log your very first flight.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'general_flying.domestic_flight', + 'name' => 'Home Turf', + 'short_description' => 'Take a domestic flight.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'general_flying.international_flight', + 'name' => 'Passport Stamp Collector', + 'short_description' => 'Take an international flight.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'general_flying.business_class', + 'name' => 'Flatbed Fan', + 'short_description' => 'Fly in Business Class.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $expensive, + ], + [ + 'internal_name' => 'general_flying.first_class', + 'name' => 'Caviar at 35,000 Feet', + 'short_description' => 'Fly in First Class.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $expensive, + ], + [ + 'internal_name' => 'general_flying.premium_economy', + 'name' => 'Legroom Lover', + 'short_description' => 'Fly in Premium Economy.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $expensive, + ], + [ + 'internal_name' => 'general_flying.business_or_first', + 'name' => 'Turn Left', + 'short_description' => 'Fly in Business or First Class.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'general_flying.fly_private', + 'name' => 'Wheels Up', + 'short_description' => 'Fly on a private aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $expensive, + ], + [ + 'internal_name' => 'general_flying.general_aviation', + 'name' => 'Little Plane, Big Sky', + 'short_description' => 'Take a general aviation flight.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'general_flying.domestic_two_countries', + 'name' => 'Local Explorer', + 'short_description' => 'Take domestic flights in two different countries.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'general_flying.10_flights', + 'name' => 'Frequent Flyer in Training', + 'short_description' => 'Log 10 flights.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'general_flying.100_flights', + 'name' => 'Century in the Sky', + 'short_description' => 'Log 100 flights.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'general_flying.500_flights', + 'name' => 'The 500 Club', + 'short_description' => 'Log 500 flights.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $hard, + ], + [ + 'internal_name' => 'general_flying.1000_flights', + 'name' => 'Skybound for Life', + 'short_description' => 'Log 1,000 flights.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $generalFlying, + 'achievement_difficulty_id'=> $expensive, + ], + + // ----------------------------------------------------------- + // Countries & Continents + // ----------------------------------------------------------- + [ + 'internal_name' => 'countries_continents.intercontinental', + 'name' => 'Intercontinental', + 'short_description' => 'Fly between two different continents.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'countries_continents.fly_to_africa', + 'name' => 'Safari Bound', + 'short_description' => 'Fly to Africa.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'countries_continents.fly_to_asia', + 'name' => 'Orient Express (Air Edition)', + 'short_description' => 'Fly to Asia.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'countries_continents.fly_to_oceania', + 'name' => "G'Day from 35,000 Feet", + 'short_description' => 'Fly to Oceania.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'countries_continents.fly_to_antarctica', + 'name' => 'Penguin Spotter', + 'short_description' => 'Fly to Antarctica.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => 'Very few commercial or charter services operate to Antarctica, making this extremely difficult to pull off.', + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $expensive, + ], + [ + 'internal_name' => 'countries_continents.fly_to_europe', + 'name' => 'Bonjour from Above', + 'short_description' => 'Fly to Europe.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'countries_continents.fly_to_south_america', + 'name' => 'Southern Cross', + 'short_description' => 'Fly to South America.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'countries_continents.fly_to_north_america', + 'name' => 'Stars, Stripes & Contrails', + 'short_description' => 'Fly to North America.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id'=> $easy, + ], + + // ----------------------------------------------------------- + // Aircraft + // ----------------------------------------------------------- + [ + 'internal_name' => 'aircraft.fly_on_a_plane', + 'name' => 'Up, Up and Away', + 'short_description' => 'Fly on an aeroplane.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'aircraft.fly_on_a_helicopter', + 'name' => 'Chop Chop', + 'short_description' => 'Fly on a helicopter.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'aircraft.fly_on_a_jet', + 'name' => 'Jet Setter', + 'short_description' => 'Fly on a jet-powered aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $easy, + ], + [ + 'internal_name' => 'aircraft.fly_on_a_prop', + 'name' => 'Propeller Head', + 'short_description' => 'Fly on a propeller driven aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'aircraft.all_boeing_7x7', + 'name' => 'Seven Heaven', + 'short_description' => 'Fly every Boeing 7x7 aircraft family.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => 'Requires flying the 707, 717, 727, 737, 747, 757, 767, 777, and 787 — some of which are no longer in commercial service.', + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $impossible, + ], + [ + 'internal_name' => 'aircraft.all_airbus_a3xx', + 'name' => 'Airbus Aristocrat', + 'short_description' => 'Fly every Airbus A3xx aircraft family.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => 'Covers the A300, A310, A318–A321, A330, A340, A350, and A380 families. You are likely going to have to go to Iran to get this one.', + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $nearImposs, + ], + [ + 'internal_name' => 'aircraft.quad_engine', + 'name' => 'Four on the Floor', + 'short_description' => 'Fly on a four-engine aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => 'Four-engine jets are becoming increasingly rare as twin-engine widebodies dominate long-haul routes.', + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $hard, + ], + [ + 'internal_name' => 'aircraft.double_decker', + 'name' => 'Upper Deck Club', + 'short_description' => 'Fly on a double-decker aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => 'Primarily the A380 and 747, which operate on a limited and shrinking number of routes.', + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $hard, + ], + [ + 'internal_name' => 'aircraft.single_engine', + 'name' => 'Solo Spinner', + 'short_description' => 'Fly on a single-engine aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $moderate, + ], + [ + 'internal_name' => 'aircraft.tri_engine', + 'name' => 'Triple Threat', + 'short_description' => 'Fly on a tri-engine aircraft.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => 'Most tri-jets are out of service nowadays, and tri-props even rarer', + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $nearImposs, + ], + [ + 'internal_name' => 'aircraft.smaller_manufacturer', + 'name' => 'Break the Duopoly', + 'short_description' => 'Fly on an aircraft from a manufacturer other than Boeing or Airbus.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => false, + 'difficulty_description' => null, + 'achievement_category_id' => $aircraft, + 'achievement_difficulty_id'=> $moderate, + ], + + // ----------------------------------------------------------- + // Airlines & Alliances + // ----------------------------------------------------------- + [ + 'internal_name' => 'airlines_alliances.all_skyteam', + 'name' => 'Team Player', + 'short_description' => 'Fly with every airline in the SkyTeam alliance.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $airlinesAndAlliances, + 'achievement_difficulty_id'=> $hard, + ], + [ + 'internal_name' => 'airlines_alliances.all_oneworld', + 'name' => 'One World Wonder', + 'short_description' => 'Fly with every airline in the Oneworld alliance.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $airlinesAndAlliances, + 'achievement_difficulty_id'=> $hard, + ], + [ + 'internal_name' => 'airlines_alliances.all_star_alliance', + 'name' => 'Star Collector', + 'short_description' => 'Fly with every airline in the Star Alliance.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $airlinesAndAlliances, + 'achievement_difficulty_id'=> $hard, + ], + [ + 'internal_name' => 'airlines_alliances.all_vanilla_alliance', + 'name' => 'Plain Extraordinary', + 'short_description' => 'Fly with every airline in the Vanilla Alliance.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $airlinesAndAlliances, + 'achievement_difficulty_id'=> $hard, + ], + + // --------------------------------------------------------------- + // Fun Challenges + // --------------------------------------------------------------- + [ + 'internal_name' => 'fun_challenges.airline_alphabet', + 'name' => 'Fly the Alphabet', + 'short_description' => 'Fly with an airline whose IATA code starts with every letter of the alphabet.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => 'Some letters have very few airlines with a matching IATA code, requiring creative routing.', + 'achievement_category_id' => $funChallenges, + 'achievement_difficulty_id' => $hard, + ], + [ + 'internal_name' => 'fun_challenges.airport_alphabet', + 'name' => 'Visit the Alphabet', + 'short_description' => 'Visit an airport whose IATA code starts with every letter of the alphabet.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => 'Certain letters — particularly Q, X, and Z — have extremely limited airport coverage, making this a serious challenge.', + 'achievement_category_id' => $funChallenges, + 'achievement_difficulty_id' => $nearImposs, + ], + + // --------------------------------------------------------------- + // Countries & Continents + // --------------------------------------------------------------- + [ + 'internal_name' => 'countries_continents.all_inhabited_continents', + 'name' => 'Six of One', + 'short_description' => 'Fly to all six inhabited continents.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => null, + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id' => $hard, + ], + [ + 'internal_name' => 'countries_continents.all_continents', + 'name' => 'Seven Wonders of the Skies', + 'short_description' => 'Fly to all seven continents, including Antarctica.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => 'Antarctica has no commercial scheduled service — expect a specialist charter or expedition flight.', + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id' => $nearImposs, + + ], + [ + 'internal_name' => 'countries_continents.all_continent_pairs_one_way', + 'name' => 'Grand Junction', + 'short_description' => 'Fly between every possible pair of continents at least one way.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => 'There are 15 unique continent pairs (excluding Antarctica)', + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id' => $hard, + ], + [ + 'internal_name' => 'countries_continents.all_continent_pairs_both_ways', + 'name' => 'There and Back Again', + 'short_description' => 'Fly between every possible pair of continents in both directions.', + 'long_description' => '', + 'icon' => $icon, + 'progressive' => true, + 'difficulty_description' => "30 different intercontinental flights required, good luck.", + 'achievement_category_id' => $countriesAndContinents, + 'achievement_difficulty_id' => $nearImposs, + ], + + + ]; + + DB::table('achievements')->insert($achievements); + } + + public function down(): void + { + // Reverse user_achievements + Schema::table('user_achievements', function (Blueprint $table) { + $table->dropColumn('progress'); + }); + + // Reverse achievements + Schema::table('achievements', function (Blueprint $table) { + $table->text('description')->after('internal_name'); + $table->dropForeign(['achievement_difficulty_id']); + $table->dropForeign(['achievement_category_id']); + $table->dropColumn([ + 'short_description', + 'long_description', + 'progressive', + 'difficulty_description', + 'achievement_category_id', + 'achievement_difficulty_id', + ]); + }); + + Schema::dropIfExists('achievement_categories'); + Schema::dropIfExists('achievement_difficulties'); + Schema::dropIfExists('notifications'); + } +};