Added Email Capability

This commit is contained in:
2026-07-07 20:59:05 +10:00
parent c6065577af
commit 6d0ecb19c9
40 changed files with 1153 additions and 128 deletions
@@ -9,11 +9,8 @@ use Illuminate\Console\Command;
#[Signature('app:recalculate-achivements')]
#[Description('Recalculate achievements for all users')]
class RecalculateAchivements extends Command
class RecalculateAchievements extends Command
{
/**
* Execute the console command.
*/
public function handle(): void
{
$users = User::all();
+2
View File
@@ -37,6 +37,8 @@ class UpdateAlliances extends Command
Airline::whereInternalName($change['airline'])->update(['alliance' => $allianceId]);
}
Alliance::recalculateChallengeThresholds();
}
}
@@ -190,6 +190,7 @@ class UserProfileController extends Controller
$aircraftFamilies = match($achievement->internal_name){
'aircraft.all_boeing_7x7' => Aircraft::BOEING_FAMILIES,
'aircraft.all_airbus_a3xx' => Aircraft::AIRBUS_FAMILIES,
'aircraft.all_boeing_737' => Aircraft::BOEING_737_VARIANTS,
default => [],
};
+16
View File
@@ -40,6 +40,22 @@ class Aircraft extends Model
'787' => ['B788', 'B789', 'B78X'],
];
public const array BOEING_737_VARIANTS = [
'737-100' => ['B731'],
'737-200' => ['B732'],
'737-300' => ['B733'],
'737-400' => ['B734'],
'737-500' => ['B735'],
'737-600' => ['B736'],
'737-700' => ['B737', 'E737'],
'737-800' => ['B738', 'P8'],
'737-900' => ['B739'],
'737 Max 7' => ['B37M'],
'737 Max 8' => ['B38M'],
'737 Max 9' => ['B39M'],
'737 Max 10' => ['B3XM']
];
public const array AIRBUS_FAMILIES = [
'A300' => ['A30B', 'A300', 'A306'],
'A310' => ['A310', 'A312', 'A313'],
+18
View File
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Alliance extends Model
{
protected $table = 'alliances';
protected $fillable = [
@@ -18,8 +19,25 @@ class Alliance extends Model
'internal_name' => 'string',
];
const ALLIANCE_CHALLENGES = [
'star_alliance' => 'airlines_alliances.all_star_alliance',
'oneworld' => 'airlines_alliances.all_oneworld',
'vanilla_alliance' => 'airlines_alliances.all_vanilla_alliance',
'skyteam' => 'airlines_alliances.all_skyteam',
];
public function airlines(): HasMany
{
return $this->hasMany(Airline::class);
}
public static function recalculateChallengeThresholds() : void {
$alliances = Alliance::all();
foreach ($alliances as $alliance) {
$airlineCount = $alliance->airlines()->count();
$challenge = self::ALLIANCE_CHALLENGES[$alliance->internal_name];
Achievement::where('internal_name', $challenge)->update(['threshold' => $airlineCount]);
}
}
}
+26 -2
View File
@@ -13,11 +13,14 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Traits\HasAchievements;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Laravel\Sanctum\HasApiTokens;
use NotificationChannels\WebPush\HasPushSubscriptions;
use Spatie\Permission\Traits\HasRoles;
use App\Notifications\ResetPasswordNotification;
#[Fillable(['name', 'email', 'password', 'distance_unit', 'settings'])]
#[Hidden(['password', 'remember_token'])]
@@ -35,6 +38,22 @@ class User extends Authenticatable
protected $appends = ['resolved_settings'];
public function calculateAchievementsAndClearCache(): void
{
$this->clearCache();
$this->calculateAchievements();
}
public function clearCache(): void
{
Cache::forget("user_flights_{$this->id}");
$this
->followers()
->get()
->each(fn ($follower) => Cache::forget("user_following_flights_{$follower->id}"));
}
public function achievements(): HasMany
{
return $this->hasMany(UserAchievement::class);
@@ -83,6 +102,11 @@ class User extends Authenticatable
});
}
public function sendPasswordResetNotification($token): void
{
$this->notify(new ResetPasswordNotification($token));
}
public function resolveRouteBinding($value, $field = null): ?User
{
return $this->where('name', 'ilike', $value)->firstOrFail();
@@ -129,8 +153,8 @@ class User extends Authenticatable
$today = now('UTC')->toDateString();
return match ($filter) {
'departed' => $collection->filter(fn($f) => $f->departure_date <= $today)->values(),
'upcoming' => $collection->filter(fn($f) => $f->departure_date > $today)->values(),
'departed' => $collection->filter(fn($f) => Carbon::parse($f->departure_date)->toDateString() <= $today)->values(),
'upcoming' => $collection->filter(fn($f) => Carbon::parse($f->departure_date)->toDateString() > $today)->values(),
default => $collection,
};
}
@@ -0,0 +1,41 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Lang;
class ResetPasswordNotification extends Notification
{
use Queueable;
public $token;
public function __construct($token)
{
$this->token = $token;
}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable)
{
$url = url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
return (new MailMessage)
->theme('flightsgoneby')
->subject(Lang::get('Reset your password'))
->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
->action(Lang::get('Reset Password'), $url)
->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
->line(Lang::get('If you did not request a password reset, no further action is required.'));
}
}
+3 -19
View File
@@ -11,25 +11,12 @@ use Illuminate\Support\Facades\Cache;
class FlightObserver
{
protected function clearCache(UserFlight $flight): void
{
Cache::forget("user_flights_{$flight->user->id}");
$flight
->user
->followers()
->get()
->each(fn ($follower) => Cache::forget("user_following_flights_{$follower->id}"));
}
/**
* Recalculate after a flight is created.
*/
public function created(UserFlight $flight): void
{
$flight->user->calculateAchievements();
$this->clearCache($flight);
$flight->user->calculateAchievementsAndClearCache();
new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::forCreation($flight));
}
@@ -41,8 +28,7 @@ class FlightObserver
public function updated(UserFlight $flight): void
{
\Log::info('Observer fired for flight ' . $flight->id);
$flight->user->calculateAchievements();
$this->clearCache($flight);
$flight->user->calculateAchievementsAndClearCache();
}
/**
@@ -51,9 +37,7 @@ class FlightObserver
*/
public function deleted(UserFlight $flight): void
{
$flight->user->calculateAchievements();
$this->clearCache($flight);
$flight->user->calculateAchievementsAndClearCache();
if ($flight->departure_date->isFuture()) {
new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Cancelled);
}
+4
View File
@@ -13,6 +13,7 @@ use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\Mail;
class AppServiceProvider extends ServiceProvider
{
@@ -29,6 +30,9 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
if(!app()->isProduction()){
Mail::alwaysTo(config('app.test_email'));
}
Vite::prefetch(concurrency: 3);
UserFlight::observe(FlightObserver::class);
Airline::observe(AirlineObserver::class);
@@ -99,9 +99,9 @@ class AircraftChecker extends BaseChecker
$flownBoeingFamilies = collect(Aircraft::BOEING_FAMILIES)
->filter(fn($designators) =>
$flightsWithAircraft->contains(
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
)
$flightsWithAircraft->contains(
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
)
)
->count();
@@ -109,14 +109,24 @@ class AircraftChecker extends BaseChecker
// --- Airbus A3xx families ---
$flownAirbusFamilie = collect(Aircraft::AIRBUS_FAMILIES)
$flownAirbusFamilies = collect(Aircraft::AIRBUS_FAMILIES)
->filter(fn($designators) =>
$flightsWithAircraft->contains(
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
)
$flightsWithAircraft->contains(
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
)
)
->count();
$this->awardProgress($flownAirbusFamilie, 'aircraft.all_airbus_a3xx');
$this->awardProgress($flownAirbusFamilies, 'aircraft.all_airbus_a3xx');
$flown737Variants = collect(Aircraft::BOEING_737_VARIANTS)
->filter(fn($designators) =>
$flightsWithAircraft->contains(
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
)
)
->count();
$this->awardProgress($flown737Variants, 'aircraft.all_boeing_737');
}
}