Compare commits

..
9 changed files with 122 additions and 13 deletions
@@ -3,19 +3,35 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Attributes\Argument;
use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command; use Illuminate\Console\Command;
#[Signature('app:recalculate-achivements')] #[Signature('app:recalculate-achievements {username? : The username of a single user to recalculate}')]
#[Description('Recalculate achievements for all users')] #[Description('Recalculate achievements for all users, or a single user by username')]
class RecalculateAchievements extends Command class RecalculateAchievements extends Command
{ {
public function handle(): void public function handle(): void
{ {
$users = User::all(); $username = $this->argument('username');
foreach ($users as $user) {
if ($username) {
$user = User::where('name', $username)->first();
if (! $user) {
$this->error("No user found with username [{$username}].");
return;
}
$user->calculateAchievements(); $user->calculateAchievements();
$this->info("Recalculated achievements for {$username}.");
return;
} }
$users = User::all();
$this->withProgressBar($users, fn (User $user) => $user->calculateAchievements());
$this->newLine();
$this->info("Recalculated achievements for {$users->count()} users.");
} }
} }
@@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Achievement; use App\Models\Achievement;
use App\Models\AchievementCategory;
use App\Models\Aircraft; use App\Models\Aircraft;
use App\Models\Airport; use App\Models\Airport;
use App\Models\Alliance; use App\Models\Alliance;
@@ -105,8 +106,14 @@ class UserProfileController extends Controller
$achievements = Achievement::with(['category', 'difficulty']) $achievements = Achievement::with(['category', 'difficulty'])
->orderBy('id', 'asc') ->orderBy('id', 'asc')
->get() ->get()
->groupBy(fn(Achievement $a) => $a->category->name) ->groupBy(fn(Achievement $a) => $a->category->internal_name)
->map(fn($group) => $group->sortBy('sort_order')->values()); ->sortBy(function ($group, $internalName) {
$index = array_search($internalName, AchievementCategory::ORDER);
return $index === false ? PHP_INT_MAX : $index;
})
->mapWithKeys(fn($group) => [
$group->first()->category->name => $group->sortBy('sort_order')->values(),
]);
$userAchievements = $user->achievements() $userAchievements = $user->achievements()
->with('achievement') ->with('achievement')
+10
View File
@@ -16,6 +16,16 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
*/ */
class AchievementCategory extends Model class AchievementCategory extends Model
{ {
const array ORDER = [
'number_of_flights',
'distance',
'general_flying',
'countries_and_continents',
'aircraft',
'airlines_and_alliances',
'fun_challenges',
];
protected $fillable = [ protected $fillable = [
'internal_name', 'internal_name',
'name', 'name',
@@ -47,6 +47,7 @@ class AchievementService
* @var Collection<int,UserFlight> $flights * @var Collection<int,UserFlight> $flights
*/ */
private Collection $flights; private Collection $flights;
private int $loggedFlightCount;
public function calculate(User $user): void public function calculate(User $user): void
{ {
@@ -64,20 +65,24 @@ class AchievementService
'arrivalAirport.region.continent', 'arrivalAirport.region.continent',
])->where('departure_date', '<=', now('UTC'))->get(); ])->where('departure_date', '<=', now('UTC'))->get();
$this->loggedFlightCount = $user->flights()->count();
foreach ($this->checkers as $checkerClass) { foreach ($this->checkers as $checkerClass) {
$checker = new $checkerClass($this); $checker = new $checkerClass($this);
$checker->check($user); $checker->check($user);
} }
} }
/**
* @return Collection<int,UserFlight>
*/
public function getFlights(): Collection public function getFlights(): Collection
{ {
return $this->flights; return $this->flights;
} }
public function getLoggedFlightCount(): int
{
return $this->loggedFlightCount;
}
// --------------------------------------------------------------- // ---------------------------------------------------------------
// Award / revoke // Award / revoke
@@ -15,6 +15,11 @@ abstract class BaseChecker implements AchievementCheckerInterface
return $this->service->getFlights(); return $this->service->getFlights();
} }
protected function allLoggedFlightsCount(): int
{
return $this->service->getLoggedFlightCount();
}
/** /**
* Resolve an achievement ID from its internal name. * Resolve an achievement ID from its internal name.
* Results are cached on the service so repeated lookups are free. * Results are cached on the service so repeated lookups are free.
@@ -15,10 +15,11 @@ class GeneralFlyingChecker extends BaseChecker
*/ */
$flights = $this->flights(); $flights = $this->flights();
$count = $flights->count(); $count = $flights->count();
$allLoggedFlightsCount = $this->allLoggedFlightsCount();
// --- Boolean achievements --- // --- Boolean achievements ---
$this->awardIf($count >= 1, 'general_flying.first_flight'); $this->awardIf($allLoggedFlightsCount >= 1, 'general_flying.first_flight');
$this->awardIf( $this->awardIf(
$flights->contains(fn ($f) => $f->isDomestic()), $flights->contains(fn ($f) => $f->isDomestic()),
@@ -0,0 +1,57 @@
<?php
use App\Models\Achievement;
use App\Models\AchievementCategory;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
$newCategory = AchievementCategory::create([
'internal_name' => 'number_of_flights',
'name' => 'Number of Flights',
'description' => 'Achievements for reaching flight count milestones.',
]);
$achievementsToUpdate = [
'general_flying.first_flight',
'general_flying.10_flights',
'general_flying.500_flights',
'general_flying.50_flights',
'general_flying.100_flights',
'general_flying.1000_flights',
];
foreach ($achievementsToUpdate as $internalName) {
$achievement = Achievement::where('internal_name', $internalName)->first();
if (!$achievement) {
continue; // or throw, depending how strict you want this
}
$achievement->update([
'achievement_category_id' => $newCategory->id,
'internal_name' => str_replace('general_flying.', 'number_of_flights.', $internalName),
]);
}
}
public function down(): void
{
$oldCategory = AchievementCategory::where('internal_name', 'general_flying')->first();
if ($oldCategory) {
Achievement::where('internal_name', 'like', 'number_of_flights.%')
->get()
->each(function (Achievement $achievement) use ($oldCategory) {
$achievement->update([
'achievement_category_id' => $oldCategory->id,
'internal_name' => str_replace('number_of_flights.', 'general_flying.', $achievement->internal_name),
]);
});
}
AchievementCategory::where('internal_name', 'number_of_flights')->delete();
}
};
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="flight-map-wrapper"> <div class="flight-map-wrapper" :class="{ tooltip }">
<PlaneLoader v-if="!mapReady" class="map-loader" /> <PlaneLoader v-if="!mapReady" class="map-loader" />
<div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" /> <div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
<button @click="exportMapBasic()" class="export-btn" title="Download as Image"> <button @click="exportMapBasic()" class="export-btn" title="Download as Image">
@@ -277,8 +277,11 @@ export default defineComponent({
compact: { compact: {
type: Boolean, type: Boolean,
default: false, default: false,
},
tooltip: {
type: Boolean,
default: false,
} }
}, },
setup(props) { setup(props) {
@@ -821,6 +824,11 @@ export default defineComponent({
aspect-ratio: 7 / 10; aspect-ratio: 7 / 10;
max-height: none; max-height: none;
} }
.flight-map-wrapper.tooltip {
aspect-ratio: 16 / 10;
max-height: 30vh;
}
} }
small.attribution{ small.attribution{
@@ -40,7 +40,7 @@ defineProps<{
<ToolTipRows v-if="isActive.value"> <ToolTipRows v-if="isActive.value">
<FlightMap :flights="[flight]" :showLegend="false" compact /> <FlightMap :flights="[flight]" :showLegend="false" compact tooltip />
</ToolTipRows> </ToolTipRows>
<ToolTipDivider /> <ToolTipDivider />