Support vague flight segments with airport/date-only details
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m44s
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m44s
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
|
||||
interface FlightSegment {
|
||||
departureDate: string;
|
||||
arrivalDate: string;
|
||||
departureDatetime: string;
|
||||
arrivalDatetime: string;
|
||||
airlineId: number | null;
|
||||
@@ -68,6 +69,7 @@
|
||||
let segments = $state<FlightSegment[]>([
|
||||
{
|
||||
departureDate: '',
|
||||
arrivalDate: '',
|
||||
departureDatetime: '',
|
||||
arrivalDatetime: '',
|
||||
airlineId: null,
|
||||
@@ -152,6 +154,7 @@
|
||||
...segments,
|
||||
{
|
||||
departureDate: '',
|
||||
arrivalDate: '',
|
||||
departureDatetime: '',
|
||||
arrivalDatetime: '',
|
||||
airlineId: null,
|
||||
@@ -179,6 +182,7 @@
|
||||
segments = [
|
||||
{
|
||||
departureDate: '',
|
||||
arrivalDate: '',
|
||||
departureDatetime: '',
|
||||
arrivalDatetime: '',
|
||||
airlineId: null,
|
||||
@@ -217,7 +221,11 @@
|
||||
let canSubmit = $derived(
|
||||
segments.length > 0 &&
|
||||
segments.every(
|
||||
(s) => s.departureDate && s.flightNumber && (s.airlineId || s.airlineIata || s.airlineName)
|
||||
(s) =>
|
||||
s.departureDate &&
|
||||
s.arrivalDate &&
|
||||
(s.departureAirportId || s.departureAirportCode) &&
|
||||
(s.arrivalAirportId || s.arrivalAirportCode)
|
||||
)
|
||||
);
|
||||
</script>
|
||||
@@ -376,15 +384,27 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-gray-700">Departure date</label>
|
||||
<input
|
||||
type="date"
|
||||
bind:value={segment.departureDate}
|
||||
name="segments[{segmentIndex}][departure_date]"
|
||||
required
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-gray-700">Departure date</label>
|
||||
<input
|
||||
type="date"
|
||||
bind:value={segment.departureDate}
|
||||
name="segments[{segmentIndex}][departure_date]"
|
||||
required
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-gray-700">Arrival date</label>
|
||||
<input
|
||||
type="date"
|
||||
bind:value={segment.arrivalDate}
|
||||
name="segments[{segmentIndex}][arrival_date]"
|
||||
required
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
@@ -465,8 +485,7 @@
|
||||
type="text"
|
||||
bind:value={segment.flightNumber}
|
||||
name="segments[{segmentIndex}][flight_number]"
|
||||
required
|
||||
placeholder="1234"
|
||||
placeholder="Optional"
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
interface FlightSegmentState {
|
||||
departureDate: string;
|
||||
arrivalDate: string;
|
||||
departureDatetime: string;
|
||||
arrivalDatetime: string;
|
||||
airlineId: number | null;
|
||||
@@ -65,6 +66,7 @@
|
||||
function emptySegment(): FlightSegmentState {
|
||||
return {
|
||||
departureDate: '',
|
||||
arrivalDate: '',
|
||||
departureDatetime: '',
|
||||
arrivalDatetime: '',
|
||||
airlineId: null,
|
||||
@@ -87,6 +89,7 @@
|
||||
selectedPassengers: booking.passengerIds ?? [],
|
||||
segments: booking.segments.map((seg) => ({
|
||||
departureDate: seg.departure_date ?? '',
|
||||
arrivalDate: seg.route?.arrival_date ?? '',
|
||||
departureDatetime: seg.route?.departure_datetime ?? '',
|
||||
arrivalDatetime: seg.route?.arrival_datetime ?? '',
|
||||
airlineId: seg.airline_id ?? null,
|
||||
@@ -207,7 +210,11 @@
|
||||
let canSubmit = $derived(
|
||||
segments.length > 0 &&
|
||||
segments.every(
|
||||
(s) => s.departureDate && s.flightNumber && (s.airlineId || s.airlineIata || s.airlineName)
|
||||
(s) =>
|
||||
s.departureDate &&
|
||||
s.arrivalDate &&
|
||||
(s.departureAirportId || s.departureAirportCode) &&
|
||||
(s.arrivalAirportId || s.arrivalAirportCode)
|
||||
)
|
||||
);
|
||||
</script>
|
||||
@@ -360,15 +367,27 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-gray-700">Departure date</label>
|
||||
<input
|
||||
type="date"
|
||||
bind:value={segment.departureDate}
|
||||
name="segments[{segmentIndex}][departure_date]"
|
||||
required
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-gray-700">Departure date</label>
|
||||
<input
|
||||
type="date"
|
||||
bind:value={segment.departureDate}
|
||||
name="segments[{segmentIndex}][departure_date]"
|
||||
required
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium text-gray-700">Arrival date</label>
|
||||
<input
|
||||
type="date"
|
||||
bind:value={segment.arrivalDate}
|
||||
name="segments[{segmentIndex}][arrival_date]"
|
||||
required
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
@@ -449,8 +468,7 @@
|
||||
type="text"
|
||||
bind:value={segment.flightNumber}
|
||||
name="segments[{segmentIndex}][flight_number]"
|
||||
required
|
||||
placeholder="1234"
|
||||
placeholder="Optional"
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
interface Props {
|
||||
plan: Plan;
|
||||
flightBooking: FlightBooking & {
|
||||
segments: Array<FlightSegment & { route: FlightRoute | null }>;
|
||||
segments: Array<
|
||||
FlightSegment & {
|
||||
route: (FlightRoute & { distanceKm: number | null; durationMins: number | null }) | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
@@ -237,8 +241,17 @@
|
||||
{/if}
|
||||
|
||||
<!-- Date -->
|
||||
{#if segment.departure_date}
|
||||
<div class="mt-0.5 text-xs text-gray-500">{formatDate(segment.departure_date)}</div>
|
||||
{#if segment.departure_date || segment.route?.arrival_date}
|
||||
<div class="mt-0.5 text-xs text-gray-500">
|
||||
{#if segment.departure_date}
|
||||
{formatDate(segment.departure_date)}
|
||||
{/if}
|
||||
{#if segment.departure_date && segment.route?.arrival_date && segment.route.arrival_date !== segment.departure_date}
|
||||
<span> -> {formatDate(segment.route.arrival_date)}</span>
|
||||
{:else if !segment.departure_date && segment.route?.arrival_date}
|
||||
{formatDate(segment.route.arrival_date)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ export function runMigrations(db: Database): void {
|
||||
airline_iata TEXT,
|
||||
airline_icao TEXT,
|
||||
airline_name TEXT,
|
||||
flight_number TEXT NOT NULL,
|
||||
flight_number TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
@@ -216,12 +216,18 @@ export function runMigrations(db: Database): void {
|
||||
arrival_airport_code TEXT,
|
||||
arrival_terminal TEXT,
|
||||
arrival_gate TEXT,
|
||||
arrival_date TEXT,
|
||||
arrival_datetime TEXT,
|
||||
arrival_timezone TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
try {
|
||||
db.run(`ALTER TABLE flight_routes ADD COLUMN arrival_date TEXT`);
|
||||
} catch {
|
||||
/* already exists */
|
||||
}
|
||||
|
||||
// Flight booking passengers - links people to flight bookings
|
||||
db.run(`
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface FlightSegment {
|
||||
airline_iata: string | null;
|
||||
airline_icao: string | null;
|
||||
airline_name: string | null;
|
||||
flight_number: string;
|
||||
flight_number: string | null;
|
||||
position: number;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -124,6 +124,7 @@ export interface FlightRoute {
|
||||
arrival_airport_code: string | null;
|
||||
arrival_terminal: string | null;
|
||||
arrival_gate: string | null;
|
||||
arrival_date: string | null;
|
||||
arrival_datetime: string | null;
|
||||
arrival_timezone: string | null;
|
||||
created_at: string;
|
||||
@@ -144,7 +145,7 @@ export interface CreateFlightInput {
|
||||
airlineIata?: string;
|
||||
airlineIcao?: string;
|
||||
airlineName?: string;
|
||||
flightNumber: string;
|
||||
flightNumber?: string;
|
||||
route?: {
|
||||
departureAirportId?: number;
|
||||
departureAirportCode?: string;
|
||||
@@ -156,6 +157,7 @@ export interface CreateFlightInput {
|
||||
arrivalAirportCode?: string;
|
||||
arrivalTerminal?: string;
|
||||
arrivalGate?: string;
|
||||
arrivalDate?: string;
|
||||
arrivalDatetime?: string;
|
||||
arrivalTimezone?: string;
|
||||
};
|
||||
@@ -230,7 +232,15 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
|
||||
// Generate a title from the first segment
|
||||
const firstSegment = input.segments[0];
|
||||
const title = firstSegment
|
||||
? `${firstSegment.airlineName || firstSegment.airlineIata || 'Flight'} ${firstSegment.flightNumber}`
|
||||
? (() => {
|
||||
const airline = firstSegment.airlineName || firstSegment.airlineIata || 'Flight';
|
||||
const flightNumber = firstSegment.flightNumber?.trim();
|
||||
const dep = firstSegment.route?.departureAirportCode?.trim();
|
||||
const arr = firstSegment.route?.arrivalAirportCode?.trim();
|
||||
if (flightNumber) return `${airline} ${flightNumber}`;
|
||||
if (dep || arr) return `Flight: ${dep || 'TBD'} -> ${arr || 'TBD'}`;
|
||||
return 'Flight';
|
||||
})()
|
||||
: 'Flight';
|
||||
|
||||
db.run(
|
||||
@@ -282,7 +292,7 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
|
||||
airlineIata ?? null,
|
||||
airlineIcao ?? null,
|
||||
airlineName ?? null,
|
||||
segment.flightNumber,
|
||||
segment.flightNumber?.trim() || '',
|
||||
segmentPosition++
|
||||
]
|
||||
);
|
||||
@@ -296,8 +306,8 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
|
||||
departure_airport_id, departure_airport_code, departure_terminal, departure_gate,
|
||||
departure_datetime, departure_timezone,
|
||||
arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate,
|
||||
arrival_datetime, arrival_timezone
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
arrival_date, arrival_datetime, arrival_timezone
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
routeId,
|
||||
segmentId,
|
||||
@@ -313,6 +323,7 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
|
||||
segment.route.arrivalAirportCode ?? null,
|
||||
segment.route.arrivalTerminal ?? null,
|
||||
segment.route.arrivalGate ?? null,
|
||||
segment.route.arrivalDate ?? null,
|
||||
segment.route.arrivalDatetime ?? null,
|
||||
segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null
|
||||
]
|
||||
@@ -481,7 +492,7 @@ export interface UpdateFlightInput {
|
||||
airlineIata?: string;
|
||||
airlineIcao?: string;
|
||||
airlineName?: string;
|
||||
flightNumber: string;
|
||||
flightNumber?: string;
|
||||
route?: {
|
||||
departureAirportId?: number;
|
||||
departureAirportCode?: string;
|
||||
@@ -493,6 +504,7 @@ export interface UpdateFlightInput {
|
||||
arrivalAirportCode?: string;
|
||||
arrivalTerminal?: string;
|
||||
arrivalGate?: string;
|
||||
arrivalDate?: string;
|
||||
arrivalDatetime?: string;
|
||||
arrivalTimezone?: string;
|
||||
};
|
||||
@@ -513,7 +525,15 @@ export function updateFlight(input: UpdateFlightInput): void {
|
||||
// Derive new title from first segment
|
||||
const firstSegment = input.segments[0];
|
||||
const title = firstSegment
|
||||
? `${firstSegment.airlineName || firstSegment.airlineIata || 'Flight'} ${firstSegment.flightNumber}`
|
||||
? (() => {
|
||||
const airline = firstSegment.airlineName || firstSegment.airlineIata || 'Flight';
|
||||
const flightNumber = firstSegment.flightNumber?.trim();
|
||||
const dep = firstSegment.route?.departureAirportCode?.trim();
|
||||
const arr = firstSegment.route?.arrivalAirportCode?.trim();
|
||||
if (flightNumber) return `${airline} ${flightNumber}`;
|
||||
if (dep || arr) return `Flight: ${dep || 'TBD'} -> ${arr || 'TBD'}`;
|
||||
return 'Flight';
|
||||
})()
|
||||
: 'Flight';
|
||||
|
||||
// Update the plan row (status + title)
|
||||
@@ -558,7 +578,7 @@ export function updateFlight(input: UpdateFlightInput): void {
|
||||
segment.airlineIata ?? null,
|
||||
segment.airlineIcao ?? null,
|
||||
segment.airlineName ?? null,
|
||||
segment.flightNumber,
|
||||
segment.flightNumber?.trim() || '',
|
||||
segmentPosition++
|
||||
]
|
||||
);
|
||||
@@ -571,8 +591,8 @@ export function updateFlight(input: UpdateFlightInput): void {
|
||||
departure_airport_id, departure_airport_code, departure_terminal, departure_gate,
|
||||
departure_datetime, departure_timezone,
|
||||
arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate,
|
||||
arrival_datetime, arrival_timezone
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
arrival_date, arrival_datetime, arrival_timezone
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
routeId,
|
||||
segmentId,
|
||||
@@ -588,6 +608,7 @@ export function updateFlight(input: UpdateFlightInput): void {
|
||||
segment.route.arrivalAirportCode ?? null,
|
||||
segment.route.arrivalTerminal ?? null,
|
||||
segment.route.arrivalGate ?? null,
|
||||
segment.route.arrivalDate ?? null,
|
||||
segment.route.arrivalDatetime ?? null,
|
||||
segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null
|
||||
]
|
||||
|
||||
@@ -327,12 +327,13 @@ export const actions: Actions = {
|
||||
airlineId?: number;
|
||||
airlineIata?: string;
|
||||
airlineName?: string;
|
||||
flightNumber: string;
|
||||
flightNumber?: string;
|
||||
route?: {
|
||||
departureAirportId?: number;
|
||||
departureAirportCode?: string;
|
||||
arrivalAirportId?: number;
|
||||
arrivalAirportCode?: string;
|
||||
arrivalDate?: string;
|
||||
departureDatetime?: string;
|
||||
arrivalDatetime?: string;
|
||||
};
|
||||
@@ -366,13 +367,23 @@ export const actions: Actions = {
|
||||
const arrivalAirportCode = (
|
||||
data.get(`segments[${index}][arrival_airport_code]`) as string
|
||||
)?.trim();
|
||||
const arrivalDate =
|
||||
(data.get(`segments[${index}][arrival_date]`) as string)?.trim() || undefined;
|
||||
const departureDatetime =
|
||||
(data.get(`segments[${index}][departure_datetime]`) as string)?.trim() || undefined;
|
||||
const arrivalDatetime =
|
||||
(data.get(`segments[${index}][arrival_datetime]`) as string)?.trim() || undefined;
|
||||
|
||||
if (!departureDate || !flightNumber) {
|
||||
return fail(400, { error: 'All segments must have a departure date and flight number' });
|
||||
if (
|
||||
!departureDate ||
|
||||
!arrivalDate ||
|
||||
!(departureAirportIdRaw || departureAirportCode) ||
|
||||
!(arrivalAirportIdRaw || arrivalAirportCode)
|
||||
) {
|
||||
return fail(400, {
|
||||
error:
|
||||
'All segments must have departure date, arrival date, and departure/arrival airports'
|
||||
});
|
||||
}
|
||||
|
||||
const hasRoute =
|
||||
@@ -380,6 +391,7 @@ export const actions: Actions = {
|
||||
departureAirportCode ||
|
||||
arrivalAirportIdRaw ||
|
||||
arrivalAirportCode ||
|
||||
arrivalDate ||
|
||||
departureDatetime ||
|
||||
arrivalDatetime;
|
||||
|
||||
@@ -397,6 +409,7 @@ export const actions: Actions = {
|
||||
departureAirportCode: departureAirportCode || undefined,
|
||||
arrivalAirportId: arrivalAirportIdRaw ? parseInt(arrivalAirportIdRaw) : undefined,
|
||||
arrivalAirportCode: arrivalAirportCode || undefined,
|
||||
arrivalDate,
|
||||
departureDatetime,
|
||||
arrivalDatetime
|
||||
}
|
||||
@@ -538,12 +551,13 @@ export const actions: Actions = {
|
||||
airlineId?: number;
|
||||
airlineIata?: string;
|
||||
airlineName?: string;
|
||||
flightNumber: string;
|
||||
flightNumber?: string;
|
||||
route?: {
|
||||
departureAirportId?: number;
|
||||
departureAirportCode?: string;
|
||||
arrivalAirportId?: number;
|
||||
arrivalAirportCode?: string;
|
||||
arrivalDate?: string;
|
||||
departureDatetime?: string;
|
||||
arrivalDatetime?: string;
|
||||
};
|
||||
@@ -573,13 +587,23 @@ export const actions: Actions = {
|
||||
const arrivalAirportCode = (
|
||||
data.get(`segments[${index}][arrival_airport_code]`) as string
|
||||
)?.trim();
|
||||
const arrivalDate =
|
||||
(data.get(`segments[${index}][arrival_date]`) as string)?.trim() || undefined;
|
||||
const departureDatetime =
|
||||
(data.get(`segments[${index}][departure_datetime]`) as string)?.trim() || undefined;
|
||||
const arrivalDatetime =
|
||||
(data.get(`segments[${index}][arrival_datetime]`) as string)?.trim() || undefined;
|
||||
|
||||
if (!departureDate || !flightNumber) {
|
||||
return fail(400, { error: 'All segments must have a departure date and flight number' });
|
||||
if (
|
||||
!departureDate ||
|
||||
!arrivalDate ||
|
||||
!(departureAirportIdRaw || departureAirportCode) ||
|
||||
!(arrivalAirportIdRaw || arrivalAirportCode)
|
||||
) {
|
||||
return fail(400, {
|
||||
error:
|
||||
'All segments must have departure date, arrival date, and departure/arrival airports'
|
||||
});
|
||||
}
|
||||
|
||||
const hasRoute =
|
||||
@@ -587,6 +611,7 @@ export const actions: Actions = {
|
||||
departureAirportCode ||
|
||||
arrivalAirportIdRaw ||
|
||||
arrivalAirportCode ||
|
||||
arrivalDate ||
|
||||
departureDatetime ||
|
||||
arrivalDatetime;
|
||||
|
||||
@@ -604,6 +629,7 @@ export const actions: Actions = {
|
||||
departureAirportCode: departureAirportCode || undefined,
|
||||
arrivalAirportId: arrivalAirportIdRaw ? parseInt(arrivalAirportIdRaw) : undefined,
|
||||
arrivalAirportCode: arrivalAirportCode || undefined,
|
||||
arrivalDate,
|
||||
departureDatetime,
|
||||
arrivalDatetime
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user