Added achievement data
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AchievementController extends Controller
|
||||
{
|
||||
public function index(User $user)
|
||||
{
|
||||
$achievements = Achievement::with(['category', 'difficulty'])
|
||||
->get()
|
||||
->groupBy(fn(Achievement $a) => $a->category->name)
|
||||
->map(fn($group) => $group->sortBy('id')->values());
|
||||
|
||||
$userAchievements = $user->achievements()
|
||||
->with('achievement')
|
||||
->orderBy('achievement_id')
|
||||
->get()
|
||||
->keyBy('achievement_id');
|
||||
|
||||
return Inertia::render('UserAchievements', [
|
||||
'user' => $user,
|
||||
'canEdit' => auth()->id() === $user->id,
|
||||
'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
|
||||
'achievements' => $achievements,
|
||||
'userAchievements' => $userAchievements,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,6 @@ class FlightController extends Controller
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $flight->user_id,
|
||||
'user_flight_id' => $flight->id,
|
||||
'data' => [
|
||||
'changes' => $changes,
|
||||
'original' => $original,
|
||||
@@ -174,29 +173,14 @@ class FlightController extends Controller
|
||||
'user_id' => $newFlight->user_id,
|
||||
'type' => $newFlight->departure_date->isFuture() ? 'flight_booked' : 'flight_logged',
|
||||
'data' => [
|
||||
'flight' => $this->flightSnapshot($newFlight->id),
|
||||
'flight' => $newFlight->snapshot($newFlight->id),
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $newFlight->id]);
|
||||
}
|
||||
|
||||
private function flightSnapshot(int $id): array
|
||||
{
|
||||
return UserFlight::with([
|
||||
'departureAirport',
|
||||
'departureAirport.region.country',
|
||||
'arrivalAirport',
|
||||
'arrivalAirport.region.country',
|
||||
'aircraft',
|
||||
'airline',
|
||||
'airline.country',
|
||||
'flightClass',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
'crewType',
|
||||
])->find($id)->toArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function update(Request $request, UserFlight $flight)
|
||||
@@ -212,11 +196,11 @@ class FlightController extends Controller
|
||||
}
|
||||
|
||||
$dirty = $flight->getDirty();
|
||||
$original = $this->flightSnapshot($flight->id);
|
||||
$original = $flight->snapshot($flight->id);
|
||||
|
||||
$flight->save();
|
||||
|
||||
$updated = $this->flightSnapshot($flight->id);
|
||||
$updated = $flight->snapshot($flight->id);
|
||||
$this->recordChanges($flight, $dirty, $original, $updated);
|
||||
|
||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\FlightClass;
|
||||
use App\Models\FlightReason;
|
||||
use App\Models\ImportedFlight;
|
||||
use App\Models\SeatType;
|
||||
use App\Models\UserAction;
|
||||
use App\Models\UserFlight;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -128,10 +129,11 @@ class FlightImportController extends Controller
|
||||
})
|
||||
->orderByDesc('active')
|
||||
->limit(10)
|
||||
->get(['id', 'name', 'IATA_code', 'ICAO_code'])
|
||||
->get(['id', 'name', 'IATA_code', 'ICAO_code', 'internal_name'])
|
||||
->map(fn($airline) => [
|
||||
'value' => $airline->id,
|
||||
'title' => $airline->display_name,
|
||||
'logo_url' => $airline->logo_url,
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
@@ -243,7 +245,7 @@ class FlightImportController extends Controller
|
||||
$arrival = $durationArrival;
|
||||
}
|
||||
|
||||
UserFlight::create([
|
||||
$newFlight = UserFlight::create([
|
||||
'user_id' => $user->id,
|
||||
'departure_date' => $departure,
|
||||
'arrival_date' => $arrival,
|
||||
@@ -260,6 +262,14 @@ class FlightImportController extends Controller
|
||||
'note' => $validated['note'] ?? null,
|
||||
]);
|
||||
|
||||
UserAction::create([
|
||||
'user_id' => $newFlight->user_id,
|
||||
'type' => $newFlight->departure_date->isFuture() ? 'flight_booked' : 'flight_imported',
|
||||
'data' => [
|
||||
'flight' => $newFlight->snapshot($newFlight->id),
|
||||
],
|
||||
]);
|
||||
|
||||
ImportedFlight::destroy($validated['imported_flight_id']);
|
||||
return to_route('reconcile');
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@ use Inertia\Inertia;
|
||||
class FlightProfileController extends Controller
|
||||
{
|
||||
public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array {
|
||||
$flights = $user->FlightController()->flights();
|
||||
return [
|
||||
'user' => $user,
|
||||
'canEdit' => auth()->check() && auth()->id() === $user->id,
|
||||
'flights' => UserFlightResource::collection($flights)->resolve(),
|
||||
'initialView' => $view,
|
||||
'selectedFlightId' => $selectedFlightId,
|
||||
'flight_api_url' => '/data/user/'.$user->name.'/flights',
|
||||
'isFollowing' => auth()->check() && auth()->user()->isFollowing($user),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Notification;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
public function markRead(Request $request, Notification $notification)
|
||||
{
|
||||
$this->authorize('update', $notification);
|
||||
$notification->markAsRead();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class UserFlightController extends Controller
|
||||
'arrivalAirport.region.country',
|
||||
'arrivalAirport.region.continent',
|
||||
'airline.country',
|
||||
'airline.alliance',
|
||||
'aircraft',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
|
||||
@@ -34,8 +34,16 @@ class HandleInertiaRequests extends Middleware
|
||||
'logo_api_url' => config('app.logo_api_url'),
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'isLoggedIn' => $request->user() !== null,
|
||||
],
|
||||
'achievement_notifications' => fn() => $request->user()
|
||||
? $request->user()
|
||||
->notifications()
|
||||
->with('achievement')
|
||||
->where('is_achievement', true)
|
||||
->whereNull('read_at')
|
||||
->latest()
|
||||
->get()
|
||||
: [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -2,18 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Http\Controllers\UserFlightController;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
use App\Traits\HasAchievements;
|
||||
use App\Models\Notification;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
@@ -21,7 +19,9 @@ class User extends Authenticatable
|
||||
{
|
||||
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, HasAchievements;
|
||||
use HasFactory, HasAchievements, HasApiTokens;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
@@ -36,6 +36,11 @@ class User extends Authenticatable
|
||||
];
|
||||
}
|
||||
|
||||
public function achievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserAchievement::class);
|
||||
}
|
||||
|
||||
public function resolveRouteBinding($value, $field = null): ?User
|
||||
{
|
||||
return $this->where('name', 'ilike', $value)->firstOrFail();
|
||||
@@ -69,4 +74,9 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->following()->where('followee_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function notifications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Notification::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class UserAction extends Model
|
||||
'flight_booked' => 'Flight Booked',
|
||||
'flight_cancelled' => 'Flight Cancelled',
|
||||
'flight_updated' => 'Flight Updated',
|
||||
'flight_imported' => 'Flight Imported',
|
||||
'flight_imported' => 'Flight Imported from FR24',
|
||||
'flight_logged' => 'Flight Logged',
|
||||
'flight_deleted' => 'Flight Deleted',
|
||||
default => 'Unknown Action'
|
||||
|
||||
@@ -156,6 +156,23 @@ class UserFlight extends Model
|
||||
return !$this->isDomestic();
|
||||
}
|
||||
|
||||
public static function snapshot($userFlightId){
|
||||
return UserFlight::with([
|
||||
'departureAirport',
|
||||
'departureAirport.region.country',
|
||||
'arrivalAirport',
|
||||
'arrivalAirport.region.country',
|
||||
'aircraft',
|
||||
'airline',
|
||||
'airline.country',
|
||||
'airline.alliance',
|
||||
'flightClass',
|
||||
'seatType',
|
||||
'flightReason',
|
||||
'crewType',
|
||||
])->find($userFlightId)->toArray();
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class NotificationPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Notification $notification): bool
|
||||
{
|
||||
return $user->id === $notification->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,8 @@ class AchievementService
|
||||
'airline.alliance',
|
||||
'aircraft',
|
||||
'flightClass',
|
||||
'departureAirport.region',
|
||||
'arrivalAirport.region',
|
||||
'departureAirport.region.continent',
|
||||
'arrivalAirport.region.continent',
|
||||
])->get();
|
||||
@@ -151,8 +153,8 @@ class AchievementService
|
||||
{
|
||||
Notification::create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Achievement Unlocked!',
|
||||
'body' => "You've earned the \"{$achievement->name}\" achievement — {$achievement->short_description}",
|
||||
'title' => $achievement->name,
|
||||
'body' => $achievement->short_description,
|
||||
'is_achievement' => true,
|
||||
'achievement_id' => $achievement->id,
|
||||
'expires_at' => null,
|
||||
|
||||
@@ -5,9 +5,14 @@ namespace App\Services\Achievements\Checkers;
|
||||
use App\Models\Alliance;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AirlinesAndAlliancesChecker extends BaseChecker
|
||||
{
|
||||
const array US_3 = ['american-airlines', 'delta', 'united-airlines'];
|
||||
const array ME_3 = ['emirates', 'etihad-airways', 'qatar-airways'];
|
||||
const array CN_3 = ['china-southern-airlines', 'china-eastern', 'air-china'];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
@@ -25,5 +30,16 @@ class AirlinesAndAlliancesChecker extends BaseChecker
|
||||
$this->awardProgress($check('oneworld'), 'airlines_alliances.all_oneworld');
|
||||
$this->awardProgress($check('star_alliance'), 'airlines_alliances.all_star_alliance');
|
||||
$this->awardProgress($check('vanilla_alliance'), 'airlines_alliances.all_vanilla_alliance');
|
||||
|
||||
$flownAirlines = $flights
|
||||
->pluck('airline.internal_name')
|
||||
->filter()
|
||||
->unique();
|
||||
|
||||
$checkGroup = fn(array $group): int => $flownAirlines->intersect($group)->count();
|
||||
|
||||
$this->awardProgress($checkGroup(self::ME_3), 'airlines_alliances.me3');
|
||||
$this->awardProgress($checkGroup(self::US_3), 'airlines_alliances.us3');
|
||||
$this->awardProgress($checkGroup(self::CN_3), 'airlines_alliances.cn3');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,60 @@
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
|
||||
class FunChallengesChecker extends BaseChecker
|
||||
{
|
||||
const array US_STATES = [
|
||||
'US-AK', 'US-AL', 'US-AR', 'US-AZ', 'US-CA', 'US-CO', 'US-CT', 'US-DC',
|
||||
'US-DE', 'US-FL', 'US-GA', 'US-HI', 'US-IA', 'US-ID', 'US-IL', 'US-IN',
|
||||
'US-KS', 'US-KY', 'US-LA', 'US-MA', 'US-MD', 'US-ME', 'US-MI', 'US-MN',
|
||||
'US-MO', 'US-MS', 'US-MT', 'US-NC', 'US-ND', 'US-NE', 'US-NH', 'US-NJ',
|
||||
'US-NM', 'US-NV', 'US-NY', 'US-OH', 'US-OK', 'US-OR', 'US-PA', 'US-RI',
|
||||
'US-SC', 'US-SD', 'US-TN', 'US-TX', 'US-UT', 'US-VA', 'US-VT', 'US-WA',
|
||||
'US-WI', 'US-WV', 'US-WY',
|
||||
];
|
||||
|
||||
const array AUSTRALIAN_STATES = [
|
||||
'AU-ACT', 'AU-NSW', 'AU-NT', 'AU-QLD', 'AU-SA', 'AU-TAS', 'AU-VIC', 'AU-WA',
|
||||
];
|
||||
|
||||
const array CHINESE_PROVINCES = [
|
||||
'CN-11', 'CN-12', 'CN-13', 'CN-14', 'CN-15', 'CN-21', 'CN-22', 'CN-23',
|
||||
'CN-31', 'CN-32', 'CN-33', 'CN-34', 'CN-35', 'CN-36', 'CN-37', 'CN-41',
|
||||
'CN-42', 'CN-43', 'CN-44', 'CN-45', 'CN-46', 'CN-50', 'CN-51', 'CN-52',
|
||||
'CN-53', 'CN-54', 'CN-61', 'CN-62', 'CN-63', 'CN-64', 'CN-65',
|
||||
|
||||
];
|
||||
|
||||
const array BRAZILIAN_STATES = [
|
||||
'BR-AC', 'BR-AL', 'BR-AM', 'BR-AP', 'BR-BA', 'BR-CE', 'BR-DF', 'BR-ES',
|
||||
'BR-GO', 'BR-MA', 'BR-MG', 'BR-MS', 'BR-MT', 'BR-PA', 'BR-PB', 'BR-PE',
|
||||
'BR-PI', 'BR-PR', 'BR-RJ', 'BR-RN', 'BR-RO', 'BR-RR', 'BR-RS', 'BR-SC',
|
||||
'BR-SE', 'BR-SP', 'BR-TO',
|
||||
];
|
||||
|
||||
const array CANADIAN_PROVINCES = [
|
||||
'CA-AB', 'CA-BC', 'CA-MB', 'CA-NB', 'CA-NL', 'CA-NS', 'CA-NT',
|
||||
'CA-NU', 'CA-ON', 'CA-PE', 'CA-QC', 'CA-SK', 'CA-YT',
|
||||
];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
|
||||
$airlineLetters = $flights
|
||||
->filter(fn($f) => $f->airline?->IATA_code !== null)
|
||||
->map(fn($f) => strtoupper($f->airline->IATA_code[0]))
|
||||
->filter(fn(UserFlight $f) => $f->airline?->IATA_code !== null)
|
||||
->map(fn(UserFlight $f) => strtoupper($f->airline->IATA_code[0]))
|
||||
->filter(fn($letter) => ctype_alpha($letter))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($airlineLetters, 'fun_challenges.airline_alphabet');
|
||||
|
||||
// --- Visit the Alphabet ---
|
||||
// Collect first letters from both departure and arrival airport IATA codes
|
||||
|
||||
$airportLetters = $flights
|
||||
->flatMap(fn($f) => [
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->iata_code,
|
||||
$f->arrivalAirport?->iata_code,
|
||||
])
|
||||
@@ -33,5 +66,70 @@ class FunChallengesChecker extends BaseChecker
|
||||
->count();
|
||||
|
||||
$this->awardProgress($airportLetters, 'fun_challenges.airport_alphabet');
|
||||
|
||||
// --- US States ---
|
||||
$visitedUsStates = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::US_STATES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedUsStates, 'fun_challenges.us_states');
|
||||
|
||||
// --- Australian States ---
|
||||
$visitedAustralianStates = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::AUSTRALIAN_STATES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedAustralianStates, 'fun_challenges.australian_states');
|
||||
|
||||
// --- Chinese Provinces ---
|
||||
$visitedChineseProvinces = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::CHINESE_PROVINCES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedChineseProvinces, 'fun_challenges.chinese_provinces');
|
||||
|
||||
// --- Brazilian States ---
|
||||
$visitedBrazilianStates = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::BRAZILIAN_STATES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedBrazilianStates, 'fun_challenges.brazilian_states');
|
||||
|
||||
// --- Canadian Provinces ---
|
||||
$visitedCanadianProvinces = $flights
|
||||
->flatMap(fn(UserFlight $f) => [
|
||||
$f->departureAirport?->region?->code,
|
||||
$f->arrivalAirport?->region?->code,
|
||||
])
|
||||
->filter()
|
||||
->filter(fn($code) => in_array($code, self::CANADIAN_PROVINCES))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedCanadianProvinces, 'fun_challenges.canadian_provinces');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class GeneralFlyingChecker extends BaseChecker
|
||||
// --- Progressive achievements ---
|
||||
|
||||
$this->awardProgress($count,'general_flying.10_flights');
|
||||
$this->awardProgress($count,'general_flying.50_flights');
|
||||
$this->awardProgress($count,'general_flying.100_flights');
|
||||
$this->awardProgress($count,'general_flying.500_flights');
|
||||
$this->awardProgress($count,'general_flying.1000_flights');
|
||||
|
||||
Reference in New Issue
Block a user