mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(weather): day-local named anchor, cache ttl and past-days forecast window (#2167)
This commit is contained in:
@@ -56,9 +56,11 @@ interface DayDetailPanelProps {
|
||||
mobile?: boolean
|
||||
/** Rename the day from here — the sidebar pencil moved to the transit search (#1065). */
|
||||
onUpdateDayTitle?: (dayId: number, title: string) => void
|
||||
/** Name of the place the day's weather is anchored to — captioned above the forecast (#2167). */
|
||||
weatherPlaceName?: string | null
|
||||
}
|
||||
|
||||
export default function DayDetailPanel({ day, days, places, categories = [], tripId, assignments, reservations = [], lat, lng, onClose, onAccommodationChange, leftWidth = 0, rightWidth = 0, collapsed: collapsedProp = false, onToggleCollapse, mobile = false, onUpdateDayTitle }: DayDetailPanelProps) {
|
||||
export default function DayDetailPanel({ day, days, places, categories = [], tripId, assignments, reservations = [], lat, lng, onClose, onAccommodationChange, leftWidth = 0, rightWidth = 0, collapsed: collapsedProp = false, onToggleCollapse, mobile = false, onUpdateDayTitle, weatherPlaceName = null }: DayDetailPanelProps) {
|
||||
const { t, language, locale } = useTranslation()
|
||||
const can = useCanDo()
|
||||
const tripObj = useTripStore((s) => s.trip)
|
||||
@@ -211,6 +213,13 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
</div>
|
||||
) : weather ? (
|
||||
<div>
|
||||
{/* Which place the forecast is for — on a roadtrip "the day's weather"
|
||||
is ambiguous without it (#2167). */}
|
||||
{weatherPlaceName && (
|
||||
<div className="text-content-faint" style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 6 }}>
|
||||
{t('day.weatherFor', { name: weatherPlaceName })}
|
||||
</div>
|
||||
)}
|
||||
{/* Summary row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}>
|
||||
<div style={{ width: 40, height: 40, borderRadius: 12, background: 'var(--bg-secondary)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
|
||||
@@ -87,7 +87,9 @@ vi.mock('../../hooks/useDayNotes', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../Weather/WeatherWidget', () => ({
|
||||
default: () => <span data-testid="weather-widget" />,
|
||||
default: (props: { locationName?: string | null }) => (
|
||||
<span data-testid="weather-widget" data-location={props.locationName ?? ''} />
|
||||
),
|
||||
}))
|
||||
|
||||
// A stable toast object so tests can assert on the messages the sidebar raises.
|
||||
@@ -2896,11 +2898,34 @@ describe('DayPlanSidebar', () => {
|
||||
expect(onReorder).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-130: the weather badge falls back to any located trip place', () => {
|
||||
// #2167 — the badge used to borrow ANY located trip place, so a roadtrip day
|
||||
// without stops silently showed another city's weather. Day-local only now.
|
||||
it('FE-PLANNER-DAYPLAN-130: no weather badge for a day without located stops or a bookend hotel (#2167)', () => {
|
||||
const located = buildPlace({ id: 5, name: 'Somewhere', lat: 48.85, lng: 2.35 })
|
||||
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||
render(<DayPlanSidebar {...makeDefaultProps({ days: [day], places: [located] })} />)
|
||||
expect(screen.getByTestId('weather-widget')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('weather-widget')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-130b: the weather badge anchors to the day bookend hotel, independent of the optimize setting (#2167)', () => {
|
||||
// The hotel fallback is unconditional (matching the mobile timeline) — turn the
|
||||
// route-optimization setting OFF to prove the weather anchor ignores it.
|
||||
seedStore(useSettingsStore, { settings: { time_format: '24h', temperature_unit: 'celsius', optimize_from_accommodation: false } } as any)
|
||||
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||
const day2 = buildDay({ id: 11, date: '2025-06-02', title: 'Day 2' })
|
||||
const accommodations = [{ id: 1, start_day_id: 10, end_day_id: 11, place_name: 'Hotel Roma', place_lat: 41.9, place_lng: 12.5 }]
|
||||
render(<DayPlanSidebar {...makeDefaultProps({ days: [day, day2], accommodations: accommodations as any })} />)
|
||||
const widgets = screen.getAllByTestId('weather-widget')
|
||||
expect(widgets.length).toBeGreaterThan(0)
|
||||
expect(widgets[0]).toHaveAttribute('data-location', 'Hotel Roma')
|
||||
})
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-130c: the weather badge names the day-local stop it is anchored to (#2167)', () => {
|
||||
const place = buildPlace({ id: 5, name: 'Louvre', lat: 48.86, lng: 2.34 })
|
||||
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||
const a = buildAssignment({ id: 99, day_id: 10, order_index: 0, place })
|
||||
render(<DayPlanSidebar {...makeDefaultProps({ days: [day], places: [place], assignments: { '10': [a] } })} />)
|
||||
expect(screen.getByTestId('weather-widget')).toHaveAttribute('data-location', 'Louvre')
|
||||
})
|
||||
|
||||
it('FE-PLANNER-DAYPLAN-131: a read-only trip drops the edit affordances from day and note rows', () => {
|
||||
|
||||
@@ -1043,10 +1043,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
||||
return formatMoneySum(entries, costBase, locale, fxRates)
|
||||
}, [days, assignments, currency, costBase, locale, fxRates])
|
||||
|
||||
// Bester verfügbarer Standort für Wetter: zugewiesene Orte zuerst, dann beliebiger Reiseort
|
||||
const anyGeoAssignment = Object.values(assignments).flatMap(da => da).find(a => a.place?.lat && a.place?.lng)
|
||||
const anyGeoPlace = anyGeoAssignment || (places || []).find(p => p.lat && p.lng)
|
||||
|
||||
return {
|
||||
tripId,
|
||||
trip,
|
||||
@@ -1182,8 +1178,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
||||
handleOptimize,
|
||||
handleDropOnDay,
|
||||
totalCostLabel,
|
||||
anyGeoAssignment,
|
||||
anyGeoPlace,
|
||||
expandedRouteDayIds,
|
||||
setExpandedRouteDayIds,
|
||||
}
|
||||
@@ -1355,8 +1349,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
handleOptimize,
|
||||
handleDropOnDay,
|
||||
totalCostLabel,
|
||||
anyGeoAssignment,
|
||||
anyGeoPlace,
|
||||
expandedRouteDayIds,
|
||||
setExpandedRouteDayIds,
|
||||
} = S
|
||||
@@ -1554,7 +1546,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
// routable when accommodation optimization can bookend it with a hotel
|
||||
// (hotel → place → hotel, the same line the map draws) — otherwise the tools
|
||||
// vanish on such a day (#1330). Purely additive to the 2+ case.
|
||||
const routeBookends = optimizeFromAccommodation !== false ? getDayBookendHotels(day, days, accommodations) : null
|
||||
// The weather anchor below must not depend on the optimize-from-accommodation
|
||||
// setting — where you wake up is a fact about the day, not a routing choice —
|
||||
// so the bookend lookup runs unconditionally (mirrors useMPlanTimeline) while
|
||||
// the route tools keep honoring the setting via routeBookends.
|
||||
const dayBookends = getDayBookendHotels(day, days, accommodations)
|
||||
const routeBookends = optimizeFromAccommodation !== false ? dayBookends : null
|
||||
const hasRouteBookend = !!(
|
||||
(routeBookends?.morning?.place_lat != null && routeBookends?.morning?.place_lng != null) ||
|
||||
(routeBookends?.evening?.place_lat != null && routeBookends?.evening?.place_lng != null)
|
||||
@@ -1668,12 +1665,14 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
>
|
||||
{/* Tages-Badge: Nummer oben, darunter (falls vorhanden) das Wetter des Tages */}
|
||||
{(() => {
|
||||
// anyGeoPlace is an assignment (has .place) or a bare place — read coords from either.
|
||||
const geoLat = anyGeoPlace ? ('place' in anyGeoPlace ? anyGeoPlace.place?.lat : anyGeoPlace.lat) : undefined
|
||||
const geoLng = anyGeoPlace ? ('place' in anyGeoPlace ? anyGeoPlace.place?.lng : anyGeoPlace.lng) : undefined
|
||||
const wLat = loc?.place?.lat ?? geoLat
|
||||
const wLng = loc?.place?.lng ?? geoLng
|
||||
const hasWeather = !!(day.date && anyGeoPlace && wLat != null && wLng != null)
|
||||
// Day-local anchor only (#2167): the day's first located stop, else the
|
||||
// hotel you wake up in. No trip-wide fallback — on a roadtrip that
|
||||
// silently showed another city's weather with nothing naming the place.
|
||||
const weatherHotel = loc == null ? dayBookends.morning : undefined
|
||||
const wLat = loc?.place?.lat ?? weatherHotel?.place_lat
|
||||
const wLng = loc?.place?.lng ?? weatherHotel?.place_lng
|
||||
const weatherName = loc?.place?.name ?? weatherHotel?.place_name ?? null
|
||||
const hasWeather = !!(day.date && wLat != null && wLng != null)
|
||||
return (
|
||||
<div style={{
|
||||
// With weather the badge is a tall stack and has to start at
|
||||
@@ -1700,7 +1699,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
<>
|
||||
<div style={{ width: '64%', height: 1, background: 'currentColor', opacity: 0.25 }} />
|
||||
<div style={{ padding: '3px 0 4px' }}>
|
||||
<WeatherWidget lat={wLat} lng={wLng} date={day.date} stacked />
|
||||
<WeatherWidget lat={wLat ?? null} lng={wLng ?? null} date={day.date} stacked locationName={weatherName} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user