diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index 74f8a7941..df97d6b3b 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -1,6 +1,6 @@ { "e2e/help/fixtures.ts": 2291, - "src/api/client.ts": 1881, + "src/api/client.ts": 1916, "src/components/Admin/AdminPluginsPanel.test.tsx": 2704, "src/components/Admin/AdminPluginsPanel.tsx": 1667, "src/components/Budget/CostsPanel.test.tsx": 2764, @@ -24,7 +24,7 @@ "src/mobile/screens/admin/MAdminPluginsPanel.tsx": 1507, "src/pages/JourneyDetailPage.test.tsx": 4030, "src/pages/LoginPage.tsx": 1348, - "src/pages/TripPlannerPage.tsx": 1148, + "src/pages/TripPlannerPage.tsx": 1150, "src/pages/atlas/useAtlas.ts": 1287, "src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3518, "src/pages/tripPlanner/useTripPlanner.test.tsx": 3309, diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 76242330a..bff0db6b0 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 { AdminFeatureToggleResponse, MapsNearbyRequest, JourneyReorderEntryPhotosRequest, TransitProvider, GoogleQuotaUpdateRequest, NotificationDefaultsUpdateRequest, TourListItem, TourImportGpxResponse, TourCreateRequest, TourCreateResponse, TourDetailResponse } from '@trek/shared' +import type { MapsNearbyRequest, JourneyReorderEntryPhotosRequest, TransitProvider, GoogleQuotaUpdateRequest, NotificationDefaultsUpdateRequest, TourListItem, TourImportGpxResponse, TourCreateRequest, TourCreateResponse, TourDetailResponse } from '@trek/shared' import { randomId } from '../utils/randomId' import { postProviderPhotosInBatches } from './providerPhotoBatches' import { @@ -525,8 +525,6 @@ export const toursApi = { apiClient.post(`/trips/${tripId}/tours`, body).then(r => r.data), update: (tripId: number | string, placeId: number | string, body: TourCreateRequest): Promise => apiClient.put(`/trips/${tripId}/tours/${placeId}`, body).then(r => r.data), - restoreRecorded: (tripId: number | string, placeId: number | string): Promise => - apiClient.post(`/trips/${tripId}/tours/${placeId}/restore-recorded`).then(r => r.data), importGpx: (tripId: number | string, file: File): Promise => { const fd = new FormData() fd.append('file', file) @@ -708,10 +706,6 @@ export const adminApi = { checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data), getBagTracking: () => apiClient.get('/admin/bag-tracking').then(r => r.data), updateBagTracking: (enabled: boolean) => apiClient.put('/admin/bag-tracking', { enabled }).then(r => r.data), - getToursWanderer: (): Promise => - apiClient.get('/admin/tours-wanderer').then(r => r.data), - updateToursWanderer: (enabled: boolean): Promise => - apiClient.put('/admin/tours-wanderer', { enabled }).then(r => r.data), getPlacesPhotos: () => apiClient.get('/admin/places-photos').then(r => r.data), updatePlacesPhotos: (enabled: boolean) => apiClient.put('/admin/places-photos', { enabled }).then(r => r.data), getPlaceShadow: () => apiClient.get('/admin/place-shadow').then(r => r.data), diff --git a/client/src/api/recordedTrack.ts b/client/src/api/recordedTrack.ts new file mode 100644 index 000000000..0eb1c4919 --- /dev/null +++ b/client/src/api/recordedTrack.ts @@ -0,0 +1,8 @@ +import type { TourDetailResponse } from '@trek/shared'; +import apiClient from './client'; + +/** An import that was planned over in place keeps its recorded track; this brings it back. */ +export const recordedTrackApi = { + restore: (tripId: number | string, placeId: number | string): Promise => + apiClient.post(`/trips/${tripId}/tours/${placeId}/restore-recorded`).then((r) => r.data), +}; diff --git a/client/src/api/wanderer.ts b/client/src/api/wanderer.ts index f6d086c86..1317e9854 100644 --- a/client/src/api/wanderer.ts +++ b/client/src/api/wanderer.ts @@ -1,4 +1,5 @@ import type { + AdminFeatureToggleResponse, WandererConnection, WandererImportPreview, WandererImportRequest, @@ -39,3 +40,10 @@ export const wandererApi = { importTrail: (tripId: number, body: WandererImportRequest): Promise => apiClient.post(`/trips/${tripId}/tours/wanderer/import`, body, upstream).then((r) => r.data), }; + +/** The admin's switch for wanderer under the Tours addon. */ +export const wandererAdminApi = { + get: (): Promise => apiClient.get('/admin/tours-wanderer').then((r) => r.data), + update: (enabled: boolean): Promise => + apiClient.put('/admin/tours-wanderer', { enabled }).then((r) => r.data), +}; diff --git a/client/src/components/Admin/addons/useAddonManager.ts b/client/src/components/Admin/addons/useAddonManager.ts index 52212c5ee..d7fd05c63 100644 --- a/client/src/components/Admin/addons/useAddonManager.ts +++ b/client/src/components/Admin/addons/useAddonManager.ts @@ -1,6 +1,7 @@ import { useEffect, useEffectEvent, useState } from 'react'; import { adminApi } from '../../../api/client'; +import { wandererAdminApi } from '../../../api/wanderer'; import { useTranslation } from '../../../i18n'; import { useAddonStore } from '../../../store/addonStore'; import { useToast } from '../../shared/Toast'; @@ -31,8 +32,8 @@ export function useAddonManager() { const loadOnMount = useEffectEvent(() => { void loadAddons().finally(() => setLoading(false)); - adminApi - .getToursWanderer() + wandererAdminApi + .get() .then((flag) => setToursWanderer(flag.enabled)) .catch(() => toast.error(t('admin.addons.toast.error'))); }); @@ -67,7 +68,7 @@ export function useAddonManager() { const next = !toursWanderer; setToursWanderer(next); try { - await adminApi.updateToursWanderer(next); + await wandererAdminApi.update(next); } catch { setToursWanderer(!next); toast.error(t('admin.addons.toast.error')); diff --git a/client/src/repo/tourRepo.ts b/client/src/repo/tourRepo.ts index 692feb858..e981fe56a 100644 --- a/client/src/repo/tourRepo.ts +++ b/client/src/repo/tourRepo.ts @@ -1,5 +1,6 @@ import { computeTourMetrics, type GeometryPoint, type TourCreateRequest, type TourCreateResponse, type TourDetailResponse, type TourImportGpxResponse, type TourListItem, type TourWaypoint } from '@trek/shared' import { toursApi } from '../api/client' +import { recordedTrackApi } from '../api/recordedTrack' import { offlineDb, replaceTripTours, type CachedTour } from '../db/offlineDb' import { mutationQueue, generateUUID, nextTempId } from '../sync/mutationQueue' import { isEffectivelyOffline } from '../sync/networkMode' @@ -178,7 +179,7 @@ export const tourRepo = { /** Online only, like the import: the recorded track is kept on the server, not in the cache. */ async restoreRecorded(tripId: number | string, placeId: number): Promise { - const result = await toursApi.restoreRecorded(tripId, placeId) + const result = await recordedTrackApi.restore(tripId, placeId) await offlineDb.tours.put({ ...result.tour, trip_id: Number(tripId), waypoints: result.waypoints }) return result }, diff --git a/server/scripts/format-baseline.json b/server/scripts/format-baseline.json index 2b858159d..e1ce926b3 100644 --- a/server/scripts/format-baseline.json +++ b/server/scripts/format-baseline.json @@ -20,6 +20,7 @@ "src/nest/plugins/runtime/plugin-sdk.ts", "src/nest/reservations/reservations.mcp.ts", "src/nest/reservations/reservations.service.ts", + "src/nest/trips/trips.service.ts", "src/nest/vacay/vacay.service.ts", "tests/unit/db/repositories/Places.repository.test.ts", "tests/unit/nest/atlas.service.test.ts", diff --git a/server/src/nest/trips/trips.service.ts b/server/src/nest/trips/trips.service.ts index 08c3ede6f..575419bf6 100644 --- a/server/src/nest/trips/trips.service.ts +++ b/server/src/nest/trips/trips.service.ts @@ -1,46 +1,31 @@ -import { AssignmentParticipants } from '../../db/entities/AssignmentParticipants.entity'; -import { BudgetCategoryOrder } from '../../db/entities/BudgetCategoryOrder.entity'; -import { BudgetItemMembers } from '../../db/entities/BudgetItemMembers.entity'; -import { BudgetItemPayers } from '../../db/entities/BudgetItemPayers.entity'; -import { BudgetItems } from '../../db/entities/BudgetItems.entity'; -import { DayAccommodations } from '../../db/entities/DayAccommodations.entity'; -import { DayAssignments } from '../../db/entities/DayAssignments.entity'; -import { DayNotes } from '../../db/entities/DayNotes.entity'; +import { Injectable } from '@nestjs/common'; +import path from 'path'; +import { EntityManager } from '@mikro-orm/core'; +import { Trips } from '../../db/entities/Trips.entity'; import { Days } from '../../db/entities/Days.entity'; import { JourneyEntries } from '../../db/entities/JourneyEntries.entity'; -import { PackingBags } from '../../db/entities/PackingBags.entity'; -import { PackingItems } from '../../db/entities/PackingItems.entity'; +import { Users } from '../../db/entities/Users.entity'; import { Places } from '../../db/entities/Places.entity'; -import { Reservations } from '../../db/entities/Reservations.entity'; -import { RoadtripDayBoundaries } from '../../db/entities/RoadtripDayBoundaries.entity'; +import { DayAssignments } from '../../db/entities/DayAssignments.entity'; +import { AssignmentParticipants } from '../../db/entities/AssignmentParticipants.entity'; +import { Tags } from '../../db/entities/Tags.entity'; +import { DayNotes } from '../../db/entities/DayNotes.entity'; +import { RoadtripVias } from '../../db/entities/RoadtripVias.entity'; import { RoadtripDayTracks } from '../../db/entities/RoadtripDayTracks.entity'; import { RoadtripPreferences } from '../../db/entities/RoadtripPreferences.entity'; -import { RoadtripVias } from '../../db/entities/RoadtripVias.entity'; -import { Tags } from '../../db/entities/Tags.entity'; +import { RoadtripDayBoundaries } from '../../db/entities/RoadtripDayBoundaries.entity'; +import { DayAccommodations } from '../../db/entities/DayAccommodations.entity'; +import { Reservations } from '../../db/entities/Reservations.entity'; +import { BudgetItems } from '../../db/entities/BudgetItems.entity'; +import { BudgetItemMembers } from '../../db/entities/BudgetItemMembers.entity'; +import { BudgetItemPayers } from '../../db/entities/BudgetItemPayers.entity'; +import { BudgetCategoryOrder } from '../../db/entities/BudgetCategoryOrder.entity'; +import { PackingBags } from '../../db/entities/PackingBags.entity'; +import { PackingItems } from '../../db/entities/PackingItems.entity'; import { TodoItems } from '../../db/entities/TodoItems.entity'; -import { TourWaypoints } from '../../db/entities/TourWaypoints.entity'; import { Tours } from '../../db/entities/Tours.entity'; -import { Trips } from '../../db/entities/Trips.entity'; -import { Users } from '../../db/entities/Users.entity'; +import { TourWaypoints } from '../../db/entities/TourWaypoints.entity'; import type { TourWaypointRow } from '../../db/repositories/TourWaypoints.repository'; -import type { Trip, User } from '../../types'; -import { BudgetService, type CurrencyRebase } from '../budget/budget.service'; -import { NotFoundError, ValidationError } from '../common/domain-errors'; -import { legacyBoundIntegerText } from '../common/row-id'; -import { appClock } from '../common/timezoneService'; -import { UnitOfWork } from '../database/unit-of-work'; -import { DaysService } from '../days/days.service'; -import { PermissionsService } from '../permissions/permissions.service'; -import { escapeLikePattern } from '../places/places.helpers'; -import { RealtimeService } from '../realtime/realtime.service'; -import { ReservationsService } from '../reservations/reservations.service'; -import { SettingsService } from '../settings/settings.service'; -import { StorageService } from '../storage/storage.service'; -import { UnsplashService } from '../unsplash/unsplash.service'; -import { VacayService } from '../vacay/vacay.service'; -import { assertTripSpan } from './trips.helpers'; -import { EntityManager } from '@mikro-orm/core'; -import { Injectable } from '@nestjs/common'; import { addIsoDays, planDayGrid, @@ -51,8 +36,22 @@ import { type TrekWsPayload, type TrekWsTripEventName, } from '@trek/shared'; - -import path from 'path'; +import { RealtimeService } from '../realtime/realtime.service'; +import { PermissionsService } from '../permissions/permissions.service'; +import type { Trip, User } from '../../types'; +import { DaysService } from '../days/days.service'; +import { BudgetService, type CurrencyRebase } from '../budget/budget.service'; +import { ReservationsService } from '../reservations/reservations.service'; +import { VacayService } from '../vacay/vacay.service'; +import { UnsplashService } from '../unsplash/unsplash.service'; +import { StorageService } from '../storage/storage.service'; +import { SettingsService } from '../settings/settings.service'; +import { escapeLikePattern } from '../places/places.helpers'; +import { NotFoundError, ValidationError } from '../common/domain-errors'; +import { UnitOfWork } from '../database/unit-of-work'; +import { legacyBoundIntegerText } from '../common/row-id'; +import { appClock } from '../common/timezoneService'; +import { assertTripSpan } from './trips.helpers'; interface CreateTripData { title: string; @@ -100,14 +99,7 @@ export interface DeleteTripInfo { } export interface AddMemberResult { - member: { - id: number; - username: string; - email: string; - avatar?: string | null; - role: string; - avatar_url: string | null; - }; + member: { id: number; username: string; email: string; avatar?: string | null; role: string; avatar_url: string | null }; targetUserId: number; tripTitle: string; } @@ -329,12 +321,7 @@ export class TripsService { return this.permissions.checkPermission(action, role, ownerId, userId, isMember); } - broadcast( - tripId: string, - event: E, - payload: TrekWsPayload, - socketId: string | undefined, - ): void { + broadcast(tripId: string, event: E, payload: TrekWsPayload, socketId: string | undefined): void { this.realtime.broadcast(tripId, event, payload, socketId); } @@ -354,40 +341,28 @@ export class TripsService { * runs in one `uow.transactional` — a savepoint when `updateTrip`'s own * block already holds the transaction, its own transaction from `create`. */ - async generateDays( - tripId: number | bigint | string, - startDate: string | null, - endDate: string | null, - dayCount?: number, - ): Promise { + async generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null, dayCount?: number): Promise { const trip_id = Number(tripId); return await this.uow.transactional(async () => { const existing = await this.daysRepo.listForDayGrid(trip_id); // TP77 const stays = await this.daysRepo.listDayGridStays(trip_id); // TP78 const plan = planDayGrid({ - days: existing.map((d) => ({ - id: d.id, - day_number: d.day_number, - date: d.date, - hasPlanItems: !!d.has_plan_items, - })), + days: existing.map(d => ({ id: d.id, day_number: d.day_number, date: d.date, hasPlanItems: !!d.has_plan_items })), stays, startDate, endDate, dayCount, }); - const dateBefore = new Map(existing.map((d) => [d.id, d.date])); + const dateBefore = new Map(existing.map(d => [d.id, d.date])); for (let i = 0; i < existing.length; i++) await this.daysRepo.setDayNumber(existing[i].id, -(i + 1)); // TP2 for (let i = 0; i < plan.rows.length; i++) { const row = plan.rows[i]; - if (row.id === null) - await this.daysRepo.insertDay({ trip_id, day_number: i + 1, date: row.date }); // TP10 + if (row.id === null) await this.daysRepo.insertDay({ trip_id, day_number: i + 1, date: row.date }); // TP10 // A row that had no date and gets none is only renumbered, so an odd // stored value is left as it was, the way the rebuild always treated it. - else if (row.date === null && !dateBefore.get(row.id)) - await this.daysRepo.setDayNumber(row.id, i + 1); // TP2 + else if (row.date === null && !dateBefore.get(row.id)) await this.daysRepo.setDayNumber(row.id, i + 1); // TP2 else await this.daysRepo.setDayNumberAndDate(row.id, i + 1, row.date); // TP9 } for (const gone of plan.removed) await this.daysRepo.deleteById(gone.id); // TP11 @@ -410,19 +385,15 @@ export class TripsService { if (startDate && !endDate) endDate = addIsoDays(startDate, 6); else if (!startDate && endDate) startDate = addIsoDays(endDate, -6); if (startDate && endDate) assertTripSpan(startDate, endDate); - const rd = - data.reminder_days !== undefined - ? Number(data.reminder_days) >= 0 && Number(data.reminder_days) <= 30 - ? Number(data.reminder_days) - : 3 - : 3; + const rd = data.reminder_days !== undefined + ? (Number(data.reminder_days) >= 0 && Number(data.reminder_days) <= 30 ? Number(data.reminder_days) : 3) + : 3; const currency = data.currency || (await this.defaultCurrencyFor(userId)); // The trip and its days commit together: a failure in between used to leave // a dated trip without a single day. const tripId = await this.uow.transactional(async () => { - const id = await this.tripsRepo.insertTrip({ - // TP18 + const id = await this.tripsRepo.insertTrip({ // TP18 user_id: userId, title: data.title, description: data.description || null, @@ -441,7 +412,7 @@ export class TripsService { /** TP20 — `TripsRepository.findForViewer`: `TRIP_SELECT` scoped to one trip AND the access predicate. */ async get(tripId: string | number, userId: number) { - return (await this.tripsRepo.findForViewer(tripId, userId)) as Trip | undefined; + return await this.tripsRepo.findForViewer(tripId, userId) as Trip | undefined; } /** @@ -457,7 +428,7 @@ export class TripsService { */ /** TP21 — `TripsRepository.activeTrip`: the triple `CASE WHEN … relevance` projection and the double-`CASE WHEN` `ORDER BY`. */ async activeTrip(userId: number, today = appClock().date) { - return (await this.tripsRepo.activeTrip(userId, today)) as (ActiveTrip & { relevance: number }) | undefined; + return await this.tripsRepo.activeTrip(userId, today) as ActiveTrip & { relevance: number } | undefined; } /** @@ -501,14 +472,8 @@ export class TripsService { * rebase, the trip row (TP25), the leave entries that follow its dates and the rebuilt day grid * are one transaction, so a failing `generateDays` or booking resync leaves all of them as they were. */ - async updateTrip( - tripId: string | number, - userId: number, - data: UpdateTripData, - userRole: string, - rebase: CurrencyRebase | null = null, - ): Promise { - const trip = (await this.tripsRepo.findRaw(tripId)) as (Trip & { reminder_days?: number }) | null; // TP24 + async updateTrip(tripId: string | number, userId: number, data: UpdateTripData, userRole: string, rebase: CurrencyRebase | null = null): Promise { + const trip = await this.tripsRepo.findRaw(tripId) as (Trip & { reminder_days?: number }) | null; // TP24 if (!trip) throw new NotFoundError('Trip not found'); const { title, description, currency, is_archived, cover_image, reminder_days } = data; @@ -520,12 +485,9 @@ export class TripsService { const newArchived = is_archived !== undefined ? (is_archived ? 1 : 0) : trip.is_archived; const newCover = cover_image !== undefined ? cover_image : trip.cover_image; const oldReminder = (trip as any).reminder_days ?? 3; - const newReminder = - reminder_days !== undefined - ? Number(reminder_days) >= 0 && Number(reminder_days) <= 30 - ? Number(reminder_days) - : oldReminder - : oldReminder; + const newReminder = reminder_days !== undefined + ? (Number(reminder_days) >= 0 && Number(reminder_days) <= 30 ? Number(reminder_days) : oldReminder) + : oldReminder; const tripIdNum = Number(tripId); // safe: `trip` above only resolved through the raw-bind seam on a real row (Task 6 review's own ruling on this exact conversion) // The row, the leave entries that follow its dates and the rebuilt day grid are @@ -535,8 +497,7 @@ export class TripsService { await this.uow.transactional(async () => { // Before the row: the rebase reads the outgoing currency off it (#1543). if (rebase) await this.budget.applyCurrencyRebase(tripId, rebase); - await this.tripsRepo.updateTripRow(tripIdNum, { - // TP25 + await this.tripsRepo.updateTripRow(tripIdNum, { // TP25 title: newTitle, description: newDesc ?? null, start_date: newStart || null, @@ -553,24 +514,19 @@ export class TripsService { }); if (trip.start_date && trip.end_date && newStart && newStart !== trip.start_date) - vacayId = await this.vacay.shiftOwnerEntriesForTripWindow( - trip.user_id, - trip.start_date, - trip.end_date, - newStart, - ); + vacayId = await this.vacay.shiftOwnerEntriesForTripWindow(trip.user_id, trip.start_date, trip.end_date, newStart); if (regenerate) { // Accommodations have no absolute date columns, so their pre-change dates must be // snapshotted before generateDays re-dates the day rows in place. const prevDays = await this.daysRepo.listOrderedForReorder(tripIdNum); // TP26 - const prevDateByDayId = new Map(prevDays.map((d) => [d.id, d.date])); + const prevDateByDayId = new Map(prevDays.map(d => [d.id, d.date])); removedDays = (await this.generateDays(tripId, newStart || null, newEnd || null, dayCount)).removed; if (data.date_shift_mode === 'shift_all') { // Explicit "shift everything": bookings stay glued to their (re-dated) day rows, // so re-stamp reservation_time to follow — same rules as reorderDays/insertDay. const newDays = await this.daysRepo.listOrderedForReorder(tripIdNum); // TP27 - const newDateByDayId = new Map(newDays.map((d) => [d.id, d.date])); + const newDateByDayId = new Map(newDays.map(d => [d.id, d.date])); await this.days.restampReservationDates(tripIdNum, prevDateByDayId, newDateByDayId); } else { // Default: generateDays re-dates day rows positionally; re-anchor dated bookings to @@ -705,8 +661,7 @@ export class TripsService { const newTitle = title || src.title; return await this.uow.transactional(async () => { - const newTripId = await this.tripsRepo.insertTripCopy({ - // TP37 + const newTripId = await this.tripsRepo.insertTripCopy({ // TP37 user_id: newOwnerId, title: newTitle, description: src.description, @@ -720,13 +675,8 @@ export class TripsService { const oldDays = await this.daysRepo.listByTrip(Number(sourceTripId)); // TP38 const dayMap = new Map(); for (const d of oldDays) { - const newDayId = await this.daysRepo.insertDayCopy({ - // TP39 - trip_id: newTripId, - day_number: d.day_number, - date: d.date, - notes: d.notes, - title: d.title, + const newDayId = await this.daysRepo.insertDayCopy({ // TP39 + trip_id: newTripId, day_number: d.day_number, date: d.date, notes: d.notes, title: d.title, }); dayMap.set(d.id, newDayId); } @@ -734,35 +684,15 @@ export class TripsService { const oldPlaces = await this.placesRepo.listAllForTrip(sourceTripId); // TP40 const placeMap = new Map(); for (const p of oldPlaces) { - const newPlaceId = await this.placesRepo.insertPlaceCopy({ - // TP41 - trip_id: newTripId, - name: p.name, - description: p.description, - lat: p.lat, - lng: p.lng, - address: p.address, - category_id: p.category_id, - price: p.price, - currency: p.currency, - reservation_status: p.reservation_status, - reservation_notes: p.reservation_notes, - reservation_datetime: p.reservation_datetime, - place_time: p.place_time, - end_time: p.end_time, - duration_minutes: p.duration_minutes, - notes: p.notes, - image_url: p.image_url, - google_place_id: p.google_place_id, - google_ftid: p.google_ftid, - website: p.website, - phone: p.phone, - transport_mode: p.transport_mode, - osm_id: p.osm_id, - amap_poi_id: p.amap_poi_id, - route_geometry: p.route_geometry, - route_color: p.route_color, - stop_type: p.stop_type, + const newPlaceId = await this.placesRepo.insertPlaceCopy({ // TP41 + trip_id: newTripId, name: p.name, description: p.description, lat: p.lat, lng: p.lng, + address: p.address, category_id: p.category_id, price: p.price, currency: p.currency, + reservation_status: p.reservation_status, reservation_notes: p.reservation_notes, + reservation_datetime: p.reservation_datetime, place_time: p.place_time, end_time: p.end_time, + duration_minutes: p.duration_minutes, notes: p.notes, image_url: p.image_url, + google_place_id: p.google_place_id, google_ftid: p.google_ftid, website: p.website, phone: p.phone, + transport_mode: p.transport_mode, osm_id: p.osm_id, amap_poi_id: p.amap_poi_id, + route_geometry: p.route_geometry, route_color: p.route_color, stop_type: p.stop_type, fill_percent: p.fill_percent, }); placeMap.set(p.id, newPlaceId); @@ -778,13 +708,7 @@ export class TripsService { for (const v of oldVias) { const newDayId = dayMap.get(v.day_id); if (newDayId) { - await this.roadtripViasRepo.insertVia({ - day_id: newDayId, - after_order_index: v.after_order_index, - sequence: v.sequence, - lat: v.lat, - lng: v.lng, - }); // TP43 + await this.roadtripViasRepo.insertVia({ day_id: newDayId, after_order_index: v.after_order_index, sequence: v.sequence, lat: v.lat, lng: v.lng }); // TP43 } } @@ -843,18 +767,11 @@ export class TripsService { const newDayId = dayMap.get(a.day_id); const newPlaceId = placeMap.get(a.place_id); if (newDayId && newPlaceId) { - const newAssignmentId = await this.dayAssignmentsRepo.insertAssignmentCopy({ - // TP49 - day_id: newDayId, - place_id: newPlaceId, - order_index: a.order_index, - notes: a.notes, - reservation_status: a.reservation_status, - reservation_notes: a.reservation_notes, - reservation_datetime: a.reservation_datetime, - assignment_time: a.assignment_time, - assignment_end_time: a.assignment_end_time, - end_day: a.end_day ?? 0, + const newAssignmentId = await this.dayAssignmentsRepo.insertAssignmentCopy({ // TP49 + day_id: newDayId, place_id: newPlaceId, order_index: a.order_index, notes: a.notes, + reservation_status: a.reservation_status, reservation_notes: a.reservation_notes, + reservation_datetime: a.reservation_datetime, assignment_time: a.assignment_time, + assignment_end_time: a.assignment_end_time, end_day: a.end_day ?? 0, }); assignmentMap.set(a.id, newAssignmentId); } @@ -873,12 +790,7 @@ export class TripsService { // new trip's `roadtrip_day_boundaries` starts empty, same reasoning // as the via/track upserts above. if (from && to !== undefined) { - await this.roadtripDayBoundariesRepo.upsertBoundary(newTripId, { - day_number: boundary.day_number, - from_assignment_id: from, - to_assignment_id: to, - fraction: boundary.fraction, - }); // TP52 + await this.roadtripDayBoundariesRepo.upsertBoundary(newTripId, { day_number: boundary.day_number, from_assignment_id: from, to_assignment_id: to, fraction: boundary.fraction }); // TP52 } } @@ -896,17 +808,9 @@ export class TripsService { const newStartDay = dayMap.get(a.start_day_id); const newEndDay = dayMap.get(a.end_day_id); if (newPlaceId && newStartDay && newEndDay) { - const newAccomId = await this.dayAccommodationsRepo.insertStay({ - // TP56 - trip_id: newTripId, - place_id: newPlaceId, - start_day_id: newStartDay, - end_day_id: newEndDay, - check_in: a.check_in, - check_in_end: a.check_in_end, - check_out: a.check_out, - confirmation: a.confirmation, - notes: a.notes, + const newAccomId = await this.dayAccommodationsRepo.insertStay({ // TP56 + trip_id: newTripId, place_id: newPlaceId, start_day_id: newStartDay, end_day_id: newEndDay, + check_in: a.check_in, check_in_end: a.check_in_end, check_out: a.check_out, confirmation: a.confirmation, notes: a.notes, }); accomMap.set(a.id, newAccomId); } @@ -943,22 +847,12 @@ export class TripsService { // the NEW value is re-formatted to the legacy raw-bound `'.0'` // TEXT shape on the way back out — see `legacyBoundIntegerText`. accommodation_id: newAccomId != null ? legacyBoundIntegerText(newAccomId) : null, - title: r.title, - reservation_time: r.reservation_time, - reservation_end_time: r.reservation_end_time, - location: r.location, - confirmation_number: r.confirmation_number, - notes: r.notes, - url: r.url, - status: r.status, - type: r.type, + title: r.title, reservation_time: r.reservation_time, reservation_end_time: r.reservation_end_time, + location: r.location, confirmation_number: r.confirmation_number, notes: r.notes, url: r.url, status: r.status, type: r.type, // ingest_state travels with the copy: a staged booking must not turn // 'live' just because the trip was duplicated, or it lands in the // duplicate's public feed. - metadata: r.metadata, - day_plan_position: r.day_plan_position, - needs_review: r.needs_review ?? 0, - ingest_state: r.ingest_state ?? 'live', + metadata: r.metadata, day_plan_position: r.day_plan_position, needs_review: r.needs_review ?? 0, ingest_state: r.ingest_state ?? 'live', }); // TP59 reservationMap.set(r.id, newReservationId); } @@ -976,7 +870,7 @@ export class TripsService { for (const row of [...oldBudgetMembers, ...oldBudgetPayers]) { if (row.user_id !== newOwnerId) sharedWithOthers.add(row.budget_item_id); } - const hasSplitRows = new Set(oldBudgetMembers.map((bm) => bm.budget_item_id)); + const hasSplitRows = new Set(oldBudgetMembers.map(bm => bm.budget_item_id)); // TP60 — Plan 3e Task 2, converted. const oldBudget = await this.budgetItemsRepo.listAllForTrip(sourceTripId); @@ -987,18 +881,11 @@ export class TripsService { // split is its member count and goes with it; a typed one stays. A receipt // split names who had which line, so it goes as well. const newItemId = await this.budgetItemsRepo.insertCopy({ - trip_id: newTripId, - category: b.category, - name: b.name, - total_price: b.total_price, - persons: planningOnly && hasSplitRows.has(b.id) ? null : b.persons, - days: b.days, - note: b.note, - sort_order: b.sort_order, + trip_id: newTripId, category: b.category, name: b.name, total_price: b.total_price, + persons: planningOnly && hasSplitRows.has(b.id) ? null : b.persons, days: b.days, + note: b.note, sort_order: b.sort_order, reservation_id: b.reservation_id ? (reservationMap.get(b.reservation_id) ?? null) : null, - currency: b.currency, - exchange_rate: b.exchange_rate ?? 1, - expense_date: b.expense_date, + currency: b.currency, exchange_rate: b.exchange_rate ?? 1, expense_date: b.expense_date, ticket_json: planningOnly ? null : b.ticket_json, paid_by_user_id: b.paid_by_user_id === newOwnerId ? b.paid_by_user_id : null, }); @@ -1009,25 +896,14 @@ export class TripsService { if (sharedWithOthers.has(bm.budget_item_id)) continue; const newItemId = budgetMap.get(bm.budget_item_id); // TP63 — Plan 3e Task 2, converted. - if (newItemId) - await this.budgetItemMembersRepo.insertIgnore({ - budget_item_id: newItemId, - user_id: bm.user_id, - paid: bm.paid ?? 0, - amount: bm.amount, - }); + if (newItemId) await this.budgetItemMembersRepo.insertIgnore({ budget_item_id: newItemId, user_id: bm.user_id, paid: bm.paid ?? 0, amount: bm.amount }); } for (const bp of oldBudgetPayers) { if (sharedWithOthers.has(bp.budget_item_id)) continue; const newItemId = budgetMap.get(bp.budget_item_id); // TP65 — Plan 3e Task 2, converted. - if (newItemId) - await this.budgetItemPayersRepo.insertIgnore({ - budget_item_id: newItemId, - user_id: bp.user_id, - amount: bp.amount ?? 0, - }); + if (newItemId) await this.budgetItemPayersRepo.insertIgnore({ budget_item_id: newItemId, user_id: bp.user_id, amount: bp.amount ?? 0 }); } // TP66 — Plan 3e Task 3, converted. @@ -1036,11 +912,7 @@ export class TripsService { for (const bag of oldBags) { // TP67 — Plan 3e Task 3, converted (`insertBag`, PK42's own column set). const newBagId = await this.packingBagsRepo.insertBag({ - trip_id: newTripId, - name: bag.name, - color: bag.color, - sort_order: bag.sort_order, - weight_limit_grams: bag.weight_limit_grams, + trip_id: newTripId, name: bag.name, color: bag.color, sort_order: bag.sort_order, weight_limit_grams: bag.weight_limit_grams, }); bagMap.set(bag.id, newBagId); } @@ -1057,14 +929,9 @@ export class TripsService { const isPrivate = p.is_private ? 1 : 0; // TP69 — Plan 3e Task 3, converted. await this.packingItemsRepo.insertCopy({ - trip_id: newTripId, - name: p.name, - category: p.category, - sort_order: p.sort_order, - weight_grams: p.weight_grams, + trip_id: newTripId, name: p.name, category: p.category, sort_order: p.sort_order, weight_grams: p.weight_grams, bag_id: p.bag_id ? (bagMap.get(p.bag_id) ?? null) : null, - is_private: isPrivate, - owner_id: isPrivate ? newOwnerId : null, + is_private: isPrivate, owner_id: isPrivate ? newOwnerId : null, }); } @@ -1072,14 +939,8 @@ export class TripsService { for (const n of oldNotes) { const newDayId = dayMap.get(n.day_id); if (newDayId) { - await this.dayNotesRepo.insertNoteCopy({ - // TP71 - day_id: newDayId, - trip_id: newTripId, - text: n.text, - time: n.time, - icon: n.icon, - sort_order: n.sort_order, + await this.dayNotesRepo.insertNoteCopy({ // TP71 + day_id: newDayId, trip_id: newTripId, text: n.text, time: n.time, icon: n.icon, sort_order: n.sort_order, }); } } @@ -1089,13 +950,8 @@ export class TripsService { for (const t of oldTodos) { // TP73 — Plan 3e Task 4, converted (`assigned_user_id` deliberately not carried over). await this.todoItemsRepo.insertCopy({ - trip_id: newTripId, - name: t.name, - category: t.category, - sort_order: t.sort_order, - due_date: t.due_date, - description: t.description, - priority: t.priority, + trip_id: newTripId, name: t.name, category: t.category, sort_order: t.sort_order, + due_date: t.due_date, description: t.description, priority: t.priority, }); } @@ -1114,6 +970,7 @@ export class TripsService { async getCopiedTrip(newTripId: number, userId: number) { return await this.tripsRepo.findForViewer(newTripId, userId); } + } // Defined in common/ so calendar and maps can raise them without importing the