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

This commit is contained in:
2026-02-26 09:18:23 -05:00
parent 118aee639e
commit 9a0636cc4a
6 changed files with 148 additions and 45 deletions

View File

@@ -22,6 +22,7 @@
interface FlightSegment { interface FlightSegment {
departureDate: string; departureDate: string;
arrivalDate: string;
departureDatetime: string; departureDatetime: string;
arrivalDatetime: string; arrivalDatetime: string;
airlineId: number | null; airlineId: number | null;
@@ -68,6 +69,7 @@
let segments = $state<FlightSegment[]>([ let segments = $state<FlightSegment[]>([
{ {
departureDate: '', departureDate: '',
arrivalDate: '',
departureDatetime: '', departureDatetime: '',
arrivalDatetime: '', arrivalDatetime: '',
airlineId: null, airlineId: null,
@@ -152,6 +154,7 @@
...segments, ...segments,
{ {
departureDate: '', departureDate: '',
arrivalDate: '',
departureDatetime: '', departureDatetime: '',
arrivalDatetime: '', arrivalDatetime: '',
airlineId: null, airlineId: null,
@@ -179,6 +182,7 @@
segments = [ segments = [
{ {
departureDate: '', departureDate: '',
arrivalDate: '',
departureDatetime: '', departureDatetime: '',
arrivalDatetime: '', arrivalDatetime: '',
airlineId: null, airlineId: null,
@@ -217,7 +221,11 @@
let canSubmit = $derived( let canSubmit = $derived(
segments.length > 0 && segments.length > 0 &&
segments.every( 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> </script>
@@ -376,15 +384,27 @@
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-1.5"> <div class="grid grid-cols-2 gap-4">
<label class="text-xs font-medium text-gray-700">Departure date</label> <div class="flex flex-col gap-1.5">
<input <label class="text-xs font-medium text-gray-700">Departure date</label>
type="date" <input
bind:value={segment.departureDate} type="date"
name="segments[{segmentIndex}][departure_date]" bind:value={segment.departureDate}
required name="segments[{segmentIndex}][departure_date]"
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" 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>
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
@@ -465,8 +485,7 @@
type="text" type="text"
bind:value={segment.flightNumber} bind:value={segment.flightNumber}
name="segments[{segmentIndex}][flight_number]" name="segments[{segmentIndex}][flight_number]"
required placeholder="Optional"
placeholder="1234"
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" 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>

View File

@@ -23,6 +23,7 @@
interface FlightSegmentState { interface FlightSegmentState {
departureDate: string; departureDate: string;
arrivalDate: string;
departureDatetime: string; departureDatetime: string;
arrivalDatetime: string; arrivalDatetime: string;
airlineId: number | null; airlineId: number | null;
@@ -65,6 +66,7 @@
function emptySegment(): FlightSegmentState { function emptySegment(): FlightSegmentState {
return { return {
departureDate: '', departureDate: '',
arrivalDate: '',
departureDatetime: '', departureDatetime: '',
arrivalDatetime: '', arrivalDatetime: '',
airlineId: null, airlineId: null,
@@ -87,6 +89,7 @@
selectedPassengers: booking.passengerIds ?? [], selectedPassengers: booking.passengerIds ?? [],
segments: booking.segments.map((seg) => ({ segments: booking.segments.map((seg) => ({
departureDate: seg.departure_date ?? '', departureDate: seg.departure_date ?? '',
arrivalDate: seg.route?.arrival_date ?? '',
departureDatetime: seg.route?.departure_datetime ?? '', departureDatetime: seg.route?.departure_datetime ?? '',
arrivalDatetime: seg.route?.arrival_datetime ?? '', arrivalDatetime: seg.route?.arrival_datetime ?? '',
airlineId: seg.airline_id ?? null, airlineId: seg.airline_id ?? null,
@@ -207,7 +210,11 @@
let canSubmit = $derived( let canSubmit = $derived(
segments.length > 0 && segments.length > 0 &&
segments.every( 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> </script>
@@ -360,15 +367,27 @@
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-1.5"> <div class="grid grid-cols-2 gap-4">
<label class="text-xs font-medium text-gray-700">Departure date</label> <div class="flex flex-col gap-1.5">
<input <label class="text-xs font-medium text-gray-700">Departure date</label>
type="date" <input
bind:value={segment.departureDate} type="date"
name="segments[{segmentIndex}][departure_date]" bind:value={segment.departureDate}
required name="segments[{segmentIndex}][departure_date]"
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" 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>
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
@@ -449,8 +468,7 @@
type="text" type="text"
bind:value={segment.flightNumber} bind:value={segment.flightNumber}
name="segments[{segmentIndex}][flight_number]" name="segments[{segmentIndex}][flight_number]"
required placeholder="Optional"
placeholder="1234"
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" 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>

View File

@@ -6,7 +6,11 @@
interface Props { interface Props {
plan: Plan; plan: Plan;
flightBooking: FlightBooking & { flightBooking: FlightBooking & {
segments: Array<FlightSegment & { route: FlightRoute | null }>; segments: Array<
FlightSegment & {
route: (FlightRoute & { distanceKm: number | null; durationMins: number | null }) | null;
}
>;
}; };
onEdit?: () => void; onEdit?: () => void;
onDelete?: () => void; onDelete?: () => void;
@@ -237,8 +241,17 @@
{/if} {/if}
<!-- Date --> <!-- Date -->
{#if segment.departure_date} {#if segment.departure_date || segment.route?.arrival_date}
<div class="mt-0.5 text-xs text-gray-500">{formatDate(segment.departure_date)}</div> <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} {/if}
</div> </div>

View File

@@ -195,7 +195,7 @@ export function runMigrations(db: Database): void {
airline_iata TEXT, airline_iata TEXT,
airline_icao TEXT, airline_icao TEXT,
airline_name TEXT, airline_name TEXT,
flight_number TEXT NOT NULL, flight_number TEXT,
position INTEGER NOT NULL DEFAULT 0, position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (datetime('now'))
) )
@@ -216,12 +216,18 @@ export function runMigrations(db: Database): void {
arrival_airport_code TEXT, arrival_airport_code TEXT,
arrival_terminal TEXT, arrival_terminal TEXT,
arrival_gate TEXT, arrival_gate TEXT,
arrival_date TEXT,
arrival_datetime TEXT, arrival_datetime TEXT,
arrival_timezone TEXT, arrival_timezone TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_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 // Flight booking passengers - links people to flight bookings
db.run(` db.run(`

View File

@@ -106,7 +106,7 @@ export interface FlightSegment {
airline_iata: string | null; airline_iata: string | null;
airline_icao: string | null; airline_icao: string | null;
airline_name: string | null; airline_name: string | null;
flight_number: string; flight_number: string | null;
position: number; position: number;
created_at: string; created_at: string;
} }
@@ -124,6 +124,7 @@ export interface FlightRoute {
arrival_airport_code: string | null; arrival_airport_code: string | null;
arrival_terminal: string | null; arrival_terminal: string | null;
arrival_gate: string | null; arrival_gate: string | null;
arrival_date: string | null;
arrival_datetime: string | null; arrival_datetime: string | null;
arrival_timezone: string | null; arrival_timezone: string | null;
created_at: string; created_at: string;
@@ -144,7 +145,7 @@ export interface CreateFlightInput {
airlineIata?: string; airlineIata?: string;
airlineIcao?: string; airlineIcao?: string;
airlineName?: string; airlineName?: string;
flightNumber: string; flightNumber?: string;
route?: { route?: {
departureAirportId?: number; departureAirportId?: number;
departureAirportCode?: string; departureAirportCode?: string;
@@ -156,6 +157,7 @@ export interface CreateFlightInput {
arrivalAirportCode?: string; arrivalAirportCode?: string;
arrivalTerminal?: string; arrivalTerminal?: string;
arrivalGate?: string; arrivalGate?: string;
arrivalDate?: string;
arrivalDatetime?: string; arrivalDatetime?: string;
arrivalTimezone?: string; arrivalTimezone?: string;
}; };
@@ -230,7 +232,15 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
// Generate a title from the first segment // Generate a title from the first segment
const firstSegment = input.segments[0]; const firstSegment = input.segments[0];
const title = firstSegment 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'; : 'Flight';
db.run( db.run(
@@ -282,7 +292,7 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
airlineIata ?? null, airlineIata ?? null,
airlineIcao ?? null, airlineIcao ?? null,
airlineName ?? null, airlineName ?? null,
segment.flightNumber, segment.flightNumber?.trim() || '',
segmentPosition++ segmentPosition++
] ]
); );
@@ -296,8 +306,8 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
departure_airport_id, departure_airport_code, departure_terminal, departure_gate, departure_airport_id, departure_airport_code, departure_terminal, departure_gate,
departure_datetime, departure_timezone, departure_datetime, departure_timezone,
arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate, arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate,
arrival_datetime, arrival_timezone arrival_date, arrival_datetime, arrival_timezone
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
routeId, routeId,
segmentId, segmentId,
@@ -313,6 +323,7 @@ export function createFlight(input: CreateFlightInput): FlightBooking {
segment.route.arrivalAirportCode ?? null, segment.route.arrivalAirportCode ?? null,
segment.route.arrivalTerminal ?? null, segment.route.arrivalTerminal ?? null,
segment.route.arrivalGate ?? null, segment.route.arrivalGate ?? null,
segment.route.arrivalDate ?? null,
segment.route.arrivalDatetime ?? null, segment.route.arrivalDatetime ?? null,
segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null
] ]
@@ -481,7 +492,7 @@ export interface UpdateFlightInput {
airlineIata?: string; airlineIata?: string;
airlineIcao?: string; airlineIcao?: string;
airlineName?: string; airlineName?: string;
flightNumber: string; flightNumber?: string;
route?: { route?: {
departureAirportId?: number; departureAirportId?: number;
departureAirportCode?: string; departureAirportCode?: string;
@@ -493,6 +504,7 @@ export interface UpdateFlightInput {
arrivalAirportCode?: string; arrivalAirportCode?: string;
arrivalTerminal?: string; arrivalTerminal?: string;
arrivalGate?: string; arrivalGate?: string;
arrivalDate?: string;
arrivalDatetime?: string; arrivalDatetime?: string;
arrivalTimezone?: string; arrivalTimezone?: string;
}; };
@@ -513,7 +525,15 @@ export function updateFlight(input: UpdateFlightInput): void {
// Derive new title from first segment // Derive new title from first segment
const firstSegment = input.segments[0]; const firstSegment = input.segments[0];
const title = firstSegment 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'; : 'Flight';
// Update the plan row (status + title) // Update the plan row (status + title)
@@ -558,7 +578,7 @@ export function updateFlight(input: UpdateFlightInput): void {
segment.airlineIata ?? null, segment.airlineIata ?? null,
segment.airlineIcao ?? null, segment.airlineIcao ?? null,
segment.airlineName ?? null, segment.airlineName ?? null,
segment.flightNumber, segment.flightNumber?.trim() || '',
segmentPosition++ segmentPosition++
] ]
); );
@@ -571,8 +591,8 @@ export function updateFlight(input: UpdateFlightInput): void {
departure_airport_id, departure_airport_code, departure_terminal, departure_gate, departure_airport_id, departure_airport_code, departure_terminal, departure_gate,
departure_datetime, departure_timezone, departure_datetime, departure_timezone,
arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate, arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate,
arrival_datetime, arrival_timezone arrival_date, arrival_datetime, arrival_timezone
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
routeId, routeId,
segmentId, segmentId,
@@ -588,6 +608,7 @@ export function updateFlight(input: UpdateFlightInput): void {
segment.route.arrivalAirportCode ?? null, segment.route.arrivalAirportCode ?? null,
segment.route.arrivalTerminal ?? null, segment.route.arrivalTerminal ?? null,
segment.route.arrivalGate ?? null, segment.route.arrivalGate ?? null,
segment.route.arrivalDate ?? null,
segment.route.arrivalDatetime ?? null, segment.route.arrivalDatetime ?? null,
segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null
] ]

View File

@@ -327,12 +327,13 @@ export const actions: Actions = {
airlineId?: number; airlineId?: number;
airlineIata?: string; airlineIata?: string;
airlineName?: string; airlineName?: string;
flightNumber: string; flightNumber?: string;
route?: { route?: {
departureAirportId?: number; departureAirportId?: number;
departureAirportCode?: string; departureAirportCode?: string;
arrivalAirportId?: number; arrivalAirportId?: number;
arrivalAirportCode?: string; arrivalAirportCode?: string;
arrivalDate?: string;
departureDatetime?: string; departureDatetime?: string;
arrivalDatetime?: string; arrivalDatetime?: string;
}; };
@@ -366,13 +367,23 @@ export const actions: Actions = {
const arrivalAirportCode = ( const arrivalAirportCode = (
data.get(`segments[${index}][arrival_airport_code]`) as string data.get(`segments[${index}][arrival_airport_code]`) as string
)?.trim(); )?.trim();
const arrivalDate =
(data.get(`segments[${index}][arrival_date]`) as string)?.trim() || undefined;
const departureDatetime = const departureDatetime =
(data.get(`segments[${index}][departure_datetime]`) as string)?.trim() || undefined; (data.get(`segments[${index}][departure_datetime]`) as string)?.trim() || undefined;
const arrivalDatetime = const arrivalDatetime =
(data.get(`segments[${index}][arrival_datetime]`) as string)?.trim() || undefined; (data.get(`segments[${index}][arrival_datetime]`) as string)?.trim() || undefined;
if (!departureDate || !flightNumber) { if (
return fail(400, { error: 'All segments must have a departure date and flight number' }); !departureDate ||
!arrivalDate ||
!(departureAirportIdRaw || departureAirportCode) ||
!(arrivalAirportIdRaw || arrivalAirportCode)
) {
return fail(400, {
error:
'All segments must have departure date, arrival date, and departure/arrival airports'
});
} }
const hasRoute = const hasRoute =
@@ -380,6 +391,7 @@ export const actions: Actions = {
departureAirportCode || departureAirportCode ||
arrivalAirportIdRaw || arrivalAirportIdRaw ||
arrivalAirportCode || arrivalAirportCode ||
arrivalDate ||
departureDatetime || departureDatetime ||
arrivalDatetime; arrivalDatetime;
@@ -397,6 +409,7 @@ export const actions: Actions = {
departureAirportCode: departureAirportCode || undefined, departureAirportCode: departureAirportCode || undefined,
arrivalAirportId: arrivalAirportIdRaw ? parseInt(arrivalAirportIdRaw) : undefined, arrivalAirportId: arrivalAirportIdRaw ? parseInt(arrivalAirportIdRaw) : undefined,
arrivalAirportCode: arrivalAirportCode || undefined, arrivalAirportCode: arrivalAirportCode || undefined,
arrivalDate,
departureDatetime, departureDatetime,
arrivalDatetime arrivalDatetime
} }
@@ -538,12 +551,13 @@ export const actions: Actions = {
airlineId?: number; airlineId?: number;
airlineIata?: string; airlineIata?: string;
airlineName?: string; airlineName?: string;
flightNumber: string; flightNumber?: string;
route?: { route?: {
departureAirportId?: number; departureAirportId?: number;
departureAirportCode?: string; departureAirportCode?: string;
arrivalAirportId?: number; arrivalAirportId?: number;
arrivalAirportCode?: string; arrivalAirportCode?: string;
arrivalDate?: string;
departureDatetime?: string; departureDatetime?: string;
arrivalDatetime?: string; arrivalDatetime?: string;
}; };
@@ -573,13 +587,23 @@ export const actions: Actions = {
const arrivalAirportCode = ( const arrivalAirportCode = (
data.get(`segments[${index}][arrival_airport_code]`) as string data.get(`segments[${index}][arrival_airport_code]`) as string
)?.trim(); )?.trim();
const arrivalDate =
(data.get(`segments[${index}][arrival_date]`) as string)?.trim() || undefined;
const departureDatetime = const departureDatetime =
(data.get(`segments[${index}][departure_datetime]`) as string)?.trim() || undefined; (data.get(`segments[${index}][departure_datetime]`) as string)?.trim() || undefined;
const arrivalDatetime = const arrivalDatetime =
(data.get(`segments[${index}][arrival_datetime]`) as string)?.trim() || undefined; (data.get(`segments[${index}][arrival_datetime]`) as string)?.trim() || undefined;
if (!departureDate || !flightNumber) { if (
return fail(400, { error: 'All segments must have a departure date and flight number' }); !departureDate ||
!arrivalDate ||
!(departureAirportIdRaw || departureAirportCode) ||
!(arrivalAirportIdRaw || arrivalAirportCode)
) {
return fail(400, {
error:
'All segments must have departure date, arrival date, and departure/arrival airports'
});
} }
const hasRoute = const hasRoute =
@@ -587,6 +611,7 @@ export const actions: Actions = {
departureAirportCode || departureAirportCode ||
arrivalAirportIdRaw || arrivalAirportIdRaw ||
arrivalAirportCode || arrivalAirportCode ||
arrivalDate ||
departureDatetime || departureDatetime ||
arrivalDatetime; arrivalDatetime;
@@ -604,6 +629,7 @@ export const actions: Actions = {
departureAirportCode: departureAirportCode || undefined, departureAirportCode: departureAirportCode || undefined,
arrivalAirportId: arrivalAirportIdRaw ? parseInt(arrivalAirportIdRaw) : undefined, arrivalAirportId: arrivalAirportIdRaw ? parseInt(arrivalAirportIdRaw) : undefined,
arrivalAirportCode: arrivalAirportCode || undefined, arrivalAirportCode: arrivalAirportCode || undefined,
arrivalDate,
departureDatetime, departureDatetime,
arrivalDatetime arrivalDatetime
} }