diff --git a/client/src/components/Planner/bookings/bookingParts.test.tsx b/client/src/components/Planner/bookings/bookingParts.test.tsx
index 97fec48d3..48972ef90 100644
--- a/client/src/components/Planner/bookings/bookingParts.test.tsx
+++ b/client/src/components/Planner/bookings/bookingParts.test.tsx
@@ -1,19 +1,39 @@
// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-002
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
-import { WhenFields } from './bookingParts';
import type { BookingFacts } from './bookingFacts';
+import { WhenFields } from './bookingParts';
function facts(day: BookingFacts['day'], time: string | null = '15:35 → 09:55'): BookingFacts {
return {
- day, time, startTime: null, endTime: null, startDate: null, endDate: null,
- cells: [], endpoints: [], legCodes: [], place: null, accommodation: null, linked: null, url: null, isHotel: false,
+ day,
+ time,
+ startTime: null,
+ endTime: null,
+ startDate: null,
+ endDate: null,
+ cells: [],
+ endpoints: [],
+ legCodes: [],
+ place: null,
+ accommodation: null,
+ linked: null,
+ url: null,
+ isHotel: false,
};
}
describe('WhenFields', () => {
it('FE-PLANNER-BKPARTS-001: a long day title is cut, the date beside it never shrinks (#2590)', () => {
- render();
+ render(
+
+ );
const title = screen.getByText('Germany Departure Flight with a much longer custom title → Korea Arrival');
const date = screen.getByText('Oct 21 → Oct 22');
diff --git a/client/src/components/Planner/reservationFormModel.test.ts b/client/src/components/Planner/reservationFormModel.test.ts
index 9eae38102..e980949bc 100644
--- a/client/src/components/Planner/reservationFormModel.test.ts
+++ b/client/src/components/Planner/reservationFormModel.test.ts
@@ -181,16 +181,24 @@ describe('reservation dates', () => {
expect(endTimeBeforeStartTime(overnight)).toBe(true);
// Time zones can make a booking end "before" it starts, so the save stays open.
expect(isEndBeforeStart(overnight)).toBe(false);
- expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T09:00', reservation_end_time: '18:00' }))).toBe(false);
- expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T18:00', reservation_end_time: '18:00' }))).toBe(false);
+ expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T09:00', reservation_end_time: '18:00' }))).toBe(
+ false
+ );
+ expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T18:00', reservation_end_time: '18:00' }))).toBe(
+ false
+ );
// With the next day as end date the times are fine, and an end date of its own is checked by isEndBeforeStart.
expect(
- endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T18:00', end_date: '2026-11-25', reservation_end_time: '09:00' }))
+ endTimeBeforeStartTime(
+ form({ reservation_time: '2026-11-24T18:00', end_date: '2026-11-25', reservation_end_time: '09:00' })
+ )
).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24', reservation_end_time: '09:00' }))).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_end_time: '09:00' }))).toBe(false);
expect(
- endTimeBeforeStartTime(form({ type: 'hotel', reservation_time: '2026-11-24T18:00', reservation_end_time: '09:00' }))
+ endTimeBeforeStartTime(
+ form({ type: 'hotel', reservation_time: '2026-11-24T18:00', reservation_end_time: '09:00' })
+ )
).toBe(false);
});
diff --git a/client/src/hooks/usePlanMapViewport.test.ts b/client/src/hooks/usePlanMapViewport.test.ts
index 27a6a30db..88f320af0 100644
--- a/client/src/hooks/usePlanMapViewport.test.ts
+++ b/client/src/hooks/usePlanMapViewport.test.ts
@@ -22,7 +22,9 @@ describe('usePlanMapViewport', () => {
});
it('raeumt ihn auch weg, sobald die Karte nicht mehr zu sehen ist, und nimmt dann nichts an', () => {
- const { result, rerender } = renderHook(({ shown }) => usePlanMapViewport(5, shown), { initialProps: { shown: true } });
+ const { result, rerender } = renderHook(({ shown }) => usePlanMapViewport(5, shown), {
+ initialProps: { shown: true },
+ });
act(() => result.current(KYOTO));
rerender({ shown: false });
expect(useTripStore.getState().planMapViewport).toBeNull();
diff --git a/client/src/hooks/usePlanMapViewport.ts b/client/src/hooks/usePlanMapViewport.ts
index 9e199cd9e..a154fb296 100644
--- a/client/src/hooks/usePlanMapViewport.ts
+++ b/client/src/hooks/usePlanMapViewport.ts
@@ -1,7 +1,12 @@
-import { useCallback, useEffect } from 'react'
-import { useTripStore } from '../store/tripStore'
+import { useCallback, useEffect } from 'react';
+import { useTripStore } from '../store/tripStore';
-interface Bounds { south: number; west: number; north: number; east: number }
+interface Bounds {
+ south: number;
+ west: number;
+ north: number;
+ east: number;
+}
/**
* Hands the plan map's viewport to the trip store, where the place search reads
@@ -11,16 +16,19 @@ interface Bounds { south: number; west: number; north: number; east: number }
* to lean on, and falls back to the day and the trip.
*/
export function usePlanMapViewport(tripId: number, shown = true): (bounds: Bounds) => void {
- const setPlanMapViewport = useTripStore((s) => s.setPlanMapViewport)
+ const setPlanMapViewport = useTripStore((s) => s.setPlanMapViewport);
useEffect(() => {
- if (!shown) setPlanMapViewport(null)
- return () => setPlanMapViewport(null)
- }, [shown, tripId, setPlanMapViewport])
+ if (!shown) setPlanMapViewport(null);
+ return () => setPlanMapViewport(null);
+ }, [shown, tripId, setPlanMapViewport]);
- return useCallback((bounds: Bounds) => {
- if (!shown || !Number.isFinite(tripId)) return
- const { south, west, north, east } = bounds
- setPlanMapViewport({ tripId, south, west, north, east })
- }, [shown, tripId, setPlanMapViewport])
+ return useCallback(
+ (bounds: Bounds) => {
+ if (!shown || !Number.isFinite(tripId)) return;
+ const { south, west, north, east } = bounds;
+ setPlanMapViewport({ tripId, south, west, north, east });
+ },
+ [shown, tripId, setPlanMapViewport]
+ );
}
diff --git a/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx b/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx
index e3ff774c3..f95bd07f0 100644
--- a/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx
+++ b/client/src/mobile/screens/trip/tabs/MReservationCardParts.test.tsx
@@ -121,7 +121,9 @@ describe('MReservationCardParts', () => {
});
it('FE-COMP-MRESCARD-009: a booking with a linked expense says the expense is deleted with it (#2604)', () => {
- render();
+ render(
+
+ );
expect(screen.getByText('reservations.confirm.deleteBodyWithExpense:LH 400')).toBeInTheDocument();
});
diff --git a/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx b/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx
index 026669198..0b89fc657 100644
--- a/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx
+++ b/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx
@@ -106,7 +106,9 @@ export function CardWhenAndCode({
{dayValue}
{dayDate}
- ) : dayValue}
+ ) : (
+ dayValue
+ )}
{timeValue}
@@ -161,7 +163,10 @@ export function CardDeleteSheet({ title, card, t }: { title: string; card: CardS
open={card.confirmingDelete}
onClose={card.cancelDelete}
title={t('reservations.confirm.deleteTitle')}
- message={t(card.deleteTakesExpense ? 'reservations.confirm.deleteBodyWithExpense' : 'reservations.confirm.deleteBody', { name: title })}
+ message={t(
+ card.deleteTakesExpense ? 'reservations.confirm.deleteBodyWithExpense' : 'reservations.confirm.deleteBody',
+ { name: title }
+ )}
confirmLabel={t('common.delete')}
cancelLabel={t('common.cancel')}
danger
diff --git a/client/src/pages/tripPlanner/usePlannerTripView.test.ts b/client/src/pages/tripPlanner/usePlannerTripView.test.ts
index 3d984b010..f024b26de 100644
--- a/client/src/pages/tripPlanner/usePlannerTripView.test.ts
+++ b/client/src/pages/tripPlanner/usePlannerTripView.test.ts
@@ -3,111 +3,135 @@
// The planner's view switches the account remembers per trip (#2639): road trip
// mode, the Dawarich trail and the route overview. The tab keeps a copy that
// answers before the settings arrive.
-import { act, renderHook, waitFor } from '@testing-library/react'
-import { http, HttpResponse } from 'msw'
-import { server } from '../../../tests/helpers/msw/server'
-import { resetAllStores, seedStore } from '../../../tests/helpers/store'
-import { buildSettings } from '../../../tests/helpers/factories'
-import { useSettingsStore } from '../../store/settingsStore'
-import { usePlannerTripView } from './usePlannerTripView'
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { http, HttpResponse } from 'msw';
+import { buildSettings } from '../../../tests/helpers/factories';
+import { server } from '../../../tests/helpers/msw/server';
+import { resetAllStores, seedStore } from '../../../tests/helpers/store';
+import { useSettingsStore } from '../../store/settingsStore';
+import { usePlannerTripView } from './usePlannerTripView';
function captureSettingWrites() {
- const writes: Array<{ key: string; value: unknown }> = []
- server.use(http.put('/api/settings', async ({ request }) => {
- const body = await request.json() as { key: string; value: unknown }
- writes.push(body)
- return HttpResponse.json({ success: true, key: body.key, value: body.value })
- }))
- return writes
+ const writes: Array<{ key: string; value: unknown }> = [];
+ server.use(
+ http.put('/api/settings', async ({ request }) => {
+ const body = (await request.json()) as { key: string; value: unknown };
+ writes.push(body);
+ return HttpResponse.json({ success: true, key: body.key, value: body.value });
+ })
+ );
+ return writes;
}
beforeEach(() => {
- resetAllStores()
-})
+ resetAllStores();
+});
describe('usePlannerTripView', () => {
it('FE-TP-TRIPVIEW-001: a fresh browser opens the trip with the switches the account has on', () => {
seedStore(useSettingsStore, {
settings: buildSettings({ planner_trip_views: { '42': { roadtrip: true } } }),
isLoaded: true,
- })
- expect(renderHook(() => usePlannerTripView(42, 'roadtrip')).result.current[0]).toBe(true)
- expect(renderHook(() => usePlannerTripView(42, 'dawarich')).result.current[0]).toBe(false)
- expect(renderHook(() => usePlannerTripView(43, 'roadtrip')).result.current[0]).toBe(false)
- })
+ });
+ expect(renderHook(() => usePlannerTripView(42, 'roadtrip')).result.current[0]).toBe(true);
+ expect(renderHook(() => usePlannerTripView(42, 'dawarich')).result.current[0]).toBe(false);
+ expect(renderHook(() => usePlannerTripView(43, 'roadtrip')).result.current[0]).toBe(false);
+ });
it('FE-TP-TRIPVIEW-002: the tab copy answers until the settings are here, then the account decides', () => {
- sessionStorage.setItem('trip-roadtrip-42', '1')
- const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'))
- expect(result.current[0]).toBe(true)
+ sessionStorage.setItem('trip-roadtrip-42', '1');
+ const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'));
+ expect(result.current[0]).toBe(true);
// Switched off on another device since this tab last looked.
- act(() => { seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true }) })
- expect(result.current[0]).toBe(false)
- })
+ act(() => {
+ seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true });
+ });
+ expect(result.current[0]).toBe(false);
+ });
it('FE-TP-TRIPVIEW-003: a switch is saved on the account next to the other trips, and only what is on', async () => {
- const writes = captureSettingWrites()
+ const writes = captureSettingWrites();
seedStore(useSettingsStore, {
settings: buildSettings({ planner_trip_views: { '7': { overview: true }, '42': { dawarich: true } } }),
isLoaded: true,
- })
- const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'))
+ });
+ const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'));
- act(() => { result.current[1]() })
- expect(result.current[0]).toBe(true)
- expect(sessionStorage.getItem('trip-roadtrip-42')).toBe('1')
- await waitFor(() => expect(writes).toHaveLength(1))
+ act(() => {
+ result.current[1]();
+ });
+ expect(result.current[0]).toBe(true);
+ expect(sessionStorage.getItem('trip-roadtrip-42')).toBe('1');
+ await waitFor(() => expect(writes).toHaveLength(1));
expect(writes[0]).toEqual({
key: 'planner_trip_views',
value: { '7': { overview: true }, '42': { dawarich: true, roadtrip: true } },
- })
+ });
- act(() => { result.current[1]() })
- expect(result.current[0]).toBe(false)
- await waitFor(() => expect(writes).toHaveLength(2))
- expect(writes[1].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } })
- })
+ act(() => {
+ result.current[1]();
+ });
+ expect(result.current[0]).toBe(false);
+ await waitFor(() => expect(writes).toHaveLength(2));
+ expect(writes[1].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } });
+ });
it('FE-TP-TRIPVIEW-004: a switch made before the settings arrived waits for them instead of erasing other trips', async () => {
- const writes = captureSettingWrites()
- server.use(http.get('/api/settings', () => HttpResponse.json({
- settings: buildSettings({ planner_trip_views: { '7': { overview: true } } }),
- })))
- const { result } = renderHook(() => usePlannerTripView(42, 'dawarich'))
+ const writes = captureSettingWrites();
+ server.use(
+ http.get('/api/settings', () =>
+ HttpResponse.json({
+ settings: buildSettings({ planner_trip_views: { '7': { overview: true } } }),
+ })
+ )
+ );
+ const { result } = renderHook(() => usePlannerTripView(42, 'dawarich'));
- act(() => { result.current[1]() })
- expect(result.current[0]).toBe(true)
- await waitFor(() => expect(writes).toHaveLength(1))
- expect(writes[0].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } })
- expect(result.current[0]).toBe(true)
- })
+ act(() => {
+ result.current[1]();
+ });
+ expect(result.current[0]).toBe(true);
+ await waitFor(() => expect(writes).toHaveLength(1));
+ expect(writes[0].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } });
+ expect(result.current[0]).toBe(true);
+ });
it('FE-TP-TRIPVIEW-005: a save that fails keeps the switch for the session', async () => {
- let tried = false
- server.use(http.put('/api/settings', () => {
- tried = true
- return HttpResponse.json({ error: 'nope' }, { status: 500 })
- }))
- seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true })
- const { result } = renderHook(() => usePlannerTripView(42, 'overview'))
+ let tried = false;
+ server.use(
+ http.put('/api/settings', () => {
+ tried = true;
+ return HttpResponse.json({ error: 'nope' }, { status: 500 });
+ })
+ );
+ seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true });
+ const { result } = renderHook(() => usePlannerTripView(42, 'overview'));
- act(() => { result.current[1]() })
- await waitFor(() => expect(tried).toBe(true))
- await act(async () => { await Promise.resolve() })
- expect(result.current[0]).toBe(true)
- expect(sessionStorage.getItem('trip-overview-42')).toBe('1')
- })
+ act(() => {
+ result.current[1]();
+ });
+ await waitFor(() => expect(tried).toBe(true));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ expect(result.current[0]).toBe(true);
+ expect(sessionStorage.getItem('trip-overview-42')).toBe('1');
+ });
it('FE-TP-TRIPVIEW-006: settings that cannot be loaded leave the tab copy in charge and write nothing', async () => {
- const writes = captureSettingWrites()
- server.use(http.get('/api/settings', () => HttpResponse.json({ error: 'offline' }, { status: 503 })))
- vi.spyOn(console, 'error').mockImplementation(() => {})
- const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'))
+ const writes = captureSettingWrites();
+ server.use(http.get('/api/settings', () => HttpResponse.json({ error: 'offline' }, { status: 503 })));
+ vi.spyOn(console, 'error').mockImplementation(() => {});
+ const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'));
- act(() => { result.current[1]() })
- await act(async () => { await new Promise(r => setTimeout(r, 20)) })
- expect(result.current[0]).toBe(true)
- expect(writes).toHaveLength(0)
- })
-})
+ act(() => {
+ result.current[1]();
+ });
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 20));
+ });
+ expect(result.current[0]).toBe(true);
+ expect(writes).toHaveLength(0);
+ });
+});
diff --git a/client/src/pages/tripPlanner/usePlannerTripView.ts b/client/src/pages/tripPlanner/usePlannerTripView.ts
index 57e2aaa04..cf5d8dd34 100644
--- a/client/src/pages/tripPlanner/usePlannerTripView.ts
+++ b/client/src/pages/tripPlanner/usePlannerTripView.ts
@@ -1,25 +1,29 @@
-import { useCallback, useState } from 'react'
-import { PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews, type PlannerTripViewFlag } from '@trek/shared'
-import { useSettingsStore } from '../../store/settingsStore'
+import { PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews, type PlannerTripViewFlag } from '@trek/shared';
+import { useCallback, useState } from 'react';
+import { useSettingsStore } from '../../store/settingsStore';
function readSession(key: string): boolean | null {
try {
- const raw = sessionStorage.getItem(key)
- return raw === '1' ? true : raw === '0' ? false : null
+ const raw = sessionStorage.getItem(key);
+ return raw === '1' ? true : raw === '0' ? false : null;
} catch {
- return null
+ return null;
}
}
function writeSession(key: string, on: boolean): void {
- try { sessionStorage.setItem(key, on ? '1' : '0') } catch { /* the account row still has it */ }
+ try {
+ sessionStorage.setItem(key, on ? '1' : '0');
+ } catch {
+ /* the account row still has it */
+ }
}
interface TabCopy {
- key: string
- on: boolean | null
+ key: string;
+ on: boolean | null;
/** A switch made here that the account row has not taken yet. */
- pending: boolean
+ pending: boolean;
}
/**
@@ -31,39 +35,39 @@ interface TabCopy {
* decide, because a switch flipped on another device is newer than this tab.
*/
export function usePlannerTripView(tripId: number, flag: PlannerTripViewFlag): [boolean, () => void] {
- const sessionKey = `trip-${flag}-${tripId}`
- const saved = useSettingsStore(s => s.settings.planner_trip_views?.[String(tripId)]?.[flag] === true)
- const loaded = useSettingsStore(s => s.isLoaded)
- const [local, setLocal] = useState(() => ({ key: sessionKey, on: readSession(sessionKey), pending: false }))
+ const sessionKey = `trip-${flag}-${tripId}`;
+ const saved = useSettingsStore((s) => s.settings.planner_trip_views?.[String(tripId)]?.[flag] === true);
+ const loaded = useSettingsStore((s) => s.isLoaded);
+ const [local, setLocal] = useState(() => ({ key: sessionKey, on: readSession(sessionKey), pending: false }));
// A different trip in the same planner reads its own copy instead of keeping this one.
- const copy = local.key === sessionKey ? local : { key: sessionKey, on: readSession(sessionKey), pending: false }
- const shown = loaded && !copy.pending ? saved : (copy.on ?? saved)
+ const copy = local.key === sessionKey ? local : { key: sessionKey, on: readSession(sessionKey), pending: false };
+ const shown = loaded && !copy.pending ? saved : (copy.on ?? saved);
const toggle = useCallback(() => {
- const next = !shown
- setLocal({ key: sessionKey, on: next, pending: true })
- writeSession(sessionKey, next)
+ const next = !shown;
+ setLocal({ key: sessionKey, on: next, pending: true });
+ writeSession(sessionKey, next);
void remember(tripId, flag, next).finally(() => {
- setLocal(prev => (prev.key === sessionKey && prev.on === next ? { ...prev, pending: false } : prev))
- })
- }, [shown, sessionKey, tripId, flag])
+ setLocal((prev) => (prev.key === sessionKey && prev.on === next ? { ...prev, pending: false } : prev));
+ });
+ }, [shown, sessionKey, tripId, flag]);
- return [shown, toggle]
+ return [shown, toggle];
}
async function remember(tripId: number, flag: PlannerTripViewFlag, on: boolean): Promise {
- const store = useSettingsStore.getState()
+ const store = useSettingsStore.getState();
// The row holds every trip, so it is only written over once the account's
// own copy is here; otherwise one switch would erase all the others. The
// store takes the new value before the request goes out, so an offline save
// still shows for the rest of the session.
- if (!store.isLoaded) await store.loadSettings()
- const { settings, isLoaded, updateSetting } = useSettingsStore.getState()
- if (!isLoaded) return
- const views = { ...settings.planner_trip_views }
- views[String(tripId)] = { ...views[String(tripId)], [flag]: on }
+ if (!store.isLoaded) await store.loadSettings();
+ const { settings, isLoaded, updateSetting } = useSettingsStore.getState();
+ if (!isLoaded) return;
+ const views = { ...settings.planner_trip_views };
+ views[String(tripId)] = { ...views[String(tripId)], [flag]: on };
try {
- await updateSetting(PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews(views))
+ await updateSetting(PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews(views));
} catch {
// Offline or refused: the store and the tab copy keep the switch for this session.
}