mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
remove route_calculation setting, always use OSRM routing (#1064)
The per-user route_calculation toggle was a second, hidden on/off layer on top of the day footer's show-route button, and made it easy to end up with straight-line routes for no obvious reason. Drop the setting entirely: routing is always on, the footer toggle stays the single switch. Old stored values are simply ignored (settings are key-value, no migration needed).
This commit is contained in:
@@ -20,7 +20,6 @@ type Defaults = {
|
|||||||
temperature_unit?: string
|
temperature_unit?: string
|
||||||
dark_mode?: string | boolean
|
dark_mode?: string | boolean
|
||||||
time_format?: string
|
time_format?: string
|
||||||
route_calculation?: boolean
|
|
||||||
blur_booking_codes?: boolean
|
blur_booking_codes?: boolean
|
||||||
map_tile_url?: string
|
map_tile_url?: string
|
||||||
}
|
}
|
||||||
@@ -208,22 +207,6 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
|
|||||||
))}
|
))}
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
|
|
||||||
{/* Route Calculation */}
|
|
||||||
<OptionRow label={<>{t('settings.routeCalculation')} <ResetButton field="route_calculation" /></>}>
|
|
||||||
{([
|
|
||||||
{ value: true, label: t('settings.on') || 'On' },
|
|
||||||
{ value: false, label: t('settings.off') || 'Off' },
|
|
||||||
] as const).map(opt => (
|
|
||||||
<OptionButton
|
|
||||||
key={String(opt.value)}
|
|
||||||
active={defaults.route_calculation === opt.value}
|
|
||||||
onClick={() => save({ route_calculation: opt.value })}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</OptionButton>
|
|
||||||
))}
|
|
||||||
</OptionRow>
|
|
||||||
|
|
||||||
{/* Blur Booking Codes */}
|
{/* Blur Booking Codes */}
|
||||||
<OptionRow label={<>{t('settings.blurBookingCodes')} <ResetButton field="blur_booking_codes" /></>}>
|
<OptionRow label={<>{t('settings.blurBookingCodes')} <ResetButton field="blur_booking_codes" /></>}>
|
||||||
{([
|
{([
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
const { t, language, locale } = useTranslation()
|
const { t, language, locale } = useTranslation()
|
||||||
const ctxMenu = useContextMenu()
|
const ctxMenu = useContextMenu()
|
||||||
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
||||||
const routeCalcEnabled = useSettingsStore(s => s.settings.route_calculation) !== false
|
|
||||||
const tripActions = useRef(useTripStore.getState()).current
|
const tripActions = useRef(useTripStore.getState()).current
|
||||||
const can = useCanDo()
|
const can = useCanDo()
|
||||||
const canEditDays = can('day_edit', trip)
|
const canEditDays = can('day_edit', trip)
|
||||||
@@ -507,7 +506,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
// the start place's assignment id. Shares RouteCalculator's cache with the map.
|
// the start place's assignment id. Shares RouteCalculator's cache with the map.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (legsAbortRef.current) legsAbortRef.current.abort()
|
if (legsAbortRef.current) legsAbortRef.current.abort()
|
||||||
if (!selectedDayId || !routeCalcEnabled || !routeShown) { setRouteLegs({}); return }
|
if (!selectedDayId || !routeShown) { setRouteLegs({}); return }
|
||||||
const merged = mergedItemsMap[selectedDayId] || []
|
const merged = mergedItemsMap[selectedDayId] || []
|
||||||
const runs: { id: number; lat: number; lng: number }[][] = []
|
const runs: { id: number; lat: number; lng: number }[][] = []
|
||||||
let cur: { id: number; lat: number; lng: number }[] = []
|
let cur: { id: number; lat: number; lng: number }[] = []
|
||||||
@@ -536,7 +535,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
}
|
}
|
||||||
if (!controller.signal.aborted) setRouteLegs(map)
|
if (!controller.signal.aborted) setRouteLegs(map)
|
||||||
})()
|
})()
|
||||||
}, [selectedDayId, routeCalcEnabled, routeShown, routeProfile, mergedItemsMap])
|
}, [selectedDayId, routeShown, routeProfile, mergedItemsMap])
|
||||||
|
|
||||||
const openAddNote = (dayId, e) => {
|
const openAddNote = (dayId, e) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ beforeEach(() => {
|
|||||||
resetAllStores();
|
resetAllStores();
|
||||||
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
||||||
seedStore(useTripStore, { trip: buildTrip({ id: 1 }) });
|
seedStore(useTripStore, { trip: buildTrip({ id: 1 }) });
|
||||||
seedStore(useSettingsStore, { settings: { time_format: '24h', blur_booking_codes: false, temperature_unit: 'celsius', language: 'en', dark_mode: false, default_currency: 'USD', default_lat: 48.8566, default_lng: 2.3522, default_zoom: 10, map_tile_url: '', show_place_description: false, route_calculation: false } });
|
seedStore(useSettingsStore, { settings: { time_format: '24h', blur_booking_codes: false, temperature_unit: 'celsius', language: 'en', dark_mode: false, default_currency: 'USD', default_lat: 48.8566, default_lng: 2.3522, default_zoom: 10, map_tile_url: '', show_place_description: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ReservationsPanel', () => {
|
describe('ReservationsPanel', () => {
|
||||||
@@ -211,7 +211,7 @@ describe('ReservationsPanel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('FE-PLANNER-RESP-022: confirmation number is blurred when blur_booking_codes=true', () => {
|
it('FE-PLANNER-RESP-022: confirmation number is blurred when blur_booking_codes=true', () => {
|
||||||
seedStore(useSettingsStore, { settings: { time_format: '24h', blur_booking_codes: true, temperature_unit: 'celsius', language: 'en', dark_mode: false, default_currency: 'USD', default_lat: 48.8566, default_lng: 2.3522, default_zoom: 10, map_tile_url: '', show_place_description: false, route_calculation: false } });
|
seedStore(useSettingsStore, { settings: { time_format: '24h', blur_booking_codes: true, temperature_unit: 'celsius', language: 'en', dark_mode: false, default_currency: 'USD', default_lat: 48.8566, default_lng: 2.3522, default_zoom: 10, map_tile_url: '', show_place_description: false } });
|
||||||
const res = buildReservation({ confirmation_number: 'ABC123', status: 'confirmed' });
|
const res = buildReservation({ confirmation_number: 'ABC123', status: 'confirmed' });
|
||||||
render(<ReservationsPanel {...defaultProps} reservations={[res]} />);
|
render(<ReservationsPanel {...defaultProps} reservations={[res]} />);
|
||||||
const codeEl = screen.getByText('ABC123');
|
const codeEl = screen.getByText('ABC123');
|
||||||
@@ -220,7 +220,7 @@ describe('ReservationsPanel', () => {
|
|||||||
|
|
||||||
it('FE-PLANNER-RESP-023: confirmation code revealed on hover when blurred', async () => {
|
it('FE-PLANNER-RESP-023: confirmation code revealed on hover when blurred', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
seedStore(useSettingsStore, { settings: { time_format: '24h', blur_booking_codes: true, temperature_unit: 'celsius', language: 'en', dark_mode: false, default_currency: 'USD', default_lat: 48.8566, default_lng: 2.3522, default_zoom: 10, map_tile_url: '', show_place_description: false, route_calculation: false } });
|
seedStore(useSettingsStore, { settings: { time_format: '24h', blur_booking_codes: true, temperature_unit: 'celsius', language: 'en', dark_mode: false, default_currency: 'USD', default_lat: 48.8566, default_lng: 2.3522, default_zoom: 10, map_tile_url: '', show_place_description: false } });
|
||||||
const res = buildReservation({ confirmation_number: 'ABC123', status: 'confirmed' });
|
const res = buildReservation({ confirmation_number: 'ABC123', status: 'confirmed' });
|
||||||
render(<ReservationsPanel {...defaultProps} reservations={[res]} />);
|
render(<ReservationsPanel {...defaultProps} reservations={[res]} />);
|
||||||
const codeEl = screen.getByText('ABC123');
|
const codeEl = screen.getByText('ABC123');
|
||||||
|
|||||||
@@ -161,29 +161,6 @@ describe('DisplaySettingsTab', () => {
|
|||||||
expect(updateSetting).toHaveBeenCalledWith('time_format', '24h');
|
expect(updateSetting).toHaveBeenCalledWith('time_format', '24h');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('FE-COMP-DISPLAY-021: shows Route Calculation section', () => {
|
|
||||||
render(<DisplaySettingsTab />);
|
|
||||||
expect(screen.getByText(/route calculation/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('FE-COMP-DISPLAY-022: route calculation On button is active when route_calculation is true', () => {
|
|
||||||
seedStore(useSettingsStore, { settings: buildSettings({ route_calculation: true }) });
|
|
||||||
render(<DisplaySettingsTab />);
|
|
||||||
const onButtons = screen.getAllByText(/^On$/i);
|
|
||||||
const routeCalcOnBtn = onButtons[0].closest('button')!;
|
|
||||||
expect(routeCalcOnBtn.style.border).toContain('var(--text-primary)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('FE-COMP-DISPLAY-023: clicking route calculation Off calls updateSetting with false', async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
|
||||||
seedStore(useSettingsStore, { settings: buildSettings({ route_calculation: true }), updateSetting });
|
|
||||||
render(<DisplaySettingsTab />);
|
|
||||||
const offButtons = screen.getAllByText(/^Off$/i);
|
|
||||||
await user.click(offButtons[0]);
|
|
||||||
expect(updateSetting).toHaveBeenCalledWith('route_calculation', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('FE-COMP-DISPLAY-024: shows Blur Booking Codes section', () => {
|
it('FE-COMP-DISPLAY-024: shows Blur Booking Codes section', () => {
|
||||||
render(<DisplaySettingsTab />);
|
render(<DisplaySettingsTab />);
|
||||||
expect(screen.getByText(/blur booking codes/i)).toBeInTheDocument();
|
expect(screen.getByText(/blur booking codes/i)).toBeInTheDocument();
|
||||||
|
|||||||
@@ -214,36 +214,6 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Route Calculation */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.routeCalculation')}</label>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{[
|
|
||||||
{ value: true, label: t('settings.on') || 'On' },
|
|
||||||
{ value: false, label: t('settings.off') || 'Off' },
|
|
||||||
].map(opt => (
|
|
||||||
<button
|
|
||||||
key={String(opt.value)}
|
|
||||||
onClick={async () => {
|
|
||||||
try { await updateSetting('route_calculation', opt.value) }
|
|
||||||
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
|
||||||
padding: '10px 20px', borderRadius: 10, cursor: 'pointer',
|
|
||||||
fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
|
|
||||||
border: (settings.route_calculation !== false) === opt.value ? '2px solid var(--text-primary)' : '2px solid var(--border-primary)',
|
|
||||||
background: (settings.route_calculation !== false) === opt.value ? 'var(--bg-hover)' : 'var(--bg-card)',
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
transition: 'all 0.15s',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Booking route labels */}
|
{/* Booking route labels */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.bookingLabels')}</label>
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.bookingLabels')}</label>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
||||||
import { useSettingsStore } from '../store/settingsStore'
|
|
||||||
import { useTripStore } from '../store/tripStore'
|
import { useTripStore } from '../store/tripStore'
|
||||||
import { calculateRouteWithLegs } from '../components/Map/RouteCalculator'
|
import { calculateRouteWithLegs } from '../components/Map/RouteCalculator'
|
||||||
import type { TripStoreState } from '../store/tripStore'
|
import type { TripStoreState } from '../store/tripStore'
|
||||||
@@ -9,14 +8,13 @@ const TRANSPORT_TYPES = ['flight', 'train', 'bus', 'car', 'cruise']
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages route calculation state for a selected day. Extracts geo-coded waypoints from
|
* Manages route calculation state for a selected day. Extracts geo-coded waypoints from
|
||||||
* day assignments, draws a straight-line route, and optionally fetches per-segment
|
* day assignments, draws a straight-line route immediately, then upgrades it to real OSRM
|
||||||
* driving/walking durations via OSRM. Aborts in-flight requests when the day changes.
|
* road geometry with per-segment durations. Aborts in-flight requests when the day changes.
|
||||||
*/
|
*/
|
||||||
export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: number | null, enabled: boolean = true, profile: 'driving' | 'walking' | 'cycling' = 'driving') {
|
export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: number | null, enabled: boolean = true, profile: 'driving' | 'walking' | 'cycling' = 'driving') {
|
||||||
const [route, setRoute] = useState<[number, number][][] | null>(null)
|
const [route, setRoute] = useState<[number, number][][] | null>(null)
|
||||||
const [routeInfo, setRouteInfo] = useState<RouteResult | null>(null)
|
const [routeInfo, setRouteInfo] = useState<RouteResult | null>(null)
|
||||||
const [routeSegments, setRouteSegments] = useState<RouteSegment[]>([])
|
const [routeSegments, setRouteSegments] = useState<RouteSegment[]>([])
|
||||||
const routeCalcEnabled = useSettingsStore((s) => s.settings.route_calculation) !== false
|
|
||||||
const routeAbortRef = useRef<AbortController | null>(null)
|
const routeAbortRef = useRef<AbortController | null>(null)
|
||||||
const reservationsForSignature = useTripStore((s) => s.reservations)
|
const reservationsForSignature = useTripStore((s) => s.reservations)
|
||||||
|
|
||||||
@@ -88,9 +86,8 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
if (runs.length === 0) { setRoute(null); setRouteSegments([]); return }
|
if (runs.length === 0) { setRoute(null); setRouteSegments([]); return }
|
||||||
|
|
||||||
// Draw straight lines immediately for snappiness, then upgrade to the real
|
// Draw straight lines immediately for snappiness, then upgrade to the real
|
||||||
// OSRM road geometry. If route calc is disabled, keep the straight lines.
|
// OSRM road geometry.
|
||||||
setRoute(straightLines())
|
setRoute(straightLines())
|
||||||
if (!routeCalcEnabled) { setRouteSegments([]); return }
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
routeAbortRef.current = controller
|
routeAbortRef.current = controller
|
||||||
@@ -113,7 +110,7 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
// Aborted (day changed) — newer call owns the state. Anything else: keep straight lines.
|
// Aborted (day changed) — newer call owns the state. Anything else: keep straight lines.
|
||||||
if (!(err instanceof Error) || err.name !== 'AbortError') setRouteSegments([])
|
if (!(err instanceof Error) || err.name !== 'AbortError') setRouteSegments([])
|
||||||
}
|
}
|
||||||
}, [routeCalcEnabled, enabled, profile])
|
}, [enabled, profile])
|
||||||
|
|
||||||
// Stable signature for transport reservations on the selected day — changes when a transport
|
// Stable signature for transport reservations on the selected day — changes when a transport
|
||||||
// is added, removed, or repositioned, ensuring route recalc fires even on transport-only reorders.
|
// is added, removed, or repositioned, ensuring route recalc fires even on transport-only reorders.
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'اللغة',
|
'settings.language': 'اللغة',
|
||||||
'settings.temperature': 'وحدة الحرارة',
|
'settings.temperature': 'وحدة الحرارة',
|
||||||
'settings.timeFormat': 'تنسيق الوقت',
|
'settings.timeFormat': 'تنسيق الوقت',
|
||||||
'settings.routeCalculation': 'حساب المسار',
|
|
||||||
'settings.blurBookingCodes': 'إخفاء رموز الحجز',
|
'settings.blurBookingCodes': 'إخفاء رموز الحجز',
|
||||||
'settings.notifications': 'الإشعارات',
|
'settings.notifications': 'الإشعارات',
|
||||||
'settings.notifyTripInvite': 'دعوات الرحلات',
|
'settings.notifyTripInvite': 'دعوات الرحلات',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Idioma',
|
'settings.language': 'Idioma',
|
||||||
'settings.temperature': 'Unidade de temperatura',
|
'settings.temperature': 'Unidade de temperatura',
|
||||||
'settings.timeFormat': 'Formato de hora',
|
'settings.timeFormat': 'Formato de hora',
|
||||||
'settings.routeCalculation': 'Cálculo de rota',
|
|
||||||
'settings.blurBookingCodes': 'Ocultar códigos de reserva',
|
'settings.blurBookingCodes': 'Ocultar códigos de reserva',
|
||||||
'settings.notifications': 'Notificações',
|
'settings.notifications': 'Notificações',
|
||||||
'settings.notifyTripInvite': 'Convites de viagem',
|
'settings.notifyTripInvite': 'Convites de viagem',
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Jazyk',
|
'settings.language': 'Jazyk',
|
||||||
'settings.temperature': 'Jednotky teploty',
|
'settings.temperature': 'Jednotky teploty',
|
||||||
'settings.timeFormat': 'Formát času',
|
'settings.timeFormat': 'Formát času',
|
||||||
'settings.routeCalculation': 'Výpočet trasy',
|
|
||||||
'settings.blurBookingCodes': 'Skrýt rezervační kódy',
|
'settings.blurBookingCodes': 'Skrýt rezervační kódy',
|
||||||
'settings.notifications': 'Oznámení',
|
'settings.notifications': 'Oznámení',
|
||||||
'settings.notifyTripInvite': 'Pozvánky na cesty',
|
'settings.notifyTripInvite': 'Pozvánky na cesty',
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Sprache',
|
'settings.language': 'Sprache',
|
||||||
'settings.temperature': 'Temperatureinheit',
|
'settings.temperature': 'Temperatureinheit',
|
||||||
'settings.timeFormat': 'Zeitformat',
|
'settings.timeFormat': 'Zeitformat',
|
||||||
'settings.routeCalculation': 'Routenberechnung',
|
|
||||||
'settings.bookingLabels': 'Orts-Labels auf Buchungsrouten',
|
'settings.bookingLabels': 'Orts-Labels auf Buchungsrouten',
|
||||||
'settings.bookingLabelsHint': 'Zeigt Bahnhofs-/Flughafennamen auf der Karte. Wenn aus, wird nur das Icon angezeigt.',
|
'settings.bookingLabelsHint': 'Zeigt Bahnhofs-/Flughafennamen auf der Karte. Wenn aus, wird nur das Icon angezeigt.',
|
||||||
'settings.blurBookingCodes': 'Buchungscodes verbergen',
|
'settings.blurBookingCodes': 'Buchungscodes verbergen',
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Language',
|
'settings.language': 'Language',
|
||||||
'settings.temperature': 'Temperature Unit',
|
'settings.temperature': 'Temperature Unit',
|
||||||
'settings.timeFormat': 'Time Format',
|
'settings.timeFormat': 'Time Format',
|
||||||
'settings.routeCalculation': 'Route Calculation',
|
|
||||||
'settings.bookingLabels': 'Booking route labels',
|
'settings.bookingLabels': 'Booking route labels',
|
||||||
'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.',
|
'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.',
|
||||||
'settings.blurBookingCodes': 'Blur Booking Codes',
|
'settings.blurBookingCodes': 'Blur Booking Codes',
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ const es: Record<string, string> = {
|
|||||||
'settings.language': 'Idioma',
|
'settings.language': 'Idioma',
|
||||||
'settings.temperature': 'Unidad de temperatura',
|
'settings.temperature': 'Unidad de temperatura',
|
||||||
'settings.timeFormat': 'Formato de hora',
|
'settings.timeFormat': 'Formato de hora',
|
||||||
'settings.routeCalculation': 'Cálculo de ruta',
|
|
||||||
'settings.blurBookingCodes': 'Difuminar códigos de reserva',
|
'settings.blurBookingCodes': 'Difuminar códigos de reserva',
|
||||||
'settings.notifications': 'Notificaciones',
|
'settings.notifications': 'Notificaciones',
|
||||||
'settings.notifyTripInvite': 'Invitaciones de viaje',
|
'settings.notifyTripInvite': 'Invitaciones de viaje',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const fr: Record<string, string> = {
|
|||||||
'settings.language': 'Langue',
|
'settings.language': 'Langue',
|
||||||
'settings.temperature': 'Unité de température',
|
'settings.temperature': 'Unité de température',
|
||||||
'settings.timeFormat': 'Format de l\'heure',
|
'settings.timeFormat': 'Format de l\'heure',
|
||||||
'settings.routeCalculation': 'Calcul d\'itinéraire',
|
|
||||||
'settings.blurBookingCodes': 'Masquer les codes de réservation',
|
'settings.blurBookingCodes': 'Masquer les codes de réservation',
|
||||||
'settings.notifications': 'Notifications',
|
'settings.notifications': 'Notifications',
|
||||||
'settings.notifyTripInvite': 'Invitations de voyage',
|
'settings.notifyTripInvite': 'Invitations de voyage',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Nyelv',
|
'settings.language': 'Nyelv',
|
||||||
'settings.temperature': 'Hőmérséklet egység',
|
'settings.temperature': 'Hőmérséklet egység',
|
||||||
'settings.timeFormat': 'Időformátum',
|
'settings.timeFormat': 'Időformátum',
|
||||||
'settings.routeCalculation': 'Útvonalszámítás',
|
|
||||||
'settings.blurBookingCodes': 'Foglalási kódok elrejtése',
|
'settings.blurBookingCodes': 'Foglalási kódok elrejtése',
|
||||||
'settings.notifications': 'Értesítések',
|
'settings.notifications': 'Értesítések',
|
||||||
'settings.notifyTripInvite': 'Utazási meghívók',
|
'settings.notifyTripInvite': 'Utazási meghívók',
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Bahasa',
|
'settings.language': 'Bahasa',
|
||||||
'settings.temperature': 'Satuan Suhu',
|
'settings.temperature': 'Satuan Suhu',
|
||||||
'settings.timeFormat': 'Format Waktu',
|
'settings.timeFormat': 'Format Waktu',
|
||||||
'settings.routeCalculation': 'Perhitungan Rute',
|
|
||||||
'settings.blurBookingCodes': 'Sembunyikan Kode Pemesanan',
|
'settings.blurBookingCodes': 'Sembunyikan Kode Pemesanan',
|
||||||
'settings.notifications': 'Notifikasi',
|
'settings.notifications': 'Notifikasi',
|
||||||
'settings.notifyTripInvite': 'Undangan perjalanan',
|
'settings.notifyTripInvite': 'Undangan perjalanan',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Lingua',
|
'settings.language': 'Lingua',
|
||||||
'settings.temperature': 'Unità di Temperatura',
|
'settings.temperature': 'Unità di Temperatura',
|
||||||
'settings.timeFormat': 'Formato Ora',
|
'settings.timeFormat': 'Formato Ora',
|
||||||
'settings.routeCalculation': 'Calcolo Percorso',
|
|
||||||
'settings.blurBookingCodes': 'Nascondi codici di prenotazione',
|
'settings.blurBookingCodes': 'Nascondi codici di prenotazione',
|
||||||
'settings.notifications': 'Notifiche',
|
'settings.notifications': 'Notifiche',
|
||||||
'settings.notifyTripInvite': 'Inviti di viaggio',
|
'settings.notifyTripInvite': 'Inviti di viaggio',
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ const ja: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': '言語',
|
'settings.language': '言語',
|
||||||
'settings.temperature': '温度単位',
|
'settings.temperature': '温度単位',
|
||||||
'settings.timeFormat': '時刻形式',
|
'settings.timeFormat': '時刻形式',
|
||||||
'settings.routeCalculation': '経路計算',
|
|
||||||
'settings.bookingLabels': '予約ルートのラベル',
|
'settings.bookingLabels': '予約ルートのラベル',
|
||||||
'settings.bookingLabelsHint': '地図に駅・空港名を表示。オフ時はアイコンのみ。',
|
'settings.bookingLabelsHint': '地図に駅・空港名を表示。オフ時はアイコンのみ。',
|
||||||
'settings.blurBookingCodes': '予約コードをぼかす',
|
'settings.blurBookingCodes': '予約コードをぼかす',
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ const ko: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': '언어',
|
'settings.language': '언어',
|
||||||
'settings.temperature': '온도 단위',
|
'settings.temperature': '온도 단위',
|
||||||
'settings.timeFormat': '시간 형식',
|
'settings.timeFormat': '시간 형식',
|
||||||
'settings.routeCalculation': '경로 계산',
|
|
||||||
'settings.bookingLabels': '예약 경로 레이블',
|
'settings.bookingLabels': '예약 경로 레이블',
|
||||||
'settings.bookingLabelsHint': '지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.',
|
'settings.bookingLabelsHint': '지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.',
|
||||||
'settings.blurBookingCodes': '예약 코드 흐리게',
|
'settings.blurBookingCodes': '예약 코드 흐리게',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const nl: Record<string, string> = {
|
|||||||
'settings.language': 'Taal',
|
'settings.language': 'Taal',
|
||||||
'settings.temperature': 'Temperatuureenheid',
|
'settings.temperature': 'Temperatuureenheid',
|
||||||
'settings.timeFormat': 'Tijdnotatie',
|
'settings.timeFormat': 'Tijdnotatie',
|
||||||
'settings.routeCalculation': 'Routeberekening',
|
|
||||||
'settings.blurBookingCodes': 'Boekingscodes vervagen',
|
'settings.blurBookingCodes': 'Boekingscodes vervagen',
|
||||||
'settings.notifications': 'Meldingen',
|
'settings.notifications': 'Meldingen',
|
||||||
'settings.notifyTripInvite': 'Reisuitnodigingen',
|
'settings.notifyTripInvite': 'Reisuitnodigingen',
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Język',
|
'settings.language': 'Język',
|
||||||
'settings.temperature': 'Jednostka temperatury',
|
'settings.temperature': 'Jednostka temperatury',
|
||||||
'settings.timeFormat': 'Format czasu',
|
'settings.timeFormat': 'Format czasu',
|
||||||
'settings.routeCalculation': 'Obliczanie trasy',
|
|
||||||
'settings.blurBookingCodes': 'Rozmyj kody rezerwacji',
|
'settings.blurBookingCodes': 'Rozmyj kody rezerwacji',
|
||||||
'settings.notifications': 'Powiadomienia',
|
'settings.notifications': 'Powiadomienia',
|
||||||
'settings.notifyTripInvite': 'Zaproszenia do podróży',
|
'settings.notifyTripInvite': 'Zaproszenia do podróży',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const ru: Record<string, string> = {
|
|||||||
'settings.language': 'Язык',
|
'settings.language': 'Язык',
|
||||||
'settings.temperature': 'Единица температуры',
|
'settings.temperature': 'Единица температуры',
|
||||||
'settings.timeFormat': 'Формат времени',
|
'settings.timeFormat': 'Формат времени',
|
||||||
'settings.routeCalculation': 'Расчёт маршрута',
|
|
||||||
'settings.blurBookingCodes': 'Скрыть коды бронирования',
|
'settings.blurBookingCodes': 'Скрыть коды бронирования',
|
||||||
'settings.notifications': 'Уведомления',
|
'settings.notifications': 'Уведомления',
|
||||||
'settings.notifyTripInvite': 'Приглашения в поездку',
|
'settings.notifyTripInvite': 'Приглашения в поездку',
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ const tr: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.language': 'Dil',
|
'settings.language': 'Dil',
|
||||||
'settings.temperature': 'Sıcaklık birimi',
|
'settings.temperature': 'Sıcaklık birimi',
|
||||||
'settings.timeFormat': 'Saat biçimi',
|
'settings.timeFormat': 'Saat biçimi',
|
||||||
'settings.routeCalculation': 'Route Calculation',
|
|
||||||
'settings.bookingLabels': 'Booking route labels',
|
'settings.bookingLabels': 'Booking route labels',
|
||||||
'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.',
|
'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.',
|
||||||
'settings.blurBookingCodes': 'Blur Booking Codes',
|
'settings.blurBookingCodes': 'Blur Booking Codes',
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ const uk: Record<string, string> = {
|
|||||||
'settings.language': 'Мова',
|
'settings.language': 'Мова',
|
||||||
'settings.temperature': 'Одиниця температури',
|
'settings.temperature': 'Одиниця температури',
|
||||||
'settings.timeFormat': 'Формат часу',
|
'settings.timeFormat': 'Формат часу',
|
||||||
'settings.routeCalculation': 'Розрахунок маршруту',
|
|
||||||
'settings.blurBookingCodes': 'Приховати коди бронювання',
|
'settings.blurBookingCodes': 'Приховати коди бронювання',
|
||||||
'settings.notifications': 'Сповіщення',
|
'settings.notifications': 'Сповіщення',
|
||||||
'settings.notifyTripInvite': 'Запрошення до поїздки',
|
'settings.notifyTripInvite': 'Запрошення до поїздки',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const zh: Record<string, string> = {
|
|||||||
'settings.language': '语言',
|
'settings.language': '语言',
|
||||||
'settings.temperature': '温度单位',
|
'settings.temperature': '温度单位',
|
||||||
'settings.timeFormat': '时间格式',
|
'settings.timeFormat': '时间格式',
|
||||||
'settings.routeCalculation': '路线计算',
|
|
||||||
'settings.blurBookingCodes': '模糊预订代码',
|
'settings.blurBookingCodes': '模糊预订代码',
|
||||||
'settings.notifications': '通知',
|
'settings.notifications': '通知',
|
||||||
'settings.notifyTripInvite': '旅行邀请',
|
'settings.notifyTripInvite': '旅行邀请',
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ const zhTw: Record<string, string> = {
|
|||||||
'settings.language': '語言',
|
'settings.language': '語言',
|
||||||
'settings.temperature': '溫度單位',
|
'settings.temperature': '溫度單位',
|
||||||
'settings.timeFormat': '時間格式',
|
'settings.timeFormat': '時間格式',
|
||||||
'settings.routeCalculation': '路線計算',
|
|
||||||
'settings.blurBookingCodes': '模糊預訂程式碼',
|
'settings.blurBookingCodes': '模糊預訂程式碼',
|
||||||
'settings.notifications': '通知',
|
'settings.notifications': '通知',
|
||||||
'settings.notifyTripInvite': '旅行邀請',
|
'settings.notifyTripInvite': '旅行邀請',
|
||||||
|
|||||||
@@ -857,7 +857,6 @@ describe('DashboardPage', () => {
|
|||||||
temperature_unit: 'fahrenheit',
|
temperature_unit: 'fahrenheit',
|
||||||
time_format: '12h',
|
time_format: '12h',
|
||||||
show_place_description: false,
|
show_place_description: false,
|
||||||
route_calculation: false,
|
|
||||||
blur_booking_codes: false,
|
blur_booking_codes: false,
|
||||||
dashboard_currency: 'on',
|
dashboard_currency: 'on',
|
||||||
dashboard_timezone: 'on',
|
dashboard_timezone: 'on',
|
||||||
|
|||||||
@@ -830,7 +830,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
|||||||
hasInspector={!!selectedPlace}
|
hasInspector={!!selectedPlace}
|
||||||
hasDayDetail={!!showDayDetail && !selectedPlace}
|
hasDayDetail={!!showDayDetail && !selectedPlace}
|
||||||
reservations={reservations}
|
reservations={reservations}
|
||||||
showReservationStats={settings.route_calculation !== false}
|
showReservationStats={true}
|
||||||
visibleConnectionIds={visibleConnections}
|
visibleConnectionIds={visibleConnections}
|
||||||
onReservationClick={(rid) => {
|
onReservationClick={(rid) => {
|
||||||
const r = reservations.find(x => x.id === rid)
|
const r = reservations.find(x => x.id === rid)
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ export interface Settings {
|
|||||||
temperature_unit: string
|
temperature_unit: string
|
||||||
time_format: string
|
time_format: string
|
||||||
show_place_description: boolean
|
show_place_description: boolean
|
||||||
route_calculation?: boolean
|
|
||||||
blur_booking_codes?: boolean
|
blur_booking_codes?: boolean
|
||||||
map_booking_labels?: boolean
|
map_booking_labels?: boolean
|
||||||
map_provider?: 'leaflet' | 'mapbox-gl'
|
map_provider?: 'leaflet' | 'mapbox-gl'
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ export function buildSettings(overrides: Partial<Settings> = {}): Settings {
|
|||||||
temperature_unit: 'fahrenheit',
|
temperature_unit: 'fahrenheit',
|
||||||
time_format: '12h',
|
time_format: '12h',
|
||||||
show_place_description: false,
|
show_place_description: false,
|
||||||
route_calculation: false,
|
|
||||||
blur_booking_codes: false,
|
blur_booking_codes: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { renderHook, act } from '@testing-library/react';
|
import { renderHook, act } from '@testing-library/react';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { useRouteCalculation } from '../../../src/hooks/useRouteCalculation';
|
import { useRouteCalculation } from '../../../src/hooks/useRouteCalculation';
|
||||||
import { useSettingsStore } from '../../../src/store/settingsStore';
|
|
||||||
import { useTripStore } from '../../../src/store/tripStore';
|
import { useTripStore } from '../../../src/store/tripStore';
|
||||||
import { buildAssignment, buildPlace } from '../../helpers/factories';
|
import { buildAssignment, buildPlace } from '../../helpers/factories';
|
||||||
import type { TripStoreState } from '../../../src/store/tripStore';
|
import type { TripStoreState } from '../../../src/store/tripStore';
|
||||||
@@ -47,8 +46,6 @@ const MOCK_ROUTE_WITH_LEGS = {
|
|||||||
describe('useRouteCalculation', () => {
|
describe('useRouteCalculation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Default: route_calculation disabled
|
|
||||||
useSettingsStore.setState({ settings: { route_calculation: false } as any });
|
|
||||||
// Reset trip store assignments so each test starts clean
|
// Reset trip store assignments so each test starts clean
|
||||||
useTripStore.setState({ assignments: {} } as any);
|
useTripStore.setState({ assignments: {} } as any);
|
||||||
(calculateRouteWithLegs as ReturnType<typeof vi.fn>).mockResolvedValue(MOCK_ROUTE_WITH_LEGS);
|
(calculateRouteWithLegs as ReturnType<typeof vi.fn>).mockResolvedValue(MOCK_ROUTE_WITH_LEGS);
|
||||||
@@ -93,9 +90,7 @@ describe('useRouteCalculation', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-004: with route_calculation enabled, calls calculateRouteWithLegs', async () => {
|
it('FE-HOOK-ROUTE-004: calls calculateRouteWithLegs and exposes the returned segments', async () => {
|
||||||
useSettingsStore.setState({ settings: { route_calculation: true } as any });
|
|
||||||
|
|
||||||
const p1 = buildPlace({ lat: 48.8566, lng: 2.3522 });
|
const p1 = buildPlace({ lat: 48.8566, lng: 2.3522 });
|
||||||
const p2 = buildPlace({ lat: 51.5074, lng: -0.1278 });
|
const p2 = buildPlace({ lat: 51.5074, lng: -0.1278 });
|
||||||
const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 });
|
const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 });
|
||||||
@@ -112,28 +107,7 @@ describe('useRouteCalculation', () => {
|
|||||||
expect(result.current.routeSegments).toEqual(MOCK_SEGMENTS);
|
expect(result.current.routeSegments).toEqual(MOCK_SEGMENTS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-005: with route_calculation disabled, does not call calculateRouteWithLegs', async () => {
|
|
||||||
useSettingsStore.setState({ settings: { route_calculation: false } as any });
|
|
||||||
|
|
||||||
const p1 = buildPlace({ lat: 48.8566, lng: 2.3522 });
|
|
||||||
const p2 = buildPlace({ lat: 51.5074, lng: -0.1278 });
|
|
||||||
const a1 = buildAssignment({ day_id: 5, order_index: 0, place: p1 });
|
|
||||||
const a2 = buildAssignment({ day_id: 5, order_index: 1, place: p2 });
|
|
||||||
const store = buildMockStore({ '5': [a1, a2] });
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useRouteCalculation(store as TripStoreState, 5)
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {});
|
|
||||||
|
|
||||||
expect(calculateRouteWithLegs).not.toHaveBeenCalled();
|
|
||||||
expect(result.current.routeSegments).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-006: assignments are sorted by order_index before extracting waypoints', async () => {
|
it('FE-HOOK-ROUTE-006: assignments are sorted by order_index before extracting waypoints', async () => {
|
||||||
useSettingsStore.setState({ settings: { route_calculation: true } as any });
|
|
||||||
|
|
||||||
const p1 = buildPlace({ lat: 10, lng: 10 });
|
const p1 = buildPlace({ lat: 10, lng: 10 });
|
||||||
const p2 = buildPlace({ lat: 20, lng: 20 });
|
const p2 = buildPlace({ lat: 20, lng: 20 });
|
||||||
// order_index 1 comes before 0 in the array, but should be sorted
|
// order_index 1 comes before 0 in the array, but should be sorted
|
||||||
@@ -170,7 +144,6 @@ describe('useRouteCalculation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-008: AbortController.abort() is called when selectedDayId changes', async () => {
|
it('FE-HOOK-ROUTE-008: AbortController.abort() is called when selectedDayId changes', async () => {
|
||||||
useSettingsStore.setState({ settings: { route_calculation: true } as any });
|
|
||||||
|
|
||||||
// Make calculateRouteWithLegs resolve slowly
|
// Make calculateRouteWithLegs resolve slowly
|
||||||
let resolveSegments!: (val: typeof MOCK_ROUTE_WITH_LEGS) => void;
|
let resolveSegments!: (val: typeof MOCK_ROUTE_WITH_LEGS) => void;
|
||||||
@@ -209,7 +182,6 @@ describe('useRouteCalculation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-009: AbortError from calculateSegments does not set routeSegments to []', async () => {
|
it('FE-HOOK-ROUTE-009: AbortError from calculateSegments does not set routeSegments to []', async () => {
|
||||||
useSettingsStore.setState({ settings: { route_calculation: true } as any });
|
|
||||||
|
|
||||||
const abortError = new Error('Aborted');
|
const abortError = new Error('Aborted');
|
||||||
abortError.name = 'AbortError';
|
abortError.name = 'AbortError';
|
||||||
@@ -231,7 +203,6 @@ describe('useRouteCalculation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-010: non-AbortError from calculateSegments sets routeSegments to []', async () => {
|
it('FE-HOOK-ROUTE-010: non-AbortError from calculateSegments sets routeSegments to []', async () => {
|
||||||
useSettingsStore.setState({ settings: { route_calculation: true } as any });
|
|
||||||
|
|
||||||
(calculateRouteWithLegs as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network error'));
|
(calculateRouteWithLegs as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
@@ -282,7 +253,6 @@ describe('useRouteCalculation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('FE-HOOK-ROUTE-013: route recalculates when assignments change via store update', async () => {
|
it('FE-HOOK-ROUTE-013: route recalculates when assignments change via store update', async () => {
|
||||||
useSettingsStore.setState({ settings: { route_calculation: true } as any });
|
|
||||||
|
|
||||||
const p1 = buildPlace({ lat: 10, lng: 10 });
|
const p1 = buildPlace({ lat: 10, lng: 10 });
|
||||||
const p2 = buildPlace({ lat: 20, lng: 20 });
|
const p2 = buildPlace({ lat: 20, lng: 20 });
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const DEFAULTABLE_USER_SETTING_KEYS = [
|
|||||||
'temperature_unit',
|
'temperature_unit',
|
||||||
'dark_mode',
|
'dark_mode',
|
||||||
'time_format',
|
'time_format',
|
||||||
'route_calculation',
|
|
||||||
'blur_booking_codes',
|
'blur_booking_codes',
|
||||||
'map_tile_url',
|
'map_tile_url',
|
||||||
] as const;
|
] as const;
|
||||||
@@ -23,7 +22,7 @@ const VALID_VALUES: Partial<Record<DefaultableKey, unknown[]>> = {
|
|||||||
dark_mode: [true, false, 'light', 'dark', 'auto'],
|
dark_mode: [true, false, 'light', 'dark', 'auto'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const BOOLEAN_KEYS = new Set<DefaultableKey>(['route_calculation', 'blur_booking_codes']);
|
const BOOLEAN_KEYS = new Set<DefaultableKey>(['blur_booking_codes']);
|
||||||
|
|
||||||
function parseValue(raw: string): unknown {
|
function parseValue(raw: string): unknown {
|
||||||
try { return JSON.parse(raw); } catch { return raw; }
|
try { return JSON.parse(raw); } catch { return raw; }
|
||||||
|
|||||||
Reference in New Issue
Block a user