From a8be1c5b45f6958dded30f4c4919374d4a18a6b3 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 20 Oct 2026 12:50:38 +0200 Subject: [PATCH] fix(tours): keep unknown actual tracks unknown and name every track a link replaces (ACT-01) A tour detail that cannot be read (offline without a cached copy, or a server error) no longer shows "No actual track yet" while the trip list names tracks of the tour; the section says they could not be loaded and offers no link over them. The day a track was recorded on is the viewer's calendar day of its first time stamp, not the UTC one, in the tour detail, the link dialog and the GPX preview alike, through one helper. Taking a tour off a day keeps its actual track without a day, so "no day" can hold several. A change now starts on the track it was asked for, and the link dialog names every track it replaces, as the server clears all of them. The migration, repository, service and MCP texts say so. The refusal for linking a tour to itself or a track with tracks of its own now reads "This track cannot be linked to this tour", which is true of both, and the offline helper of the tour cache sits above the doc of replaceTripTours again. --- .../components/Tours/TourGpxPreviewDialog.tsx | 8 +-- .../activity/TourActivityDialog.test.tsx | 4 +- .../Tours/activity/TourActivityDialog.tsx | 8 +-- .../activity/TourActivitySection.test.tsx | 18 +++++- .../Tours/activity/TourActivitySection.tsx | 60 ++++++++++++------- .../Tours/activity/tourActivityModel.test.ts | 14 ++++- .../Tours/activity/tourActivityModel.ts | 16 +++++ .../Tours/activity/useTourActivities.test.ts | 18 +++++- .../Tours/activity/useTourActivities.ts | 28 ++++++--- .../Tours/activity/useTrackAssignment.test.ts | 21 +++++-- .../Tours/activity/useTrackAssignment.ts | 17 ++++-- client/src/db/offlineDb.ts | 12 ++-- ...a_tour_keeps_the_track_it_was_done_with.ts | 11 ++-- .../repositories/TourActivities.repository.ts | 3 +- server/src/nest/tours/tour-activities.mcp.ts | 2 +- .../src/nest/tours/tour-activities.service.ts | 5 +- .../TourActivities.repository.test.ts | 12 ++++ shared/src/i18n/ar/tours.ts | 3 +- shared/src/i18n/az/tours.ts | 3 +- shared/src/i18n/br/tours.ts | 3 +- shared/src/i18n/ca/tours.ts | 3 +- shared/src/i18n/cs/tours.ts | 3 +- shared/src/i18n/de/tours.ts | 3 +- shared/src/i18n/en/tours.ts | 3 +- shared/src/i18n/es/tours.ts | 3 +- shared/src/i18n/et/tours.ts | 3 +- shared/src/i18n/fr/tours.ts | 3 +- shared/src/i18n/gr/tours.ts | 3 +- shared/src/i18n/hu/tours.ts | 3 +- shared/src/i18n/id/tours.ts | 3 +- shared/src/i18n/it/tours.ts | 3 +- shared/src/i18n/ja/tours.ts | 3 +- shared/src/i18n/ko/tours.ts | 3 +- shared/src/i18n/nl/tours.ts | 3 +- shared/src/i18n/pl/tours.ts | 3 +- shared/src/i18n/ru/tours.ts | 3 +- shared/src/i18n/sk/tours.ts | 3 +- shared/src/i18n/sv/tours.ts | 3 +- shared/src/i18n/th/tours.ts | 3 +- shared/src/i18n/tours-locales.spec.ts | 2 +- shared/src/i18n/tr/tours.ts | 3 +- shared/src/i18n/uk/tours.ts | 3 +- shared/src/i18n/vi/tours.ts | 3 +- shared/src/i18n/zh-TW/tours.ts | 3 +- shared/src/i18n/zh/tours.ts | 3 +- shared/src/tours/tour-activity.schema.ts | 3 +- 46 files changed, 243 insertions(+), 100 deletions(-) diff --git a/client/src/components/Tours/TourGpxPreviewDialog.tsx b/client/src/components/Tours/TourGpxPreviewDialog.tsx index 7df48539d..5b04b7874 100644 --- a/client/src/components/Tours/TourGpxPreviewDialog.tsx +++ b/client/src/components/Tours/TourGpxPreviewDialog.tsx @@ -5,7 +5,6 @@ import { OFM_POSITRON } from '../../constants/mapDefaults'; import { useTileUrl } from '../../hooks/useTileUrl'; import { useTranslation } from '../../i18n'; import { useSettingsStore } from '../../store/settingsStore'; -import { formatDate } from '../../utils/formatters'; import { formatDistance, formatElevation } from '../../utils/units'; import { MapViewAuto } from '../Map/MapViewAuto'; import { SoftPill } from '../Planner/planParts'; @@ -20,7 +19,7 @@ import { fs, NEUTRAL_TINT, } from '../shared/DialogShell'; -import { recordedMinutes } from './activity/tourActivityModel'; +import { recordedDay, recordedMinutes } from './activity/tourActivityModel'; import { importFields, importMissing, useGpxImportChoices } from './gpxImportChoices'; import { GpxImportTypeSection } from './GpxImportTypeSection'; import { TourNotice } from './tourParts'; @@ -34,11 +33,12 @@ const BADGE = function RecordedBadge({ track }: { track: GpxPreviewFile['tracks'][number] }) { const { locale } = useTranslation(); const minutes = recordedMinutes(track.started_at, track.ended_at); - if (minutes === null || !track.started_at) return null; + const day = recordedDay(track.started_at, locale); + if (minutes === null || !day) return null; return ( - {formatDate(track.started_at.slice(0, 10), locale)} + {day} {formatPlannedTourDuration(minutes)} ); diff --git a/client/src/components/Tours/activity/TourActivityDialog.test.tsx b/client/src/components/Tours/activity/TourActivityDialog.test.tsx index 9feebc6a6..5852ceb42 100644 --- a/client/src/components/Tours/activity/TourActivityDialog.test.tsx +++ b/client/src/components/Tours/activity/TourActivityDialog.test.tsx @@ -37,7 +37,7 @@ function assignment(overrides: Partial = {}): TrackAssignment { selected: 20, choose: vi.fn(), choice: watch, - replaces: null, + replaces: [], gpxImport: { importing: false, preview: null, @@ -93,7 +93,7 @@ describe('TourActivityDialog (ACT-01)', () => { }); it('FE-ACT-DIALOG-003: nothing to link until a free recording is chosen; what it replaces is said', () => { - hook.assignment = assignment({ ready: false, choice: null, selected: null, replaces: 'Old export' }); + hook.assignment = assignment({ ready: false, choice: null, selected: null, replaces: ['Old export'] }); const current = state(); show(current); expect(screen.getByTestId('tour-activity-confirm')).toBeDisabled(); diff --git a/client/src/components/Tours/activity/TourActivityDialog.tsx b/client/src/components/Tours/activity/TourActivityDialog.tsx index 415872db4..75f1fbcb3 100644 --- a/client/src/components/Tours/activity/TourActivityDialog.tsx +++ b/client/src/components/Tours/activity/TourActivityDialog.tsx @@ -23,7 +23,7 @@ import { PillSelect } from '../../shared/dialogParts'; import TourGpxPreviewDialog from '../TourGpxPreviewDialog'; import { CardAction, TourMetricFields, TourNotice } from '../tourParts'; import { formatPlannedTourDuration, hikeSourceBadgeLabel } from '../tourPresentation'; -import { recordedMinutes, type TourExecution, type TrackCandidate } from './tourActivityModel'; +import { recordedDay, recordedMinutes, type TourExecution, type TrackCandidate } from './tourActivityModel'; import type { TourActivities } from './useTourActivities'; import { useTrackAssignment, type TrackAssignment } from './useTrackAssignment'; @@ -44,7 +44,7 @@ function CandidateRow({ const unit = useSettingsStore((store) => store.settings.distance_unit) ?? 'metric'; const { tour, takenBy } = candidate; const minutes = recordedMinutes(tour.track_started_at, tour.track_ended_at); - const recordedOn = tour.track_started_at ? formatDate(tour.track_started_at.slice(0, 10), locale) : null; + const recordedOn = recordedDay(tour.track_started_at, locale); return (