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 {
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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(`

View File

@@ -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
]

View File

@@ -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
}