From 2449616ee9bd6f6205c8dfe66e29fb35232db098 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 20 Oct 2026 12:39:08 +0200 Subject: [PATCH] feat(tours): plan a multi-day tour as one route split into stages A hut-to-hut hike or a paddle trip over several days is now planned in the Tours planner as one tour: the whole route is planned as before, and stops along it are marked as stage ends (MDT-01). The stages are read off that one route in its order, never kept as copies, so a change inside a stage changes the whole tour and every figure on it. - A Day tour / Multi-day tour switch beside the tour type; a stop's menu ends a stage there or lets it go on. Back to a day tour removes every stage end after a confirmation, as one step undo brings back. - The list of points is headed per stage with its distance and moving time; a stop that ends a stage carries a moon at its number, on the map too. - The overview lists the stages from stop to stop with distance, climb, descent and time (hiking rule per stage, paddle estimate per stage, the router's time shared by distance otherwise), marks where each ends across the elevation profile, and a stage of more than a day on the move warns. - A stage end a change cannot keep (its stop removed, moved to an end or made a shaping point) is dropped and said so, with undo, never moved to another point. - A multi-day tour may take a day a stage, so its time limit grows with its stages; the tour keeps its one type, portages included. - The list and the MCP tools show the stage count and where stages end. Stage ends are a flag on the waypoint (migration 046000); a tour without one is a day tour, as every tour saved before. --- .../components/Map/PlannerWaypointMarker.tsx | 10 +- client/src/components/Map/mapViewContract.ts | 2 + .../components/Map/plannerPinShape.test.ts | 12 +- client/src/components/Map/plannerPinShape.ts | 18 ++ .../Map/usePlannerWaypointMarkersGL.test.ts | 35 +++ .../Map/usePlannerWaypointMarkersGL.ts | 5 +- client/src/components/Tours/TourListRow.tsx | 2 + .../Tours/planner/TourPlannerPanels.tsx | 10 + .../Tours/planner/TourWaypointList.test.tsx | 49 +++- .../Tours/planner/TourWaypointList.tsx | 28 +- .../Tours/planner/tourPlannerDraft.ts | 3 +- .../Tours/planner/tourPlannerFacts.ts | 5 +- .../Tours/planner/tourPlannerParts.test.ts | 9 + .../planner/tourPlannerWaypoints.test.ts | 24 +- .../Tours/planner/tourPlannerWaypoints.ts | 34 ++- .../planner/useTourPlanner.stages.test.tsx | 232 +++++++++++++++ .../Tours/planner/useTourPlanner.ts | 35 ++- .../components/Tours/stages/StageParts.tsx | 143 +++++++++ .../Tours/stages/TourStageLossNotice.tsx | 32 ++ .../Tours/stages/TourStageModeField.tsx | 64 ++++ .../Tours/stages/TourStagesSection.tsx | 91 ++++++ .../Tours/stages/stageViews.test.tsx | 178 ++++++++++++ .../Tours/stages/tourStages.test.ts | 157 ++++++++++ .../src/components/Tours/stages/tourStages.ts | 274 ++++++++++++++++++ .../components/Tours/stages/useTourStages.ts | 79 +++++ .../components/shared/ElevationProfile.tsx | 8 +- .../tripPlanner/useTourMapPoints.test.tsx | 56 ++++ .../src/pages/tripPlanner/useTourMapPoints.ts | 6 +- client/tests/helpers/tourPlannerStub.ts | 10 + .../src/db/entities/TourWaypoints.entity.ts | 3 + server/src/db/kysely/TourWaypoints.table.ts | 1 + ...1046000_a_tour_waypoint_can_end_a_stage.ts | 17 ++ .../repositories/TourWaypoints.repository.ts | 6 + .../src/db/repositories/Tours.repository.ts | 13 +- server/src/nest/tours/tours.mcp.ts | 4 +- server/src/nest/tours/tours.service.ts | 26 +- server/tests/e2e/tours.e2e.test.ts | 44 +++ server/tests/helpers/factories/tours.ts | 4 +- server/tests/unit/db/legacy-baseline.test.ts | 1 + .../TourWaypoints.repository.test.ts | 15 + .../db/repositories/Tours.repository.test.ts | 30 +- server/tests/unit/nest/tours.mcp.test.ts | 28 ++ server/tests/unit/nest/tours.service.test.ts | 66 +++++ shared/src/i18n/ar/tours.ts | 34 +++ shared/src/i18n/az/tours.ts | 22 ++ shared/src/i18n/br/tours.ts | 23 ++ shared/src/i18n/ca/tours.ts | 23 ++ shared/src/i18n/cs/tours.ts | 25 ++ shared/src/i18n/de/tours.ts | 26 ++ shared/src/i18n/en/tours.ts | 22 ++ shared/src/i18n/es/tours.ts | 22 ++ shared/src/i18n/et/tours.ts | 22 ++ shared/src/i18n/fr/tours.ts | 23 ++ shared/src/i18n/gr/tours.ts | 23 ++ shared/src/i18n/hu/tours.ts | 23 ++ shared/src/i18n/id/tours.ts | 19 ++ shared/src/i18n/it/tours.ts | 22 ++ shared/src/i18n/ja/tours.ts | 18 ++ shared/src/i18n/ko/tours.ts | 18 ++ shared/src/i18n/nl/tours.ts | 24 ++ shared/src/i18n/pl/tours.ts | 28 ++ shared/src/i18n/ru/tours.ts | 28 ++ shared/src/i18n/sk/tours.ts | 25 ++ shared/src/i18n/sv/tours.ts | 22 ++ shared/src/i18n/th/tours.ts | 19 ++ shared/src/i18n/tours-locales.spec.ts | 2 +- shared/src/i18n/tr/tours.ts | 22 ++ shared/src/i18n/uk/tours.ts | 28 ++ shared/src/i18n/vi/tours.ts | 19 ++ shared/src/i18n/zh-TW/tours.ts | 18 ++ shared/src/i18n/zh/tours.ts | 18 ++ shared/src/index.ts | 1 + shared/src/tours/tour-stages.spec.ts | 135 +++++++++ shared/src/tours/tour-stages.ts | 57 ++++ shared/src/tours/tours.schema.ts | 40 ++- wiki/MCP-Addon-Tools.md | 4 +- 76 files changed, 2669 insertions(+), 55 deletions(-) create mode 100644 client/src/components/Map/usePlannerWaypointMarkersGL.test.ts create mode 100644 client/src/components/Tours/planner/useTourPlanner.stages.test.tsx create mode 100644 client/src/components/Tours/stages/StageParts.tsx create mode 100644 client/src/components/Tours/stages/TourStageLossNotice.tsx create mode 100644 client/src/components/Tours/stages/TourStageModeField.tsx create mode 100644 client/src/components/Tours/stages/TourStagesSection.tsx create mode 100644 client/src/components/Tours/stages/stageViews.test.tsx create mode 100644 client/src/components/Tours/stages/tourStages.test.ts create mode 100644 client/src/components/Tours/stages/tourStages.ts create mode 100644 client/src/components/Tours/stages/useTourStages.ts create mode 100644 client/src/pages/tripPlanner/useTourMapPoints.test.tsx create mode 100644 server/src/db/migrations/Migration20200101046000_a_tour_waypoint_can_end_a_stage.ts create mode 100644 shared/src/tours/tour-stages.spec.ts create mode 100644 shared/src/tours/tour-stages.ts diff --git a/client/src/components/Map/PlannerWaypointMarker.tsx b/client/src/components/Map/PlannerWaypointMarker.tsx index d620c583e..0af95908a 100644 --- a/client/src/components/Map/PlannerWaypointMarker.tsx +++ b/client/src/components/Map/PlannerWaypointMarker.tsx @@ -2,7 +2,7 @@ import L from 'leaflet'; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Marker, Tooltip, useMap } from 'react-leaflet'; import { MAP_LABEL_BADGE_CLASS } from './mapHoverLabel'; -import { plannerPinShape, type PlannerPinPoint } from './plannerPinShape'; +import { plannerPinBadge, plannerPinShape, type PlannerPinPoint } from './plannerPinShape'; import { wirePlannerWaypointDrag } from './plannerWaypointDrag'; /** The pin's inline style for a size and selection; the renderer owns the colour. */ @@ -20,7 +20,7 @@ function plannerWaypointIcon( className: 'tour-planner-waypoint-marker', iconSize: [size, size], iconAnchor: [size / 2, size / 2], - html: `${text}`, + html: `${text}${plannerPinBadge(point)}`, }); } @@ -46,10 +46,10 @@ export default function PlannerWaypointMarker({ handlersRef.current = { onClick, onMove }; }, [onClick, onMove]); const position = useMemo(() => [point.lat, point.lng] as [number, number], [point.lat, point.lng]); - const { label, shaping } = point; + const { label, shaping, stageEnd } = point; const icon = useMemo( - () => plannerWaypointIcon(pinCss, { label, shaping }, index, selected, !!onMove), - [index, label, onMove, pinCss, selected, shaping] + () => plannerWaypointIcon(pinCss, { label, shaping, stageEnd }, index, selected, !!onMove), + [index, label, onMove, pinCss, selected, shaping, stageEnd] ); const eventHandlers = useMemo( () => ({ diff --git a/client/src/components/Map/mapViewContract.ts b/client/src/components/Map/mapViewContract.ts index f8ebe9e1a..ebe0cc691 100644 --- a/client/src/components/Map/mapViewContract.ts +++ b/client/src/components/Map/mapViewContract.ts @@ -118,6 +118,8 @@ export interface MapViewProps { detail?: string; label?: string; shaping?: boolean; + /** MDT-01: a stage of a multi-day tour ends here; the pin carries a moon at its corner. */ + stageEnd?: boolean; }>; selectedPlannerWaypointId?: string | null; onPlannerWaypointClick?: (id: string) => void; diff --git a/client/src/components/Map/plannerPinShape.test.ts b/client/src/components/Map/plannerPinShape.test.ts index 8e15b4764..ea1d2c654 100644 --- a/client/src/components/Map/plannerPinShape.test.ts +++ b/client/src/components/Map/plannerPinShape.test.ts @@ -1,6 +1,7 @@ // FE-PLANNER-PIN-001: a stop is a numbered pin, a shaping point a small dot without a number (P2A-03). +// FE-PLANNER-PIN-002: the stop where a stage of a multi-day tour ends carries the moon at its corner (MDT-01). import { describe, expect, it } from 'vitest'; -import { plannerPinShape } from './plannerPinShape'; +import { plannerPinBadge, plannerPinShape } from './plannerPinShape'; describe('plannerPinShape', () => { it('FE-PLANNER-PIN-001: numbers a stop by its label or its place, and leaves a shaping point blank and small', () => { @@ -9,4 +10,13 @@ describe('plannerPinShape', () => { expect(plannerPinShape({ shaping: true, label: '9' }, 1, false)).toEqual({ size: 14, text: '' }); expect(plannerPinShape({ shaping: true }, 1, true)).toEqual({ size: 18, text: '' }); }); + + it('FE-PLANNER-PIN-002: marks a stage end with the moon at the corner, and nothing else', () => { + const badge = plannerPinBadge({ stageEnd: true, label: '3' }); + expect(badge).toContain('data-stage-end="true"'); + expect(badge).toContain('