From 50609b078afd82d9c7266701fffbf17eaadcf011 Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 29 Jun 2026 23:01:16 +0200 Subject: [PATCH] feat(places): bulk "change category" from the selection toolbar Closes the UI half of #1168: in the Places selection mode, a new tag button before delete opens a category picker that applies one category (or "No category") to every selected place in a single request. Adds a REST /places/bulk-update endpoint reusing updatePlacesMany, an offline-aware repo + store action that patches both the place pool and the day-assignment projections, undo grouped by each place's prior category, and the i18n keys across all locales. --- client/src/api/client.ts | 3 + .../Planner/PlacesBulkCategoryModal.tsx | 65 +++++++++++++++++++ .../src/components/Planner/PlacesSidebar.tsx | 10 +++ .../Planner/PlacesSidebarSelectionBar.tsx | 21 +++++- .../components/Planner/usePlacesSidebar.ts | 3 + client/src/pages/TripPlannerPage.tsx | 5 +- .../src/pages/tripPlanner/useTripPlanner.ts | 28 +++++++- client/src/repo/placeRepo.ts | 27 ++++++++ client/src/store/slices/placesSlice.ts | 30 +++++++++ client/tests/unit/slices/placesSlice.test.ts | 29 +++++++++ server/src/nest/places/places.controller.ts | 30 +++++++++ server/src/nest/places/places.service.ts | 4 ++ .../tests/unit/nest/places.controller.test.ts | 32 +++++++++ shared/src/i18n/ar/places.ts | 2 + shared/src/i18n/ar/undo.ts | 1 + shared/src/i18n/br/places.ts | 2 + shared/src/i18n/br/undo.ts | 1 + shared/src/i18n/cs/places.ts | 2 + shared/src/i18n/cs/undo.ts | 1 + shared/src/i18n/de/places.ts | 2 + shared/src/i18n/de/undo.ts | 1 + shared/src/i18n/en/places.ts | 2 + shared/src/i18n/en/undo.ts | 1 + shared/src/i18n/es/places.ts | 2 + shared/src/i18n/es/undo.ts | 1 + shared/src/i18n/fr/places.ts | 2 + shared/src/i18n/fr/undo.ts | 1 + shared/src/i18n/gr/places.ts | 2 + shared/src/i18n/gr/undo.ts | 1 + shared/src/i18n/hu/places.ts | 2 + shared/src/i18n/hu/undo.ts | 1 + shared/src/i18n/id/places.ts | 2 + shared/src/i18n/id/undo.ts | 1 + shared/src/i18n/it/places.ts | 2 + shared/src/i18n/it/undo.ts | 1 + shared/src/i18n/ja/places.ts | 2 + shared/src/i18n/ja/undo.ts | 1 + shared/src/i18n/ko/places.ts | 2 + shared/src/i18n/ko/undo.ts | 1 + shared/src/i18n/nl/places.ts | 2 + shared/src/i18n/nl/undo.ts | 1 + shared/src/i18n/pl/places.ts | 2 + shared/src/i18n/pl/undo.ts | 1 + shared/src/i18n/ru/places.ts | 2 + shared/src/i18n/ru/undo.ts | 1 + shared/src/i18n/sv/places.ts | 2 + shared/src/i18n/sv/undo.ts | 1 + shared/src/i18n/tr/places.ts | 2 + shared/src/i18n/tr/undo.ts | 1 + shared/src/i18n/uk/places.ts | 2 + shared/src/i18n/uk/undo.ts | 1 + shared/src/i18n/vi/places.ts | 2 + shared/src/i18n/vi/undo.ts | 1 + shared/src/i18n/zh-TW/places.ts | 2 + shared/src/i18n/zh-TW/undo.ts | 1 + shared/src/i18n/zh/places.ts | 2 + shared/src/i18n/zh/undo.ts | 1 + shared/src/place/place.schema.ts | 8 +++ 58 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 client/src/components/Planner/PlacesBulkCategoryModal.tsx diff --git a/client/src/api/client.ts b/client/src/api/client.ts index e55e56fa1..5b5ac1a58 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -27,6 +27,7 @@ import { type TodoCreateItemRequest, type TodoUpdateItemRequest, type AssignmentCreateRequest, type AssignmentParticipantsRequest, type AssignmentTimeRequest, type PlaceBulkDeleteRequest, + type PlaceBulkUpdateRequest, type DayNoteCreateRequest, type DayNoteUpdateRequest, type PackingImportRequest, type PackingBagMembersRequest, type PackingUpdateBagRequest, type PackingCategoryAssigneesRequest, @@ -379,6 +380,8 @@ export const placesApi = { 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), + bulkUpdate: (tripId: number | string, ids: number[], data: Omit) => + apiClient.post(`/trips/${tripId}/places/bulk-update`, { ids, ...data } satisfies PlaceBulkUpdateRequest).then(r => r.data), } export const assignmentsApi = { diff --git a/client/src/components/Planner/PlacesBulkCategoryModal.tsx b/client/src/components/Planner/PlacesBulkCategoryModal.tsx new file mode 100644 index 000000000..56165234e --- /dev/null +++ b/client/src/components/Planner/PlacesBulkCategoryModal.tsx @@ -0,0 +1,65 @@ +import { createPortal } from 'react-dom' +import { X, MapPin } from 'lucide-react' +import { getCategoryIcon } from '../shared/categoryIcons' +import { useTranslation } from '../../i18n' +import type { Category } from '../../types' + +interface PlacesBulkCategoryModalProps { + count: number + categories: Category[] + onPick: (categoryId: number | null) => void + onClose: () => void +} + +const rowStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', gap: 9, width: '100%', + padding: '8px 10px', borderRadius: 7, border: 'none', cursor: 'pointer', + fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))', textAlign: 'left', +} +const hoverOn = (e: React.MouseEvent) => { e.currentTarget.style.background = 'var(--bg-hover)' } +const hoverOff = (e: React.MouseEvent) => { e.currentTarget.style.background = 'transparent' } + +/** + * Popup for the Places selection toolbar: pick one category to apply to every + * currently-selected place. Reuses the category swatch styling from the header's + * filter dropdown; clicking a row applies immediately and closes. + */ +export function PlacesBulkCategoryModal({ count, categories, onPick, onClose }: PlacesBulkCategoryModalProps) { + const { t } = useTranslation() + return createPortal( +
{ if (e.target === e.currentTarget) onClose() }} + style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }} + > +
+
+ {t('places.changeCategory')} + +
+

