From 3e9626fce99e661d4eefc422763b089a2591c611 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:54:11 +0200 Subject: [PATCH] feat(places): enrich list-imported places via the Places API (#886) (#1161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(places): enrich list-imported places via the Places API (#886) Google/Naver list imports only carry a name and coordinates, so the places open as bare pins — the Maps tab jumps to coordinates, with no photo, address or open/closed. Add an opt-in "Enrich places via Google" toggle to the list-import dialog, shown only when a Google Maps key is configured. When enabled, after the (fast, unchanged) import the server runs a background pass that re-resolves each place by name — biased to and validated against the imported coordinates so a common-name search cannot overwrite the wrong place — and fills the empty address/website/phone/photo columns plus the resolved google_place_id, pushing each row over the live sync. Opening hours and the proper Maps link then work on demand from the stored id. Enrichment only fills empty fields, runs detached so a long list never blocks the import, and no-ops when no key is configured. * fix(places): use the ToggleSwitch component for the enrich toggle Match the rest of the app — the import-enrichment opt-in used a raw checkbox; swap it for the shared ToggleSwitch (text left, switch right) like the settings toggles. --- client/src/api/client.ts | 8 +- .../Planner/PlacesSidebarListImportModal.tsx | 11 ++ .../components/Planner/usePlacesSidebar.ts | 10 +- server/src/nest/places/places.controller.ts | 17 +- server/src/nest/places/places.service.ts | 8 +- server/src/services/placeEnrichment.ts | 164 ++++++++++++++++++ server/src/services/placeService.ts | 19 +- .../unit/services/placeEnrichment.test.ts | 49 ++++++ shared/src/i18n/ar/places.ts | 3 + shared/src/i18n/br/places.ts | 3 + shared/src/i18n/cs/places.ts | 3 + shared/src/i18n/de/places.ts | 3 + shared/src/i18n/en/places.ts | 3 + shared/src/i18n/es/places.ts | 3 + shared/src/i18n/fr/places.ts | 3 + shared/src/i18n/gr/places.ts | 3 + shared/src/i18n/hu/places.ts | 3 + shared/src/i18n/id/places.ts | 3 + shared/src/i18n/it/places.ts | 3 + shared/src/i18n/ja/places.ts | 3 + shared/src/i18n/ko/places.ts | 3 + shared/src/i18n/nl/places.ts | 3 + shared/src/i18n/pl/places.ts | 3 + shared/src/i18n/ru/places.ts | 3 + shared/src/i18n/tr/places.ts | 3 + shared/src/i18n/uk/places.ts | 3 + shared/src/i18n/zh-TW/places.ts | 3 + shared/src/i18n/zh/places.ts | 3 + shared/src/place/place.schema.ts | 3 + 29 files changed, 331 insertions(+), 18 deletions(-) create mode 100644 server/src/services/placeEnrichment.ts create mode 100644 server/tests/unit/services/placeEnrichment.test.ts diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 086294f4..899453c8 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -366,10 +366,10 @@ export const placesApi = { if (opts?.paths !== undefined) fd.append('importPaths', String(opts.paths)) return apiClient.post(`/trips/${tripId}/places/import/map`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) }, - importGoogleList: (tripId: number | string, url: string) => - apiClient.post(`/trips/${tripId}/places/import/google-list`, { url } satisfies PlaceImportListRequest).then(r => r.data), - importNaverList: (tripId: number | string, url: string) => - apiClient.post(`/trips/${tripId}/places/import/naver-list`, { url }).then(r => r.data), + importGoogleList: (tripId: number | string, url: string, enrich?: boolean) => + apiClient.post(`/trips/${tripId}/places/import/google-list`, { url, enrich } satisfies PlaceImportListRequest).then(r => r.data), + importNaverList: (tripId: number | string, url: string, enrich?: boolean) => + apiClient.post(`/trips/${tripId}/places/import/naver-list`, { url, enrich } satisfies PlaceImportListRequest).then(r => r.data), bulkDelete: (tripId: number | string, ids: number[]) => apiClient.post(`/trips/${tripId}/places/bulk-delete`, { ids } satisfies PlaceBulkDeleteRequest).then(r => r.data), } diff --git a/client/src/components/Planner/PlacesSidebarListImportModal.tsx b/client/src/components/Planner/PlacesSidebarListImportModal.tsx index 1dffc549..731b1ec4 100644 --- a/client/src/components/Planner/PlacesSidebarListImportModal.tsx +++ b/client/src/components/Planner/PlacesSidebarListImportModal.tsx @@ -1,10 +1,12 @@ import ReactDOM from 'react-dom' +import ToggleSwitch from '../Settings/ToggleSwitch' import type { SidebarState } from './usePlacesSidebar' export function ListImportModal(S: SidebarState) { const { setListImportOpen, setListImportUrl, t, hasMultipleListImportProviders, availableListImportProviders, listImportProvider, setListImportProvider, listImportUrl, listImportLoading, handleListImport, + listImportEnrich, setListImportEnrich, canEnrichImport, } = S return ReactDOM.createPortal(
+ {canEnrichImport && ( +
+
+
{t('places.enrichOnImport')}
+
{t('places.enrichOnImportHint')}
+
+ setListImportEnrich(!listImportEnrich)} /> +
+ )}