mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix(native): the Live Activity keeps a day with flights ahead open instead of calling it done
A booking with stopovers was one stop at its first departure, and a stop counted as done the moment it began. With a car at one and a connecting flight at two, the Live Activity said all done for today mid-afternoon. Each leg is now a stop of its own, as the planner shows it, and a flight or anything else with an end time counts as under way until it lands or ends.
This commit is contained in:
@@ -211,6 +211,60 @@ describe('the richer snapshot', () => {
|
||||
expect(withBookings(plain, [], '2026-11-06', (t) => t)).toBe(plain);
|
||||
});
|
||||
|
||||
it('makes a stop of every leg of a flight with a stopover, and keeps a flight in the air ahead until it lands', () => {
|
||||
const plain = todayOf(trip(1, '2026-11-09', '2026-11-10'), [day(5, 1, '2026-11-09', []), day(6, 2, '2026-11-10', [])], '2026-11-09', () => 'D', (t) => t);
|
||||
if (!plain) throw new Error('the trip runs today');
|
||||
const legs = [
|
||||
{ from: 'SNA', to: 'ATL', dep_day_id: 5, dep_time: '14:00', arr_day_id: 5, arr_time: '21:11' },
|
||||
{ from: 'ATL', to: 'ORF', dep_day_id: 5, dep_time: '22:40', arr_day_id: 6, arr_time: '00:31' },
|
||||
];
|
||||
const today = withBookings(
|
||||
plain,
|
||||
[
|
||||
{ id: 1, trip_id: 1, title: 'Car', type: 'car', status: 'confirmed', reservation_time: '2026-11-09T13:00' },
|
||||
{
|
||||
id: 2,
|
||||
trip_id: 1,
|
||||
title: 'SNA → ATL → ORF',
|
||||
type: 'flight',
|
||||
status: 'confirmed',
|
||||
reservation_time: '2026-11-09T14:00',
|
||||
metadata: JSON.stringify({ legs }),
|
||||
},
|
||||
] as never,
|
||||
'2026-11-09',
|
||||
(t) => t
|
||||
);
|
||||
expect(today.stops.map((s) => s.name)).toEqual(['Car', 'SNA → ATL', 'ATL → ORF']);
|
||||
// In the air on the first leg, between the legs, and on the second leg past midnight.
|
||||
expect(tripDayState(today, 16 * 60 + 22, 'done')).toMatchObject({ nextName: 'SNA → ATL', done: 1, total: 3 });
|
||||
expect(tripDayState(today, 21 * 60 + 30, 'done')).toMatchObject({ nextName: 'ATL → ORF', done: 2 });
|
||||
expect(tripDayState(today, 23 * 60 + 30, 'done')).toMatchObject({ nextName: 'ATL → ORF', done: 2 });
|
||||
});
|
||||
|
||||
it('counts a booking with an end time as under way until it ends', () => {
|
||||
const plain = todayOf(trip(1, '2026-11-09', '2026-11-09'), [day(5, 1, '2026-11-09', [])], '2026-11-09', () => 'D', (t) => t);
|
||||
if (!plain) throw new Error('the trip runs today');
|
||||
const today = withBookings(
|
||||
plain,
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
trip_id: 1,
|
||||
title: 'Tour',
|
||||
type: 'tour',
|
||||
status: 'confirmed',
|
||||
reservation_time: '2026-11-09T10:00',
|
||||
reservation_end_time: '2026-11-09T13:00',
|
||||
},
|
||||
] as never,
|
||||
'2026-11-09',
|
||||
(t) => t
|
||||
);
|
||||
expect(tripDayState(today, 11 * 60, 'done')).toMatchObject({ nextName: 'Tour', done: 0 });
|
||||
expect(tripDayState(today, 13 * 60 + 1, 'done')).toMatchObject({ nextName: 'done', done: 1 });
|
||||
});
|
||||
|
||||
it('carries the extras and leaves out what is empty', () => {
|
||||
const snapshot = buildSnapshot(
|
||||
{ ...trip(1, '2026-11-06', '2026-11-07'), day_count: 2 } as Trip,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Day, Reservation, Trip } from '../types';
|
||||
import { getFlightLegs, getTrainLegs, usesStationRoute } from '../utils/flightLegs';
|
||||
import { splitReservationDateTime } from '../utils/formatters';
|
||||
import { heroTrip, sortTripsByRelevance } from '../utils/heroTrip';
|
||||
import { hasNativeFeature } from './capabilities';
|
||||
@@ -56,6 +57,8 @@ interface WidgetStop {
|
||||
minutes: number | null;
|
||||
color?: string | null;
|
||||
symbol?: string | null;
|
||||
/** Minutes after midnight it ends, past midnight as 1440: a flight in the air is not done yet. */
|
||||
endMinutes?: number | null;
|
||||
}
|
||||
|
||||
interface SnapshotStop {
|
||||
@@ -120,6 +123,8 @@ export interface TripDay {
|
||||
path: string;
|
||||
dayLabel: string;
|
||||
stops: WidgetStop[];
|
||||
/** The day's id, which the legs of a multi-leg booking name. */
|
||||
dayId?: number;
|
||||
/** 1-based position of the day in the trip, and the trip's day count. */
|
||||
index?: number;
|
||||
total?: number;
|
||||
@@ -211,6 +216,7 @@ export function todayOf(
|
||||
path: `/trips/${trip.id}`,
|
||||
dayLabel: dayLabel(index + 1, ordered.length),
|
||||
stops,
|
||||
dayId: day.id,
|
||||
index: index + 1,
|
||||
total: ordered.length,
|
||||
};
|
||||
@@ -224,15 +230,37 @@ export function withBookings(
|
||||
formatTime: (time: string) => string
|
||||
): TripDay {
|
||||
const booked = reservations.flatMap((r): WidgetStop[] => {
|
||||
if (r.status === 'cancelled') return [];
|
||||
const symbol = RESERVATION_SYMBOLS[r.type] ?? 'bookmark.fill';
|
||||
// A booking with stopovers is a stop per leg, as the planner shows it: one
|
||||
// stop at the first departure counted the day done while legs were ahead.
|
||||
const legs = r.type === 'flight' ? getFlightLegs(r) : usesStationRoute(r.type) ? getTrainLegs(r) : [];
|
||||
if (legs.length > 1 && day.dayId != null && legs.every((l) => l.dep_day_id != null)) {
|
||||
return legs.flatMap((leg): WidgetStop[] => {
|
||||
if (leg.dep_day_id !== day.dayId || !leg.dep_time) return [];
|
||||
return [
|
||||
{
|
||||
name: leg.from && leg.to ? `${leg.from} → ${leg.to}` : r.title,
|
||||
time: formatTime(leg.dep_time),
|
||||
minutes: minutesOf(leg.dep_time),
|
||||
endMinutes: leg.arr_day_id === day.dayId ? minutesOf(leg.arr_time) : 24 * 60,
|
||||
color: null,
|
||||
symbol,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
const { date, time } = splitReservationDateTime(r.reservation_time);
|
||||
if (r.status === 'cancelled' || !time || date !== today) return [];
|
||||
if (!time || date !== today) return [];
|
||||
const end = splitReservationDateTime(r.reservation_end_time);
|
||||
return [
|
||||
{
|
||||
name: r.title,
|
||||
time: formatTime(time),
|
||||
minutes: minutesOf(time),
|
||||
endMinutes: !end.time ? null : end.date && end.date !== today ? 24 * 60 : minutesOf(end.time),
|
||||
color: null,
|
||||
symbol: RESERVATION_SYMBOLS[r.type] ?? 'bookmark.fill',
|
||||
symbol,
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -303,10 +331,15 @@ interface TripDayState {
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** The Live Activity's state: the first stop whose time is still ahead, else the last. */
|
||||
/**
|
||||
* The Live Activity's state: the first stop still ahead or still under way (a
|
||||
* flight in the air until it lands), else the last.
|
||||
*/
|
||||
export function tripDayState(day: TripDay, nowMinutes: number, allDone: string): TripDayState {
|
||||
const timed = day.stops.filter((s) => s.minutes != null);
|
||||
const nextIndex = day.stops.findIndex((s) => s.minutes != null && s.minutes >= nowMinutes);
|
||||
const nextIndex = day.stops.findIndex(
|
||||
(s) => s.minutes != null && (s.minutes >= nowMinutes || (s.endMinutes != null && s.endMinutes > nowMinutes))
|
||||
);
|
||||
const done = nextIndex < 0 ? timed.length : day.stops.slice(0, nextIndex).filter((s) => s.minutes != null).length;
|
||||
const next = nextIndex >= 0 ? day.stops[nextIndex] : null;
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user