diff --git a/app/Console/Commands/GetAircraftInfoForArrivedFlights.php b/app/Console/Commands/GetAircraftInfoForArrivedFlights.php new file mode 100644 index 0000000..1f6a390 --- /dev/null +++ b/app/Console/Commands/GetAircraftInfoForArrivedFlights.php @@ -0,0 +1,217 @@ +utc(); + + $userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString()) + ->where('auto_update', true) + ->whereNotNull('flight_number') + ->get(); + + $this->info("Found {$userFlights->count()} flights."); + + foreach ($userFlights as $flight) { + $this->processFlight($flight); + } + } + + protected function processFlight(UserFlight $flight): void + { + preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches); + + // Case 1: unparseable flight number on our end — disable and move on, no notification. + if (empty($matches)) { + $this->warn("Could not parse flight number: {$flight->flight_number}"); + Log::warning("Could not parse flight number for auto-update", ['flight_id' => $flight->id, 'flight_number' => $flight->flight_number]); + $flight->update(['auto_update' => false]); + return; + } + + $airlineCode = strtoupper($matches[1]); + $flightNumber = $matches[2]; + $arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone); + + $data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate); + + // Case 2: valid flight number, but the API had nothing for it. + if (!$data) { + $this->warn("No flight data returned for {$airlineCode}{$flightNumber}"); + $this->notifyLookupFailed($flight, $airlineCode, $flightNumber); + return; + } + + // Case 3: API returned data, but for the wrong route — treat like a failed lookup. + if ($data->departure_iata !== $flight->departureAirport->iata_code || + $data->arrival_iata !== $flight->arrivalAirport->iata_code) { + $this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}→{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}→{$flight->arrivalAirport->iata_code}"); + $this->notifyLookupFailed($flight, $airlineCode, $flightNumber); + return; + } + + // Case 4/5: valid data — apply whatever changed, but always report every field. + $this->applyAndNotify($flight, $data, $airlineCode, $flightNumber); + } + + protected function notifyLookupFailed(UserFlight $flight, string $airlineCode, string $flightNumber): void + { + Notification::create([ + 'user_id' => $flight->user_id, + 'title' => "Auto update failed for {$flight->flight_number}", + 'body' => "We tried to find up-to-date information for {$airlineCode}{$flightNumber}, but couldn't find a reliable match. Please manually check the aircraft type, registration and departure/arrival times.", + 'url' => '/flights/' . $flight->id . '/edit', + ]); + + $flight->update(['auto_update' => false]); + } + + protected function applyAndNotify(UserFlight $flight, FlightStatData $data, string $airlineCode, string $flightNumber): void + { + // Snapshot originals before we touch anything, so descriptions reflect the real "from" state. + $originalRegistration = $flight->aircraft_registration; + $originalDeparture = $flight->departure_date; + $originalArrival = $flight->arrival_date; + $originalAircraft = $flight->aircraft; + + $updates = []; + $aircraftNotFound = false; + + if ($data->aircraft_registration && $data->aircraft_registration !== $originalRegistration) { + $updates['aircraft_registration'] = $data->aircraft_registration; + } + + if ($data->estimated_departure_utc?->ne($originalDeparture)) { + $updates['departure_date'] = $data->estimated_departure_utc; + } + + if ($data->estimated_arrival_utc?->ne($originalArrival)) { + $updates['arrival_date'] = $data->estimated_arrival_utc; + } + + if ($data->equipment_iata && $originalAircraft?->iata_code !== $data->equipment_iata) { + $match = $this->flightStats->guessAircraftFromIata($data->equipment_iata); + + if ($match) { + $updates['aircraft_id'] = $match->id; + } else { + $aircraftNotFound = true; + Log::warning("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}", [ + 'flight_id' => $flight->id, + ]); + } + } + + if (!empty($updates)) { + $flight->update($updates); + $this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates))); + } else { + $this->info("No changes for {$airlineCode}{$flightNumber}"); + } + + $flight->update(['auto_update' => false]); + + $newAircraft = isset($updates['aircraft_id']) ? Aircraft::find($updates['aircraft_id']) : $originalAircraft; + + $lines = [ + $this->describeValue( + 'Aircraft Registration', + $originalRegistration, + $data->aircraft_registration, + $updates['aircraft_registration'] ?? null, + ), + $this->describeDate('Departure', $originalDeparture, $data->estimated_departure_utc, $updates['departure_date'] ?? null), + $this->describeDate('Arrival', $originalArrival, $data->estimated_arrival_utc, $updates['arrival_date'] ?? null), + $this->describeAircraftType($originalAircraft, $newAircraft, $data->equipment_iata, $aircraftNotFound), + ]; + + $title = $aircraftNotFound + ? "Flight {$airlineCode}{$flightNumber} updated — aircraft type not found" + : "Flight {$airlineCode}{$flightNumber} updated"; + + Notification::create([ + 'user_id' => $flight->user_id, + 'title' => $title, + 'body' => implode("\n", $lines), + 'url' => '/u/' . $flight->user->name . '/flight/' . $flight->id, + ]); + } + + /** + * Describe a simple string field: from → to, or "no change" / "not found" as appropriate. + */ + protected function describeValue(string $label, ?string $from, ?string $apiValue, ?string $applied): string + { + $fromLabel = $from ?: 'None'; + + if (!$apiValue) { + return "{$label}: {$fromLabel} (not returned by API — no change)"; + } + + if (!$applied) { + return "{$label}: {$apiValue} (no change)"; + } + + return "{$label}: {$fromLabel} → {$apiValue}"; + } + + protected function describeDate(string $label, ?CarbonInterface $from, ?CarbonInterface $apiValue, mixed $applied): string + { + $fromLabel = $from?->format('j M Y H:i') . ' ' . ($from?->tzName ?? '') ?: 'Unknown'; + + if (!$apiValue) { + return "{$label}: {$fromLabel} (not returned by API — no change)"; + } + + $toLabel = $apiValue->format('j M Y H:i') . ' ' . $apiValue->tzName; + + if (!$applied) { + return "{$label}: {$toLabel} (no change)"; + } + + return "{$label}: {$fromLabel} → {$toLabel}"; + } + + protected function describeAircraftType(?Aircraft $from, ?Aircraft $to, ?string $equipmentIata, bool $notFound): string + { + $fromLabel = $from?->display_name_short ?? 'None'; + + if (!$equipmentIata) { + return "Aircraft Type: {$fromLabel} (not returned by API — no change)"; + } + + if ($notFound) { + return "Aircraft Type: {$fromLabel} (API reported equipment code {$equipmentIata}, but no matching aircraft was found in our database — no change)"; + } + + if ($to?->id === $from?->id) { + return "Aircraft Type: {$fromLabel} (no change)"; + } + + return "Aircraft Type: {$fromLabel} → {$to?->display_name_short}"; + } +} diff --git a/app/Console/Commands/UpdateArrivedFlights.php b/app/Console/Commands/UpdateArrivedFlights.php index 9a73a45..b0c6219 100644 --- a/app/Console/Commands/UpdateArrivedFlights.php +++ b/app/Console/Commands/UpdateArrivedFlights.php @@ -2,151 +2,32 @@ namespace App\Console\Commands; -use App\DTOs\FlightStatData; -use App\Models\Aircraft; -use App\Models\IataEquipmentCode; -use App\Models\Notification; +use App\Enums\FlightNotificationType; use App\Models\UserFlight; -use App\Services\FlightStatsService; -use Carbon\Carbon; -use Carbon\CarbonImmutable; +use App\Services\FollowerNotificationService; use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; #[Signature('app:update-arrived-flights')] -#[Description('Command description')] +#[Description('When a flight arrives, update the arrival_processed_at timestamp and notify followers')] class UpdateArrivedFlights extends Command { - - public function __construct(protected FlightStatsService $flightStats) - { - parent::__construct(); - } - - protected function notifyDataError(UserFlight $flight): void - { - Notification::create([ - 'user_id' => $flight->user_id, - 'title' => "Auto update failed for {$flight->flight_number}", - 'body' => "There was an error fetching flight data for {$flight->flight_number}. Please manually check the aircraft type, registration and departure/arrival times.", - 'url' => '/flights/' . $flight->id . '/edit' - ]); - - $flight->update(['auto_update' => false]); - } - - /** - * Execute the console command. - */ public function handle(): void { $now = now()->utc(); - $userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString()) - ->where('auto_update', true) - ->whereNotNull('flight_number') + $userFlights = UserFlight::where('arrival_date', '<=', $now->toDateTimeString()) + ->where(function ($query) { + $query->whereNull('arrival_processed_at') + ->orWhereColumn('arrival_processed_at', '<', 'arrival_date'); + }) ->get(); - $this->info("Found {$userFlights->count()} flights."); - + echo $userFlights->count() . " flights to update\n"; foreach ($userFlights as $flight) { - preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches); - - if (empty($matches)) { - $this->warn("Could not parse flight number: {$flight->flight_number}"); - $this->notifyDataError($flight); - continue; - } - - $airlineCode = strtoupper($matches[1]); - $flightNumber = $matches[2]; - - $arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone); - - $data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate); - - if (!$data) { - $this->warn("No flight data returned for {$airlineCode}{$flightNumber}"); - $this->notifyDataError($flight); - continue; - } - - if ($data->departure_iata !== $flight->departureAirport->iata_code || - $data->arrival_iata !== $flight->arrivalAirport->iata_code) { - $this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}→{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}→{$flight->arrivalAirport->iata_code}"); - $this->notifyDataError($flight); - - continue; - } - - $updates = []; - - if ($data->aircraft_registration && $data->aircraft_registration !== $flight->aircraft_registration) { - $updates['aircraft_registration'] = $data->aircraft_registration; - } - - if ($data->estimated_departure_utc?->ne($flight->departure_date)) { - $updates['departure_date'] = $data->estimated_departure_utc; - } - - if ($data->estimated_arrival_utc?->ne($flight->arrival_date)) { - $updates['arrival_date'] = $data->estimated_arrival_utc; - } - - if ($data->equipment_iata) { - $currentAircraft = $flight->aircraft; - - if ($currentAircraft?->iata_code !== $data->equipment_iata) { - $match = $this->flightStats->guessAircraftFromIata($data->equipment_iata); - - if ($match) { - $updates['aircraft_id'] = $match->id; - } else { - Log::info("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}"); - } - } - } - - if (!empty($updates)) { - $flight->update($updates); - $this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates))); - - $changeDescriptions = []; - if (isset($updates['aircraft_registration'])) { - $changeDescriptions[] = "Registration updated to {$updates['aircraft_registration']}"; - } - if (isset($updates['departure_date'])) { - $changeDescriptions[] = "Departure updated to {$updates['departure_date']}"; - } - if (isset($updates['arrival_date'])) { - $changeDescriptions[] = "Arrival updated to {$updates['arrival_date']}"; - } - if (isset($updates['aircraft_id'])) { - $aircraft = Aircraft::find($updates['aircraft_id']); - $changeDescriptions[] = "Aircraft type updated to {$aircraft->display_name_short}"; - } - - Notification::create([ - 'user_id' => $flight->user_id, - 'title' => "Flight {$airlineCode}{$flightNumber} updated", - 'body' => implode("\n", $changeDescriptions), - 'url' => '/u/'. $flight->user->name . '/flight/'. $flight->id, - ]); - } else { - $this->info("No changes for {$airlineCode}{$flightNumber}"); - - Notification::create([ - 'user_id' => $flight->user_id, - 'title' => "Flight {$airlineCode}{$flightNumber} updated — no changes", - 'body' => "Your flight was completed and no updates were made to aircraft, registration, or departure/arrival times.", - 'url' => '/u/'. $flight->user->name . '/flight/'. $flight->id, - ]); - } - - $flight->update(['auto_update' => false]); + $flight->update(['arrival_processed_at' => $now]); + new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Arrived); } } } diff --git a/app/Console/Commands/UpdateDepartedFlights.php b/app/Console/Commands/UpdateDepartedFlights.php index 4ff323d..b0ffa15 100644 --- a/app/Console/Commands/UpdateDepartedFlights.php +++ b/app/Console/Commands/UpdateDepartedFlights.php @@ -2,22 +2,18 @@ namespace App\Console\Commands; -use App\Models\Aircraft; -use App\Models\Notification; -use App\Models\User; +use App\Enums\FlightNotificationType; use App\Models\UserFlight; +use App\Services\FollowerNotificationService; use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Log; #[Signature('app:update-departed-flights')] #[Description('Command description')] class UpdateDepartedFlights extends Command { - /** - * Execute the console command. - */ + public function handle(): void { $now = now()->utc(); @@ -32,33 +28,7 @@ class UpdateDepartedFlights extends Command foreach ($userFlights as $flight) { $flight->update(['departure_processed_at' => $now]); - $this->notifyFollowersOnDeparture($flight); + new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Departed); } } - - protected function notifyFollowersOnDeparture(UserFlight $flight): void - { - $user = $flight->user; - $title = "{$user->name} is taking off!"; - $flightNumber = $flight->flight_number ? "On {$flight->flight_number} from" : 'From'; - $body = "{$flightNumber} {$flight->departureAirport->municipality} → {$flight->arrivalAirport->municipality}"; - $url = route('profile.flight', [ - 'user' => $user->id, - 'userFlight' => $flight->id, - ]); - - $user->followers()->get()->each(function (User $follower) use ($title, $body, $url) { - $notify = $follower->getSetting('notify_on_flight_departure'); - - if ($notify) { - Notification::create([ - 'user_id' => $follower->id, - 'title' => $title, - 'body' => $body, - 'url' => $url, - ]); - } - - }); - } } diff --git a/app/Enums/FlightNotificationType.php b/app/Enums/FlightNotificationType.php new file mode 100644 index 0000000..ffe21d5 --- /dev/null +++ b/app/Enums/FlightNotificationType.php @@ -0,0 +1,63 @@ + 'notify_on_upcoming_flight_added', + self::Logged => 'notify_on_historic_flight_added', + self::Cancelled => 'notify_on_flight_cancellation', + self::Departed => 'notify_on_flight_departure', + self::Arrived => 'notify_on_flight_arrival', + }; + } + + public function title(User $user): string + { + return match ($this) { + self::Booked => "{$user->name} booked a new flight", + self::Logged => "{$user->name} logged a new flight", + self::Cancelled => "{$user->name} cancelled a flight", + self::Departed => "{$user->name} is taking off!", + self::Arrived => "{$user->name} has landed!", + }; + } + + public function body(UserFlight $flight): string + { + $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From'; + + return match ($this) { + self::Departed => "{$flightNumber} {$flight->departureAirport->municipality} → {$flight->arrivalAirport->municipality}", + self::Arrived => "In {$flight->arrivalAirport->municipality}", + default => "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → " + . "{$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}", + }; + } + + public function url(UserFlight $flight): string + { + return match ($this) { + self::Cancelled => route('profile.view', [$flight->user->name]), + self::Departed, self::Arrived => route('profile.flight', ['user' => $flight->user->id, 'userFlight' => $flight->id]), + default => route('profile.departure-board', [$flight->user->name, $flight->id]), + }; + } + + public static function forCreation(UserFlight $flight): self + { + return $flight->departure_date->isFuture() ? self::Booked : self::Logged; + } +} diff --git a/app/Http/Controllers/Api/UserApiController.php b/app/Http/Controllers/Api/UserApiController.php index 4f7dfa4..8a20106 100644 --- a/app/Http/Controllers/Api/UserApiController.php +++ b/app/Http/Controllers/Api/UserApiController.php @@ -3,12 +3,10 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\ApiController; -use App\Http\Controllers\UserFlightController; use App\Models\User; use App\Models\UserFlight; use Carbon\Carbon; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; class UserApiController extends ApiController diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index d0d4a77..6bcf171 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -232,8 +232,6 @@ class FlightController extends Controller ]; } - - public function store(Request $request) { $validated = $request->validate($this->rules()); @@ -248,42 +246,10 @@ class FlightController extends Controller ], ]); - $this->notifyFollowersOnCreation($newFlight); return redirect()->route('profile.departure-board', [Auth::user()->name, $newFlight->id]); } - protected function notifyFollowersOnCreation(UserFlight $flight): void - { - $user = $flight->user; - $isFuture = $flight->departure_date->isFuture(); - - $title = $isFuture - ? "{$user->name} booked a new flight" - : "{$user->name} logged a new flight"; - - - $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From'; - $body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}"; - $url = route('profile.departure-board', [$user->name, $flight->id]); - - $user->followers()->get()->each(function (User $follower) use ($title, $body, $url, $isFuture) { - $notifyForFuture = $follower->getSetting('notify_on_upcoming_flight_added'); - $notifyForHistoric = $follower->getSetting('notify_on_historic_flight_added'); - - if (($isFuture && $notifyForFuture) || (!$isFuture && $notifyForHistoric)) { - Notification::create([ - 'user_id' => $follower->id, - 'title' => $title, - 'body' => $body, - 'url' => $url, - ]); - } - - } - ); - } - public function update(Request $request, UserFlight $flight) { @@ -323,36 +289,11 @@ class FlightController extends Controller ], ]); - if ($isFuture) { - $this->notifyFollowersOnCancellation($flight); - } - $flight->delete(); return redirect()->route('profile.'.$referrer, [Auth::user()->name]); } - protected function notifyFollowersOnCancellation(UserFlight $flight): void - { - $user = $flight->user; - - $title = "{$user->name} cancelled a flight"; - $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From'; - $body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}"; - $url = route('profile.view', [$user->name]); - - $user->followers()->get()->each(function (User $follower) use ($title, $body, $url) { - if ($follower->getSetting('notify_on_flight_cancellation')) { - Notification::create([ - 'user_id' => $follower->id, - 'title' => $title, - 'body' => $body, - 'url' => $url, - ]); - } - }); - } - public function staticData() : array { return [ 'seat_types' => SeatType::orderBy('id')->get()->toArray(), diff --git a/app/Http/Controllers/FlightImportController.php b/app/Http/Controllers/FlightImportController.php index ab9f0fc..1db5cac 100644 --- a/app/Http/Controllers/FlightImportController.php +++ b/app/Http/Controllers/FlightImportController.php @@ -271,44 +271,11 @@ class FlightImportController extends Controller ], ]); - $this->notifyFollowersOnCreation($newFlight); ImportedFlight::destroy($validated['imported_flight_id']); return to_route('reconcile'); } - protected function notifyFollowersOnCreation(UserFlight $flight): void - { - $user = $flight->user; - $isFuture = $flight->departure_date->isFuture(); - - $title = $isFuture - ? "{$user->name} booked a new flight" - : "{$user->name} logged a new flight"; - - - $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From'; - $body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}"; - $url = route('profile.departure-board', [$user->name, $flight->id]); - - $user->followers()->get()->each(function (User $follower) use ($title, $body, $url, $isFuture) { - $notifyForFuture = $follower->getSetting('notify_on_upcoming_flight_added'); - $notifyForHistoric = $follower->getSetting('notify_on_historic_flight_added'); - - if (($isFuture && $notifyForFuture) || (!$isFuture && $notifyForHistoric)) { - Notification::create([ - 'user_id' => $follower->id, - 'title' => $title, - 'body' => $body, - 'url' => $url, - ]); - } - - } - ); - } - - private function validateCsvFormat(string $path): ?string { $handle = fopen($path, 'r'); diff --git a/app/Http/Controllers/FollowerController.php b/app/Http/Controllers/FollowerController.php index 9d063cf..3910524 100644 --- a/app/Http/Controllers/FollowerController.php +++ b/app/Http/Controllers/FollowerController.php @@ -67,7 +67,7 @@ class FollowerController extends Controller ? auth()->user()->name . ' is now following you.' : auth()->user()->name . ' wants to follow you.', 'is_achievement' => false, - 'url' => $canView ? '/u/' . auth()->user()->name : '/follow-requests', + 'url' => $canView ? '/u/' . auth()->user()->name : '/settings/followers', ]); } diff --git a/app/Models/Guest.php b/app/Models/Guest.php deleted file mode 100644 index 00afd48..0000000 --- a/app/Models/Guest.php +++ /dev/null @@ -1,11 +0,0 @@ - 'immutable_datetime', 'auto_update' => 'boolean', 'departure_processed_at' => 'datetime', + 'arrival_processed_at' => 'datetime', ]; protected $appends = [ diff --git a/app/Observers/FlightObserver.php b/app/Observers/FlightObserver.php index ddfed4b..b107eb2 100644 --- a/app/Observers/FlightObserver.php +++ b/app/Observers/FlightObserver.php @@ -2,12 +2,15 @@ namespace App\Observers; +use App\Enums\FlightNotificationType; use App\Models\User; use App\Models\UserFlight; +use App\Services\FollowerNotificationService; use Illuminate\Support\Facades\Cache; class FlightObserver { + protected function clearCache(UserFlight $flight): void { Cache::forget("user_flights_{$flight->user->id}"); @@ -27,6 +30,7 @@ class FlightObserver { $flight->user->calculateAchievements(); $this->clearCache($flight); + new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::forCreation($flight)); } /** @@ -49,5 +53,9 @@ class FlightObserver { $flight->user->calculateAchievements(); $this->clearCache($flight); + + if ($flight->departure_date->isFuture()) { + new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Cancelled); + } } } diff --git a/app/Services/FollowerNotificationService.php b/app/Services/FollowerNotificationService.php new file mode 100644 index 0000000..adbea34 --- /dev/null +++ b/app/Services/FollowerNotificationService.php @@ -0,0 +1,42 @@ +notify( + user: $flight->user, + settingKey: $type->settingKey(), + title: $type->title($flight->user), + body: $type->body($flight), + url: $type->url($flight), + ); + } + + /** + * Generic entry point for any future "notify all followers about X" need + * that isn't tied to a flight event. + */ + public function notify(User $user, string $settingKey, string $title, string $body, string $url): void + { + $user->followers() + ->get() + ->filter(fn (User $follower) => $follower->getSetting($settingKey)) + ->each(fn (User $follower) => Notification::create([ + 'user_id' => $follower->id, + 'title' => $title, + 'body' => $body, + 'url' => $url, + ])); + } +} diff --git a/app/Settings/SettingsRegistry.php b/app/Settings/SettingsRegistry.php index 7367795..fd995d3 100644 --- a/app/Settings/SettingsRegistry.php +++ b/app/Settings/SettingsRegistry.php @@ -145,6 +145,13 @@ class SettingsRegistry 'label' => 'Notify Me When Someone I Follow Logs an Upcoming Flight', 'default' => true, ], + [ + 'category' => 'Notifications', + 'key' => 'notify_on_flight_cancellation', + 'type' => 'checkbox', + 'label' => 'Notify Me When Someone I Follow Cancels an Upcoming Flight', + 'default' => false, + ], [ 'category' => 'Notifications', 'key' => 'notify_on_flight_departure', @@ -152,19 +159,12 @@ class SettingsRegistry 'label' => 'Notify Me When Someone I Follow Departs', 'default' => true, ], -/* [ + [ 'category' => 'Notifications', 'key' => 'notify_on_flight_arrival', 'type' => 'checkbox', 'label' => 'Notify Me When Someone I Follow Arrives', 'default' => true, - ],*/ - [ - 'category' => 'Notifications', - 'key' => 'notify_on_flight_cancellation', - 'type' => 'checkbox', - 'label' => 'Notify Me When Someone I Follow Cancels an Upcoming Flight', - 'default' => false, ], ]; } diff --git a/database/migrations/2026_07_02_121810_add_arrival_processing_column_to_user_flights.php b/database/migrations/2026_07_02_121810_add_arrival_processing_column_to_user_flights.php new file mode 100644 index 0000000..93b6b0c --- /dev/null +++ b/database/migrations/2026_07_02_121810_add_arrival_processing_column_to_user_flights.php @@ -0,0 +1,30 @@ +timestamp('arrival_processed_at')->nullable()->after('departure_processed_at'); + }); + + DB::table('user_flights') + ->where('arrival_date', '<=', now()->utc()) + ->whereNull('arrival_processed_at') + ->update(['arrival_processed_at' => now()->utc()]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue index b1bc2cb..532673d 100644 --- a/resources/js/Pages/Auth/Login.vue +++ b/resources/js/Pages/Auth/Login.vue @@ -10,10 +10,17 @@ defineProps<{ status?: string; }>(); +const isStandalone = (): boolean => { + if (typeof window === 'undefined') return false; + + return window.matchMedia('(display-mode: standalone)').matches + || (window.navigator as any).standalone === true; +}; + const form = useForm({ email: '', password: '', - remember: false, + remember: isStandalone(), }); const submit = () => { diff --git a/resources/js/Pages/Welcome.vue b/resources/js/Pages/Welcome.vue deleted file mode 100644 index 63eed9c..0000000 --- a/resources/js/Pages/Welcome.vue +++ /dev/null @@ -1,386 +0,0 @@ - - - diff --git a/routes/console.php b/routes/console.php index 3855bd1..c6f88c7 100644 --- a/routes/console.php +++ b/routes/console.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; Schedule::command('app:update-alliances')->dailyAt('09:00')->runInBackground(); -Schedule::command('app:update-arrived-flights')->hourly()->runInBackground(); Schedule::command('app:update-departed-flights')->everyMinute()->runInBackground(); +Schedule::command('app:update-arrived-flights')->everyMinute()->runInBackground(); +Schedule::command('app:get-aircraft-info-for-arrived-flights')->hourly()->runInBackground(); Schedule::command('app:flight-feed-update')->everyMinute()->runInBackground();