From 8fd46c4c188d33651dcec0c35abdc30ac3e1cdb2 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 30 Sep 2026 23:00:12 +0200 Subject: [PATCH] feat: quality of life sweep from the feature discussions Small requests from the discussions, each built end to end: planner and map (cycling mode, date first headings, clear a day, stay times per day, off-route stops, map lock, compact unplanned markers, pending stays, cable car, transit quick picks), places (location filter, category from OSM, open in maps, geo: link, image from an attachment, enrichment on file import, places near a pin, place name language, Amap first in China), lists and costs (packed counter, inherited sharing, Markdown to-dos, payment notes, bookings CSV, my plan PDF), sharing (unplanned places, travel only, without photos), journey (photo zoom, drafts, photo order, trip suggestion, day jump), atlas (region counter, region bucket list, places per country), vacay half company holidays, studio copy and paste plus the book's own layouts, and RESTORE_FROM_BACKUP for a first start restore. --- client/src/api/client.ts | 24 +- .../Atlas/AtlasCountryPlaces.test.tsx | 32 +++ .../components/Atlas/AtlasCountryPlaces.tsx | 85 +++++++ client/src/components/Budget/CostsPanel.tsx | 54 +++-- .../src/components/Budget/costsCategories.tsx | 2 +- .../Collections/AddPlaceToCollectionModal.tsx | 4 +- .../Collections/CollectionPlaceDetail.tsx | 4 +- .../components/Files/FileManager.constants.ts | 2 +- .../components/Files/FileManager.helpers.ts | 3 +- .../Journey/JourneyDayJump.test.tsx | 46 ++++ .../src/components/Journey/JourneyDayJump.tsx | 90 +++++++ .../JourneyDetailPageEntryCard.test.tsx | 9 + .../Journey/JourneyDetailPageEntryCard.tsx | 14 +- .../Journey/JourneyDetailPageEntryEditor.tsx | 71 ++++-- .../components/Journey/PhotoLightbox.test.tsx | 63 ++++- .../src/components/Journey/PhotoLightbox.tsx | 56 ++++- .../src/components/Journey/photoZoom.test.ts | 21 ++ client/src/components/Journey/photoZoom.ts | 47 ++++ .../Journey/useEntryPhotoOrder.test.ts | 13 + .../components/Journey/useEntryPhotoOrder.ts | 53 ++++ .../Journey/useJourneyTripSuggestion.test.ts | 20 ++ .../Journey/useJourneyTripSuggestion.ts | 71 ++++++ client/src/components/Journey/usePhotoZoom.ts | 146 +++++++++++ client/src/components/Map/MapLockPill.tsx | 21 ++ client/src/components/Map/MapTogglePill.tsx | 50 ++++ client/src/components/Map/MapView.tsx | 57 +++-- client/src/components/Map/MapViewGL.tsx | 54 +++-- .../src/components/Map/ReservationOverlay.tsx | 8 +- .../src/components/Map/TripRouteOverview.tsx | 47 +--- client/src/components/Map/dayRoutePlan.ts | 4 +- client/src/components/Map/markerLook.ts | 41 ++++ .../src/components/Map/reservationsMapbox.ts | 7 +- client/src/components/PDF/TripPDF.tsx | 14 +- client/src/components/PDF/pdfScope.test.ts | 29 +++ client/src/components/PDF/pdfScope.ts | 29 +++ .../Packing/PackingListPanelItemRow.tsx | 13 +- .../Packing/PackingPackedCounter.test.tsx | 31 +++ .../Packing/PackingPackedCounter.tsx | 55 +++++ .../Packing/packingListPanel.helpers.test.ts | 24 +- .../Packing/packingListPanel.helpers.ts | 27 +++ .../components/Packing/usePackingListPanel.ts | 7 +- .../src/components/Planner/AddressInput.tsx | 4 +- .../Planner/DayDetailPanel.test.tsx | 31 +++ .../src/components/Planner/DayDetailPanel.tsx | 18 +- .../Planner/DayPlanSidebar.constants.ts | 8 +- .../Planner/DayPlanSidebar.test.tsx | 27 ++- .../src/components/Planner/DayPlanSidebar.tsx | 96 ++++---- .../Planner/DayPlanSidebarRouteConnector.tsx | 14 +- .../components/Planner/FileImportModal.tsx | 20 +- .../src/components/Planner/LocationSelect.tsx | 4 +- .../Planner/PlaceFormModal.helpers.test.ts | 12 + .../Planner/PlaceFormModal.helpers.ts | 12 + .../Planner/PlaceFormModal.test.tsx | 32 +++ .../src/components/Planner/PlaceFormModal.tsx | 121 +++++++++- .../src/components/Planner/PlaceInspector.tsx | 24 +- .../Planner/PlacesSidebarHeader.tsx | 44 +++- .../components/Planner/ReservationsPanel.tsx | 17 ++ .../Planner/TransitSearchPanel.test.tsx | 38 ++- .../components/Planner/TransitSearchPanel.tsx | 101 +++++++- .../src/components/Planner/TransportModal.tsx | 5 +- .../Planner/TripExportModal.test.tsx | 21 +- .../components/Planner/TripExportModal.tsx | 42 +++- .../Planner/bookings/BookingsHeader.tsx | 9 +- .../Planner/bookings/bookingsExport.test.ts | 42 ++++ .../Planner/bookings/bookingsExport.ts | 54 +++++ .../Planner/bookings/bookingsModel.ts | 6 +- .../components/Planner/parsedItemToDraft.ts | 2 +- .../Planner/placeCategoryGuess.test.ts | 53 ++++ .../components/Planner/placeCategoryGuess.ts | 72 ++++++ .../Planner/placeLocalityFilter.test.ts | 29 +++ .../components/Planner/placeLocalityFilter.ts | 42 ++++ .../Planner/placeNavigation.test.ts | 23 +- .../src/components/Planner/placeNavigation.ts | 34 ++- client/src/components/Planner/routeModes.ts | 38 +++ .../components/Planner/stayDayTimes.test.ts | 24 ++ client/src/components/Planner/stayDayTimes.ts | 25 ++ .../components/Planner/usePlacesSidebar.ts | 13 +- .../Settings/DisplaySettingsTab.test.tsx | 17 +- .../Settings/DisplaySettingsTab.tsx | 213 ++++++---------- client/src/components/Studio/StudioCanvas.tsx | 16 +- .../src/components/Studio/StudioInspector.tsx | 13 +- .../components/Studio/StudioSavedLayouts.tsx | 84 +++++++ client/src/components/Studio/StudioShell.tsx | 28 ++- .../src/components/Studio/StudioSidebar.tsx | 2 + client/src/components/Studio/savedLayouts.ts | 131 ++++++++++ .../src/components/Studio/studioClipboard.ts | 40 +++ client/src/components/Todo/TodoRow.tsx | 5 +- client/src/components/Todo/TodoTaskFields.tsx | 12 +- .../Trips/TripMembersModal.test.tsx | 17 ++ .../src/components/Trips/TripMembersModal.tsx | 10 +- .../src/components/Trips/TripShareDialog.tsx | 46 ++-- client/src/components/Trips/useTripShare.ts | 28 ++- client/src/components/Vacay/VacayCalendar.tsx | 16 +- .../src/components/Vacay/VacayMonthCard.tsx | 9 +- .../components/Vacay/companyHolidays.test.ts | 24 ++ .../src/components/Vacay/companyHolidays.ts | 35 +++ .../shared/MarkdownEditable.test.tsx | 49 ++++ .../components/shared/MarkdownEditable.tsx | 46 ++++ .../src/components/shared/NavigationMenu.tsx | 6 +- .../components/shared/PlaceAvatarUpload.tsx | 119 ++++++++- client/src/hooks/usePlaceLanguage.test.ts | 16 ++ client/src/hooks/usePlaceLanguage.ts | 18 ++ client/src/hooks/useRouteCalculation.ts | 39 ++- client/src/index.css | 2 +- client/src/mobile/screens/atlas/MAtlas.tsx | 15 +- .../screens/atlas/MAtlasCountryPopup.tsx | 2 + .../screens/collections/MCollAddSheet.tsx | 4 +- .../screens/journey/MJourneyEntrySheet.tsx | 64 +++-- .../screens/settings/MSettingsGeneral.tsx | 14 ++ .../src/mobile/screens/trip/map/MMapArea.tsx | 1 + .../screens/trip/plan/MPlanTimeline.tsx | 10 +- .../screens/trip/plan/MPlanTimelineRows.tsx | 5 +- .../screens/trip/plan/planTimelineModel.ts | 2 + .../screens/trip/plan/useMPlanTimeline.ts | 12 +- .../screens/trip/roadtrip/MRoadtripTab.tsx | 4 +- .../screens/trip/roadtrip/MRtStopSheet.tsx | 4 +- .../screens/trip/sheets/ImpFileStep.tsx | 19 +- .../mobile/screens/trip/sheets/MDaySheet.tsx | 62 ++--- .../screens/trip/sheets/MExportSheet.tsx | 23 +- .../screens/trip/sheets/MPlaceSheet.tsx | 18 +- .../trip/sheets/MTransportFormSheet.tsx | 5 +- .../screens/trip/sheets/MTripSheetUi.tsx | 11 +- .../screens/trip/sheets/MTripSheets.tsx | 12 + .../screens/trip/sheets/PlPlaceSearch.tsx | 5 +- .../mobile/screens/trip/tabs/MCostsTab.tsx | 67 ++++-- .../screens/trip/tabs/MPackingListTab.tsx | 37 ++- .../mobile/screens/trip/tabs/MTaskSheet.tsx | 20 +- .../mobile/screens/trip/tabs/MTodoListTab.tsx | 3 +- .../mobile/screens/trip/tabs/costsModel.ts | 2 + .../screens/trip/tabs/transportsModel.ts | 1 + client/src/mobile/screens/vacay/useMVacay.ts | 15 +- .../src/mobile/screens/vacay/vacayDayModel.ts | 5 + client/src/pages/AtlasPage.tsx | 64 ++++- client/src/pages/JourneyDetailPage.tsx | 16 +- client/src/pages/SharedTripPage.test.tsx | 39 +++ client/src/pages/SharedTripPage.tsx | 1 + client/src/pages/TripPlannerPage.tsx | 21 +- client/src/pages/atlas/atlasModel.test.ts | 42 ++++ client/src/pages/atlas/atlasModel.ts | 44 ++++ client/src/pages/atlas/useAtlas.ts | 67 +++++- client/src/pages/sharedTrip/SharedDayCard.tsx | 16 +- .../src/pages/sharedTrip/SharedPlanView.tsx | 15 +- .../pages/sharedTrip/SharedUnplannedCard.tsx | 53 ++++ .../pages/sharedTrip/sharedTripModel.test.ts | 19 +- .../src/pages/sharedTrip/sharedTripModel.ts | 18 ++ client/src/pages/tripPlanner/useDayClear.ts | 81 +++++++ .../src/pages/tripPlanner/useTripPlanner.ts | 54 ++++- client/src/repo/assignmentRepo.ts | 40 +++ client/src/store/journeyStore.ts | 2 + client/src/store/slices/assignmentsSlice.ts | 28 +++ client/src/store/slices/packingSlice.test.ts | 40 +++ client/src/store/slices/packingSlice.ts | 26 +- client/src/store/slices/placesSlice.ts | 13 + client/src/store/studioStore.ts | 57 ++++- client/src/store/vacayStore.ts | 19 +- client/src/styles/studio.css | 11 + client/src/types.ts | 8 + client/src/utils/dayLabel.test.ts | 17 +- client/src/utils/dayLabel.ts | 10 + client/src/utils/dayMerge.ts | 2 +- client/src/utils/pendingStays.ts | 33 +++ client/src/utils/placeLocality.test.ts | 19 ++ client/src/utils/placeLocality.ts | 43 ++++ client/tests/helpers/mobileTrip.ts | 2 +- .../tests/unit/mobile/trip/MDaySheet.test.tsx | 24 +- .../unit/mobile/trip/MPackingListTab.test.tsx | 20 +- .../unit/mobile/trip/MTaskSheet.test.tsx | 2 + .../studio/studioClipboardLayouts.test.ts | 161 +++++++++++++ server/.env.example | 1 + server/src/app-config/derive.ts | 2 + server/src/app-config/env.schema.ts | 2 + server/src/db/migrations.ts | 66 +++++ server/src/index.ts | 45 +++- .../assignments/assignments.controller.ts | 38 +++ .../src/nest/assignments/assignments.dto.ts | 2 + .../src/nest/assignments/assignments.mcp.ts | 48 ++++ .../nest/assignments/assignments.service.ts | 22 ++ server/src/nest/atlas/atlas.controller.ts | 4 +- server/src/nest/atlas/atlas.mcp.ts | 11 +- server/src/nest/atlas/atlas.service.ts | 16 +- server/src/nest/backup/backup-archive.ts | 123 ++++++++++ server/src/nest/backup/backup.impl.ts | 104 +------- server/src/nest/backup/boot-restore.ts | 93 +++++++ server/src/nest/budget/budget.controller.ts | 3 +- server/src/nest/budget/budget.mcp.ts | 14 +- server/src/nest/budget/budget.service.ts | 27 ++- server/src/nest/common/place-image-upload.ts | 5 +- server/src/nest/common/rowShape.ts | 2 + server/src/nest/days/days.service.ts | 1 + .../nest/journey/journey-domain.service.ts | 34 ++- server/src/nest/journey/journey-entry-row.ts | 6 +- .../src/nest/journey/journey-share.service.ts | 18 +- server/src/nest/journey/journey.controller.ts | 11 +- server/src/nest/journey/journey.dto.ts | 2 + server/src/nest/journey/journey.mcp.ts | 27 ++- server/src/nest/journey/journey.service.ts | 1 + server/src/nest/maps/maps-nearby.helpers.ts | 100 ++++++++ server/src/nest/maps/maps.controller.ts | 17 +- server/src/nest/maps/maps.dto.ts | 2 + server/src/nest/maps/maps.mcp.ts | 25 ++ server/src/nest/maps/maps.service.ts | 178 ++++++++++++-- server/src/nest/packing/packing.controller.ts | 4 +- server/src/nest/packing/packing.mcp.ts | 7 +- server/src/nest/packing/packing.service.ts | 26 +- server/src/nest/places/places.controller.ts | 26 ++ server/src/nest/places/places.dto.ts | 2 + server/src/nest/places/places.mcp.ts | 29 +++ server/src/nest/places/places.service.ts | 47 +++- .../src/nest/reservations/reservations.mcp.ts | 4 +- server/src/nest/settings/settings.mcp.ts | 5 + server/src/nest/share/share-view.helpers.ts | 51 ++++ server/src/nest/share/share.controller.ts | 2 + server/src/nest/share/share.mcp.ts | 7 +- server/src/nest/share/share.service.ts | 40 ++- server/src/nest/vacay/vacay.controller.ts | 2 +- server/src/nest/vacay/vacay.mcp.ts | 7 +- server/src/nest/vacay/vacay.rpc.ts | 2 +- server/src/nest/vacay/vacay.service.ts | 63 +++-- server/src/types.ts | 3 + server/tests/e2e/assignments.e2e.test.ts | 34 +++ server/tests/e2e/atlas.e2e.test.ts | 10 + server/tests/e2e/budget.e2e.test.ts | 17 ++ server/tests/e2e/maps.e2e.test.ts | 14 ++ server/tests/e2e/packing.e2e.test.ts | 12 + server/tests/e2e/share.e2e.test.ts | 12 + server/tests/integration/vacay.test.ts | 13 + server/tests/unit/app-config/derive.test.ts | 6 + .../tests/unit/mcp/tools-assignments.test.ts | 40 +++ server/tests/unit/mcp/tools-atlas.test.ts | 14 ++ server/tests/unit/mcp/tools-budget.test.ts | 16 ++ server/tests/unit/mcp/tools-journey.test.ts | 28 +++ .../unit/mcp/tools-leg-transport-mode.test.ts | 31 +++ server/tests/unit/mcp/tools-packing.test.ts | 13 + server/tests/unit/mcp/tools-places.test.ts | 17 ++ server/tests/unit/mcp/tools-settings.test.ts | 13 + server/tests/unit/mcp/tools-share.test.ts | 10 + .../unit/mcp/tools-tags-maps-weather.test.ts | 38 +++ server/tests/unit/mcp/tools-vacay.test.ts | 10 + .../unit/nest/assignments.controller.test.ts | 30 +++ .../unit/nest/assignments.service.test.ts | 30 +++ .../tests/unit/nest/atlas.controller.test.ts | 6 + server/tests/unit/nest/atlas.service.test.ts | 13 +- server/tests/unit/nest/boot-restore.test.ts | 132 ++++++++++ .../tests/unit/nest/budget.controller.test.ts | 11 + .../tests/unit/nest/budget.service.db.test.ts | 14 ++ .../unit/nest/journey-domain.service.test.ts | 49 ++++ .../unit/nest/journey-share.service.test.ts | 23 ++ .../unit/nest/journey.controller.test.ts | 8 + server/tests/unit/nest/maps.amap.test.ts | 117 +++++++++ .../tests/unit/nest/maps.controller.test.ts | 20 ++ server/tests/unit/nest/maps.nearby.test.ts | 227 ++++++++++++++++++ server/tests/unit/nest/maps.service.test.ts | 17 ++ .../unit/nest/packing.controller.test.ts | 7 + .../tests/unit/nest/packing.service.test.ts | 17 ++ .../tests/unit/nest/places.controller.test.ts | 38 +++ server/tests/unit/nest/places.service.test.ts | 51 ++++ .../unit/nest/share-view.helpers.test.ts | 36 +++ .../tests/unit/nest/share.controller.test.ts | 6 + server/tests/unit/nest/share.service.test.ts | 38 +++ server/tests/unit/nest/vacay.service.test.ts | 32 +++ shared/src/assignment/assignment.schema.ts | 8 + shared/src/atlas/atlas.schema.spec.ts | 16 ++ shared/src/atlas/atlas.schema.ts | 31 ++- shared/src/book/book.schema.spec.ts | 35 +++ shared/src/book/book.schema.ts | 29 +++ shared/src/budget/budget.schema.ts | 6 + shared/src/i18n/ar/atlas.ts | 4 + shared/src/i18n/ar/budget.ts | 1 + shared/src/i18n/ar/dayplan.ts | 9 + shared/src/i18n/ar/inspector.ts | 1 + shared/src/i18n/ar/journey.ts | 21 ++ shared/src/i18n/ar/map.ts | 2 + shared/src/i18n/ar/mobileTrip.ts | 1 + shared/src/i18n/ar/packing.ts | 3 + shared/src/i18n/ar/places.ts | 10 + shared/src/i18n/ar/reservations.ts | 11 + shared/src/i18n/ar/settings.ts | 7 + shared/src/i18n/ar/share.ts | 5 + shared/src/i18n/ar/shared.ts | 1 + shared/src/i18n/ar/todo.ts | 1 + shared/src/i18n/ar/undo.ts | 1 + shared/src/i18n/az/atlas.ts | 4 + shared/src/i18n/az/budget.ts | 1 + shared/src/i18n/az/dayplan.ts | 10 + shared/src/i18n/az/inspector.ts | 1 + shared/src/i18n/az/journey.ts | 26 ++ shared/src/i18n/az/map.ts | 2 + shared/src/i18n/az/mobileTrip.ts | 1 + shared/src/i18n/az/packing.ts | 3 + shared/src/i18n/az/places.ts | 10 + shared/src/i18n/az/reservations.ts | 11 + shared/src/i18n/az/settings.ts | 10 + shared/src/i18n/az/share.ts | 6 + shared/src/i18n/az/shared.ts | 1 + shared/src/i18n/az/todo.ts | 1 + shared/src/i18n/az/undo.ts | 1 + shared/src/i18n/br/atlas.ts | 4 + shared/src/i18n/br/budget.ts | 1 + shared/src/i18n/br/dayplan.ts | 10 + shared/src/i18n/br/inspector.ts | 1 + shared/src/i18n/br/journey.ts | 23 ++ shared/src/i18n/br/map.ts | 2 + shared/src/i18n/br/mobileTrip.ts | 1 + shared/src/i18n/br/packing.ts | 3 + shared/src/i18n/br/places.ts | 10 + shared/src/i18n/br/reservations.ts | 11 + shared/src/i18n/br/settings.ts | 10 + shared/src/i18n/br/share.ts | 5 + shared/src/i18n/br/shared.ts | 1 + shared/src/i18n/br/todo.ts | 1 + shared/src/i18n/br/undo.ts | 1 + shared/src/i18n/ca/atlas.ts | 4 + shared/src/i18n/ca/budget.ts | 1 + shared/src/i18n/ca/dayplan.ts | 10 + shared/src/i18n/ca/inspector.ts | 1 + shared/src/i18n/ca/journey.ts | 25 ++ shared/src/i18n/ca/map.ts | 2 + shared/src/i18n/ca/mobileTrip.ts | 1 + shared/src/i18n/ca/packing.ts | 3 + shared/src/i18n/ca/places.ts | 10 + shared/src/i18n/ca/reservations.ts | 11 + shared/src/i18n/ca/settings.ts | 10 + shared/src/i18n/ca/share.ts | 5 + shared/src/i18n/ca/shared.ts | 1 + shared/src/i18n/ca/todo.ts | 1 + shared/src/i18n/ca/undo.ts | 1 + shared/src/i18n/cs/atlas.ts | 4 + shared/src/i18n/cs/budget.ts | 1 + shared/src/i18n/cs/dayplan.ts | 10 + shared/src/i18n/cs/inspector.ts | 1 + shared/src/i18n/cs/journey.ts | 21 ++ shared/src/i18n/cs/map.ts | 2 + shared/src/i18n/cs/mobileTrip.ts | 1 + shared/src/i18n/cs/packing.ts | 3 + shared/src/i18n/cs/places.ts | 10 + shared/src/i18n/cs/reservations.ts | 11 + shared/src/i18n/cs/settings.ts | 7 + shared/src/i18n/cs/share.ts | 5 + shared/src/i18n/cs/shared.ts | 1 + shared/src/i18n/cs/todo.ts | 1 + shared/src/i18n/cs/undo.ts | 1 + shared/src/i18n/de/atlas.ts | 4 + shared/src/i18n/de/budget.ts | 1 + shared/src/i18n/de/dayplan.ts | 10 + shared/src/i18n/de/inspector.ts | 1 + shared/src/i18n/de/journey.ts | 25 ++ shared/src/i18n/de/map.ts | 2 + shared/src/i18n/de/mobileTrip.ts | 1 + shared/src/i18n/de/packing.ts | 3 + shared/src/i18n/de/places.ts | 10 + shared/src/i18n/de/reservations.ts | 11 + shared/src/i18n/de/settings.ts | 10 + shared/src/i18n/de/share.ts | 5 + shared/src/i18n/de/shared.ts | 1 + shared/src/i18n/de/todo.ts | 1 + shared/src/i18n/de/undo.ts | 1 + shared/src/i18n/en/atlas.ts | 4 + shared/src/i18n/en/budget.ts | 1 + shared/src/i18n/en/dayplan.ts | 10 + shared/src/i18n/en/inspector.ts | 1 + shared/src/i18n/en/journey.ts | 21 ++ shared/src/i18n/en/map.ts | 2 + shared/src/i18n/en/mobileTrip.ts | 1 + shared/src/i18n/en/packing.ts | 3 + shared/src/i18n/en/places.ts | 10 + shared/src/i18n/en/reservations.ts | 11 + shared/src/i18n/en/settings.ts | 8 + shared/src/i18n/en/share.ts | 5 + shared/src/i18n/en/shared.ts | 1 + shared/src/i18n/en/todo.ts | 1 + shared/src/i18n/en/undo.ts | 1 + shared/src/i18n/es/atlas.ts | 4 + shared/src/i18n/es/budget.ts | 1 + shared/src/i18n/es/dayplan.ts | 10 + shared/src/i18n/es/inspector.ts | 1 + shared/src/i18n/es/journey.ts | 24 ++ shared/src/i18n/es/map.ts | 2 + shared/src/i18n/es/mobileTrip.ts | 1 + shared/src/i18n/es/packing.ts | 3 + shared/src/i18n/es/places.ts | 10 + shared/src/i18n/es/reservations.ts | 11 + shared/src/i18n/es/settings.ts | 10 + shared/src/i18n/es/share.ts | 5 + shared/src/i18n/es/shared.ts | 1 + shared/src/i18n/es/todo.ts | 1 + shared/src/i18n/es/undo.ts | 1 + shared/src/i18n/et/atlas.ts | 4 + shared/src/i18n/et/budget.ts | 1 + shared/src/i18n/et/dayplan.ts | 10 + shared/src/i18n/et/inspector.ts | 1 + shared/src/i18n/et/journey.ts | 24 ++ shared/src/i18n/et/map.ts | 2 + shared/src/i18n/et/mobileTrip.ts | 1 + shared/src/i18n/et/packing.ts | 3 + shared/src/i18n/et/places.ts | 10 + shared/src/i18n/et/reservations.ts | 11 + shared/src/i18n/et/settings.ts | 10 + shared/src/i18n/et/share.ts | 5 + shared/src/i18n/et/shared.ts | 1 + shared/src/i18n/et/todo.ts | 1 + shared/src/i18n/et/undo.ts | 1 + shared/src/i18n/fr/atlas.ts | 4 + shared/src/i18n/fr/budget.ts | 1 + shared/src/i18n/fr/dayplan.ts | 10 + shared/src/i18n/fr/inspector.ts | 1 + shared/src/i18n/fr/journey.ts | 24 ++ shared/src/i18n/fr/map.ts | 2 + shared/src/i18n/fr/mobileTrip.ts | 1 + shared/src/i18n/fr/packing.ts | 3 + shared/src/i18n/fr/places.ts | 10 + shared/src/i18n/fr/reservations.ts | 11 + shared/src/i18n/fr/settings.ts | 10 + shared/src/i18n/fr/share.ts | 6 + shared/src/i18n/fr/shared.ts | 1 + shared/src/i18n/fr/todo.ts | 1 + shared/src/i18n/fr/undo.ts | 1 + shared/src/i18n/gr/atlas.ts | 4 + shared/src/i18n/gr/budget.ts | 1 + shared/src/i18n/gr/dayplan.ts | 10 + shared/src/i18n/gr/inspector.ts | 1 + shared/src/i18n/gr/journey.ts | 21 ++ shared/src/i18n/gr/map.ts | 2 + shared/src/i18n/gr/mobileTrip.ts | 1 + shared/src/i18n/gr/packing.ts | 3 + shared/src/i18n/gr/places.ts | 10 + shared/src/i18n/gr/reservations.ts | 11 + shared/src/i18n/gr/settings.ts | 7 + shared/src/i18n/gr/share.ts | 6 + shared/src/i18n/gr/shared.ts | 1 + shared/src/i18n/gr/todo.ts | 1 + shared/src/i18n/gr/undo.ts | 1 + shared/src/i18n/hu/atlas.ts | 4 + shared/src/i18n/hu/budget.ts | 1 + shared/src/i18n/hu/dayplan.ts | 10 + shared/src/i18n/hu/inspector.ts | 1 + shared/src/i18n/hu/journey.ts | 21 ++ shared/src/i18n/hu/map.ts | 2 + shared/src/i18n/hu/mobileTrip.ts | 1 + shared/src/i18n/hu/packing.ts | 3 + shared/src/i18n/hu/places.ts | 10 + shared/src/i18n/hu/reservations.ts | 11 + shared/src/i18n/hu/settings.ts | 7 + shared/src/i18n/hu/share.ts | 5 + shared/src/i18n/hu/shared.ts | 1 + shared/src/i18n/hu/todo.ts | 1 + shared/src/i18n/hu/undo.ts | 1 + shared/src/i18n/id/atlas.ts | 4 + shared/src/i18n/id/budget.ts | 1 + shared/src/i18n/id/dayplan.ts | 10 + shared/src/i18n/id/inspector.ts | 1 + shared/src/i18n/id/journey.ts | 21 ++ shared/src/i18n/id/map.ts | 2 + shared/src/i18n/id/mobileTrip.ts | 1 + shared/src/i18n/id/packing.ts | 3 + shared/src/i18n/id/places.ts | 10 + shared/src/i18n/id/reservations.ts | 11 + shared/src/i18n/id/settings.ts | 7 + shared/src/i18n/id/share.ts | 6 + shared/src/i18n/id/shared.ts | 1 + shared/src/i18n/id/todo.ts | 1 + shared/src/i18n/id/undo.ts | 1 + shared/src/i18n/it/atlas.ts | 4 + shared/src/i18n/it/budget.ts | 1 + shared/src/i18n/it/dayplan.ts | 10 + shared/src/i18n/it/inspector.ts | 1 + shared/src/i18n/it/journey.ts | 24 ++ shared/src/i18n/it/map.ts | 2 + shared/src/i18n/it/mobileTrip.ts | 1 + shared/src/i18n/it/packing.ts | 3 + shared/src/i18n/it/places.ts | 10 + shared/src/i18n/it/reservations.ts | 11 + shared/src/i18n/it/settings.ts | 10 + shared/src/i18n/it/share.ts | 5 + shared/src/i18n/it/shared.ts | 1 + shared/src/i18n/it/todo.ts | 1 + shared/src/i18n/it/undo.ts | 1 + shared/src/i18n/ja/atlas.ts | 4 + shared/src/i18n/ja/budget.ts | 1 + shared/src/i18n/ja/dayplan.ts | 9 + shared/src/i18n/ja/inspector.ts | 1 + shared/src/i18n/ja/journey.ts | 21 ++ shared/src/i18n/ja/map.ts | 2 + shared/src/i18n/ja/mobileTrip.ts | 1 + shared/src/i18n/ja/packing.ts | 3 + shared/src/i18n/ja/places.ts | 10 + shared/src/i18n/ja/reservations.ts | 11 + shared/src/i18n/ja/settings.ts | 7 + shared/src/i18n/ja/share.ts | 5 + shared/src/i18n/ja/shared.ts | 1 + shared/src/i18n/ja/todo.ts | 1 + shared/src/i18n/ja/undo.ts | 1 + shared/src/i18n/ko/atlas.ts | 4 + shared/src/i18n/ko/budget.ts | 1 + shared/src/i18n/ko/dayplan.ts | 10 + shared/src/i18n/ko/inspector.ts | 1 + shared/src/i18n/ko/journey.ts | 21 ++ shared/src/i18n/ko/map.ts | 2 + shared/src/i18n/ko/mobileTrip.ts | 1 + shared/src/i18n/ko/packing.ts | 3 + shared/src/i18n/ko/places.ts | 10 + shared/src/i18n/ko/reservations.ts | 11 + shared/src/i18n/ko/settings.ts | 7 + shared/src/i18n/ko/share.ts | 5 + shared/src/i18n/ko/shared.ts | 1 + shared/src/i18n/ko/todo.ts | 1 + shared/src/i18n/ko/undo.ts | 1 + shared/src/i18n/nl/atlas.ts | 4 + shared/src/i18n/nl/budget.ts | 1 + shared/src/i18n/nl/dayplan.ts | 10 + shared/src/i18n/nl/inspector.ts | 1 + shared/src/i18n/nl/journey.ts | 24 ++ shared/src/i18n/nl/map.ts | 2 + shared/src/i18n/nl/mobileTrip.ts | 1 + shared/src/i18n/nl/packing.ts | 3 + shared/src/i18n/nl/places.ts | 10 + shared/src/i18n/nl/reservations.ts | 11 + shared/src/i18n/nl/settings.ts | 10 + shared/src/i18n/nl/share.ts | 5 + shared/src/i18n/nl/shared.ts | 1 + shared/src/i18n/nl/todo.ts | 1 + shared/src/i18n/nl/undo.ts | 1 + shared/src/i18n/pl/atlas.ts | 4 + shared/src/i18n/pl/budget.ts | 1 + shared/src/i18n/pl/dayplan.ts | 10 + shared/src/i18n/pl/inspector.ts | 1 + shared/src/i18n/pl/journey.ts | 21 ++ shared/src/i18n/pl/map.ts | 2 + shared/src/i18n/pl/mobileTrip.ts | 1 + shared/src/i18n/pl/packing.ts | 3 + shared/src/i18n/pl/places.ts | 10 + shared/src/i18n/pl/reservations.ts | 11 + shared/src/i18n/pl/settings.ts | 7 + shared/src/i18n/pl/share.ts | 5 + shared/src/i18n/pl/shared.ts | 1 + shared/src/i18n/pl/todo.ts | 1 + shared/src/i18n/pl/undo.ts | 1 + shared/src/i18n/ru/atlas.ts | 4 + shared/src/i18n/ru/budget.ts | 1 + shared/src/i18n/ru/dayplan.ts | 10 + shared/src/i18n/ru/inspector.ts | 1 + shared/src/i18n/ru/journey.ts | 23 ++ shared/src/i18n/ru/map.ts | 2 + shared/src/i18n/ru/mobileTrip.ts | 1 + shared/src/i18n/ru/packing.ts | 3 + shared/src/i18n/ru/places.ts | 10 + shared/src/i18n/ru/reservations.ts | 11 + shared/src/i18n/ru/settings.ts | 10 + shared/src/i18n/ru/share.ts | 5 + shared/src/i18n/ru/shared.ts | 1 + shared/src/i18n/ru/todo.ts | 1 + shared/src/i18n/ru/undo.ts | 1 + shared/src/i18n/sk/atlas.ts | 4 + shared/src/i18n/sk/budget.ts | 1 + shared/src/i18n/sk/dayplan.ts | 10 + shared/src/i18n/sk/inspector.ts | 1 + shared/src/i18n/sk/journey.ts | 25 ++ shared/src/i18n/sk/map.ts | 2 + shared/src/i18n/sk/mobileTrip.ts | 1 + shared/src/i18n/sk/packing.ts | 3 + shared/src/i18n/sk/places.ts | 10 + shared/src/i18n/sk/reservations.ts | 11 + shared/src/i18n/sk/settings.ts | 10 + shared/src/i18n/sk/share.ts | 5 + shared/src/i18n/sk/shared.ts | 1 + shared/src/i18n/sk/todo.ts | 1 + shared/src/i18n/sk/undo.ts | 1 + shared/src/i18n/sv/atlas.ts | 4 + shared/src/i18n/sv/budget.ts | 1 + shared/src/i18n/sv/dayplan.ts | 10 + shared/src/i18n/sv/inspector.ts | 1 + shared/src/i18n/sv/journey.ts | 21 ++ shared/src/i18n/sv/map.ts | 2 + shared/src/i18n/sv/mobileTrip.ts | 1 + shared/src/i18n/sv/packing.ts | 3 + shared/src/i18n/sv/places.ts | 10 + shared/src/i18n/sv/reservations.ts | 11 + shared/src/i18n/sv/settings.ts | 7 + shared/src/i18n/sv/share.ts | 5 + shared/src/i18n/sv/shared.ts | 1 + shared/src/i18n/sv/todo.ts | 1 + shared/src/i18n/sv/undo.ts | 1 + shared/src/i18n/th/atlas.ts | 4 + shared/src/i18n/th/budget.ts | 1 + shared/src/i18n/th/dayplan.ts | 9 + shared/src/i18n/th/inspector.ts | 1 + shared/src/i18n/th/journey.ts | 21 ++ shared/src/i18n/th/map.ts | 2 + shared/src/i18n/th/mobileTrip.ts | 1 + shared/src/i18n/th/packing.ts | 3 + shared/src/i18n/th/places.ts | 10 + shared/src/i18n/th/reservations.ts | 11 + shared/src/i18n/th/settings.ts | 7 + shared/src/i18n/th/share.ts | 5 + shared/src/i18n/th/shared.ts | 1 + shared/src/i18n/th/todo.ts | 1 + shared/src/i18n/th/undo.ts | 1 + shared/src/i18n/tr/atlas.ts | 4 + shared/src/i18n/tr/budget.ts | 1 + shared/src/i18n/tr/dayplan.ts | 10 + shared/src/i18n/tr/inspector.ts | 1 + shared/src/i18n/tr/journey.ts | 23 ++ shared/src/i18n/tr/map.ts | 2 + shared/src/i18n/tr/mobileTrip.ts | 1 + shared/src/i18n/tr/packing.ts | 3 + shared/src/i18n/tr/places.ts | 10 + shared/src/i18n/tr/reservations.ts | 11 + shared/src/i18n/tr/settings.ts | 9 + shared/src/i18n/tr/share.ts | 5 + shared/src/i18n/tr/shared.ts | 1 + shared/src/i18n/tr/todo.ts | 1 + shared/src/i18n/tr/undo.ts | 1 + shared/src/i18n/uk/atlas.ts | 4 + shared/src/i18n/uk/budget.ts | 1 + shared/src/i18n/uk/dayplan.ts | 10 + shared/src/i18n/uk/inspector.ts | 1 + shared/src/i18n/uk/journey.ts | 21 ++ shared/src/i18n/uk/map.ts | 2 + shared/src/i18n/uk/mobileTrip.ts | 1 + shared/src/i18n/uk/packing.ts | 3 + shared/src/i18n/uk/places.ts | 10 + shared/src/i18n/uk/reservations.ts | 11 + shared/src/i18n/uk/settings.ts | 7 + shared/src/i18n/uk/share.ts | 5 + shared/src/i18n/uk/shared.ts | 1 + shared/src/i18n/uk/todo.ts | 1 + shared/src/i18n/uk/undo.ts | 1 + shared/src/i18n/vi/atlas.ts | 4 + shared/src/i18n/vi/budget.ts | 1 + shared/src/i18n/vi/dayplan.ts | 10 + shared/src/i18n/vi/inspector.ts | 1 + shared/src/i18n/vi/journey.ts | 21 ++ shared/src/i18n/vi/map.ts | 2 + shared/src/i18n/vi/mobileTrip.ts | 1 + shared/src/i18n/vi/packing.ts | 3 + shared/src/i18n/vi/places.ts | 10 + shared/src/i18n/vi/reservations.ts | 11 + shared/src/i18n/vi/settings.ts | 7 + shared/src/i18n/vi/share.ts | 5 + shared/src/i18n/vi/shared.ts | 1 + shared/src/i18n/vi/todo.ts | 1 + shared/src/i18n/vi/undo.ts | 1 + shared/src/i18n/zh-TW/atlas.ts | 4 + shared/src/i18n/zh-TW/budget.ts | 1 + shared/src/i18n/zh-TW/dayplan.ts | 9 + shared/src/i18n/zh-TW/inspector.ts | 1 + shared/src/i18n/zh-TW/journey.ts | 21 ++ shared/src/i18n/zh-TW/map.ts | 2 + shared/src/i18n/zh-TW/mobileTrip.ts | 1 + shared/src/i18n/zh-TW/packing.ts | 3 + shared/src/i18n/zh-TW/places.ts | 9 + shared/src/i18n/zh-TW/reservations.ts | 11 + shared/src/i18n/zh-TW/settings.ts | 7 + shared/src/i18n/zh-TW/share.ts | 5 + shared/src/i18n/zh-TW/shared.ts | 1 + shared/src/i18n/zh-TW/todo.ts | 1 + shared/src/i18n/zh-TW/undo.ts | 1 + shared/src/i18n/zh/atlas.ts | 4 + shared/src/i18n/zh/budget.ts | 1 + shared/src/i18n/zh/dayplan.ts | 9 + shared/src/i18n/zh/inspector.ts | 1 + shared/src/i18n/zh/journey.ts | 21 ++ shared/src/i18n/zh/map.ts | 2 + shared/src/i18n/zh/mobileTrip.ts | 1 + shared/src/i18n/zh/packing.ts | 3 + shared/src/i18n/zh/places.ts | 9 + shared/src/i18n/zh/reservations.ts | 11 + shared/src/i18n/zh/settings.ts | 7 + shared/src/i18n/zh/share.ts | 5 + shared/src/i18n/zh/shared.ts | 1 + shared/src/i18n/zh/todo.ts | 1 + shared/src/i18n/zh/undo.ts | 1 + shared/src/index.ts | 1 + shared/src/journey/journey.schema.ts | 10 + shared/src/maps/maps.schema.spec.ts | 12 + shared/src/maps/maps.schema.ts | 15 ++ shared/src/packing/packed-count.spec.ts | 59 +++++ shared/src/packing/packed-count.ts | 35 +++ shared/src/packing/packing.schema.ts | 6 + shared/src/place/place.schema.ts | 16 ++ shared/src/roadtrip/planning.schema.ts | 7 + shared/src/share/share.schema.ts | 4 + shared/src/vacay/vacay.schema.ts | 3 + wiki/Backups.md | 15 ++ wiki/Display-Settings.md | 6 +- wiki/Environment-Variables.md | 1 + wiki/MCP-Addon-Tools.md | 8 +- wiki/MCP-Tools-and-Resources.md | 12 +- wiki/Places-and-Search.md | 2 +- wiki/Public-Share-Links.md | 13 +- wiki/TREK-Places-API.md | 1 + wiki/User-Settings.md | 1 + 691 files changed, 9491 insertions(+), 927 deletions(-) create mode 100644 client/src/components/Atlas/AtlasCountryPlaces.test.tsx create mode 100644 client/src/components/Atlas/AtlasCountryPlaces.tsx create mode 100644 client/src/components/Journey/JourneyDayJump.test.tsx create mode 100644 client/src/components/Journey/JourneyDayJump.tsx create mode 100644 client/src/components/Journey/photoZoom.test.ts create mode 100644 client/src/components/Journey/photoZoom.ts create mode 100644 client/src/components/Journey/useEntryPhotoOrder.test.ts create mode 100644 client/src/components/Journey/useEntryPhotoOrder.ts create mode 100644 client/src/components/Journey/useJourneyTripSuggestion.test.ts create mode 100644 client/src/components/Journey/useJourneyTripSuggestion.ts create mode 100644 client/src/components/Journey/usePhotoZoom.ts create mode 100644 client/src/components/Map/MapLockPill.tsx create mode 100644 client/src/components/Map/MapTogglePill.tsx create mode 100644 client/src/components/Map/markerLook.ts create mode 100644 client/src/components/PDF/pdfScope.test.ts create mode 100644 client/src/components/PDF/pdfScope.ts create mode 100644 client/src/components/Packing/PackingPackedCounter.test.tsx create mode 100644 client/src/components/Packing/PackingPackedCounter.tsx create mode 100644 client/src/components/Planner/bookings/bookingsExport.test.ts create mode 100644 client/src/components/Planner/bookings/bookingsExport.ts create mode 100644 client/src/components/Planner/placeCategoryGuess.test.ts create mode 100644 client/src/components/Planner/placeCategoryGuess.ts create mode 100644 client/src/components/Planner/placeLocalityFilter.test.ts create mode 100644 client/src/components/Planner/placeLocalityFilter.ts create mode 100644 client/src/components/Planner/routeModes.ts create mode 100644 client/src/components/Planner/stayDayTimes.test.ts create mode 100644 client/src/components/Planner/stayDayTimes.ts create mode 100644 client/src/components/Studio/StudioSavedLayouts.tsx create mode 100644 client/src/components/Studio/savedLayouts.ts create mode 100644 client/src/components/Studio/studioClipboard.ts create mode 100644 client/src/components/Vacay/companyHolidays.test.ts create mode 100644 client/src/components/Vacay/companyHolidays.ts create mode 100644 client/src/components/shared/MarkdownEditable.test.tsx create mode 100644 client/src/components/shared/MarkdownEditable.tsx create mode 100644 client/src/hooks/usePlaceLanguage.test.ts create mode 100644 client/src/hooks/usePlaceLanguage.ts create mode 100644 client/src/pages/sharedTrip/SharedUnplannedCard.tsx create mode 100644 client/src/pages/tripPlanner/useDayClear.ts create mode 100644 client/src/utils/pendingStays.ts create mode 100644 client/src/utils/placeLocality.test.ts create mode 100644 client/src/utils/placeLocality.ts create mode 100644 client/tests/unit/studio/studioClipboardLayouts.test.ts create mode 100644 server/src/nest/backup/backup-archive.ts create mode 100644 server/src/nest/backup/boot-restore.ts create mode 100644 server/src/nest/maps/maps-nearby.helpers.ts create mode 100644 server/src/nest/share/share-view.helpers.ts create mode 100644 server/tests/unit/nest/boot-restore.test.ts create mode 100644 server/tests/unit/nest/maps.nearby.test.ts create mode 100644 server/tests/unit/nest/share-view.helpers.test.ts create mode 100644 shared/src/packing/packed-count.spec.ts create mode 100644 shared/src/packing/packed-count.ts diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 91191a218..1b4e0af41 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -2,7 +2,7 @@ import { isEffectivelyOffline } from '../sync/networkMode' import axios, { AxiosInstance } from 'axios' import type { z } from 'zod' import type { Day, Place, Trip } from '../types' -import type { TransitProvider } from '@trek/shared' +import type { MapsNearbyRequest, JourneyReorderEntryPhotosRequest, TransitProvider } from '@trek/shared' import { randomId } from '../utils/randomId' import { postProviderPhotosInBatches } from './providerPhotoBatches' import { @@ -28,13 +28,13 @@ import { type PackingReorderRequest, type PackingCreateBagRequest, type TodoReorderRequest, type TripCreateRequest, type TripUpdateRequest, type TripCopyRequest, type ActiveTripResponse, type DayCreateRequest, type DayUpdateRequest, type DayReorderRequest, - type PlaceCreateRequest, type PlaceUpdateRequest, + type PlaceCreateRequest, type PlaceUpdateRequest, type PlaceImageFromFileRequest, type ReservationCreateRequest, type ReservationUpdateRequest, type AccommodationCreateRequest, type AccommodationUpdateRequest, type BudgetCreateItemRequest, type BudgetUpdateItemRequest, type PackingCreateItemRequest, type PackingUpdateItemRequest, type PackingSetSharingRequest, type TodoCreateItemRequest, type TodoUpdateItemRequest, - type AssignmentCreateRequest, type AssignmentNotesRequest, type AssignmentParticipantsRequest, type AssignmentTimeRequest, type AssignmentTransportRequest, + type AssignmentCreateRequest, type AssignmentNotesRequest, type AssignmentParticipantsRequest, type AssignmentRouteRequest, type AssignmentTimeRequest, type AssignmentTransportRequest, type PlaceBulkDeleteRequest, type PlaceBulkUpdateRequest, type DayNoteCreateRequest, type DayNoteUpdateRequest, @@ -471,23 +471,28 @@ export const placesApi = { fd.append('image', file) return postMultipart<{ place: Place }>(`/trips/${tripId}/places/${id}/image`, fd) }, + // A picture already attached in the trip, copied in as the place's image (#1242). + imageFromFile: (tripId: number | string, id: number | string, fileId: number): Promise<{ place: Place }> => + apiClient.put(`/trips/${tripId}/places/${id}/image/from-file`, { file_id: fileId } satisfies PlaceImageFromFileRequest).then(r => r.data), rate: (tripId: number | string, id: number | string, rating: number | null): Promise<{ place: Place }> => rating === null ? apiClient.delete(`/trips/${tripId}/places/${id}/rating`).then(r => r.data) : apiClient.put(`/trips/${tripId}/places/${id}/rating`, { rating }).then(r => r.data), - importGpx: (tripId: number | string, file: File, opts?: { waypoints?: boolean; routes?: boolean; tracks?: boolean }) => { + importGpx: (tripId: number | string, file: File, opts?: { waypoints?: boolean; routes?: boolean; tracks?: boolean; enrich?: boolean }) => { const fd = new FormData() fd.append('file', file) if (opts?.waypoints !== undefined) fd.append('importWaypoints', String(opts.waypoints)) if (opts?.routes !== undefined) fd.append('importRoutes', String(opts.routes)) if (opts?.tracks !== undefined) fd.append('importTracks', String(opts.tracks)) + if (opts?.enrich) fd.append('enrich', 'true') return postMultipart(`/trips/${tripId}/places/import/gpx`, fd) }, - importMapFile: (tripId: number | string, file: File, opts?: { points?: boolean; paths?: boolean }) => { + importMapFile: (tripId: number | string, file: File, opts?: { points?: boolean; paths?: boolean; enrich?: boolean }) => { const fd = new FormData() fd.append('file', file) if (opts?.points !== undefined) fd.append('importPoints', String(opts.points)) if (opts?.paths !== undefined) fd.append('importPaths', String(opts.paths)) + if (opts?.enrich) fd.append('enrich', 'true') return postMultipart(`/trips/${tripId}/places/import/map`, fd) }, // A longer timeout than the shared 8 s, like the other routes here that wait @@ -511,12 +516,16 @@ export const assignmentsApi = { list: (tripId: number | string, dayId: number | string) => apiClient.get(`/trips/${tripId}/days/${dayId}/assignments`).then(r => r.data), create: (tripId: number | string, dayId: number | string, data: AssignmentCreateRequest) => apiClient.post(`/trips/${tripId}/days/${dayId}/assignments`, data).then(r => r.data), delete: (tripId: number | string, dayId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/days/${dayId}/assignments/${id}`).then(r => r.data), + // Takes every place off the day at once; the day, its notes and bookings stay (#2470). + clearDay: (tripId: number | string, dayId: number | string) => apiClient.delete(`/trips/${tripId}/days/${dayId}/assignments`).then(r => r.data as { success: true; removedIds: number[] }), reorder: (tripId: number | string, dayId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/days/${dayId}/assignments/reorder`, { orderedIds } satisfies AssignmentReorderRequest).then(r => r.data), move: (tripId: number | string, assignmentId: number, newDayId: number | string, orderIndex: number | null) => apiClient.put(`/trips/${tripId}/assignments/${assignmentId}/move`, { new_day_id: newDayId, order_index: orderIndex }).then(r => r.data), update: (tripId: number | string, dayId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/days/${dayId}/assignments/${id}`, data).then(r => r.data), getParticipants: (tripId: number | string, id: number) => apiClient.get(`/trips/${tripId}/assignments/${id}/participants`).then(r => r.data), setParticipants: (tripId: number | string, id: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/assignments/${id}/participants`, { user_ids: userIds } satisfies AssignmentParticipantsRequest).then(r => r.data), updateTime: (tripId: number | string, id: number, times: AssignmentTimeRequest) => apiClient.put(`/trips/${tripId}/assignments/${id}/time`, times).then(r => r.data), + // Keeps the stop on the day but out of its route (#2532). + setRouteExcluded: (tripId: number | string, id: number, excluded: boolean) => apiClient.put(`/trips/${tripId}/assignments/${id}/route`, { excluded } satisfies AssignmentRouteRequest).then(r => r.data), // Day-specific note on an assignment (#2163) — null clears it. updateNotes: (tripId: number | string, id: number, data: AssignmentNotesRequest) => apiClient.put(`/trips/${tripId}/assignments/${id}/notes`, data).then(r => r.data), // Per-segment travel mode (#1281): mode of the leg leaving this stop (null = inherit day default). @@ -1033,6 +1042,8 @@ export const journeyApi = { unlinkPhoto: (entryId: number, journeyPhotoId: number) => apiClient.delete(`/journeys/entries/${entryId}/photos/${journeyPhotoId}`).then(r => r.data), deleteGalleryPhoto: (journeyId: number, journeyPhotoId: number) => apiClient.delete(`/journeys/${journeyId}/gallery/${journeyPhotoId}`).then(r => r.data), updatePhoto: (photoId: number, data: Record) => apiClient.patch(`/journeys/photos/${photoId}`, data).then(r => r.data), + // The photos of one entry in their new order, in one request (#824). + reorderEntryPhotos: (entryId: number, orderedIds: number[]) => apiClient.put(`/journeys/entries/${entryId}/photos/reorder`, { orderedIds } satisfies JourneyReorderEntryPhotosRequest).then(r => r.data), deletePhoto: (photoId: number) => apiClient.delete(`/journeys/photos/${photoId}`).then(r => r.data), // Cover @@ -1253,6 +1264,9 @@ export const mapsApi = { const from = [...new Set(extra.map(p => String(p.source ?? 'plugin')))].join('+') return { places: [...core.places, ...extra], source: `${core.source}+${from}` } }), + /** Places of any kind around a point, nearest first, each with `distance_m` (#976). */ + nearby: (lat: number, lng: number, lang?: string) => + apiClient.post(`/maps/nearby?lang=${lang || 'en'}`, { lat, lng } satisfies MapsNearbyRequest).then(r => checkInDev(mapsSearchResultSchema, r.data, 'maps.nearby')), autocomplete: (input: string, lang?: string, locationBias?: { low: { lat: number; lng: number }; high: { lat: number; lng: number } }, signal?: AbortSignal, sessionToken?: string) => withCachedPlaces( input, diff --git a/client/src/components/Atlas/AtlasCountryPlaces.test.tsx b/client/src/components/Atlas/AtlasCountryPlaces.test.tsx new file mode 100644 index 000000000..17501d774 --- /dev/null +++ b/client/src/components/Atlas/AtlasCountryPlaces.test.tsx @@ -0,0 +1,32 @@ +// FE-ATLAS-PLACES-001 to FE-ATLAS-PLACES-002 (#2174) +import { vi } from 'vitest' +import { render, screen, fireEvent } from '../../../tests/helpers/render' +import AtlasCountryPlaces from './AtlasCountryPlaces' + +const detail = { + trips: [{ id: 1, title: 'Berlin 2026' }], + places: Array.from({ length: 8 }, (_, i) => ({ id: i + 1, name: `Place ${i + 1}`, lat: 0, lng: 0, trip_id: 1, address: i === 0 ? 'Pariser Platz' : null })), +} + +describe('AtlasCountryPlaces', () => { + it('FE-ATLAS-PLACES-001: lists the places under their trip and opens the trip on a click', () => { + const onOpenTrip = vi.fn() + render() + expect(screen.getByText('Berlin 2026')).toBeInTheDocument() + expect(screen.getByText('Pariser Platz')).toBeInTheDocument() + fireEvent.click(screen.getByText('Place 3')) + expect(onOpenTrip).toHaveBeenCalledWith(1) + }) + + it('FE-ATLAS-PLACES-002: narrows a long list and says when nothing matches', () => { + render( {}} variant="mobile" />) + const search = screen.getByRole('textbox', { name: 'Search places' }) + fireEvent.change(search, { target: { value: 'Place 2' } }) + expect(screen.getByText('Place 2')).toBeInTheDocument() + expect(screen.queryByText('Place 3')).toBeNull() + fireEvent.change(search, { target: { value: 'zzz' } }) + expect(screen.getByText('No place matches')).toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'Clear' })) + expect(screen.getByText('Place 3')).toBeInTheDocument() + }) +}) diff --git a/client/src/components/Atlas/AtlasCountryPlaces.tsx b/client/src/components/Atlas/AtlasCountryPlaces.tsx new file mode 100644 index 000000000..bf05517c8 --- /dev/null +++ b/client/src/components/Atlas/AtlasCountryPlaces.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react' +import { Briefcase, ChevronRight, MapPin, Search, X } from 'lucide-react' +import { useTranslation } from '../../i18n' +import { groupCountryPlaces, type CountryDetail } from '../../pages/atlas/atlasModel' + +interface AtlasCountryPlacesProps { + detail: Pick + onOpenTrip: (tripId: number) => void + /** Phone sheets use their own tokens; the desktop dialog reads the planner's. */ + variant?: 'desktop' | 'mobile' +} + +/** + * Every place counted for a country, by trip (#2174), so an old trip imported into + * the wrong country, or a place that is missing, can be found and opened. A search + * narrows long lists; a click opens the trip the place belongs to. + */ +export default function AtlasCountryPlaces({ detail, onOpenTrip, variant = 'desktop' }: AtlasCountryPlacesProps) { + const { t, locale } = useTranslation() + const [query, setQuery] = useState('') + const groups = groupCountryPlaces(detail, query, locale) + const mobile = variant === 'mobile' + const ink = mobile ? 'text-m-ink' : 'text-content' + const muted = mobile ? 'text-m-muted' : 'text-content-muted' + const faint = mobile ? 'text-m-faint' : 'text-content-faint' + const rowHover = mobile ? 'active:bg-[color:var(--m-ic)]' : 'hover:bg-surface-hover' + const placeholder = mobile ? 'placeholder:text-m-faint' : 'placeholder:text-content-faint' + + return ( +
+ {detail.places.length > 6 && ( +
+ + setQuery(e.target.value)} + placeholder={t('atlas.placesSearch')} + aria-label={t('atlas.placesSearch')} + className={`min-w-0 flex-1 border-0 bg-transparent text-[0.8125rem] outline-none ${ink} ${placeholder}`} + /> + {query && ( + + )} +
+ )} + + {groups.length === 0 ? ( +

{t('atlas.placesNone')}

+ ) : ( +
+ {groups.map(({ trip, places }) => ( +
+ + {places.map(place => ( + + ))} +
+ ))} +
+ )} +
+ ) +} diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx index d0ed712e3..4cdcdfe34 100644 --- a/client/src/components/Budget/CostsPanel.tsx +++ b/client/src/components/Budget/CostsPanel.tsx @@ -58,6 +58,8 @@ interface Settlement { // was recorded). Null/absent on rows predating this field — settlementDate() // falls back to created_at for those. settled_at?: string | null + /** A free-text note on the payment (#2340). */ + note?: string | null from_username?: string to_username?: string } @@ -117,7 +119,8 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps const [previewReceipts, setPreviewReceipts] = useState<{ receipts: BudgetItemReceipt[]; initialIndex: number } | null>(null) // One note open at a time: two expanded rows next to each other read as a mess, // and the point of the collapse is that the list stays scannable. - const [expandedNoteId, setExpandedNoteId] = useState(null) + // Keyed 'e' for an expense and 's' for a payment, whose ids are counted apart. + const [expandedNoteId, setExpandedNoteId] = useState(null) // One open final-budget breakdown at a time, for the same reason a single note // is expanded at a time: the card is a sidebar, not a report. const [expandedFinalId, setExpandedFinalId] = useState(null) @@ -798,7 +801,6 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps const net = round2(myPaidOf(e) - myShareOf(e)) const unfinished = isUnfinished(e) const note = readUserNote(e) - const open = expandedNoteId === e.id // Fixed columns, identical on every row, so the eye can run down the list // instead of re-finding each field on each line. A row without a note leaves @@ -891,21 +893,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps {/* The note, marked by a rule in the category colour instead of a box, so it reads as part of the row rather than something dropped onto it. */} - {!isMobile && ( - note ? ( - - - - ) : - )} + {!isMobile && NoteCell({ note, noteKey: 'e' + e.id, color: c.color })} {/* The money, and what to do with it. */}
@@ -924,6 +912,25 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps ) } + // A row's note as a pill that opens in place; an empty one keeps the column. + function NoteCell({ note, noteKey, color }: { note: string; noteKey: string; color: string }) { + if (!note) return + const open = expandedNoteId === noteKey + return ( + + + + ) + } + // A settle-up payment as a ledger row — visually distinct from an expense, with // inline edit + undo (reuses deleteSettlement) so it isn't buried in a modal. function SettlementRow({ s }: { s: Settlement }) { @@ -947,9 +954,9 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
- {/* A payment carries no note, but it keeps the column so its amount lands - on the same axis as every expense above it. */} - {!isMobile && } + {/* The payment's note (#2340), in the column every expense keeps for its own, + so its amount lands on the same axis as every expense above it. */} + {!isMobile && NoteCell({ note: s.note || '', noteKey: 's' + s.id, color: '#16a34a' })}
{fmt(settled(s))}
@@ -1220,6 +1227,7 @@ function SettlementModal({ tripId, people, me, editing, currency, tripCurrency, const [amount, setAmount] = useState(editing ? amountToInputString(editing.amount, (editing.currency || currency).toUpperCase()) : '') const [cur, setCur] = useState((editing?.currency || currency).toUpperCase()) const [day, setDay] = useState(editing ? settlementDate(editing) : localToday()) + const [note, setNote] = useState(editing?.note || '') const [saving, setSaving] = useState(false) const amt = Number.parseFloat(amount) || 0 @@ -1229,7 +1237,7 @@ function SettlementModal({ tripId, people, me, editing, currency, tripCurrency, const save = async () => { if (!valid) return setSaving(true) - const data = withFallbackFx({ from_user_id: Number(fromId), to_user_id: Number(toId), amount: amt, currency: cur, settled_at: day }, tripCurrency) + const data = withFallbackFx({ from_user_id: Number(fromId), to_user_id: Number(toId), amount: amt, currency: cur, settled_at: day, note: note.trim() || null }, tripCurrency) try { if (editing) await budgetApi.updateSettlement(tripId, editing.id, data) else await budgetApi.createSettlement(tripId, data) @@ -1294,6 +1302,10 @@ function SettlementModal({ tripId, people, me, editing, currency, tripCurrency, + +