Updated logo API
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\UserAction;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class FeedController extends Controller
|
||||||
|
{
|
||||||
|
public function view()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$followeeIds = $user->following()->pluck('followee_id');
|
||||||
|
|
||||||
|
$feed = UserAction::whereIn('user_id', $followeeIds)
|
||||||
|
->whereNotIn('type', ['flight_deleted'])
|
||||||
|
->with([
|
||||||
|
'user',
|
||||||
|
])
|
||||||
|
|
||||||
|
->latest()
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
return Inertia::render('Feed', [
|
||||||
|
'user' => $user,
|
||||||
|
'feed' => $feed,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,9 @@ class FlightController extends Controller
|
|||||||
$airlines = $code
|
$airlines = $code
|
||||||
? Airline::where($codeColumn, $code)
|
? Airline::where($codeColumn, $code)
|
||||||
->get()
|
->get()
|
||||||
->map(fn ($airline) => ['value' => $airline->id, 'title' => $airline->display_name])
|
->map(fn ($airline) => ['value' => $airline->id, 'title' => $airline->display_name, 'logo_url' => $airline->logo_url])
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
: collect()->toArray();
|
: collect()->toArray();
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'airline_options' => $airlines,
|
'airline_options' => $airlines,
|
||||||
@@ -74,27 +76,23 @@ class FlightController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function recordChanges(UserFlight $flight): void
|
private function recordChanges(UserFlight $flight, array $dirty, array $original, array $updated): void
|
||||||
{
|
{
|
||||||
$dirty = $flight->getDirty();
|
$changes = [];
|
||||||
|
foreach ($dirty as $field => $newValue) {
|
||||||
if (empty($dirty)) {
|
$changes[] = $this->formatChange($field, $flight->getOriginal($field), $newValue);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$actions = [];
|
UserAction::create([
|
||||||
foreach ($dirty as $field => $newValue) {
|
|
||||||
$original = $flight->getOriginal($field);
|
|
||||||
$actions[] = [
|
|
||||||
'user_id' => $flight->user_id,
|
'user_id' => $flight->user_id,
|
||||||
'user_flight_id' => $flight->id,
|
'user_flight_id' => $flight->id,
|
||||||
'message' => $this->formatChange($field, $original, $newValue),
|
'data' => [
|
||||||
'created_at' => now(),
|
'changes' => $changes,
|
||||||
'updated_at' => now(),
|
'original' => $original,
|
||||||
];
|
'updated' => $updated,
|
||||||
}
|
],
|
||||||
|
'type' => 'flight_updated',
|
||||||
UserAction::insert($actions);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private array $labelCache = [];
|
private array $labelCache = [];
|
||||||
@@ -129,13 +127,17 @@ class FlightController extends Controller
|
|||||||
return $this->labelCache[$cacheKey] = $label;
|
return $this->labelCache[$cacheKey] = $label;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatChange(string $field, mixed $from, mixed $to): string
|
private function formatChange(string $field, mixed $from, mixed $to): array
|
||||||
{
|
{
|
||||||
$label = str($field)->replace('_id', '')->replace('_', ' ')->title();
|
$label = str($field)->replace('_id', '')->replace('_', ' ')->title();
|
||||||
$fromLabel = $this->resolveLabel($field, $from);
|
$fromLabel = $this->resolveLabel($field, $from);
|
||||||
$toLabel = $this->resolveLabel($field, $to);
|
$toLabel = $this->resolveLabel($field, $to);
|
||||||
|
|
||||||
return "{$label} changed from {$fromLabel} to {$toLabel}";
|
return [
|
||||||
|
'field' => $field,
|
||||||
|
'from' => $fromLabel,
|
||||||
|
'to' => $toLabel,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function flightPayload(array $validated): array
|
private function flightPayload(array $validated): array
|
||||||
@@ -168,9 +170,35 @@ class FlightController extends Controller
|
|||||||
|
|
||||||
$newFlight = auth()->user()->flights()->create($this->flightPayload($validated));
|
$newFlight = auth()->user()->flights()->create($this->flightPayload($validated));
|
||||||
|
|
||||||
|
UserAction::create([
|
||||||
|
'user_id' => $newFlight->user_id,
|
||||||
|
'type' => $newFlight->departure_date->isFuture() ? 'flight_booked' : 'flight_logged',
|
||||||
|
'data' => [
|
||||||
|
'flight' => $this->flightSnapshot($newFlight->id),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $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)
|
public function update(Request $request, UserFlight $flight)
|
||||||
{
|
{
|
||||||
$this->authorize('update', $flight);
|
$this->authorize('update', $flight);
|
||||||
@@ -178,12 +206,46 @@ class FlightController extends Controller
|
|||||||
$validated = $request->validate($this->rules());
|
$validated = $request->validate($this->rules());
|
||||||
|
|
||||||
$flight->fill($this->flightPayload($validated));
|
$flight->fill($this->flightPayload($validated));
|
||||||
$this->recordChanges($flight);
|
|
||||||
|
if (!$flight->isDirty()) {
|
||||||
|
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dirty = $flight->getDirty();
|
||||||
|
$original = $this->flightSnapshot($flight->id);
|
||||||
|
|
||||||
$flight->save();
|
$flight->save();
|
||||||
|
|
||||||
|
$updated = $this->flightSnapshot($flight->id);
|
||||||
|
$this->recordChanges($flight, $dirty, $original, $updated);
|
||||||
|
|
||||||
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete(UserFlight $flight)
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $flight);
|
||||||
|
|
||||||
|
$snapshot = $this->flightSnapshot($flight->id);
|
||||||
|
|
||||||
|
if(now()->utc()->isBefore($flight->departure_date)){
|
||||||
|
$action = 'flight_deleted';
|
||||||
|
} else {
|
||||||
|
$action = 'flight_cancelled';
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAction::create([
|
||||||
|
'user_id' => $flight->user_id,
|
||||||
|
'type' => $action,
|
||||||
|
'data' => [
|
||||||
|
'flight' => $snapshot,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$flight->delete();
|
||||||
|
return redirect()->route('profile.departure-board', [Auth::user()->name]);
|
||||||
|
}
|
||||||
|
|
||||||
public function staticData() : array {
|
public function staticData() : array {
|
||||||
return [
|
return [
|
||||||
'seat_types' => SeatType::orderBy('id')->get()->toArray(),
|
'seat_types' => SeatType::orderBy('id')->get()->toArray(),
|
||||||
|
|||||||
@@ -9,34 +9,5 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||||||
/** @mixin UserFlight */
|
/** @mixin UserFlight */
|
||||||
class UserFlightResource extends JsonResource
|
class UserFlightResource extends JsonResource
|
||||||
{
|
{
|
||||||
public function toArray($request): array
|
|
||||||
{
|
|
||||||
$departureTz = $this->departureAirport->timezone;
|
|
||||||
$arrivalTz = $this->arrivalAirport->timezone;
|
|
||||||
$duration = $this->departure_date->diffInMinutes($this->arrival_date);
|
|
||||||
$hours = intdiv($duration, 60);
|
|
||||||
$minutes = $duration % 60;
|
|
||||||
$durationDisplay = $hours . 'h ' . str_pad($minutes, 2, '0', STR_PAD_LEFT) . 'm';
|
|
||||||
|
|
||||||
$departureLocal = $this->departure_date->copy()->setTimezone($departureTz);
|
|
||||||
$arrivalLocal = $this->arrival_date?->copy()->setTimezone($arrivalTz);
|
|
||||||
|
|
||||||
$distance = $this->calculateGreatCircleDistance();
|
|
||||||
|
|
||||||
$dayDifference = (int) abs(Carbon::parse($arrivalLocal->toDateString())
|
|
||||||
->diffInDays(Carbon::parse($departureLocal->toDateString())));
|
|
||||||
|
|
||||||
|
|
||||||
return [
|
|
||||||
...$this->resource->toArray(),
|
|
||||||
'departure_date_display' => $departureLocal->format('j M Y'),
|
|
||||||
'departure_time_display' => $departureLocal->format('g:iA'),
|
|
||||||
'arrival_date_display' => $arrivalLocal?->format('j M Y'),
|
|
||||||
'arrival_time_display' => $arrivalLocal?->format('g:iA'),
|
|
||||||
'arrival_day_difference' => $dayDifference,
|
|
||||||
'duration' => $duration,
|
|
||||||
'duration_display' => $durationDisplay,
|
|
||||||
'distance' => $distance,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,46 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class UserAction extends Model
|
class UserAction extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
'user_flight_id',
|
'type',
|
||||||
'message',
|
'data'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'user_flight_id' => 'integer',
|
|
||||||
'user_id' => 'integer',
|
'user_id' => 'integer',
|
||||||
'message' => 'string',
|
'data' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'display_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function displayType(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => match ($this->type) {
|
||||||
|
'flight_booked' => 'Flight Booked',
|
||||||
|
'flight_cancelled' => 'Flight Cancelled',
|
||||||
|
'flight_updated' => 'Flight Updated',
|
||||||
|
'flight_imported' => 'Flight Imported',
|
||||||
|
'flight_logged' => 'Flight Logged',
|
||||||
|
'flight_deleted' => 'Flight Deleted',
|
||||||
|
default => 'Unknown Action'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
@@ -34,6 +36,17 @@ class UserFlight extends Model
|
|||||||
'auto_update' => 'boolean',
|
'auto_update' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'departure_date_display',
|
||||||
|
'departure_time_display',
|
||||||
|
'arrival_date_display',
|
||||||
|
'arrival_time_display',
|
||||||
|
'arrival_day_difference',
|
||||||
|
'duration',
|
||||||
|
'duration_display',
|
||||||
|
'distance',
|
||||||
|
];
|
||||||
|
|
||||||
public function calculateGreatCircleDistance(): float{
|
public function calculateGreatCircleDistance(): float{
|
||||||
$earthRadiusKm = 6371;
|
$earthRadiusKm = 6371;
|
||||||
[$depLat, $depLong] = [$this->departureAirport->latitude_deg, $this->departureAirport->longitude_deg];
|
[$depLat, $depLong] = [$this->departureAirport->latitude_deg, $this->departureAirport->longitude_deg];
|
||||||
@@ -52,6 +65,89 @@ class UserFlight extends Model
|
|||||||
return $earthRadiusKm * $c;
|
return $earthRadiusKm * $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function departureDateDisplay(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->departure_date
|
||||||
|
->toMutable()
|
||||||
|
->setTimezone($this->departureAirport->timezone)
|
||||||
|
->format('j M Y')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function departureTimeDisplay(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->departure_date
|
||||||
|
->toMutable()
|
||||||
|
->setTimezone($this->departureAirport->timezone)
|
||||||
|
->format('g:iA')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function arrivalDateDisplay(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->arrival_date
|
||||||
|
?->copy()
|
||||||
|
->setTimezone($this->arrivalAirport->timezone)
|
||||||
|
->format('j M Y')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function arrivalTimeDisplay(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->arrival_date
|
||||||
|
?->copy()
|
||||||
|
->setTimezone($this->arrivalAirport->timezone)
|
||||||
|
->format('g:iA')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function arrivalDayDifference(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: function () {
|
||||||
|
if (!$this->arrival_date) return 0;
|
||||||
|
|
||||||
|
$departureLocal = $this->departure_date->copy()->setTimezone($this->departureAirport->timezone);
|
||||||
|
$arrivalLocal = $this->arrival_date->copy()->setTimezone($this->arrivalAirport->timezone);
|
||||||
|
|
||||||
|
return (int) abs(
|
||||||
|
Carbon::parse($arrivalLocal->toDateString())
|
||||||
|
->diffInDays(Carbon::parse($departureLocal->toDateString()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function duration(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->departure_date->diffInMinutes($this->arrival_date)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function durationDisplay(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: function () {
|
||||||
|
$hours = intdiv($this->duration, 60);
|
||||||
|
$minutes = $this->duration % 60;
|
||||||
|
|
||||||
|
return $hours . 'h ' . str_pad($minutes, 2, '0', STR_PAD_LEFT) . 'm';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function distance(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->calculateGreatCircleDistance()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class UserFlightPolicy
|
|||||||
*/
|
*/
|
||||||
public function delete(User $user, UserFlight $userFlight): bool
|
public function delete(User $user, UserFlight $userFlight): bool
|
||||||
{
|
{
|
||||||
return false;
|
return $user->id === $userFlight->user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Airport;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('user_actions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('message');
|
||||||
|
$table->string('type')->after('user_flight_id');
|
||||||
|
$table->dropColumn('user_flight_id');
|
||||||
|
$table->json('data')->after('type');
|
||||||
|
});
|
||||||
|
|
||||||
|
Airport::whereMunicipality('Fayetteville/Springdale/Rogers')->update(['municipality' => 'Fayetteville']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('user_actions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['type', 'data']);
|
||||||
|
$table->text('message')->after('user_flight_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -67,7 +67,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
a {
|
a, a:visited {
|
||||||
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const props = defineProps<{
|
|||||||
errorMessages?: string[] | string
|
errorMessages?: string[] | string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const page = usePage<SharedProps>().props
|
|
||||||
|
|
||||||
const model = defineModel<{ value: number, title: string, logo_url: string } | null>()
|
const model = defineModel<{ value: number, title: string, logo_url: string } | null>()
|
||||||
|
|
||||||
@@ -58,6 +57,7 @@ const searchAirlines = async (query: string) => {
|
|||||||
style="padding: 0.25em"
|
style="padding: 0.25em"
|
||||||
width="40"
|
width="40"
|
||||||
height="40"
|
height="40"
|
||||||
|
:alt="`${model.title}`"
|
||||||
:src="`${model.logo_url}`"
|
:src="`${model.logo_url}`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ defineProps<{
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.feed-boarding-pass{
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.boarding-pass{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pass-stats-row {
|
.pass-stats-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
|||||||
import {FlightStats} from "@/Composables/useFlightStats";
|
import {FlightStats} from "@/Composables/useFlightStats";
|
||||||
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue";
|
||||||
import CrewTooltip from "@/Components/FlightsGoneBy/CrewTooltip.vue";
|
import CrewTooltip from "@/Components/FlightsGoneBy/CrewTooltip.vue";
|
||||||
|
import {Link, router} from "@inertiajs/vue3";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
flightStats: FlightStats
|
flightStats: FlightStats
|
||||||
@@ -22,6 +23,8 @@ function editRoute(id: number) {
|
|||||||
return route('flights.edit', { flight: id })
|
return route('flights.edit', { flight: id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25
|
const ITEMS_PER_PAGE = 25
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
@@ -35,6 +38,7 @@ const headers = [
|
|||||||
{ title: 'DURATION', key: 'duration', sortable: true },
|
{ title: 'DURATION', key: 'duration', sortable: true },
|
||||||
{ title: 'DISTANCE', key: 'distance', sortable: true },
|
{ title: 'DISTANCE', key: 'distance', sortable: true },
|
||||||
{ title: 'AIRCRAFT', key: 'aircraft.designator', sortable: true },
|
{ title: 'AIRCRAFT', key: 'aircraft.designator', sortable: true },
|
||||||
|
{ title: 'REG', key: 'aircraft_registration', sortable: true },
|
||||||
{ title: 'CLASS', key: 'flight_class', sortable: true },
|
{ title: 'CLASS', key: 'flight_class', sortable: true },
|
||||||
{ title: '', key: 'actions', sortable: false },
|
{ title: '', key: 'actions', sortable: false },
|
||||||
]
|
]
|
||||||
@@ -64,6 +68,8 @@ const customKeySort = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const deleting = ref(false)
|
||||||
const sortBy = ref<DataTableSortItem[]>([])
|
const sortBy = ref<DataTableSortItem[]>([])
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
@@ -236,6 +242,12 @@ watch(
|
|||||||
</AircraftToolTip>
|
</AircraftToolTip>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="v-data-table__td ">
|
||||||
|
<span class="mono-tag">
|
||||||
|
{{(item as Flight).aircraft_registration}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="v-data-table__td ">
|
<td class="v-data-table__td ">
|
||||||
<span class="class-cell">
|
<span class="class-cell">
|
||||||
<CrewTooltip v-if="(item as Flight).flight_reason?.name == 'Crew'" :crew-type="(item as Flight).crew_type!">
|
<CrewTooltip v-if="(item as Flight).flight_reason?.name == 'Crew'" :crew-type="(item as Flight).crew_type!">
|
||||||
@@ -266,8 +278,30 @@ watch(
|
|||||||
title="Edit"
|
title="Edit"
|
||||||
:href="editRoute((item as Flight).id)"
|
:href="editRoute((item as Flight).id)"
|
||||||
/>
|
/>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-trash-can-outline"
|
||||||
|
title="Delete"
|
||||||
|
@click="showDeleteDialog = true"
|
||||||
|
/>
|
||||||
|
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||||
|
<v-card title="Delete Flight">
|
||||||
|
<v-card-text>Are you sure you want to delete this flight?</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn v-if="!deleting" @click="showDeleteDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:loading="deleting"
|
||||||
|
@click="deleting = true; router.delete(route('flights.delete', { flight: (item as Flight).id }), { onFinish: () => deleting = false })"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -277,7 +311,7 @@ watch(
|
|||||||
|
|
||||||
<template #no-data>
|
<template #no-data>
|
||||||
<div class="no-data">
|
<div class="no-data">
|
||||||
<span>NO FLIGHTS ON RECORD</span>
|
<span>NO FLIGHTS DEPARTING</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
UserAction,
|
||||||
|
UserActionFlightBookedData,
|
||||||
|
UserActionFlightCancelledData,
|
||||||
|
UserActionFlightUpdatedData
|
||||||
|
} from "@/Types/types";
|
||||||
|
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||||
|
import FlightBookedFeedItem from "@/Components/FlightsGoneBy/Feed/FlightBookedFeedItem.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {Link} from "@inertiajs/vue3";
|
||||||
|
import FlightUpdatedFeedItem from "@/Components/FlightsGoneBy/Feed/FlightUpdatedFeedItem.vue";
|
||||||
|
import FlightCancelledFeedItem from "@/Components/FlightsGoneBy/Feed/FlightCancelledFeedItem.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
action: UserAction
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const flight = computed(() =>{
|
||||||
|
if (props.action.type === 'flight_booked' || props.action.type === 'flight_logged'){
|
||||||
|
return (props.action.data as UserActionFlightBookedData).flight
|
||||||
|
} else if (props.action.type === 'flight_updated'){
|
||||||
|
return (props.action.data as UserActionFlightUpdatedData).updated
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
switch (props.action.type) {
|
||||||
|
case 'flight_booked': return 'generic'
|
||||||
|
case 'flight_logged': return 'generic'
|
||||||
|
case 'flight_updated': return 'economy'
|
||||||
|
case 'flight_cancelled': return 'crew'
|
||||||
|
default: return 'economy'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function timeAgo(dateStr: string): string {
|
||||||
|
const diff = Date.now() - new Date(dateStr).getTime()
|
||||||
|
const mins = Math.floor(diff / 60000)
|
||||||
|
if (mins < 1) return 'just now'
|
||||||
|
if (mins < 60) return `${mins}m ago`
|
||||||
|
const hrs = Math.floor(mins / 60)
|
||||||
|
if (hrs < 24) return `${hrs}h ago`
|
||||||
|
return `${Math.floor(hrs / 24)}d ago`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="feed-item glass glass-border">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="avatar">
|
||||||
|
<Link :href="route('profile.view', { user: action.user?.name })">
|
||||||
|
{{ action.user?.name?.charAt(0).toUpperCase() ?? '?' }}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="name">
|
||||||
|
<Link :href="route('profile.view', { user: action.user?.name })">
|
||||||
|
{{ action.user?.name ?? 'Unknown' }}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span class="time">{{ timeAgo(action.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="type-badge">
|
||||||
|
<InlineBadge :variant="badgeVariant">{{ action.display_type }}</InlineBadge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<FlightBookedFeedItem v-if="action.type == 'flight_booked' || action.type == 'flight_logged'" :flight="(action.data as UserActionFlightBookedData).flight" ></FlightBookedFeedItem>
|
||||||
|
<FlightUpdatedFeedItem v-if="action.type == 'flight_updated'" :data="(action.data as UserActionFlightUpdatedData)" ></FlightUpdatedFeedItem>
|
||||||
|
<FlightCancelledFeedItem v-if="action.type == 'flight_cancelled'" :data="(action.data as UserActionFlightCancelledData)" flight=""></FlightCancelledFeedItem>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<Link v-if="flight" :href="`/u/${action.user?.name}/departure-board/${flight?.id}`" class="view-flight-link">
|
||||||
|
View Flight
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M5 12h14M13 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feed-item {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(99, 102, 241, 0.2);
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #818cf8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.3rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes dt {
|
||||||
|
color: #9ca3af;
|
||||||
|
text-transform: capitalize;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes dd {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-flight-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #818cf8;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.25);
|
||||||
|
background: rgba(99, 102, 241, 0.08);
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-flight-link:hover {
|
||||||
|
background: rgba(99, 102, 241, 0.18);
|
||||||
|
border-color: rgba(99, 102, 241, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-flight-link svg {
|
||||||
|
width: 0.85rem;
|
||||||
|
height: 0.85rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionChange} from "@/Types/types";
|
||||||
|
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||||
|
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||||
|
import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
original: Flight
|
||||||
|
updated: Flight
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GenericFieldChange label="Aircraft">
|
||||||
|
<template #from>
|
||||||
|
<AircraftToolTip v-if="original.aircraft" :aircraft="original.aircraft">
|
||||||
|
{{ original.aircraft.display_name}}
|
||||||
|
</AircraftToolTip>
|
||||||
|
<span v-else>None</span>
|
||||||
|
</template>
|
||||||
|
<template #to>
|
||||||
|
<AircraftToolTip v-if="updated.aircraft" :aircraft="updated.aircraft">
|
||||||
|
{{ updated.aircraft.display_name}}
|
||||||
|
</AircraftToolTip>
|
||||||
|
<span v-else>None</span>
|
||||||
|
</template>
|
||||||
|
</GenericFieldChange>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionChange} from "@/Types/types";
|
||||||
|
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||||
|
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
original: Flight
|
||||||
|
updated: Flight
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GenericFieldChange label="Airline">
|
||||||
|
<template #from>
|
||||||
|
<span class="airline">
|
||||||
|
<AirlineLogo :airline="original.airline" />
|
||||||
|
{{ original.airline?.display_name ?? 'None' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #to>
|
||||||
|
<span class="airline">
|
||||||
|
<AirlineLogo :airline="updated.airline" />
|
||||||
|
{{ updated.airline?.display_name ?? 'None' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</GenericFieldChange>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.airline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from .airline {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionChange} from "@/Types/types";
|
||||||
|
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
change: UserActionChange
|
||||||
|
label: string
|
||||||
|
original: Flight
|
||||||
|
updated: Flight
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const originalAirport = computed(() =>
|
||||||
|
props.change.field === 'departure_airport_id'
|
||||||
|
? props.original.departure_airport
|
||||||
|
: props.original.arrival_airport
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedAirport = computed(() =>
|
||||||
|
props.change.field === 'departure_airport_id'
|
||||||
|
? props.updated.departure_airport
|
||||||
|
: props.updated.arrival_airport
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GenericFieldChange :label="label">
|
||||||
|
<template #from>
|
||||||
|
<AirportToolTip :airport="originalAirport">
|
||||||
|
{{ originalAirport.municipality }} ({{originalAirport.display_code}})
|
||||||
|
</AirportToolTip>
|
||||||
|
</template>
|
||||||
|
<template #to>
|
||||||
|
<AirportToolTip :airport="updatedAirport">
|
||||||
|
{{ updatedAirport.municipality }} ({{updatedAirport.display_code}})
|
||||||
|
</AirportToolTip>
|
||||||
|
</template>
|
||||||
|
</GenericFieldChange>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionChange} from "@/Types/types";
|
||||||
|
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
change: UserActionChange
|
||||||
|
label: string
|
||||||
|
original: Flight
|
||||||
|
updated: Flight
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const displayField = computed(() =>
|
||||||
|
props.change.field === 'departure_date' ? 'departure_date_display' : 'arrival_date_display'
|
||||||
|
)
|
||||||
|
|
||||||
|
const timeField = computed(() =>
|
||||||
|
props.change.field === 'departure_date' ? 'departure_time_display' : 'arrival_time_display'
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GenericFieldChange :label="label">
|
||||||
|
<template #from>{{ (original as any)[displayField] }} at {{ (original as any)[timeField] }}</template>
|
||||||
|
<template #to>{{ (updated as any)[displayField] }} at {{ (updated as any)[timeField] }}</template>
|
||||||
|
</GenericFieldChange>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionChange} from "@/Types/types";
|
||||||
|
|
||||||
|
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||||
|
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
original: Flight
|
||||||
|
updated: Flight
|
||||||
|
}>()
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GenericFieldChange label="Flight Class">
|
||||||
|
<template #from>
|
||||||
|
<span style="display:inline-flex"><FlightClassBadge style="text-decoration:line-through" :flight="original"/></span>
|
||||||
|
</template>
|
||||||
|
<template #to>
|
||||||
|
<span style="display:inline-flex"><FlightClassBadge :flight="updated"/></span>
|
||||||
|
</template>
|
||||||
|
</GenericFieldChange>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
label: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field-change">
|
||||||
|
<span class="field-label">{{ label }}</span>
|
||||||
|
<div class="field-values">
|
||||||
|
<span class="from">
|
||||||
|
<slot name="from" />
|
||||||
|
</span>
|
||||||
|
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M5 12h14M13 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="to">
|
||||||
|
<slot name="to" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field-change {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 180px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-change:nth-child(odd) {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #6b7280;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-values {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from {
|
||||||
|
color: #6b7280;
|
||||||
|
text-decoration: line-through;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
width: 0.85rem;
|
||||||
|
height: 0.85rem;
|
||||||
|
color: #374151;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.to {
|
||||||
|
color: #f9fafb;
|
||||||
|
font-weight: 500;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.field-change {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-values {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1rem 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.to {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight} from "@/Types/types";
|
||||||
|
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
flight: Flight
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flight-booked">
|
||||||
|
<BoardingPass :class="`feed-boarding-pass`" :flight="flight" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionFlightCancelledData} from "@/Types/types";
|
||||||
|
import AirportToolTip from "@/Components/FlightsGoneBy/AirportToolTip.vue";
|
||||||
|
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||||
|
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||||
|
import AircraftToolTip from "@/Components/FlightsGoneBy/AircraftToolTip.vue";
|
||||||
|
import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: UserActionFlightCancelledData
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flight-booked">
|
||||||
|
<div class="cancelled-flight">
|
||||||
|
<span v-if="data.flight.flight_number" class="flight-summary">
|
||||||
|
<AirlineLogo :airline="data.flight.airline" />
|
||||||
|
<span>Flight <strong>{{ data.flight.flight_number }}</strong> on {{ data.flight.departure_date_display }} at {{ data.flight.departure_time_display }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else class="flight-summary">
|
||||||
|
<AirlineLogo :airline="data.flight.airline" />
|
||||||
|
<span>Flight from {{ data.flight.departure_airport.municipality }} ({{ data.flight.departure_airport.display_code }}) → {{ data.flight.arrival_airport.municipality }} ({{ data.flight.arrival_airport.display_code }}) on {{ data.flight.departure_date_display }} at {{ data.flight.departure_time_display }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.cancelled-flight {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-summary strong {
|
||||||
|
color: #f9fafb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Flight, UserActionFlightUpdatedData} from "@/Types/types";
|
||||||
|
import AirportFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/AirportFieldChange.vue";
|
||||||
|
import AircraftFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/AircraftFieldChange.vue";
|
||||||
|
import AirlineFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/AirlineFieldChange.vue";
|
||||||
|
import GenericFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/GenericFieldChange.vue";
|
||||||
|
import DateFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/DateFieldChange.vue";
|
||||||
|
import FlightClassFieldChange from "@/Components/FlightsGoneBy/Feed/FieldChanges/FlightClassFieldChange.vue";
|
||||||
|
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||||
|
import FlightClassBadge from "@/Components/FlightsGoneBy/FlightClassBadge.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: UserActionFlightUpdatedData
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const fieldLabels: Record<string, string> = {
|
||||||
|
flight_number: 'Flight Number',
|
||||||
|
departure_date: 'Departure Date',
|
||||||
|
arrival_date: 'Arrival Date',
|
||||||
|
departure_airport_id: 'Departure Airport',
|
||||||
|
arrival_airport_id: 'Arrival Airport',
|
||||||
|
airline_id: 'Airline',
|
||||||
|
aircraft_id: 'Aircraft',
|
||||||
|
aircraft_registration: 'Aircraft Registration',
|
||||||
|
flight_class_id: 'Flight Class',
|
||||||
|
seat_number: 'Seat Number',
|
||||||
|
seat_type_id: 'Seat Type',
|
||||||
|
flight_reason_id: 'Flight Reason',
|
||||||
|
crew_type_id: 'Crew Type',
|
||||||
|
note: 'Note',
|
||||||
|
auto_update: 'Auto Update',
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationMap: Record<string, { relation: string, display: string }> = {
|
||||||
|
flight_reason_id: { relation: 'flight_reason', display: 'name' },
|
||||||
|
seat_type_id: { relation: 'seat_type', display: 'name' },
|
||||||
|
flight_class_id: { relation: 'flight_class', display: 'name' },
|
||||||
|
crew_type_id: { relation: 'crew_type', display: 'name' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveValue(flight: Flight, field: string): string {
|
||||||
|
const mapping = relationMap[field]
|
||||||
|
if (mapping) {
|
||||||
|
return (flight as any)[mapping.relation]?.[mapping.display] ?? 'None'
|
||||||
|
}
|
||||||
|
return (flight as any)[field] ?? 'None'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="updated-flight">
|
||||||
|
<span v-if="data.updated.flight_number" class="flight-summary">
|
||||||
|
<AirlineLogo :airline="data.updated.airline" />
|
||||||
|
<span>Flight <strong>{{ data.updated.flight_number }}</strong> on {{ data.updated.departure_date_display }} at {{ data.updated.departure_time_display }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else class="flight-summary">
|
||||||
|
<AirlineLogo :airline="data.updated.airline" />
|
||||||
|
<span>Flight from {{ data.updated.departure_airport.municipality }} ({{ data.updated.departure_airport.display_code }}) → {{ data.updated.arrival_airport.municipality }} ({{ data.updated.arrival_airport.display_code }}) on {{ data.updated.departure_date_display }} at {{ data.updated.departure_time_display }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="changes">
|
||||||
|
|
||||||
|
<template v-for="change in data.changes" :key="change.field">
|
||||||
|
<AirportFieldChange
|
||||||
|
v-if="change.field === 'departure_airport_id' || change.field === 'arrival_airport_id'"
|
||||||
|
:change="change"
|
||||||
|
:label="change.field === 'departure_airport_id' ? 'Departure Airport' : 'Arrival Airport'"
|
||||||
|
:original="data.original"
|
||||||
|
:updated="data.updated"
|
||||||
|
/>
|
||||||
|
<AircraftFieldChange
|
||||||
|
v-else-if="change.field === 'aircraft_id'"
|
||||||
|
:original="data.original"
|
||||||
|
:updated="data.updated"
|
||||||
|
/>
|
||||||
|
<AirlineFieldChange
|
||||||
|
v-else-if="change.field === 'airline_id'"
|
||||||
|
:original="data.original"
|
||||||
|
:updated="data.updated"
|
||||||
|
/>
|
||||||
|
<DateFieldChange
|
||||||
|
v-else-if="change.field === 'departure_date' || change.field === 'arrival_date'"
|
||||||
|
:change="change"
|
||||||
|
:label="change.field === 'departure_date' ? 'Departure Date' : 'Arrival Date'"
|
||||||
|
:original="data.original"
|
||||||
|
:updated="data.updated"
|
||||||
|
/>
|
||||||
|
<FlightClassFieldChange
|
||||||
|
v-else-if="change.field === 'flight_class_id'"
|
||||||
|
:original="data.original"
|
||||||
|
:updated="data.updated"
|
||||||
|
/>
|
||||||
|
<GenericFieldChange
|
||||||
|
v-else
|
||||||
|
:label="fieldLabels[change.field] ?? change.field"
|
||||||
|
>
|
||||||
|
<template #from>{{ resolveValue(data.original, change.field) }}</template>
|
||||||
|
<template #to>{{ resolveValue(data.updated, change.field) }}</template>
|
||||||
|
</GenericFieldChange>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.changes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.updated-flight {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-summary strong {
|
||||||
|
color: #f9fafb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -12,6 +12,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="view-toolbar">
|
<div class="view-toolbar">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="view-btn"
|
class="view-btn"
|
||||||
:class="{ active: activeView === 'map' }"
|
:class="{ active: activeView === 'map' }"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const lookupError = ref<string | null>(null)
|
|||||||
const lookupComplete = ref(true)
|
const lookupComplete = ref(true)
|
||||||
|
|
||||||
interface LookupResult {
|
interface LookupResult {
|
||||||
airline_options: { value: number; title: string }[]
|
airline_options: { value: number; title: string, logo_url: string }[]
|
||||||
from_options: { value: number; title: string; country_code: string }[]
|
from_options: { value: number; title: string; country_code: string }[]
|
||||||
to_options: { value: number; title: string; country_code: string }[]
|
to_options: { value: number; title: string; country_code: string }[]
|
||||||
aircraft_options: { value: number; title: string }[]
|
aircraft_options: { value: number; title: string }[]
|
||||||
@@ -72,7 +72,6 @@ async function lookupFlight() {
|
|||||||
}
|
}
|
||||||
lookupResult.value = data
|
lookupResult.value = data
|
||||||
lookupComplete.value = true
|
lookupComplete.value = true
|
||||||
|
|
||||||
if (data.airline_options?.length) {
|
if (data.airline_options?.length) {
|
||||||
airlineOptionsData.value = data.airline_options
|
airlineOptionsData.value = data.airline_options
|
||||||
if (!form.airline) form.airline = data.airline_options[0]
|
if (!form.airline) form.airline = data.airline_options[0]
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||||
|
import {User, UserAction} from "@/Types/types";
|
||||||
|
import { Head } from "@inertiajs/vue3";
|
||||||
|
import FeedItem from "@/Components/FlightsGoneBy/Feed/FeedItem.vue";
|
||||||
|
|
||||||
|
defineOptions({ layout: MainLayout })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: User
|
||||||
|
feed: UserAction[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Feed" />
|
||||||
|
|
||||||
|
<div class="feed-page">
|
||||||
|
<header class="feed-header">
|
||||||
|
<h1>Feed</h1>
|
||||||
|
<span class="feed-count">{{ feed.length }} updates</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="feed.length === 0" class="empty">
|
||||||
|
<p>Nothing here yet — follow someone to see their flight updates!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feed-list">
|
||||||
|
<FeedItem v-for="action in feed" :key="action.id" :action="action" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feed-page {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
width: 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.feed-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-count {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty p {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Vendored
+33
-1
@@ -8,7 +8,7 @@ declare module '@vue/runtime-core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProfileView = 'map' | 'board' | 'passes';
|
export type ProfileView = 'map' | 'board' | 'passes' ;
|
||||||
export type ChartType = "line" | "area" | "bar" | "pie" | "donut" | "radialBar" | "scatter" | "bubble" | "heatmap" | "candlestick" | "boxPlot" | "radar" | "polarArea" | "rangeBar" | "rangeArea" | "treemap" | undefined
|
export type ChartType = "line" | "area" | "bar" | "pie" | "donut" | "radialBar" | "scatter" | "bubble" | "heatmap" | "candlestick" | "boxPlot" | "radar" | "polarArea" | "rangeBar" | "rangeArea" | "treemap" | undefined
|
||||||
export type BadgeVariant = 'first' | 'business' | 'premium' | 'economy' | 'private' | 'unspecified' | 'generic' | 'general_aviation' | 'crew'
|
export type BadgeVariant = 'first' | 'business' | 'premium' | 'economy' | 'private' | 'unspecified' | 'generic' | 'general_aviation' | 'crew'
|
||||||
|
|
||||||
@@ -19,6 +19,38 @@ export interface User {
|
|||||||
email_verified_at: string | null
|
email_verified_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserActionType = "flight_cancelled" | "flight_booked" | "flight_updated" | "flight_logged" | "flight_deleted"
|
||||||
|
export type UserActionDataKey = "field" | "from" | "to"
|
||||||
|
|
||||||
|
export type UserActionFlightBookedData = {
|
||||||
|
flight: Flight
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserActionFlightCancelledData = {
|
||||||
|
flight: Flight
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserActionFlightUpdatedData = {
|
||||||
|
changes: UserActionChange[],
|
||||||
|
original: Flight,
|
||||||
|
updated: Flight
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserActionChange = {field: string, from: string, to: string}
|
||||||
|
export type UserActionData = UserActionFlightBookedData | UserActionFlightCancelledData | UserActionFlightUpdatedData
|
||||||
|
|
||||||
|
export interface UserAction {
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
type: UserActionType
|
||||||
|
data: UserActionData
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
user: User
|
||||||
|
display_type: string
|
||||||
|
user_flight: Flight
|
||||||
|
}
|
||||||
|
|
||||||
export type SharedProps = import('@inertiajs/core').PageProps & {
|
export type SharedProps = import('@inertiajs/core').PageProps & {
|
||||||
auth: {
|
auth: {
|
||||||
user: User | null
|
user: User | null
|
||||||
|
|||||||
+4
-3
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Api\AirlineApiController;
|
use App\Http\Controllers\Api\AirlineApiController;
|
||||||
use App\Http\Controllers\Api\UserApiController;
|
use App\Http\Controllers\Api\UserApiController;
|
||||||
|
use App\Http\Controllers\FeedController;
|
||||||
use App\Http\Controllers\FlightController;
|
use App\Http\Controllers\FlightController;
|
||||||
use App\Http\Controllers\FlightImportController;
|
use App\Http\Controllers\FlightImportController;
|
||||||
use App\Http\Controllers\FlightProfileController;
|
use App\Http\Controllers\FlightProfileController;
|
||||||
@@ -44,6 +45,7 @@ Route::domain(config('app.domain'))->group(
|
|||||||
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
Route::get('/flights/add', [FlightController::class, 'add'])->name('flights.add');
|
||||||
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
Route::get('/flights/{flight}/edit', [FlightController::class, 'edit'])->name('flights.edit');
|
||||||
Route::put('/flights/{flight}', [FlightController::class, 'update'])->name('flights.update');
|
Route::put('/flights/{flight}', [FlightController::class, 'update'])->name('flights.update');
|
||||||
|
Route::delete('/flights/{flight}', [FlightController::class, 'delete'])->name('flights.delete');
|
||||||
|
|
||||||
Route::get('/reconcile', function () {
|
Route::get('/reconcile', function () {
|
||||||
$flight = new FlightImportController()->reconcile(request());
|
$flight = new FlightImportController()->reconcile(request());
|
||||||
@@ -63,9 +65,8 @@ Route::domain(config('app.domain'))->group(
|
|||||||
|
|
||||||
Route::post('/u/{user}/follow', [UserController::class, 'follow'])->name('profile.follow');
|
Route::post('/u/{user}/follow', [UserController::class, 'follow'])->name('profile.follow');
|
||||||
|
|
||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
Route::get('/feed', [FeedController::class, 'view'])->name('feed');
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::post('/import/save', [FlightImportController::class, 'save'])->name('import.save');
|
Route::post('/import/save', [FlightImportController::class, 'save'])->name('import.save');
|
||||||
|
|||||||
Reference in New Issue
Block a user