diff --git a/src/lib/components/AddFlightForm.svelte b/src/lib/components/AddFlightForm.svelte index 4711377..aec7d0e 100644 --- a/src/lib/components/AddFlightForm.svelte +++ b/src/lib/components/AddFlightForm.svelte @@ -22,6 +22,7 @@ interface FlightSegment { departureDate: string; + arrivalDate: string; departureDatetime: string; arrivalDatetime: string; airlineId: number | null; @@ -68,6 +69,7 @@ let segments = $state([ { 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) ) ); @@ -376,15 +384,27 @@
-
- - +
+
+ + +
+
+ + +
@@ -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" />
diff --git a/src/lib/components/EditFlightForm.svelte b/src/lib/components/EditFlightForm.svelte index 0d270eb..dd63404 100644 --- a/src/lib/components/EditFlightForm.svelte +++ b/src/lib/components/EditFlightForm.svelte @@ -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) ) ); @@ -360,15 +367,27 @@
-
- - +
+
+ + +
+
+ + +
@@ -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" />
diff --git a/src/lib/components/FlightCard.svelte b/src/lib/components/FlightCard.svelte index 3018b58..dfb939c 100644 --- a/src/lib/components/FlightCard.svelte +++ b/src/lib/components/FlightCard.svelte @@ -6,7 +6,11 @@ interface Props { plan: Plan; flightBooking: FlightBooking & { - segments: Array; + segments: Array< + FlightSegment & { + route: (FlightRoute & { distanceKm: number | null; durationMins: number | null }) | null; + } + >; }; onEdit?: () => void; onDelete?: () => void; @@ -237,8 +241,17 @@ {/if} - {#if segment.departure_date} -
{formatDate(segment.departure_date)}
+ {#if segment.departure_date || segment.route?.arrival_date} +
+ {#if segment.departure_date} + {formatDate(segment.departure_date)} + {/if} + {#if segment.departure_date && segment.route?.arrival_date && segment.route.arrival_date !== segment.departure_date} + -> {formatDate(segment.route.arrival_date)} + {:else if !segment.departure_date && segment.route?.arrival_date} + {formatDate(segment.route.arrival_date)} + {/if} +
{/if}
diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts index 8565ff3..584513d 100644 --- a/src/lib/server/db/migrations.ts +++ b/src/lib/server/db/migrations.ts @@ -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(` diff --git a/src/lib/server/flights.ts b/src/lib/server/flights.ts index e885dba..685952f 100644 --- a/src/lib/server/flights.ts +++ b/src/lib/server/flights.ts @@ -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 ] diff --git a/src/routes/(protected)/trips/[id]/+page.server.ts b/src/routes/(protected)/trips/[id]/+page.server.ts index d327d80..3b6b0e6 100644 --- a/src/routes/(protected)/trips/[id]/+page.server.ts +++ b/src/routes/(protected)/trips/[id]/+page.server.ts @@ -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 }