mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(tours): give a tour its start and end in the day plan
A tour on a day had no place in the day's timeline: its time was not tied to the slot it was planned in, so several hikes on one day could not be lined up. A tour now sits there the way a place does, on the times of its day entry, one model for every tour type. With a start and a known time (the planned total, else the estimate of an import) its end follows as the start plus that time; it is shown fainter, marked with an approximate sign where it comes from an estimate, and says in its tooltip how it was worked out. An end typed by hand wins. A day that holds one stage of a multi-day tour gets no end from the whole route. When the end runs past the start of the day's next timed item, the row says so in one quiet line, as a late connection between two city walks does; nothing is moved. The existing time fields of the day entry, on the desktop form and the phone sheet, say what the end works out to while none is typed. City walks keep their arrivals per station. The logic lives in one module both day plans run on.
This commit is contained in:
@@ -208,6 +208,7 @@ export default function MPlanTimeline({ planner, shell }: MPlanTimelineProps) {
|
||||
onOpen={() => openPlace(row.assignment)}
|
||||
onEdit={canEditPlaces ? () => tl.editAssignment(row.assignment) : undefined}
|
||||
onRemove={() => tl.removeAssignment(row.assignment)}
|
||||
overrun={planner.toursEnabled ? tl.tourOverrun(row.item) : null}
|
||||
/>
|
||||
{dayScheduleFor('assignment', row.assignment.id)}
|
||||
</Fragment>
|
||||
|
||||
@@ -73,6 +73,20 @@ describe('RC-01 mobile PLAN Tour rows', () => {
|
||||
expect(onOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('TIME-02-019: a tour with a start shows its end worked out in the time chip, and its overrun below', () => {
|
||||
const assignment = buildAssignment({
|
||||
id: 9,
|
||||
day_id: 1,
|
||||
assignment_time: '09:00',
|
||||
place: { ...place, place_time: '09:00' },
|
||||
});
|
||||
const overrun = { name: 'Lunch', time: '10:00', end: '10:03' };
|
||||
render(<PlaceRow {...props} assignment={assignment} tour={tour} overrun={overrun} />);
|
||||
// 53 min walking and 10 min of breaks.
|
||||
expect(screen.getByTestId('tour-day-end-worked-out')).toHaveTextContent('10:03');
|
||||
expect(screen.getByTestId('tour-day-overrun')).toHaveTextContent('Ends at 10:03, after Lunch starts at 10:00');
|
||||
});
|
||||
|
||||
it('CW-03: a hike on a day with marks shows no progress', () => {
|
||||
useTourVisitStore.setState({ tripId: '1', byAssignment: { 9: { 'stop-01': 'visited' } } });
|
||||
render(<PlaceRow {...props} tour={tour} />);
|
||||
|
||||
@@ -20,6 +20,8 @@ import type { PluginDayScheduleItem } from '../../../../api/client'
|
||||
import type { TourListItem } from '@trek/shared'
|
||||
import TourDayRowFacts, { TourDayRowIcon } from '../../../../components/Tours/TourDayRowFacts'
|
||||
import { WalkLateLine } from '../../../../components/Tours/walk/WalkTransitNote'
|
||||
import { TourDayTimeText, TourOverrunLine } from '../../../../components/Tours/TourDayTimeParts'
|
||||
import { tourDayEntryOf, type TourOverrun } from '../../../../components/Tours/tourDayTime'
|
||||
import type { WalkLate } from '../../../../components/Tours/walk/walkTransit'
|
||||
|
||||
/**
|
||||
@@ -124,10 +126,12 @@ const ROW_CODE = 'flex-none whitespace-nowrap font-geist text-[0.71875rem] text-
|
||||
|
||||
// ── b3) Place row ────────────────────────────────────────────────────────────
|
||||
|
||||
export function PlaceRow({ assignment, fullPlace, tour, linkedReservations, chrome, reorder, drag, onOpen, onEdit, onRemove }: {
|
||||
export function PlaceRow({ assignment, fullPlace, tour, linkedReservations, chrome, reorder, drag, onOpen, onEdit, onRemove, overrun }: {
|
||||
assignment: Assignment
|
||||
fullPlace: Place | undefined
|
||||
tour?: TourListItem
|
||||
/** TIME-02: the day's next timed item this tour runs into, or null. */
|
||||
overrun?: TourOverrun | null
|
||||
linkedReservations: Reservation[]
|
||||
chrome: RowChrome
|
||||
reorder: ReactNode
|
||||
@@ -205,7 +209,7 @@ export function PlaceRow({ assignment, fullPlace, tour, linkedReservations, chro
|
||||
{tour && <div className="mt-[2px]"><TourDayRowFacts tour={tour} assignmentId={assignment.id} mobile /></div>}
|
||||
{(time || sub) && (
|
||||
<div className="mt-[2px] flex min-w-0 items-center gap-1.5">
|
||||
{time && <span className={TIME_CHIP}>{time}</span>}
|
||||
{time && <span className={TIME_CHIP}>{tour ? <TourDayTimeText entry={tourDayEntryOf(assignment)} tour={tour} mobile /> : time}</span>}
|
||||
{sub && <MarkdownText clamp className="min-w-0 font-geist text-[0.71875rem] text-m-muted">{sub}</MarkdownText>}
|
||||
{bookingLines[0]?.code && <BlurredCode interactive={false} className={ROW_CODE}>{bookingLines[0].code}</BlurredCode>}
|
||||
</div>
|
||||
@@ -225,6 +229,7 @@ export function PlaceRow({ assignment, fullPlace, tour, linkedReservations, chro
|
||||
<MarkdownText clamp className="min-w-0 font-geist text-[0.65625rem] text-m-faint">{assignment.notes}</MarkdownText>
|
||||
</div>
|
||||
)}
|
||||
<TourOverrunLine overrun={overrun} />
|
||||
</div>
|
||||
{chrome.editing && (
|
||||
<span className="flex flex-none items-center gap-1.5">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getDisplayTimeForDay, getMergedItems, getTransportForDay, hasCarrierEnd
|
||||
import { dayCoMapsUrl, dayExportStops, dayGoogleMapsUrl, optimizeDayOrder, type DayCarrier } from '../../../../components/Planner/dayRoute'
|
||||
import type { PlannedTransitLeg, TransitLeg } from '../../../../components/Planner/transitLeg'
|
||||
import { useTransitLegPlanner } from '../../../../components/Planner/useTransitLegPlanner'
|
||||
import { dayItemOverrun } from '../../../../components/Tours/tourDayTime'
|
||||
import {
|
||||
buildPlanRows, breaksChronology, findUpNext, hotelChipsForDay, hotelLegsForDay, itemHasTime,
|
||||
type HotelLegs, type PlanRow, type TransportEntry,
|
||||
@@ -377,6 +378,9 @@ export function useMPlanTimeline(planner: TripPlanner) {
|
||||
// A saved journey that now arrives after the walk it leads to starts (CW-08).
|
||||
const journeyLate = useCallback((item: MergedItem) =>
|
||||
(day ? lateBefore(day, item.data, merged.slice(merged.indexOf(item) + 1)) : null), [day, lateBefore, merged])
|
||||
// A tour whose end runs past the start of the day's next timed item (TIME-02).
|
||||
const tourOverrun = useCallback((item: MergedItem) =>
|
||||
(day ? dayItemOverrun(merged, merged.indexOf(item), day.id, tourOf) : null), [day, merged, tourOf])
|
||||
|
||||
return {
|
||||
day, rows, hotelLegs, merged, hotelChips, weather, weatherTemp, upNext,
|
||||
@@ -387,7 +391,7 @@ export function useMPlanTimeline(planner: TripPlanner) {
|
||||
moveRowTo,
|
||||
addPlace, addBooking, addTransport,
|
||||
optimize, canExportRoute, exportGoogleMaps, exportCoMaps, renameDay, fullPlaceOf,
|
||||
routeModeOptions, setLegMode, transitLegFor, planTransitLeg, journeyLate,
|
||||
routeModeOptions, setLegMode, transitLegFor, planTransitLeg, journeyLate, tourOverrun,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Eyebrow } from './PlSheetChrome'
|
||||
import CustomTimePicker from '../../../../components/shared/CustomTimePicker'
|
||||
import { timeCollisions } from '../../../../components/Planner/PlaceFormModal.helpers'
|
||||
import type { Assignment } from '../../../../types'
|
||||
import { TourEndHint } from '../../../../components/Tours/TourDayTimeParts'
|
||||
import type { TripPlanner } from '../MTripShell'
|
||||
|
||||
interface PlTimeFieldsProps {
|
||||
@@ -39,6 +40,9 @@ export default function PlTimeFields({
|
||||
() => timeCollisions(assignmentId, dayAssignments, startTime, endTime),
|
||||
[assignmentId, dayAssignments, startTime, endTime],
|
||||
)
|
||||
// TIME-02: a tour's end follows from its start and its time while none is typed.
|
||||
const assignment = dayAssignments.find(a => a.id === assignmentId)
|
||||
const tour = assignment ? planner.tours.find(item => item.place_id === assignment.place_id) : undefined
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
@@ -55,6 +59,7 @@ export default function PlTimeFields({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TourEndHint start={startTime} end={endTime} assignment={assignment} tour={tour} mobile />
|
||||
{hasTimeError && (
|
||||
<div className={WARNING_CLS}>
|
||||
<AlertTriangle size={13} strokeWidth={2} className="mt-px flex-none" />
|
||||
|
||||
Reference in New Issue
Block a user