mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(share): a stop's stay comes from its time window, not the one-hour place default
The public page showed places.duration_minutes under every stop, and that is 60 on every place nobody set a road-trip stay for, so a shared plan read "1 h" everywhere. The stay is now the stop's start to end time, per visit, past midnight included, and a stop without an end shows none. Closes #2676
This commit is contained in:
@@ -1586,8 +1586,9 @@ describe('SharedTripPage', () => {
|
||||
id: 301, day_id: 21, order_index: 0, notes: 'go early, before the coaches',
|
||||
place: {
|
||||
id: 501, name: 'Louvre', description: 'The big one', address: 'Rue de Rivoli, Paris',
|
||||
lat: 48.86, lng: 2.34, category_id: null, price: null, place_time: '09:00', end_time: null,
|
||||
duration_minutes: 150, notes: 'Skip the pyramid queue', website: 'https://louvre.fr',
|
||||
// The stay comes from the window; the place default of an hour is not a plan (#2676).
|
||||
lat: 48.86, lng: 2.34, category_id: null, price: null, place_time: '09:00', end_time: '11:30',
|
||||
duration_minutes: 60, notes: 'Skip the pyramid queue', website: 'https://louvre.fr',
|
||||
phone: '+33 1 40 20 50 50', image_url: null, transport_mode: 'walking', category: null, tags: [],
|
||||
},
|
||||
},
|
||||
@@ -1639,6 +1640,23 @@ describe('SharedTripPage', () => {
|
||||
expect(document.querySelector('a[href^="javascript:"]')).toBeNull();
|
||||
expect(screen.queryByText(/min$/)).toBeNull();
|
||||
});
|
||||
|
||||
it('shows no stay for a stop with only a start, whatever the place default says (#2676)', async () => {
|
||||
const startOnly = {
|
||||
21: [{
|
||||
id: 303, day_id: 21, order_index: 0, notes: null,
|
||||
place: {
|
||||
id: 503, name: 'Café', description: null, address: 'Somewhere 1', lat: null, lng: null,
|
||||
duration_minutes: 60, notes: null, website: null, phone: null,
|
||||
category_id: null, price: null, place_time: '15:00', end_time: null, image_url: null,
|
||||
transport_mode: 'walking', category: null, tags: [],
|
||||
},
|
||||
}],
|
||||
};
|
||||
await open('start-only-token', payload({ days, assignments: startOnly }));
|
||||
await waitFor(() => expect(screen.getByText('Somewhere 1')).toBeInTheDocument());
|
||||
expect(screen.queryByText('1 h')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-SHARED-039: a booking shows its note and its link, and nothing that is not http(s) (#2320)', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fs } from '../../components/shared/DialogShell'
|
||||
import { Tooltip } from '../../components/shared/Tooltip'
|
||||
import { WhiteBadge } from '../../components/Planner/planParts'
|
||||
import { getGoogleMapsUrlForPlace } from '../../components/Planner/placeGoogleMaps'
|
||||
import { formatDurationMinutes, isHttpUrl } from './sharedTripModel'
|
||||
import { formatDurationMinutes, isHttpUrl, plannedStayMinutes } from './sharedTripModel'
|
||||
|
||||
/**
|
||||
* The read-only detail of one stop on a shared plan (#2320).
|
||||
@@ -14,7 +14,8 @@ import { formatDurationMinutes, isHttpUrl } from './sharedTripModel'
|
||||
* usually also left a note on the place and another on the day it was
|
||||
* planned for, and a link would show none of that. This shows what the owner
|
||||
* chose to share, laid out the way the planner lays it out: address, then
|
||||
* description, then the two notes, then how long they meant to stay, then
|
||||
* description, then the two notes, then how long they meant to stay (from the
|
||||
* stop's start and end time), then
|
||||
* the ways to reach the place.
|
||||
*
|
||||
* Nothing here is editable and nothing here is a route into the app: every
|
||||
@@ -46,7 +47,7 @@ export function SharedPlaceDetails({ place, assignmentNotes }: { place: SharedPl
|
||||
name: place.name, address: place.address ?? null, lat: place.lat ?? null, lng: place.lng ?? null,
|
||||
google_place_id: null, google_ftid: null,
|
||||
})
|
||||
const duration = formatDurationMinutes(place.duration_minutes)
|
||||
const duration = formatDurationMinutes(plannedStayMinutes(place.place_time, place.end_time))
|
||||
const dayNote = assignmentNotes?.trim() || null
|
||||
const placeNote = place.notes?.trim() || null
|
||||
const hasLinks = !!(website || phone || mapsUrl)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { AxiosError, AxiosHeaders } from 'axios'
|
||||
import {
|
||||
coverSrc, dayHasEntries, formatDateRange, formatDurationMinutes, groupInOrder, isHttpUrl, isSharedTripPayload, legFacts, linkHost,
|
||||
coverSrc, dayHasEntries, formatDateRange, formatDurationMinutes, groupInOrder, isHttpUrl, isSharedTripPayload, legFacts, linkHost, plannedStayMinutes,
|
||||
sharedTripLoadError, spanLabelKey, stopNumbers, transportFacts, unplannedPlaces,
|
||||
} from './sharedTripModel'
|
||||
|
||||
@@ -25,6 +25,15 @@ describe('sharedTripModel (#2320)', () => {
|
||||
expect(formatDurationMinutes(89.6)).toBe('1 h 30 min')
|
||||
})
|
||||
|
||||
it('reads the planned stay off the time window, never off the place default (#2676)', () => {
|
||||
expect(plannedStayMinutes('10:00', '12:30')).toBe(150)
|
||||
expect(plannedStayMinutes('22:30', '01:00')).toBe(150)
|
||||
expect(plannedStayMinutes('10:00', null)).toBeNull()
|
||||
expect(plannedStayMinutes(null, '12:00')).toBeNull()
|
||||
expect(plannedStayMinutes('10:00', '10:00')).toBeNull()
|
||||
expect(plannedStayMinutes(undefined, undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('writes nothing for a missing, zero or nonsense figure', () => {
|
||||
expect(formatDurationMinutes(null)).toBeNull()
|
||||
expect(formatDurationMinutes(undefined)).toBeNull()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* page and its detail blocks can share them and a test can drive them
|
||||
* without rendering.
|
||||
*/
|
||||
import { parseTimeToMinutes } from '../../utils/dayMerge'
|
||||
|
||||
/**
|
||||
* Whether a string is a link the page may render as one.
|
||||
@@ -22,6 +23,22 @@ export function isHttpUrl(value: unknown): value is string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the owner planned to stay, read off the stop's time window.
|
||||
*
|
||||
* Not `places.duration_minutes`: every place carries one, 60 unless someone
|
||||
* set a road-trip stay, so a link showed "1 h" under every stop (#2676). The
|
||||
* window is what the owner actually planned, and it can differ per visit.
|
||||
* Without an end there is no stay to show. An end before the start runs past
|
||||
* midnight.
|
||||
*/
|
||||
export function plannedStayMinutes(start: string | null | undefined, end: string | null | undefined): number | null {
|
||||
const from = parseTimeToMinutes(start)
|
||||
const to = parseTimeToMinutes(end)
|
||||
if (from == null || to == null || from === to) return null
|
||||
return to > from ? to - from : to + 24 * 60 - from
|
||||
}
|
||||
|
||||
/**
|
||||
* "45 min", "2 h", "1 h 30 min" — the planned time at a place.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user