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',
|
id: 301, day_id: 21, order_index: 0, notes: 'go early, before the coaches',
|
||||||
place: {
|
place: {
|
||||||
id: 501, name: 'Louvre', description: 'The big one', address: 'Rue de Rivoli, Paris',
|
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,
|
// The stay comes from the window; the place default of an hour is not a plan (#2676).
|
||||||
duration_minutes: 150, notes: 'Skip the pyramid queue', website: 'https://louvre.fr',
|
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: [],
|
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(document.querySelector('a[href^="javascript:"]')).toBeNull();
|
||||||
expect(screen.queryByText(/min$/)).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)', () => {
|
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 { Tooltip } from '../../components/shared/Tooltip'
|
||||||
import { WhiteBadge } from '../../components/Planner/planParts'
|
import { WhiteBadge } from '../../components/Planner/planParts'
|
||||||
import { getGoogleMapsUrlForPlace } from '../../components/Planner/placeGoogleMaps'
|
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).
|
* 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
|
* 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
|
* 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
|
* 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.
|
* the ways to reach the place.
|
||||||
*
|
*
|
||||||
* Nothing here is editable and nothing here is a route into the app: every
|
* 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,
|
name: place.name, address: place.address ?? null, lat: place.lat ?? null, lng: place.lng ?? null,
|
||||||
google_place_id: null, google_ftid: 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 dayNote = assignmentNotes?.trim() || null
|
||||||
const placeNote = place.notes?.trim() || null
|
const placeNote = place.notes?.trim() || null
|
||||||
const hasLinks = !!(website || phone || mapsUrl)
|
const hasLinks = !!(website || phone || mapsUrl)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { AxiosError, AxiosHeaders } from 'axios'
|
import { AxiosError, AxiosHeaders } from 'axios'
|
||||||
import {
|
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,
|
sharedTripLoadError, spanLabelKey, stopNumbers, transportFacts, unplannedPlaces,
|
||||||
} from './sharedTripModel'
|
} from './sharedTripModel'
|
||||||
|
|
||||||
@@ -25,6 +25,15 @@ describe('sharedTripModel (#2320)', () => {
|
|||||||
expect(formatDurationMinutes(89.6)).toBe('1 h 30 min')
|
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', () => {
|
it('writes nothing for a missing, zero or nonsense figure', () => {
|
||||||
expect(formatDurationMinutes(null)).toBeNull()
|
expect(formatDurationMinutes(null)).toBeNull()
|
||||||
expect(formatDurationMinutes(undefined)).toBeNull()
|
expect(formatDurationMinutes(undefined)).toBeNull()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* page and its detail blocks can share them and a test can drive them
|
* page and its detail blocks can share them and a test can drive them
|
||||||
* without rendering.
|
* without rendering.
|
||||||
*/
|
*/
|
||||||
|
import { parseTimeToMinutes } from '../../utils/dayMerge'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a string is a link the page may render as one.
|
* 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.
|
* "45 min", "2 h", "1 h 30 min" — the planned time at a place.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user