fix(native): the widgets show the trip the dashboard features

With several trips running at once, the widgets took whichever came first
on the device, so an old test trip spanning today pushed out the journey
under way, and the today widget counted its days. The dashboard's choice
moves to utils/heroTrip.ts (running first, earliest start, else the next to
come) and the widgets ask the same function, so the home screen always
shows the trip the app opens on.
This commit is contained in:
Maurice
2026-11-06 20:25:41 +01:00
parent 6b8eb939c3
commit ae082c1fd0
6 changed files with 81 additions and 25 deletions
+9
View File
@@ -51,6 +51,15 @@ describe('pickTrip', () => {
expect(pickTrip(trips, '2026-11-12')?.id).toBe(2);
});
it('of several running trips takes the dashboard hero: the one that started first', () => {
const trips = [
trip(1, '2026-11-02', '2026-11-06'),
trip(2, '2026-10-20', '2026-11-18'),
trip(3, '2026-11-04', '2026-11-12'),
];
expect(pickTrip(trips, '2026-11-06')?.id).toBe(2);
});
it('skips undated, archived and finished trips', () => {
expect(
pickTrip(
+6 -9
View File
@@ -1,5 +1,6 @@
import type { Day, Reservation, Trip } from '../types';
import { splitReservationDateTime } from '../utils/formatters';
import { heroTrip, sortTripsByRelevance } from '../utils/heroTrip';
import { hasNativeFeature } from './capabilities';
import { isNativeIos } from './platform';
import { reportNativeFailure } from './reportFailure';
@@ -121,17 +122,13 @@ function minutesOf(time: string | null | undefined): number | null {
}
/**
* The running trip if there is one, else the next to start. Trips without dates
* and trips already over are not candidates.
* The trip the dashboard's hero features (utils/heroTrip.ts): the running one,
* else the next to start, so the widgets show the trip the app opens on.
* Archived trips are not candidates, as on the dashboard.
*/
export function pickTrip(trips: Trip[], today: string): Trip | null {
const dated = trips.flatMap((t) =>
t.start_date && t.end_date && !t.is_archived ? [{ trip: t, start: t.start_date, end: t.end_date }] : []
);
const running = dated.find((d) => d.start <= today && d.end >= today);
if (running) return running.trip;
const upcoming = dated.filter((d) => d.start > today).sort((a, b) => a.start.localeCompare(b.start));
return upcoming[0]?.trip ?? null;
const active = trips.filter((t) => !t.is_archived);
return heroTrip(sortTripsByRelevance(active, today), today);
}
/** The symbols of the booking kinds a day's timeline shows next to its stops. */
+2 -13
View File
@@ -7,6 +7,7 @@
import type { Trip } from '../../types'
import { localIsoDate } from '../../utils/localDate'
import { sortTripsByRelevance } from '../../utils/heroTrip'
// The dashboard works with the canonical Trip shape returned by the list/get
// endpoints (it already carries the computed day_count/place_count/is_owner/
@@ -83,17 +84,5 @@ export function getTripStatus(trip: DashboardTrip): 'ongoing' | 'today' | 'tomor
}
export function sortTrips(trips: DashboardTrip[]): DashboardTrip[] {
const today = localIsoToday()
const rank = (t: DashboardTrip) => {
if (t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) return 0
if (t.start_date && t.start_date >= today) return 1
return 2
}
return [...trips].sort((a, b) => {
const ra = rank(a), rb = rank(b)
if (ra !== rb) return ra - rb
const ad = a.start_date || '', bd = b.start_date || ''
if (ra <= 1) return ad.localeCompare(bd)
return bd.localeCompare(ad)
})
return sortTripsByRelevance(trips, localIsoToday())
}
+2 -3
View File
@@ -9,6 +9,7 @@ import { useTranslation } from '../../i18n'
import { useToast } from '../../components/shared/Toast'
import { getApiErrorMessage } from '../../types'
import { localIsoToday } from './dashboardModel'
import { heroTrip } from '../../utils/heroTrip'
import { useTripSearch } from './useTripSearch'
import type { TripCreateRequest } from '@trek/shared'
import {
@@ -120,9 +121,7 @@ export function useDashboard() {
// A trip the hero features on its own merit: one that is running, else the next
// one coming up. Only that one is taken out of the grid below, so the same trip
// isn't shown twice.
const featured = trips.find(t => t.start_date && t.end_date && t.start_date <= today && t.end_date >= today)
|| trips.find(t => t.start_date && t.start_date >= today)
|| null
const featured = heroTrip(trips, today)
// With neither, the hero still shows something rather than sitting empty — but
// that trip is only borrowed for the header and must stay in the grid, or a user
// whose trips are all finished (or undated) sees "No trips yet". #1706
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { heroTrip, sortTripsByRelevance } from './heroTrip';
const trip = (id: number, start_date: string | null, end_date: string | null) => ({ id, start_date, end_date });
describe('heroTrip', () => {
const today = '2026-11-06';
const trips = [
trip(1, '2026-01-01', '2026-01-05'),
trip(2, '2026-12-01', '2026-12-05'),
trip(3, '2026-11-04', '2026-11-12'),
trip(4, '2026-10-20', '2026-11-18'),
trip(5, null, null),
];
it('orders running trips first, then the ones coming up, then the rest, latest first', () => {
expect(sortTripsByRelevance(trips, today).map((t) => t.id)).toEqual([4, 3, 2, 1, 5]);
});
it('features the running trip that started first, else the next to come', () => {
expect(heroTrip(sortTripsByRelevance(trips, today), today)?.id).toBe(4);
expect(heroTrip(sortTripsByRelevance([trips[0], trips[1]], today), today)?.id).toBe(2);
expect(heroTrip(sortTripsByRelevance([trips[0], trips[4]], today), today)).toBeNull();
});
});
+37
View File
@@ -0,0 +1,37 @@
/**
* Which trip the dashboard features, in one place: the dashboard's hero and
* the iOS widgets (native/widgets.ts) both ask here, so the trip on the home
* screen is always the one the app opens on.
*/
interface DatedTrip {
start_date?: string | null;
end_date?: string | null;
}
/** 0 running, 1 coming up, 2 past or undated. */
function rank(trip: DatedTrip, today: string): number {
if (trip.start_date && trip.end_date && trip.start_date <= today && trip.end_date >= today) return 0;
if (trip.start_date && trip.start_date >= today) return 1;
return 2;
}
/** Running trips first, then the ones coming up, both soonest first; the rest latest first. */
export function sortTripsByRelevance<T extends DatedTrip>(trips: T[], today: string): T[] {
return [...trips].sort((a, b) => {
const ra = rank(a, today);
const rb = rank(b, today);
if (ra !== rb) return ra - rb;
const ad = a.start_date || '';
const bd = b.start_date || '';
if (ra <= 1) return ad.localeCompare(bd);
return bd.localeCompare(ad);
});
}
/**
* The trip the hero features on its own merit: the first running one in that
* order, else the first coming up; null when every trip is past or undated.
*/
export function heroTrip<T extends DatedTrip>(sorted: T[], today: string): T | null {
return sorted.find((t) => rank(t, today) === 0) || sorted.find((t) => rank(t, today) === 1) || null;
}