Files
TREK/server/src/nest/places/places.service.ts
T
Maurice 3e9626fce9 feat(places): enrich list-imported places via the Places API (#886) (#1161)
* 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.
2026-06-14 00:54:11 +02:00

84 lines
3.0 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { broadcast } from '../../websocket';
import { canAccessTrip } from '../../db/database';
import { checkPermission } from '../../services/permissions';
import type { User } from '../../types';
import * as svc from '../../services/placeService';
import { onPlaceCreated, onPlaceUpdated, onPlaceDeleted } from '../../services/journeyService';
type Trip = { user_id: number };
/**
* Thin Nest wrapper around the existing place service. Trip access mirrors the
* requireTripAccess middleware (canAccessTrip); mutations use 'place_edit'. The
* SQL, the GPX/map/list importers and the journey hooks reuse the legacy code
* unchanged.
*/
@Injectable()
export class PlacesService {
verifyTripAccess(tripId: string, userId: number) {
return canAccessTrip(Number(tripId), userId) as Trip | null | undefined;
}
canEdit(trip: Trip, user: User): boolean {
return checkPermission('place_edit', user.role, trip.user_id, user.id, trip.user_id !== user.id);
}
broadcast(tripId: string, event: string, payload: Record<string, unknown>, socketId: string | undefined): void {
broadcast(tripId, event, payload, socketId);
}
list(tripId: string, filters: { search?: string; category?: string; tag?: string }) {
return svc.listPlaces(tripId, filters);
}
get(tripId: string, id: string) {
return svc.getPlace(tripId, id);
}
create(tripId: string, data: Parameters<typeof svc.createPlace>[1]) {
return svc.createPlace(tripId, data);
}
update(tripId: string, id: string, data: Parameters<typeof svc.updatePlace>[2]) {
return svc.updatePlace(tripId, id, data);
}
remove(tripId: string, id: string): boolean {
return svc.deletePlace(tripId, id);
}
removeMany(tripId: string, ids: number[]): number[] {
return svc.deletePlacesMany(tripId, ids);
}
importGpx(
tripId: string,
buffer: Buffer,
opts: { importWaypoints: boolean; importRoutes: boolean; importTracks: boolean; defaultName?: string },
) {
return svc.importGpx(tripId, buffer, opts);
}
importMapFile(tripId: string, buffer: Buffer, filename: string, opts: svc.KmlImportOptions) {
return svc.importMapFile(tripId, buffer, filename, opts);
}
importGoogleList(tripId: string, url: string, opts?: Parameters<typeof svc.importGoogleList>[2]) {
return svc.importGoogleList(tripId, url, opts);
}
importNaverList(tripId: string, url: string, opts?: Parameters<typeof svc.importNaverList>[2]) {
return svc.importNaverList(tripId, url, opts);
}
searchImage(tripId: string, id: string, userId: number) {
return svc.searchPlaceImage(tripId, id, userId);
}
// Journey hooks — non-fatal, mirroring the route's try/catch wrappers.
onCreated(tripId: string, placeId: number): void { try { onPlaceCreated(Number(tripId), placeId); } catch { /* non-fatal */ } }
onUpdated(placeId: number): void { try { onPlaceUpdated(placeId); } catch { /* non-fatal */ } }
onDeleted(placeId: number): void { try { onPlaceDeleted(placeId); } catch { /* non-fatal */ } }
}