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.
This commit is contained in:
Maurice
2026-06-14 00:54:11 +02:00
committed by GitHub
parent 3398da633b
commit 3e9626fce9
29 changed files with 331 additions and 18 deletions
+10 -7
View File
@@ -163,27 +163,30 @@ export class PlacesController {
}
@Post('import/google-list')
async importGoogle(@CurrentUser() user: User, @Param('tripId') tripId: string, @Body('url') url: unknown, @Headers('x-socket-id') socketId?: string) {
return this.importList('google', user, tripId, url, socketId);
async importGoogle(@CurrentUser() user: User, @Param('tripId') tripId: string, @Body('url') url: unknown, @Body('enrich') enrich: unknown, @Headers('x-socket-id') socketId?: string) {
return this.importList('google', user, tripId, url, enrich, socketId);
}
@Post('import/naver-list')
async importNaver(@CurrentUser() user: User, @Param('tripId') tripId: string, @Body('url') url: unknown, @Headers('x-socket-id') socketId?: string) {
return this.importList('naver', user, tripId, url, socketId);
async importNaver(@CurrentUser() user: User, @Param('tripId') tripId: string, @Body('url') url: unknown, @Body('enrich') enrich: unknown, @Headers('x-socket-id') socketId?: string) {
return this.importList('naver', user, tripId, url, enrich, socketId);
}
/** Shared google/naver list import — identical flow, different provider + error string. */
private async importList(provider: 'google' | 'naver', user: User, tripId: string, url: unknown, socketId?: string) {
private async importList(provider: 'google' | 'naver', user: User, tripId: string, url: unknown, enrich: unknown, socketId?: string) {
const trip = this.requireTrip(tripId, user);
this.requireEdit(trip, user);
if (!url || typeof url !== 'string') {
throw new HttpException({ error: 'URL is required' }, 400);
}
// Opt-in: re-resolve each imported place via the Places API to fill in
// photo / address / website / phone and persist a google_place_id (#886).
const opts = { enrich: parseBool(enrich, false), userId: user.id };
const label = provider === 'google' ? 'Google' : 'Naver';
try {
const result = provider === 'google'
? await this.places.importGoogleList(tripId, url)
: await this.places.importNaverList(tripId, url);
? await this.places.importGoogleList(tripId, url, opts)
: await this.places.importNaverList(tripId, url, opts);
if ('error' in result) {
throw new HttpException({ error: result.error }, result.status);
}