{t('places.selectionCount', { count })}

+ +
+ {categories.map(c => { + const CatIcon = getCategoryIcon(c.icon) + return ( + + ) + })} + +
+
+
, + document.body, + ) +} diff --git a/client/src/components/Planner/PlacesSidebar.tsx b/client/src/components/Planner/PlacesSidebar.tsx index 053faf3b0..b2c30aec1 100644 --- a/client/src/components/Planner/PlacesSidebar.tsx +++ b/client/src/components/Planner/PlacesSidebar.tsx @@ -8,6 +8,7 @@ import { PlacesSelectionBar } from './PlacesSidebarSelectionBar' import { PlacesList } from './PlacesSidebarList' import { MobileDayPickerSheet } from './PlacesSidebarMobileDayPicker' import { ListImportModal } from './PlacesSidebarListImportModal' +import { PlacesBulkCategoryModal } from './PlacesBulkCategoryModal' const PlacesSidebar = React.memo(function PlacesSidebar(props: PlacesSidebarProps) { const S = usePlacesSidebar(props) @@ -16,6 +17,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar(props: PlacesSidebarProp selectMode, filtered, t, dayPickerPlace, listImportOpen, fileImportOpen, setFileImportOpen, sidebarDropFile, setSidebarDropFile, tripId, pushUndo, ctxMenu, isMobile, pendingDeleteIds, setPendingDeleteIds, onBulkDeleteConfirm, + categories, selectedIds, exitSelectMode, onBulkChangeCategory, categoryPickerOpen, setCategoryPickerOpen, } = S return (
+ {categoryPickerOpen && ( + setCategoryPickerOpen(false)} + onPick={(catId) => { onBulkChangeCategory?.(Array.from(selectedIds), catId); setCategoryPickerOpen(false); exitSelectMode() }} + /> + )} {isMobile && ( + + +
diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index 7a354909e..adfcb0ad7 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -525,6 +525,32 @@ export function useTripPlanner() { } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) } }, [deletePlaceIds, tripId, toast, selectedPlaceId, selectedDayId, updateRouteForDay, pushUndo]) + const confirmChangeCategory = useCallback(async (ids: number[], categoryId: number | null) => { + if (!ids.length) return + const state = useTripStore.getState() + // Capture each place's prior category so undo can restore them per group. + const captured = state.places.filter(p => ids.includes(p.id)).map(p => ({ id: p.id, prev: p.category_id ?? null })) + try { + await tripActions.updatePlacesMany(tripId, ids, { category_id: categoryId }) + toast.success(t('places.categoryChanged', { count: ids.length })) + if (captured.length > 0) { + pushUndo(t('undo.changeCategory'), async () => { + // Group the captured ids by their prior category so each set is restored + // in one call ('null' key = previously uncategorized). Map is shadowed by + // the lucide icon import in this file, so use a plain object. + const byPrev: Record = {} + for (const { id, prev } of captured) { + const key = prev === null ? 'null' : String(prev) + ;(byPrev[key] ??= []).push(id) + } + for (const [key, group] of Object.entries(byPrev)) { + await tripActions.updatePlacesMany(tripId, group, { category_id: key === 'null' ? null : Number(key) }) + } + }) + } + } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) } + }, [tripId, toast, pushUndo]) + const handleAssignToDay = useCallback(async (placeId: number, dayId?: number, position?: number) => { const target = dayId || selectedDayId if (!target) { toast.error(t('trip.toast.selectDay')); return } @@ -841,7 +867,7 @@ export function useTripPlanner() { expandedDayIds, setExpandedDayIds, mapPlaces, route, routeSegments, routeInfo, setRoute, setRouteInfo, updateRouteForDay, handleSelectDay, handlePlaceClick, handleMarkerClick, handleMapClick, handleMapContextMenu, openAddPlaceFromPoi, - handleSavePlace, openPlaceEditor, handleDeletePlace, confirmDeletePlace, confirmDeletePlaces, + handleSavePlace, openPlaceEditor, handleDeletePlace, confirmDeletePlace, confirmDeletePlaces, confirmChangeCategory, handleAssignToDay, handleRemoveAssignment, handleReorder, handleReorderDays, handleAddDay, handleUpdateDayTitle, handleSaveReservation, handleSaveTransport, handleDeleteReservation, selectedPlace, dayOrderMap, dayPlaces, diff --git a/client/src/repo/placeRepo.ts b/client/src/repo/placeRepo.ts index 752f0ad48..1f20e1824 100644 --- a/client/src/repo/placeRepo.ts +++ b/client/src/repo/placeRepo.ts @@ -115,4 +115,31 @@ export const placeRepo = { await offlineDb.places.bulkDelete(ids) return result }, + + async updateMany(tripId: number | string, ids: number[], data: Record): Promise<{ updated: number[]; count: number }> { + if (!navigator.onLine) { + // Offline fans out one queued PUT per id (mirrors deleteMany's DELETE fan-out). + for (const id of ids) { + const existing = await offlineDb.places.get(id) + if (existing) await offlineDb.places.put({ ...existing, ...(data as Partial) }) + const mutId = generateUUID() + const isTemp = id < 0 + await mutationQueue.enqueue({ + id: mutId, + tripId: Number(tripId), + method: 'PUT', + url: isTemp ? `/trips/${tripId}/places/{id}` : `/trips/${tripId}/places/${id}`, + body: data, + resource: 'places', + entityId: id, + ...(isTemp ? { tempEntityId: id } : {}), + }) + } + return { updated: ids, count: ids.length } + } + const result = await placesApi.bulkUpdate(tripId, ids, data as Parameters[2]) + const cached = await offlineDb.places.bulkGet(ids) + await offlineDb.places.bulkPut(cached.filter(Boolean).map(p => ({ ...(p as Place), ...(data as Partial) }))) + return result + }, } diff --git a/client/src/store/slices/placesSlice.ts b/client/src/store/slices/placesSlice.ts index 6f53d85e7..a6382e3a3 100644 --- a/client/src/store/slices/placesSlice.ts +++ b/client/src/store/slices/placesSlice.ts @@ -13,6 +13,7 @@ export interface PlacesSlice { updatePlace: (tripId: number | string, placeId: number, placeData: Partial) => Promise deletePlace: (tripId: number | string, placeId: number) => Promise deletePlacesMany: (tripId: number | string, placeIds: number[]) => Promise + updatePlacesMany: (tripId: number | string, placeIds: number[], patch: Partial) => Promise } export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice => ({ @@ -105,4 +106,33 @@ export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice => throw new Error(getApiErrorMessage(err, 'Error deleting places')) } }, + + updatePlacesMany: async (tripId, placeIds, patch) => { + if (placeIds.length === 0) return + try { + await placeRepo.updateMany(tripId, placeIds, patch as Record) + const idSet = new Set(placeIds) + set(state => { + // Patch both the place pool and the embedded place on each day assignment + // (preserving the assignment's own place_time/end_time) so itinerary cards + // reflect the change immediately, like single updatePlace does. + const updatedAssignments = { ...state.assignments } + let changed = false + for (const [dayId, items] of Object.entries(state.assignments)) { + if (items.some((a: Assignment) => a.place?.id != null && idSet.has(a.place.id))) { + updatedAssignments[dayId] = items.map((a: Assignment) => + a.place?.id != null && idSet.has(a.place.id) ? { ...a, place: { ...a.place, ...patch } } : a + ) + changed = true + } + } + return { + places: state.places.map(p => idSet.has(p.id) ? { ...p, ...patch } : p), + ...(changed ? { assignments: updatedAssignments } : {}), + } + }) + } catch (err: unknown) { + throw new Error(getApiErrorMessage(err, 'Error updating places')) + } + }, }) diff --git a/client/tests/unit/slices/placesSlice.test.ts b/client/tests/unit/slices/placesSlice.test.ts index 93a9310e1..59ba9a44a 100644 --- a/client/tests/unit/slices/placesSlice.test.ts +++ b/client/tests/unit/slices/placesSlice.test.ts @@ -91,6 +91,35 @@ describe('placesSlice', () => { }); }); + describe('updatePlacesMany', () => { + it('FE-PLACES-008: applies the patch to every listed place and cascades to assignments', async () => { + const a = buildPlace({ id: 10, trip_id: 1, category_id: 1 }); + const b = buildPlace({ id: 20, trip_id: 1, category_id: 1 }); + const c = buildPlace({ id: 30, trip_id: 1, category_id: 9 }); + const assignment = buildAssignment({ id: 100, day_id: 1, place: a }); + seedStore(useTripStore, { places: [a, b, c], assignments: { '1': [assignment] } }); + + server.use( + http.post('/api/trips/1/places/bulk-update', () => HttpResponse.json({ updated: [10, 20], count: 2 })), + ); + + await useTripStore.getState().updatePlacesMany(1, [10, 20], { category_id: 5 }); + + const places = useTripStore.getState().places; + expect(places.find(p => p.id === 10)?.category_id).toBe(5); + expect(places.find(p => p.id === 20)?.category_id).toBe(5); + expect(places.find(p => p.id === 30)?.category_id).toBe(9); // untouched + expect(useTripStore.getState().assignments['1'][0].place.category_id).toBe(5); // cascaded + }); + + it('FE-PLACES-009: no-ops on an empty id list without calling the API', async () => { + const a = buildPlace({ id: 10, trip_id: 1, category_id: 1 }); + seedStore(useTripStore, { places: [a] }); + await useTripStore.getState().updatePlacesMany(1, [], { category_id: 5 }); + expect(useTripStore.getState().places[0].category_id).toBe(1); + }); + }); + describe('deletePlace', () => { it('FE-PLACES-005: deletePlace removes place from places array', async () => { const place1 = buildPlace({ id: 10, trip_id: 1 }); diff --git a/server/src/nest/places/places.controller.ts b/server/src/nest/places/places.controller.ts index cb8fbe5e9..dd290dcd2 100644 --- a/server/src/nest/places/places.controller.ts +++ b/server/src/nest/places/places.controller.ts @@ -225,6 +225,36 @@ export class PlacesController { return { deleted, count: deleted.length }; } + @Post('bulk-update') + @HttpCode(200) + bulkUpdate( + @CurrentUser() user: User, + @Param('tripId') tripId: string, + @Body() body: { ids?: unknown; category_id?: unknown }, + @Headers('x-socket-id') socketId?: string, + ) { + const trip = this.requireTrip(tripId, user); + this.requireEdit(trip, user); + const { ids } = body; + if (!Array.isArray(ids) || ids.some((v) => typeof v !== 'number')) { + throw new HttpException({ error: 'ids must be an array of numbers' }, 400); + } + if (ids.length === 0) { + return { updated: [], count: 0 }; + } + // `category_id` may be a number or null (null clears it), so key presence — + // not truthiness — is what signals there's something to change. + if (!('category_id' in body)) { + throw new HttpException({ error: 'Provide at least one field to update' }, 400); + } + const updated = this.places.updateMany(tripId, ids, { category_id: body.category_id as number | null }); + for (const place of updated) { + this.places.broadcast(tripId, 'place:updated', { place }, socketId); + this.places.onUpdated(place.id); + } + return { updated: updated.map((p) => p.id), count: updated.length }; + } + @Get(':id') get(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string) { this.requireTrip(tripId, user); diff --git a/server/src/nest/places/places.service.ts b/server/src/nest/places/places.service.ts index 869e852ed..a1ab0f2d4 100644 --- a/server/src/nest/places/places.service.ts +++ b/server/src/nest/places/places.service.ts @@ -52,6 +52,10 @@ export class PlacesService { return svc.deletePlacesMany(tripId, ids); } + updateMany(tripId: string, ids: number[], data: Parameters[2]) { + return svc.updatePlacesMany(tripId, ids, data); + } + importGpx( tripId: string, buffer: Buffer, diff --git a/server/tests/unit/nest/places.controller.test.ts b/server/tests/unit/nest/places.controller.test.ts index 492b419a6..425074bb9 100644 --- a/server/tests/unit/nest/places.controller.test.ts +++ b/server/tests/unit/nest/places.controller.test.ts @@ -190,6 +190,38 @@ describe('PlacesController (parity with the legacy /api/trips/:tripId/places rou }); }); + describe('POST /bulk-update', () => { + it('404 when trip not accessible, 403 without place_edit (before any write)', () => { + expect(thrown(() => new PlacesController(svc({ verifyTripAccess: vi.fn().mockReturnValue(undefined) })).bulkUpdate(user, '5', { ids: [1], category_id: 3 }))).toEqual({ status: 404, body: { error: 'Trip not found' } }); + expect(thrown(() => new PlacesController(svc({ canEdit: vi.fn().mockReturnValue(false) })).bulkUpdate(user, '5', { ids: [1], category_id: 3 }))).toEqual({ status: 403, body: { error: 'No permission' } }); + }); + it('400 when ids is not an array of numbers', () => { + expect(thrown(() => new PlacesController(svc()).bulkUpdate(user, '5', { ids: ['a'] }))).toEqual({ status: 400, body: { error: 'ids must be an array of numbers' } }); + }); + it('400 when no patch field is present', () => { + expect(thrown(() => new PlacesController(svc()).bulkUpdate(user, '5', { ids: [1] }))).toEqual({ status: 400, body: { error: 'Provide at least one field to update' } }); + }); + it('returns empty for an empty list without touching the service', () => { + const updateMany = vi.fn(); + expect(new PlacesController(svc({ updateMany } as Partial)).bulkUpdate(user, '5', { ids: [] })).toEqual({ updated: [], count: 0 }); + expect(updateMany).not.toHaveBeenCalled(); + }); + it('updates, fires hooks + broadcasts per updated place', () => { + const updateMany = vi.fn().mockReturnValue([{ id: 1 }, { id: 2 }]); const onUpdated = vi.fn(); const broadcast = vi.fn(); + const s = svc({ updateMany, onUpdated, broadcast } as Partial); + expect(new PlacesController(s).bulkUpdate(user, '5', { ids: [1, 2], category_id: 3 }, 'sock')).toEqual({ updated: [1, 2], count: 2 }); + expect(updateMany).toHaveBeenCalledWith('5', [1, 2], { category_id: 3 }); + expect(onUpdated).toHaveBeenCalledTimes(2); + expect(broadcast).toHaveBeenCalledWith('5', 'place:updated', { place: { id: 1 } }, 'sock'); + }); + it('passes category_id: null through to clear the category', () => { + const updateMany = vi.fn().mockReturnValue([{ id: 1 }]); + const s = svc({ updateMany } as Partial); + expect(new PlacesController(s).bulkUpdate(user, '5', { ids: [1], category_id: null })).toEqual({ updated: [1], count: 1 }); + expect(updateMany).toHaveBeenCalledWith('5', [1], { category_id: null }); + }); + }); + it('GET /:id returns the place when found, 404 when missing', () => { expect(thrown(() => new PlacesController(svc({ get: vi.fn().mockReturnValue(undefined) } as Partial)).get(user, '5', '9'))).toEqual({ status: 404, body: { error: 'Place not found' } }); const s = svc({ get: vi.fn().mockReturnValue({ id: 9 }) } as Partial); diff --git a/shared/src/i18n/ar/places.ts b/shared/src/i18n/ar/places.ts index 29c154f91..1c1da8aa1 100644 --- a/shared/src/i18n/ar/places.ts +++ b/shared/src/i18n/ar/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'اختر نوعًا واحدًا على الأقل.', 'places.selectionCount': '{count} محدد', 'places.deleteSelected': 'حذف المحدد', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': 'تم استيراد {count} مكان من KMZ/KML', 'places.urlResolved': 'تم استيراد المكان من الرابط', 'places.importList': 'استيراد قائمة', diff --git a/shared/src/i18n/ar/undo.ts b/shared/src/i18n/ar/undo.ts index 8df50bdfa..52a665c32 100644 --- a/shared/src/i18n/ar/undo.ts +++ b/shared/src/i18n/ar/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'تم تحسين المسار', 'undo.deletePlace': 'تم حذف المكان', 'undo.deletePlaces': 'تم حذف الأماكن', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'تم نقل المكان إلى يوم آخر', 'undo.lock': 'تم تبديل قفل المكان', 'undo.importGpx': 'استيراد GPX', diff --git a/shared/src/i18n/br/places.ts b/shared/src/i18n/br/places.ts index 4dbdbbc07..73c90e25f 100644 --- a/shared/src/i18n/br/places.ts +++ b/shared/src/i18n/br/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Selecione pelo menos um tipo.', 'places.selectionCount': '{count} selecionado(s)', 'places.deleteSelected': 'Excluir seleção', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} lugares importados de KMZ/KML', 'places.urlResolved': 'Lugar importado da URL', 'places.importList': 'Importar lista', diff --git a/shared/src/i18n/br/undo.ts b/shared/src/i18n/br/undo.ts index 788a846af..e9a8696d9 100644 --- a/shared/src/i18n/br/undo.ts +++ b/shared/src/i18n/br/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Rota otimizada', 'undo.deletePlace': 'Local excluído', 'undo.deletePlaces': 'Lugares excluídos', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Local movido para outro dia', 'undo.lock': 'Bloqueio do local alternado', 'undo.importGpx': 'Importação de GPX', diff --git a/shared/src/i18n/cs/places.ts b/shared/src/i18n/cs/places.ts index 42b14f0c9..3538b869c 100644 --- a/shared/src/i18n/cs/places.ts +++ b/shared/src/i18n/cs/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Vyberte alespoň jeden typ.', 'places.selectionCount': '{count} vybráno', 'places.deleteSelected': 'Smazat vybrané', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': 'Importováno {count} míst z KMZ/KML', 'places.urlResolved': 'Místo importováno z URL', 'places.importList': 'Import seznamu', diff --git a/shared/src/i18n/cs/undo.ts b/shared/src/i18n/cs/undo.ts index 51c7331e5..b1524b087 100644 --- a/shared/src/i18n/cs/undo.ts +++ b/shared/src/i18n/cs/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Trasa optimalizována', 'undo.deletePlace': 'Místo smazáno', 'undo.deletePlaces': 'Místa smazána', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Místo přesunuto na jiný den', 'undo.lock': 'Zámek místa přepnut', 'undo.importGpx': 'Import GPX', diff --git a/shared/src/i18n/de/places.ts b/shared/src/i18n/de/places.ts index 52dfffe0b..b48ca438c 100644 --- a/shared/src/i18n/de/places.ts +++ b/shared/src/i18n/de/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Wähle mindestens einen Typ aus.', 'places.selectionCount': '{count} ausgewählt', 'places.deleteSelected': 'Auswahl löschen', + 'places.changeCategory': "Kategorie ändern", + 'places.categoryChanged': "{count} Orte aktualisiert", 'places.kmlKmzImported': '{count} Orte aus KMZ/KML importiert', 'places.urlResolved': 'Ort aus URL importiert', 'places.importList': 'Listenimport', diff --git a/shared/src/i18n/de/undo.ts b/shared/src/i18n/de/undo.ts index c877d9f1a..bc38f15ce 100644 --- a/shared/src/i18n/de/undo.ts +++ b/shared/src/i18n/de/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Route optimiert', 'undo.deletePlace': 'Ort gelöscht', 'undo.deletePlaces': 'Orte gelöscht', + 'undo.changeCategory': "Kategorie geändert", 'undo.moveDay': 'Ort zu anderem Tag verschoben', 'undo.lock': 'Ortssperre umgeschaltet', 'undo.importGpx': 'GPX-Import', diff --git a/shared/src/i18n/en/places.ts b/shared/src/i18n/en/places.ts index 2cc7a51f6..9e7e8ab85 100644 --- a/shared/src/i18n/en/places.ts +++ b/shared/src/i18n/en/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Select at least one type to import.', 'places.selectionCount': '{count} selected', 'places.deleteSelected': 'Delete selected', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} places imported from KMZ/KML', 'places.urlResolved': 'Place imported from URL', 'places.importList': 'List Import', diff --git a/shared/src/i18n/en/undo.ts b/shared/src/i18n/en/undo.ts index 2a96e1526..52d94aacf 100644 --- a/shared/src/i18n/en/undo.ts +++ b/shared/src/i18n/en/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Route optimized', 'undo.deletePlace': 'Place deleted', 'undo.deletePlaces': 'Places deleted', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Place moved to another day', 'undo.lock': 'Place lock toggled', 'undo.importGpx': 'GPX import', diff --git a/shared/src/i18n/es/places.ts b/shared/src/i18n/es/places.ts index 7405f708c..f74d450d2 100644 --- a/shared/src/i18n/es/places.ts +++ b/shared/src/i18n/es/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Selecciona al menos un tipo.', 'places.selectionCount': '{count} seleccionado(s)', 'places.deleteSelected': 'Eliminar selección', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} lugares importados desde KMZ/KML', 'places.urlResolved': 'Lugar importado desde URL', 'places.importList': 'Importar lista', diff --git a/shared/src/i18n/es/undo.ts b/shared/src/i18n/es/undo.ts index 53277ae18..bc6bd4fb9 100644 --- a/shared/src/i18n/es/undo.ts +++ b/shared/src/i18n/es/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Ruta optimizada', 'undo.deletePlace': 'Lugar eliminado', 'undo.deletePlaces': 'Lugares eliminados', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Lugar movido a otro día', 'undo.lock': 'Bloqueo de lugar activado/desactivado', 'undo.importGpx': 'Importación GPX', diff --git a/shared/src/i18n/fr/places.ts b/shared/src/i18n/fr/places.ts index 55f313d9d..eeaa8bd4d 100644 --- a/shared/src/i18n/fr/places.ts +++ b/shared/src/i18n/fr/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Sélectionnez au moins un type.', 'places.selectionCount': '{count} sélectionné(s)', 'places.deleteSelected': 'Supprimer la sélection', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} lieux importés depuis KMZ/KML', 'places.urlResolved': "Lieu importé depuis l'URL", 'places.importList': 'Import de liste', diff --git a/shared/src/i18n/fr/undo.ts b/shared/src/i18n/fr/undo.ts index b241c3c84..1fb41aa15 100644 --- a/shared/src/i18n/fr/undo.ts +++ b/shared/src/i18n/fr/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Itinéraire optimisé', 'undo.deletePlace': 'Lieu supprimé', 'undo.deletePlaces': 'Lieux supprimés', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Lieu déplacé vers un autre jour', 'undo.lock': 'Verrouillage du lieu modifié', 'undo.importGpx': 'Import GPX', diff --git a/shared/src/i18n/gr/places.ts b/shared/src/i18n/gr/places.ts index cdf7b841b..0a15e87c3 100644 --- a/shared/src/i18n/gr/places.ts +++ b/shared/src/i18n/gr/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Επιλέξτε τουλάχιστον έναν τύπο για εισαγωγή.', 'places.selectionCount': '{count} επιλεγμένα', 'places.deleteSelected': 'Διαγραφή επιλεγμένων', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} μέρη εισήχθησαν από KMZ/KML', 'places.urlResolved': 'Το μέρος εισήχθη από URL', 'places.importList': 'Εισαγωγή Λίστας', diff --git a/shared/src/i18n/gr/undo.ts b/shared/src/i18n/gr/undo.ts index ba019d3fb..30d4048b6 100644 --- a/shared/src/i18n/gr/undo.ts +++ b/shared/src/i18n/gr/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Η διαδρομή βελτιστοποιήθηκε', 'undo.deletePlace': 'Η τοποθεσία διαγράφηκε', 'undo.deletePlaces': 'Οι τοποθεσίες διαγράφηκαν', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Η τοποθεσία μεταφέρθηκε σε άλλη ημέρα', 'undo.lock': 'Το κλείδωμα τοποθεσίας εναλλάχθηκε', 'undo.importGpx': 'Εισαγωγή GPX', diff --git a/shared/src/i18n/hu/places.ts b/shared/src/i18n/hu/places.ts index 5d05eeb68..5802754a0 100644 --- a/shared/src/i18n/hu/places.ts +++ b/shared/src/i18n/hu/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Válassz legalább egy típust.', 'places.selectionCount': '{count} kiválasztva', 'places.deleteSelected': 'Kijelöltek törlése', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} hely importálva KMZ/KML-ből', 'places.urlResolved': 'Hely importálva URL-ből', 'places.importList': 'Lista importálás', diff --git a/shared/src/i18n/hu/undo.ts b/shared/src/i18n/hu/undo.ts index 91fecdba5..5487f1da9 100644 --- a/shared/src/i18n/hu/undo.ts +++ b/shared/src/i18n/hu/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Útvonal optimalizálva', 'undo.deletePlace': 'Hely törölve', 'undo.deletePlaces': 'Helyek törölve', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Hely áthelyezve másik napra', 'undo.lock': 'Hely zárolása váltva', 'undo.importGpx': 'GPX importálás', diff --git a/shared/src/i18n/id/places.ts b/shared/src/i18n/id/places.ts index be0c411c9..e749ad5b2 100644 --- a/shared/src/i18n/id/places.ts +++ b/shared/src/i18n/id/places.ts @@ -23,6 +23,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Pilih setidaknya satu jenis.', 'places.selectionCount': '{count} dipilih', 'places.deleteSelected': 'Hapus yang dipilih', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} tempat diimpor dari KMZ/KML', 'places.urlResolved': 'Tempat diimpor dari URL', 'places.importList': 'Impor Daftar', diff --git a/shared/src/i18n/id/undo.ts b/shared/src/i18n/id/undo.ts index b6de356c4..b2d66defd 100644 --- a/shared/src/i18n/id/undo.ts +++ b/shared/src/i18n/id/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Rute dioptimalkan', 'undo.deletePlace': 'Tempat dihapus', 'undo.deletePlaces': 'Tempat dihapus', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Tempat dipindah ke hari lain', 'undo.lock': 'Kunci tempat diubah', 'undo.importGpx': 'Impor GPX', diff --git a/shared/src/i18n/it/places.ts b/shared/src/i18n/it/places.ts index fef448b6e..41d0b6d78 100644 --- a/shared/src/i18n/it/places.ts +++ b/shared/src/i18n/it/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Seleziona almeno un tipo.', 'places.selectionCount': '{count} selezionato/i', 'places.deleteSelected': 'Elimina selezionati', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} luoghi importati da KMZ/KML', 'places.urlResolved': "Luogo importato dall'URL", 'places.importList': 'Importa lista', diff --git a/shared/src/i18n/it/undo.ts b/shared/src/i18n/it/undo.ts index a037f0ee5..ba5150e87 100644 --- a/shared/src/i18n/it/undo.ts +++ b/shared/src/i18n/it/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Percorso ottimizzato', 'undo.deletePlace': 'Luogo eliminato', 'undo.deletePlaces': 'Luoghi eliminati', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Luogo spostato in altro giorno', 'undo.lock': 'Blocco luogo modificato', 'undo.importGpx': 'Importazione GPX', diff --git a/shared/src/i18n/ja/places.ts b/shared/src/i18n/ja/places.ts index 2579384c5..85aa6773b 100644 --- a/shared/src/i18n/ja/places.ts +++ b/shared/src/i18n/ja/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': '少なくとも1つ選択してください。', 'places.selectionCount': '{count} 件選択中', 'places.deleteSelected': '選択を削除', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': 'KMZ/KMLから {count} 件の場所をインポートしました', 'places.urlResolved': 'URLから場所をインポートしました', 'places.importList': 'リストをインポート', diff --git a/shared/src/i18n/ja/undo.ts b/shared/src/i18n/ja/undo.ts index 1b5c942ac..5ef9e24f9 100644 --- a/shared/src/i18n/ja/undo.ts +++ b/shared/src/i18n/ja/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'ルートを最適化', 'undo.deletePlace': '場所を削除', 'undo.deletePlaces': '場所を削除', + 'undo.changeCategory': "Category changed", 'undo.moveDay': '場所を別の日に移動', 'undo.lock': '場所のロックを切り替え', 'undo.importGpx': 'GPXをインポート', diff --git a/shared/src/i18n/ko/places.ts b/shared/src/i18n/ko/places.ts index c74460837..7fd2871ef 100644 --- a/shared/src/i18n/ko/places.ts +++ b/shared/src/i18n/ko/places.ts @@ -23,6 +23,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': '가져올 유형을 하나 이상 선택하세요.', 'places.selectionCount': '{count}개 선택됨', 'places.deleteSelected': '선택 항목 삭제', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': 'KMZ/KML에서 {count}개 장소를 가져왔습니다', 'places.urlResolved': 'URL에서 장소를 가져왔습니다', 'places.importList': '목록 가져오기', diff --git a/shared/src/i18n/ko/undo.ts b/shared/src/i18n/ko/undo.ts index dfe5cacbf..d85acfa63 100644 --- a/shared/src/i18n/ko/undo.ts +++ b/shared/src/i18n/ko/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': '경로가 최적화되었습니다', 'undo.deletePlace': '장소가 삭제되었습니다', 'undo.deletePlaces': '장소들이 삭제되었습니다', + 'undo.changeCategory': "Category changed", 'undo.moveDay': '장소가 다른 날로 이동되었습니다', 'undo.lock': '장소 잠금이 변경되었습니다', 'undo.importGpx': 'GPX 가져오기', diff --git a/shared/src/i18n/nl/places.ts b/shared/src/i18n/nl/places.ts index 51f30f96d..ab17c43c3 100644 --- a/shared/src/i18n/nl/places.ts +++ b/shared/src/i18n/nl/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Selecteer minstens één type.', 'places.selectionCount': '{count} geselecteerd', 'places.deleteSelected': 'Selectie verwijderen', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} plaatsen geïmporteerd uit KMZ/KML', 'places.urlResolved': 'Plaats geïmporteerd van URL', 'places.importList': 'Lijst importeren', diff --git a/shared/src/i18n/nl/undo.ts b/shared/src/i18n/nl/undo.ts index 4215dc3ef..a9da2bc09 100644 --- a/shared/src/i18n/nl/undo.ts +++ b/shared/src/i18n/nl/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Route geoptimaliseerd', 'undo.deletePlace': 'Locatie verwijderd', 'undo.deletePlaces': 'Plaatsen verwijderd', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Locatie naar andere dag verplaatst', 'undo.lock': 'Vergrendeling locatie gewijzigd', 'undo.importGpx': 'GPX-import', diff --git a/shared/src/i18n/pl/places.ts b/shared/src/i18n/pl/places.ts index 19caa7a0f..3d7b21900 100644 --- a/shared/src/i18n/pl/places.ts +++ b/shared/src/i18n/pl/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Wybierz co najmniej jeden typ.', 'places.selectionCount': '{count} zaznaczono', 'places.deleteSelected': 'Usuń wybrane', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': 'Zaimportowano {count} miejsc z KMZ/KML', 'places.urlResolved': 'Miejsce zaimportowane z URL', 'places.kmlKmzSummaryValues': 'Placemarks: {total} • Zaimportowano: {created} • Pominięto: {skipped}', diff --git a/shared/src/i18n/pl/undo.ts b/shared/src/i18n/pl/undo.ts index 241fd91d0..2b3c72992 100644 --- a/shared/src/i18n/pl/undo.ts +++ b/shared/src/i18n/pl/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Trasa zoptymalizowana', 'undo.deletePlace': 'Miejsce usunięte', 'undo.deletePlaces': 'Miejsca usunięte', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Miejsce przeniesione', 'undo.lock': 'Blokada przełączona', 'undo.importGpx': 'Import GPX', diff --git a/shared/src/i18n/ru/places.ts b/shared/src/i18n/ru/places.ts index ab8d15572..47dd9b613 100644 --- a/shared/src/i18n/ru/places.ts +++ b/shared/src/i18n/ru/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Выберите хотя бы один тип.', 'places.selectionCount': '{count} выбрано', 'places.deleteSelected': 'Удалить выбранные', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} мест импортировано из KMZ/KML', 'places.urlResolved': 'Место импортировано из URL', 'places.importList': 'Импорт списка', diff --git a/shared/src/i18n/ru/undo.ts b/shared/src/i18n/ru/undo.ts index 377156a58..0636843cb 100644 --- a/shared/src/i18n/ru/undo.ts +++ b/shared/src/i18n/ru/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Маршрут оптимизирован', 'undo.deletePlace': 'Место удалено', 'undo.deletePlaces': 'Места удалены', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Место перемещено в другой день', 'undo.lock': 'Блокировка места изменена', 'undo.importGpx': 'Импорт GPX', diff --git a/shared/src/i18n/sv/places.ts b/shared/src/i18n/sv/places.ts index b3daa3098..b7839c83d 100644 --- a/shared/src/i18n/sv/places.ts +++ b/shared/src/i18n/sv/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Välj minst en typ att importera.', 'places.selectionCount': '{count} markerade', 'places.deleteSelected': 'Ta bort det markerade', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} platser som importerats från KMZ/KML', 'places.urlResolved': 'Plats importerad från URL', 'places.importList': 'Importera lista', diff --git a/shared/src/i18n/sv/undo.ts b/shared/src/i18n/sv/undo.ts index 57295c055..611fac0fc 100644 --- a/shared/src/i18n/sv/undo.ts +++ b/shared/src/i18n/sv/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Rutt optimerad', 'undo.deletePlace': 'Plats borttagen', 'undo.deletePlaces': 'Platser borttagna', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Plats har flyttats till en annan dag', 'undo.lock': 'Plats lås växlad', 'undo.importGpx': 'GPX importering', diff --git a/shared/src/i18n/tr/places.ts b/shared/src/i18n/tr/places.ts index 4392e0b96..74f2c7751 100644 --- a/shared/src/i18n/tr/places.ts +++ b/shared/src/i18n/tr/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'İçe aktarılacak en az bir tür seçin.', 'places.selectionCount': '{count} seçildi', 'places.deleteSelected': 'Seçileni sil', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': "KMZ/KML'den {count} yer içe Aktarıldı", 'places.urlResolved': "URL'den içe aktarılan Yer", 'places.importList': 'Liste İçe Aktarma', diff --git a/shared/src/i18n/tr/undo.ts b/shared/src/i18n/tr/undo.ts index 75c283509..f553587fd 100644 --- a/shared/src/i18n/tr/undo.ts +++ b/shared/src/i18n/tr/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Rota optimize edildi', 'undo.deletePlace': 'Yer silindi', 'undo.deletePlaces': 'Yerler silindi', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Yer başka güne taşındı', 'undo.lock': 'Yer kilidi değiştirildi', 'undo.importGpx': 'GPX içe aktarma', diff --git a/shared/src/i18n/uk/places.ts b/shared/src/i18n/uk/places.ts index 454a9274a..0653fca51 100644 --- a/shared/src/i18n/uk/places.ts +++ b/shared/src/i18n/uk/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Виберіть щонайменше один тип.', 'places.selectionCount': '{count} вибрано', 'places.deleteSelected': 'Видалити вибране', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} місць імпортовано з KMZ/KML', 'places.urlResolved': 'Місце імпортовано з URL', 'places.importList': 'Імпорт списку', diff --git a/shared/src/i18n/uk/undo.ts b/shared/src/i18n/uk/undo.ts index 3b81c22bd..9d563c61c 100644 --- a/shared/src/i18n/uk/undo.ts +++ b/shared/src/i18n/uk/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Маршрут оптимізовано', 'undo.deletePlace': 'Місце видалено', 'undo.deletePlaces': 'Місця видалено', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Місце переміщено в інший день', 'undo.lock': 'Блокування місця змінено', 'undo.importGpx': 'Імпорт GPX', diff --git a/shared/src/i18n/vi/places.ts b/shared/src/i18n/vi/places.ts index 71272178b..1b22ee83a 100644 --- a/shared/src/i18n/vi/places.ts +++ b/shared/src/i18n/vi/places.ts @@ -24,6 +24,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': 'Chọn ít nhất một loại để nhập.', 'places.selectionCount': '{count} đã chọn', 'places.deleteSelected': 'Xóa đã chọn', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '{count} địa điểm được nhập từ KMZ/KML', 'places.urlResolved': 'Địa điểm được nhập từ URL', 'places.importList': 'Nhập danh sách', diff --git a/shared/src/i18n/vi/undo.ts b/shared/src/i18n/vi/undo.ts index b9c400e91..a70402866 100644 --- a/shared/src/i18n/vi/undo.ts +++ b/shared/src/i18n/vi/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': 'Tuyến đường được tối ưu hóa', 'undo.deletePlace': 'Địa điểm đã bị xóa', 'undo.deletePlaces': 'Địa điểm đã bị xóa', + 'undo.changeCategory': "Category changed", 'undo.moveDay': 'Địa điểm đã chuyển sang ngày khác', 'undo.lock': 'Đã bật khóa địa điểm', 'undo.importGpx': 'GPX nhập khẩu', diff --git a/shared/src/i18n/zh-TW/places.ts b/shared/src/i18n/zh-TW/places.ts index b1182be63..f16b0bb43 100644 --- a/shared/src/i18n/zh-TW/places.ts +++ b/shared/src/i18n/zh-TW/places.ts @@ -23,6 +23,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': '請至少選擇一種類型。', 'places.selectionCount': '已選 {count} 項', 'places.deleteSelected': '刪除所選', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '已從 KMZ/KML 匯入 {count} 個地點', 'places.urlResolved': '已從 URL 匯入地點', 'places.importList': '列表匯入', diff --git a/shared/src/i18n/zh-TW/undo.ts b/shared/src/i18n/zh-TW/undo.ts index 147945707..3bc7383f3 100644 --- a/shared/src/i18n/zh-TW/undo.ts +++ b/shared/src/i18n/zh-TW/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': '路線已最佳化', 'undo.deletePlace': '地點已刪除', 'undo.deletePlaces': '地點已刪除', + 'undo.changeCategory': "Category changed", 'undo.moveDay': '地點已移至另一天', 'undo.lock': '地點鎖定已切換', 'undo.importGpx': 'GPX 匯入', diff --git a/shared/src/i18n/zh/places.ts b/shared/src/i18n/zh/places.ts index 3ba9f5908..e095e0f73 100644 --- a/shared/src/i18n/zh/places.ts +++ b/shared/src/i18n/zh/places.ts @@ -23,6 +23,8 @@ const places: TranslationStrings = { 'places.kmlImportNoneSelected': '请至少选择一种类型。', 'places.selectionCount': '已选 {count} 项', 'places.deleteSelected': '删除所选', + 'places.changeCategory': "Change category", + 'places.categoryChanged': "{count} places updated", 'places.kmlKmzImported': '已从 KMZ/KML 导入 {count} 个地点', 'places.urlResolved': '已从 URL 导入地点', 'places.importList': '列表导入', diff --git a/shared/src/i18n/zh/undo.ts b/shared/src/i18n/zh/undo.ts index 14f157525..79ccbf63c 100644 --- a/shared/src/i18n/zh/undo.ts +++ b/shared/src/i18n/zh/undo.ts @@ -9,6 +9,7 @@ const undo: TranslationStrings = { 'undo.optimize': '路线已优化', 'undo.deletePlace': '地点已删除', 'undo.deletePlaces': '地点已删除', + 'undo.changeCategory': "Category changed", 'undo.moveDay': '地点已移至另一天', 'undo.lock': '地点锁定已切换', 'undo.importGpx': 'GPX 导入', diff --git a/shared/src/place/place.schema.ts b/shared/src/place/place.schema.ts index 6bf947cc8..979bb7078 100644 --- a/shared/src/place/place.schema.ts +++ b/shared/src/place/place.schema.ts @@ -113,6 +113,14 @@ export const placeBulkDeleteRequestSchema = z.object({ }); export type PlaceBulkDeleteRequest = z.infer; +export const placeBulkUpdateRequestSchema = z.object({ + ids: z.array(z.number()).min(1), + // null clears the category ("No category"); a number sets it. Optional so the + // field can be omitted, but the endpoint requires it to be present to act. + category_id: z.number().nullable().optional(), +}); +export type PlaceBulkUpdateRequest = z.infer; + export const placeImportListRequestSchema = z.object({ url: z.string().min(1), // Opt-in: enrich imported places via the Places API (#886). Requires a Google