diff --git a/client/src/components/Collab/CollabNotes.test.tsx b/client/src/components/Collab/CollabNotes.test.tsx index 9c8fc884..73aa6c3b 100644 --- a/client/src/components/Collab/CollabNotes.test.tsx +++ b/client/src/components/Collab/CollabNotes.test.tsx @@ -175,7 +175,7 @@ describe('CollabNotes', () => { expect(document.body).toBeInTheDocument(); }); - it('FE-COMP-NOTES-013: delete note calls DELETE API and removes it from grid', async () => { + it('FE-COMP-NOTES-013: deleting a note asks for confirmation, then calls DELETE API and removes it', async () => { const user = userEvent.setup(); server.use( http.get('/api/trips/1/collab/notes', () => @@ -193,8 +193,11 @@ describe('CollabNotes', () => { ); render(); await screen.findByText('Remove Me'); - const deleteBtn = screen.getByTitle('Delete'); - await user.click(deleteBtn); + await user.click(screen.getByTitle('Delete')); + // Deleting now asks for confirmation first — the note stays until confirmed. + expect(screen.getByText('Delete note?')).toBeInTheDocument(); + expect(screen.getByText('Remove Me')).toBeInTheDocument(); + await user.click(document.querySelector('button.bg-red-600') as HTMLElement); await waitFor(() => expect(screen.queryByText('Remove Me')).not.toBeInTheDocument()); }); diff --git a/client/src/components/Collab/CollabNotes.tsx b/client/src/components/Collab/CollabNotes.tsx index e19bce3b..1a1556f8 100644 --- a/client/src/components/Collab/CollabNotes.tsx +++ b/client/src/components/Collab/CollabNotes.tsx @@ -10,6 +10,7 @@ import { useTripStore } from '../../store/tripStore' import { addListener, removeListener } from '../../api/websocket' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' +import ConfirmDialog from '../shared/ConfirmDialog' import type { User } from '../../types' import type { CollabNote } from './CollabNotes.types' import { FONT, NOTE_COLORS } from './CollabNotes.constants' @@ -44,6 +45,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) { const [previewFile, setPreviewFile] = useState(null) const [showSettings, setShowSettings] = useState(false) const [activeCategory, setActiveCategory] = useState(null) + const [pendingDeleteNoteId, setPendingDeleteNoteId] = useState(null) // Empty categories (no notes yet) stored in localStorage const [emptyCategories, setEmptyCategories] = useState(() => { @@ -231,6 +233,7 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) { activeCategory, setActiveCategory, categoryColors, getCategoryColor, handleCreateNote, handleUpdateNote, saveCategoryColors, handleEditSubmit, handleDeleteNoteFile, handleDeleteNote, categories, sortedNotes, + pendingDeleteNoteId, setPendingDeleteNoteId, } } @@ -319,7 +322,7 @@ function CollabCategoryPills({ categories, activeCategory, setActiveCategory, t function CollabNotesGrid(S: NotesState) { const { - sortedNotes, currentUser, canEdit, handleUpdateNote, handleDeleteNote, + sortedNotes, currentUser, canEdit, handleUpdateNote, setPendingDeleteNoteId, setEditingNote, setViewingNote, setPreviewFile, getCategoryColor, tripId, t, } = S return ( @@ -352,7 +355,7 @@ function CollabNotesGrid(S: NotesState) { currentUser={currentUser} canEdit={canEdit} onUpdate={handleUpdateNote} - onDelete={handleDeleteNote} + onDelete={setPendingDeleteNoteId} onEdit={setEditingNote} onView={setViewingNote} onPreviewFile={setPreviewFile} @@ -470,6 +473,7 @@ export default function CollabNotes(props: CollabNotesProps) { viewingNote, showNewModal, editingNote, previewFile, showSettings, setShowNewModal, setEditingNote, setPreviewFile, setShowSettings, handleCreateNote, handleEditSubmit, handleDeleteNoteFile, saveCategoryColors, handleUpdateNote, + handleDeleteNote, pendingDeleteNoteId, setPendingDeleteNoteId, } = S if (loading) return @@ -527,6 +531,15 @@ export default function CollabNotes(props: CollabNotesProps) { t={t} /> )} + + {/* Confirm: delete a collab note — guards against accidental deletion */} + setPendingDeleteNoteId(null)} + onConfirm={() => { if (pendingDeleteNoteId !== null) handleDeleteNote(pendingDeleteNoteId) }} + title={t('collab.notes.confirmDeleteTitle')} + message={t('collab.notes.confirmDeleteBody')} + /> ) } diff --git a/client/src/components/Collab/CollabNotesCard.tsx b/client/src/components/Collab/CollabNotesCard.tsx index 00a65473..9437393d 100644 --- a/client/src/components/Collab/CollabNotesCard.tsx +++ b/client/src/components/Collab/CollabNotesCard.tsx @@ -16,7 +16,7 @@ interface NoteCardProps { currentUser: User canEdit: boolean onUpdate: (noteId: number, data: Partial) => Promise - onDelete: (noteId: number) => Promise + onDelete: (noteId: number) => void onEdit: (note: CollabNote) => void onView: (note: CollabNote) => void onPreviewFile: (file: NoteFile) => void diff --git a/client/src/components/Map/RouteCalculator.test.ts b/client/src/components/Map/RouteCalculator.test.ts index 6ff60572..38858ae1 100644 --- a/client/src/components/Map/RouteCalculator.test.ts +++ b/client/src/components/Map/RouteCalculator.test.ts @@ -161,6 +161,62 @@ describe('optimizeRoute', () => { expect(result[1]).toEqual(c) expect(result[2]).toEqual(b) }) + + it('FE-COMP-ROUTECALCULATOR-016: start anchor begins the chain at the anchor-nearest stop', () => { + const a = { lat: 10, lng: 1 } + const b = { lat: 2, lng: 1 } + const c = { lat: 5, lng: 1 } + // From the accommodation anchor (1,1): nearest is b(2,1), then c(5,1), then a(10,1) + const result = optimizeRoute([a, b, c], { start: { lat: 1, lng: 1 } }) + expect(result).toEqual([b, c, a]) + }) + + it('FE-COMP-ROUTECALCULATOR-017: start + end anchors reorder a shuffled day and keep the end-nearest stop last', () => { + const a = { lat: 2, lng: 1 } + const b = { lat: 5, lng: 1 } + const c = { lat: 8, lng: 1 } + // Transfer day: start at hotel A (1,1), end at hotel B (9,1). c is nearest B, so it must be last. + const result = optimizeRoute([c, a, b], { start: { lat: 1, lng: 1 }, end: { lat: 9, lng: 1 } }) + expect(result).toEqual([a, b, c]) + }) + + it('FE-COMP-ROUTECALCULATOR-018: an anchor makes even a two-stop day sortable', () => { + const a = { lat: 10, lng: 1 } + const b = { lat: 2, lng: 1 } + // Without anchors two stops are returned unchanged; the start anchor orders them by proximity. + const result = optimizeRoute([a, b], { start: { lat: 1, lng: 1 } }) + expect(result).toEqual([b, a]) + }) + + it('FE-COMP-ROUTECALCULATOR-019: 2-opt untangles a round-trip into a clean loop around the hotel', () => { + const hotel = { lat: 48.8668, lng: 2.3013 } // Rue Marbeuf + const stops = [ + { id: 1, lat: 48.8565, lng: 2.3324 }, + { id: 2, lat: 48.8813, lng: 2.3151 }, + { id: 3, lat: 48.8796, lng: 2.308 }, + { id: 4, lat: 48.8723, lng: 2.2926 }, + { id: 5, lat: 48.866, lng: 2.3102 }, // nearest the hotel + ] + const d = (a: { lat: number; lng: number }, b: { lat: number; lng: number }) => + Math.hypot(a.lat - b.lat, a.lng - b.lng) + const loop = (order: typeof stops) => + d(hotel, order[0]) + order.slice(1).reduce((s, p, i) => s + d(order[i], p), 0) + d(order[order.length - 1], hotel) + + const result = optimizeRoute(stops, { start: hotel, end: hotel }) + // The optimized loop is no longer than the original order… + expect(loop(result)).toBeLessThanOrEqual(loop(stops) + 1e-9) + // …and the hotel-adjacent stop sits at one end of the loop, right next to the hotel. + expect([result[0].id, result[result.length - 1].id]).toContain(5) + }) + + it('FE-COMP-ROUTECALCULATOR-020: an end anchor without a start finishes at the stop nearest it', () => { + const a = { lat: 2, lng: 1 } + const b = { lat: 5, lng: 1 } + const c = { lat: 9, lng: 1 } + // a is nearest the end anchor, so the route must finish at a rather than start there. + const result = optimizeRoute([a, b, c], { end: { lat: 1, lng: 1 } }) + expect(result[result.length - 1]).toEqual(a) + }) }) // ── generateGoogleMapsUrl ────────────────────────────────────────────────────── diff --git a/client/src/components/Map/RouteCalculator.ts b/client/src/components/Map/RouteCalculator.ts index 8a2a9e3b..e9a8b415 100644 --- a/client/src/components/Map/RouteCalculator.ts +++ b/client/src/components/Map/RouteCalculator.ts @@ -1,4 +1,4 @@ -import type { RouteResult, RouteSegment, RouteWithLegs, Waypoint } from '../../types' +import type { RouteResult, RouteSegment, RouteWithLegs, Waypoint, RouteAnchors } from '../../types' const OSRM_BASE = 'https://router.project-osrm.org/route/v1' @@ -77,35 +77,98 @@ export function generateGoogleMapsUrl(places: Waypoint[]): string | null { return `https://www.google.com/maps/dir/${stops}` } -/** Reorders waypoints using a nearest-neighbor heuristic to minimize total Euclidean distance. */ -export function optimizeRoute(places: T[]): T[] { - const valid = places.filter((p) => p.lat && p.lng) - if (valid.length <= 2) return places +// Squared planar distance — enough for nearest-neighbor comparisons and cheaper than a full haversine. +function sqDist(a: Waypoint, b: Waypoint): number { + return (a.lat - b.lat) ** 2 + (a.lng - b.lng) ** 2 +} +// Length of visiting `order` in sequence, optionally pinned to a fixed start and/or end anchor. +// With start === end this is a closed loop back to the anchor (a day out from and back to the hotel). +function tourLength(order: Waypoint[], start?: Waypoint, end?: Waypoint): number { + if (order.length === 0) return 0 + let total = 0 + if (start) total += Math.sqrt(sqDist(start, order[0])) + for (let i = 0; i < order.length - 1; i++) total += Math.sqrt(sqDist(order[i], order[i + 1])) + if (end) total += Math.sqrt(sqDist(order[order.length - 1], end)) + return total +} + +// Greedy nearest-neighbor ordering, seeded at the start anchor when there is one. +function nearestNeighborOrder(valid: T[], start?: Waypoint): T[] { const visited = new Set() const result: T[] = [] - let current = valid[0] - visited.add(0) - result.push(current) - + let current: Waypoint + if (start) { + current = start + } else { + current = valid[0] + visited.add(0) + result.push(valid[0]) + } while (result.length < valid.length) { let nearestIdx = -1 let minDist = Infinity for (let i = 0; i < valid.length; i++) { if (visited.has(i)) continue - const d = Math.sqrt( - Math.pow(valid[i].lat - current.lat, 2) + Math.pow(valid[i].lng - current.lng, 2) - ) + const d = sqDist(valid[i], current) if (d < minDist) { minDist = d; nearestIdx = i } } if (nearestIdx === -1) break visited.add(nearestIdx) current = valid[nearestIdx] - result.push(current) + result.push(valid[nearestIdx]) } return result } +// 2-opt: repeatedly reverse a sub-segment whenever it shortens the tour. This removes the crossings +// a pure nearest-neighbor pass leaves behind. The start/end anchors stay fixed, so a round trip +// (start === end) is untangled into a clean loop rather than an open path. +function twoOptImprove(order: T[], start?: Waypoint, end?: Waypoint): T[] { + if (order.length < 3) return order + let best = order + let bestLen = tourLength(best, start, end) + let improved = true + while (improved) { + improved = false + for (let i = 0; i < best.length - 1; i++) { + for (let j = i + 1; j < best.length; j++) { + const candidate = best.slice(0, i).concat(best.slice(i, j + 1).reverse(), best.slice(j + 1)) + const len = tourLength(candidate, start, end) + if (len < bestLen - 1e-12) { + best = candidate + bestLen = len + improved = true + } + } + } + } + return best +} + +/** + * Reorders waypoints to minimize travel distance: a nearest-neighbor pass for a good starting order, + * then 2-opt to untangle crossings. Optional anchors (e.g. the day's accommodation) pin the route's + * ends — start === end makes it a loop out from and back to the hotel; a transfer day runs start → end. + */ +export function optimizeRoute(places: T[], anchors: RouteAnchors = {}): T[] { + const { start, end } = anchors + const valid = places.filter((p) => p.lat && p.lng) + if (valid.length <= 1) return places + // Two unanchored stops have no meaningful order to optimize; anchors can still flip them. + if (valid.length === 2 && !start && !end) return places + + const order = twoOptImprove(nearestNeighborOrder(valid, start), start, end) + + // A round trip's loop direction is arbitrary, so orient it to begin at the stop nearest the hotel — + // that reads naturally as "leave the hotel, head to the closest place, …, come back". + if (start && end && start.lat === end.lat && start.lng === end.lng && order.length > 1) { + if (sqDist(order[order.length - 1], start) < sqDist(order[0], start)) order.reverse() + } + + return order +} + /** Fetches per-leg distance/duration from OSRM and returns segment metadata (midpoints, walking/driving times). */ export async function calculateSegments( waypoints: Waypoint[], diff --git a/client/src/components/Planner/DayPlanSidebar.test.tsx b/client/src/components/Planner/DayPlanSidebar.test.tsx index 1035878a..6942e697 100644 --- a/client/src/components/Planner/DayPlanSidebar.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.test.tsx @@ -982,7 +982,7 @@ describe('DayPlanSidebar', () => { } }) - it('FE-PLANNER-DAYPLAN-065: note card delete button calls deleteNote', async () => { + it('FE-PLANNER-DAYPLAN-065: deleting a note asks for confirmation before calling deleteNote', async () => { const user = userEvent.setup() const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) const note = buildDayNote({ id: 55, day_id: 10, text: 'My note' }) @@ -992,6 +992,11 @@ describe('DayPlanSidebar', () => { const noteEditBtns = document.querySelectorAll('.note-edit-buttons button') if (noteEditBtns.length > 1) { await user.click(noteEditBtns[1] as HTMLElement) + // Clicking delete opens a confirmation dialog rather than deleting immediately. + expect(mockDayNotesState.deleteNote).not.toHaveBeenCalled() + expect(screen.getByText('Delete note?')).toBeInTheDocument() + // Confirming triggers the actual delete. + await user.click(screen.getByRole('button', { name: /^delete$/i })) expect(mockDayNotesState.deleteNote).toHaveBeenCalled() } }) diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 710299d5..342ffdba 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -7,6 +7,7 @@ import { ChevronDown, ChevronRight, ChevronUp, Navigation, RotateCcw, ExternalLi import { assignmentsApi, reservationsApi } from '../../api/client' import { calculateRoute, calculateRouteWithLegs, optimizeRoute } from '../Map/RouteCalculator' import PlaceAvatar from '../shared/PlaceAvatar' +import ConfirmDialog from '../shared/ConfirmDialog' import { useContextMenu, ContextMenu } from '../shared/ContextMenu' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' @@ -17,7 +18,7 @@ import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' import { useSettingsStore } from '../../store/settingsStore' import { useTranslation } from '../../i18n' -import { isDayInAccommodationRange } from '../../utils/dayOrder' +import { isDayInAccommodationRange, getAccommodationAnchors } from '../../utils/dayOrder' import { TRANSPORT_TYPES, parseTimeToMinutes, getSpanPhase, getDisplayTimeForDay, getTransportForDay as _getTransportForDay, getMergedItems as _getMergedItems, @@ -451,6 +452,10 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { _openEditNote(dayId, note) } + // Deleting a note asks for confirmation first — the edit/delete icons sit close together and are + // easy to mis-tap on touch devices, where an accidental delete was previously unrecoverable. + const [pendingDeleteNote, setPendingDeleteNote] = useState<{ dayId: number; noteId: number } | null>(null) + const deleteNote = async (dayId: number, noteId: number, e?: React.MouseEvent) => { e?.stopPropagation() await _deleteNote(dayId, noteId) @@ -703,8 +708,14 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { // Optimize only unlocked assignments (work on assignments, not places) const unlockedWithCoords = unlocked.filter(a => a.place?.lat && a.place?.lng) const unlockedNoCoords = unlocked.filter(a => !a.place?.lat || !a.place?.lng) + // Anchor the route on the day's accommodation (when enabled): a loop out from and back to the + // hotel, or — on a transfer day — a run from the hotel you leave to the one you arrive at. + const day = days.find(d => d.id === selectedDayId) + const anchors = day && useSettingsStore.getState().settings.optimize_from_accommodation !== false + ? getAccommodationAnchors(day, days, accommodations) + : {} const optimizedAssignments = unlockedWithCoords.length >= 2 - ? optimizeRoute(unlockedWithCoords.map(a => ({ ...a.place, _assignmentId: a.id }))).map(p => unlockedWithCoords.find(a => a.id === p._assignmentId)).filter(Boolean) + ? optimizeRoute(unlockedWithCoords.map(a => ({ ...a.place, _assignmentId: a.id })), anchors).map(p => unlockedWithCoords.find(a => a.id === p._assignmentId)).filter(Boolean) : unlockedWithCoords const optimizedQueue = [...optimizedAssignments, ...unlockedNoCoords] @@ -717,7 +728,8 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { } await onReorder(selectedDayId, result.map(a => a.id)) - toast.success(t('dayplan.toast.routeOptimized')) + const usedHotel = !!(anchors.start || anchors.end) + toast.success(usedHotel ? t('dayplan.toast.routeOptimizedFromHotel') : t('dayplan.toast.routeOptimized')) const capturedDayId = selectedDayId pushUndo?.(t('undo.optimize'), async () => { await tripActions.reorderAssignments(tripId, capturedDayId, prevIds) @@ -851,6 +863,8 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { cancelNote, saveNote, deleteNote, + pendingDeleteNote, + setPendingDeleteNote, moveNote, expandedDays, setExpandedDays, @@ -993,6 +1007,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP cancelNote, saveNote, deleteNote, + pendingDeleteNote, + setPendingDeleteNote, moveNote, expandedDays, setExpandedDays, @@ -1908,7 +1924,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP onContextMenu={canEditDays ? e => ctxMenu.open(e, [ { label: t('common.edit'), icon: Pencil, onClick: () => openEditNote(day.id, note) }, { divider: true }, - { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => deleteNote(day.id, note.id) }, + { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => setPendingDeleteNote({ dayId: day.id, noteId: note.id }) }, ]) : undefined} onMouseEnter={e => { const grip = e.currentTarget.querySelector('.dp-grip') as HTMLElement | null @@ -1950,7 +1966,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP {canEditDays &&
- +
} {canEditDays &&
@@ -2093,6 +2109,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP t={t} /> + {/* Confirm: delete a day note — guards against accidental taps on touch devices */} + setPendingDeleteNote(null)} + onConfirm={() => { if (pendingDeleteNote) deleteNote(pendingDeleteNote.dayId, pendingDeleteNote.noteId) }} + title={t('dayplan.confirmDeleteNoteTitle')} + message={t('dayplan.confirmDeleteNoteBody')} + /> + {/* Transport-Detail-Modal */}
+ + {/* Optimize route from accommodation */} +
+ +
+ {[ + { value: true, label: t('settings.on') || 'On' }, + { value: false, label: t('settings.off') || 'Off' }, + ].map(opt => ( + + ))} +
+

{t('settings.optimizeFromAccommodationHint')}

+
) } diff --git a/client/src/store/settingsStore.ts b/client/src/store/settingsStore.ts index 2ed81e59..75942fd4 100644 --- a/client/src/store/settingsStore.ts +++ b/client/src/store/settingsStore.ts @@ -32,6 +32,7 @@ export const useSettingsStore = create((set, get) => ({ temperature_unit: 'fahrenheit', time_format: '12h', show_place_description: false, + optimize_from_accommodation: true, map_provider: 'leaflet', mapbox_access_token: '', mapbox_style: 'mapbox://styles/mapbox/standard', diff --git a/client/src/types.ts b/client/src/types.ts index 204234f3..a4114f2d 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -113,6 +113,7 @@ export interface Settings { show_place_description: boolean blur_booking_codes?: boolean map_booking_labels?: boolean + optimize_from_accommodation?: boolean map_provider?: 'leaflet' | 'mapbox-gl' mapbox_access_token?: string mapbox_style?: string @@ -162,6 +163,12 @@ export interface Waypoint { lng: number } +// Optional fixed start/end points for route optimization (e.g. the day's accommodation). +export interface RouteAnchors { + start?: Waypoint + end?: Waypoint +} + // User with optional OIDC fields export interface UserWithOidc extends User { oidc_issuer?: string | null diff --git a/client/src/utils/dayOrder.test.ts b/client/src/utils/dayOrder.test.ts new file mode 100644 index 00000000..8640d6b4 --- /dev/null +++ b/client/src/utils/dayOrder.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest' +import type { Day, Accommodation } from '../types' +import { getDayOrder, isDayInAccommodationRange, getAccommodationAnchors } from './dayOrder' + +const days = [ + { id: 10, day_number: 1 }, + { id: 20, day_number: 2 }, + { id: 30, day_number: 3 }, +] as unknown as Day[] + +const hotel = (over: Partial): Accommodation => + ({ place_lat: 48.1, place_lng: 11.5, start_day_id: 10, end_day_id: 30, ...over }) as Accommodation + +describe('getDayOrder', () => { + it('prefers day_number when present', () => { + expect(getDayOrder(days[1], days)).toBe(2) + }) + it('falls back to array index when day_number is missing', () => { + const noNumber = [{ id: 5 }, { id: 6 }] as unknown as Day[] + expect(getDayOrder(noNumber[1], noNumber)).toBe(1) + }) +}) + +describe('isDayInAccommodationRange', () => { + it('is inclusive of both the check-in and check-out day', () => { + expect(isDayInAccommodationRange(days[0], 10, 30, days)).toBe(true) // check-in morning + expect(isDayInAccommodationRange(days[1], 10, 30, days)).toBe(true) // mid-stay + expect(isDayInAccommodationRange(days[2], 10, 30, days)).toBe(true) // check-out day + }) + it('excludes days outside the stay', () => { + expect(isDayInAccommodationRange(days[0], 20, 30, days)).toBe(false) + }) +}) + +describe('getAccommodationAnchors', () => { + it('returns no anchors when the day has no accommodation', () => { + expect(getAccommodationAnchors(days[1], days, [])).toEqual({}) + }) + + it('anchors both ends to the same hotel on a mid-stay day (round trip)', () => { + const accs = [hotel({ start_day_id: 10, end_day_id: 30, place_lat: 48.1, place_lng: 11.5 })] + expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ + start: { lat: 48.1, lng: 11.5 }, + end: { lat: 48.1, lng: 11.5 }, + }) + }) + + it('loops a single hotel on its check-out day (home base for the day)', () => { + const accs = [hotel({ start_day_id: 10, end_day_id: 20, place_lat: 1, place_lng: 2 })] + expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ start: { lat: 1, lng: 2 }, end: { lat: 1, lng: 2 } }) + }) + + it('loops a single hotel on its check-in day (home base for the day)', () => { + const accs = [hotel({ start_day_id: 20, end_day_id: 30, place_lat: 3, place_lng: 4 })] + expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ start: { lat: 3, lng: 4 }, end: { lat: 3, lng: 4 } }) + }) + + it('uses the checked-out hotel as start and the checked-in hotel as end on a transfer day', () => { + const accs = [ + hotel({ start_day_id: 10, end_day_id: 20, place_lat: 1, place_lng: 1 }), // checkout today + hotel({ start_day_id: 20, end_day_id: 30, place_lat: 9, place_lng: 9 }), // check-in today + ] + expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ + start: { lat: 1, lng: 1 }, + end: { lat: 9, lng: 9 }, + }) + }) + + it('ignores accommodations that have no coordinates', () => { + const accs = [hotel({ start_day_id: 10, end_day_id: 30, place_lat: null, place_lng: null })] + expect(getAccommodationAnchors(days[1], days, accs)).toEqual({}) + }) +}) diff --git a/client/src/utils/dayOrder.ts b/client/src/utils/dayOrder.ts index 8ef66046..c96b948b 100644 --- a/client/src/utils/dayOrder.ts +++ b/client/src/utils/dayOrder.ts @@ -1,8 +1,34 @@ -import type { Day } from '../types' +import type { Day, Accommodation, RouteAnchors } from '../types' export const getDayOrder = (day: Day, days: Day[]): number => day.day_number ?? days.indexOf(day) +// Derives route anchors from the accommodation(s) active on a day. A single hotel is the day's home +// base, so the route is a loop that starts and ends there. A transfer day — checking out of one hotel +// and into another — instead runs from the morning hotel to the evening one. +export const getAccommodationAnchors = ( + day: Day, + days: Day[], + accommodations: Accommodation[], +): RouteAnchors => { + const located = accommodations.filter(a => + a.place_lat != null && a.place_lng != null && + isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days), + ) + if (located.length === 0) return {} + + const toAnchor = (a: Accommodation) => ({ lat: a.place_lat as number, lng: a.place_lng as number }) + + const checkOut = located.find(a => a.end_day_id === day.id) // the hotel you leave this morning + const checkIn = located.find(a => a.start_day_id === day.id) // the hotel you arrive at tonight + if (checkOut && checkIn && checkOut !== checkIn) { + return { start: toAnchor(checkOut), end: toAnchor(checkIn) } + } + + const hotel = toAnchor(located[0]) + return { start: hotel, end: hotel } +} + export const isDayInAccommodationRange = ( day: Day, startDayId: number, diff --git a/shared/src/i18n/ar/collab.ts b/shared/src/i18n/ar/collab.ts index 55177539..5ecf0c89 100644 --- a/shared/src/i18n/ar/collab.ts +++ b/shared/src/i18n/ar/collab.ts @@ -39,6 +39,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'إلغاء', 'collab.notes.edit': 'تعديل', 'collab.notes.delete': 'حذف', + 'collab.notes.confirmDeleteTitle': 'حذف الملاحظة؟', + 'collab.notes.confirmDeleteBody': 'سيتم حذف هذه الملاحظة نهائيًا.', 'collab.notes.pin': 'تثبيت', 'collab.notes.unpin': 'إلغاء التثبيت', 'collab.notes.daysAgo': 'منذ {n} يوم', diff --git a/shared/src/i18n/ar/dayplan.ts b/shared/src/i18n/ar/dayplan.ts index ccb83dd2..061a0dc0 100644 --- a/shared/src/i18n/ar/dayplan.ts +++ b/shared/src/i18n/ar/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.', 'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل', + 'dayplan.confirmDeleteNoteTitle': 'حذف الملاحظة؟', + 'dayplan.confirmDeleteNoteBody': 'سيتم حذف هذه الملاحظة نهائيًا.', 'dayplan.cannotDropOnTimed': 'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت', 'dayplan.cannotBreakChronology': @@ -29,6 +31,7 @@ const dayplan: TranslationStrings = { 'dayplan.routeError': 'فشل حساب المسار', 'dayplan.toast.needTwoPlaces': 'يلزم مكانان على الأقل لتحسين المسار', 'dayplan.toast.routeOptimized': 'تم تحسين المسار', + 'dayplan.toast.routeOptimizedFromHotel': 'تم تحسين المسار انطلاقًا من مكان إقامتك', 'dayplan.toast.noGeoPlaces': 'لم يتم العثور على أماكن بإحداثيات لحساب المسار', 'dayplan.confirmed': 'مؤكد', 'dayplan.pendingRes': 'قيد الانتظار', diff --git a/shared/src/i18n/ar/settings.ts b/shared/src/i18n/ar/settings.ts index 1f9d0814..e734dcc0 100644 --- a/shared/src/i18n/ar/settings.ts +++ b/shared/src/i18n/ar/settings.ts @@ -60,6 +60,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.', 'settings.blurBookingCodes': 'إخفاء رموز الحجز', + 'settings.optimizeFromAccommodation': 'تحسين المسار انطلاقًا من مكان الإقامة', + 'settings.optimizeFromAccommodationHint': + 'عند تحسين يوم ما، يبدأ المسار من الفندق الذي تستيقظ فيه وينتهي عند الفندق الذي تسجّل الوصول إليه في تلك الليلة.', 'settings.notifications': 'الإشعارات', 'settings.notifyTripInvite': 'دعوات الرحلات', 'settings.notifyBookingChange': 'تغييرات الحجز', diff --git a/shared/src/i18n/br/collab.ts b/shared/src/i18n/br/collab.ts index b2cc54e7..c0eda81c 100644 --- a/shared/src/i18n/br/collab.ts +++ b/shared/src/i18n/br/collab.ts @@ -41,6 +41,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Cancelar', 'collab.notes.edit': 'Editar', 'collab.notes.delete': 'Excluir', + 'collab.notes.confirmDeleteTitle': 'Excluir nota?', + 'collab.notes.confirmDeleteBody': 'Esta nota será excluída permanentemente.', 'collab.notes.pin': 'Fixar', 'collab.notes.unpin': 'Desafixar', 'collab.notes.daysAgo': 'há {n} d', diff --git a/shared/src/i18n/br/dayplan.ts b/shared/src/i18n/br/dayplan.ts index a1b1c7c3..53ee5afd 100644 --- a/shared/src/i18n/br/dayplan.ts +++ b/shared/src/i18n/br/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'São necessários pelo menos dois lugares para otimizar a rota', 'dayplan.toast.routeOptimized': 'Rota otimizada', + 'dayplan.toast.routeOptimizedFromHotel': + 'Rota otimizada a partir da sua hospedagem', 'dayplan.toast.noGeoPlaces': 'Nenhum lugar com coordenadas para calcular a rota', 'dayplan.confirmed': 'Confirmada', @@ -33,6 +35,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Este lugar tem um horário fixo ({time}). Movê-lo removerá o horário e permitirá ordenação livre.', 'dayplan.confirmRemoveTimeAction': 'Remover horário e mover', + 'dayplan.confirmDeleteNoteTitle': 'Excluir nota?', + 'dayplan.confirmDeleteNoteBody': 'Esta nota será excluída permanentemente.', 'dayplan.cannotDropOnTimed': 'Itens não podem ser colocados entre entradas com horário fixo', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/br/settings.ts b/shared/src/i18n/br/settings.ts index 93d999cf..7a8cc233 100644 --- a/shared/src/i18n/br/settings.ts +++ b/shared/src/i18n/br/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Unidade de temperatura', 'settings.timeFormat': 'Formato de hora', 'settings.blurBookingCodes': 'Ocultar códigos de reserva', + 'settings.optimizeFromAccommodation': 'Otimizar rota a partir da hospedagem', + 'settings.optimizeFromAccommodationHint': + 'Ao otimizar um dia, comece a rota no hotel onde você acorda e termine no hotel em que você faz check-in à noite.', 'settings.notifications': 'Notificações', 'settings.notifyTripInvite': 'Convites de viagem', 'settings.notifyBookingChange': 'Alterações de reserva', diff --git a/shared/src/i18n/cs/collab.ts b/shared/src/i18n/cs/collab.ts index 7d314168..9e504870 100644 --- a/shared/src/i18n/cs/collab.ts +++ b/shared/src/i18n/cs/collab.ts @@ -36,6 +36,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Zrušit', 'collab.notes.edit': 'Upravit', 'collab.notes.delete': 'Smazat', + 'collab.notes.confirmDeleteTitle': 'Smazat poznámku?', + 'collab.notes.confirmDeleteBody': 'Tato poznámka bude trvale smazána.', 'collab.notes.pin': 'Připnout', 'collab.notes.unpin': 'Odepnout', 'collab.notes.daysAgo': 'před {n} dny', diff --git a/shared/src/i18n/cs/dayplan.ts b/shared/src/i18n/cs/dayplan.ts index 9b72421d..b13a5177 100644 --- a/shared/src/i18n/cs/dayplan.ts +++ b/shared/src/i18n/cs/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Pro optimalizaci trasy jsou potřeba alespoň dvě místa', 'dayplan.toast.routeOptimized': 'Trasa byla optimalizována', + 'dayplan.toast.routeOptimizedFromHotel': + 'Trasa byla optimalizována od vašeho ubytování', 'dayplan.toast.noGeoPlaces': 'Nebyla nalezena žádná místa se souřadnicemi pro výpočet trasy', 'dayplan.confirmed': 'Potvrzeno', @@ -33,6 +35,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Toto místo má pevný čas ({time}). Přesunutím se čas odebere a povolí se volné řazení.', 'dayplan.confirmRemoveTimeAction': 'Odebrat čas a přesunout', + 'dayplan.confirmDeleteNoteTitle': 'Smazat poznámku?', + 'dayplan.confirmDeleteNoteBody': 'Tato poznámka bude trvale smazána.', 'dayplan.cannotDropOnTimed': 'Položky nelze umístit mezi záznamy s pevným časem', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/cs/settings.ts b/shared/src/i18n/cs/settings.ts index 790a7709..08f1339e 100644 --- a/shared/src/i18n/cs/settings.ts +++ b/shared/src/i18n/cs/settings.ts @@ -63,6 +63,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Jednotky teploty', 'settings.timeFormat': 'Formát času', 'settings.blurBookingCodes': 'Skrýt rezervační kódy', + 'settings.optimizeFromAccommodation': 'Optimalizovat trasu od ubytování', + 'settings.optimizeFromAccommodationHint': + 'Při optimalizaci dne začne trasa v hotelu, ve kterém se ráno probudíte, a skončí v hotelu, do kterého se večer ubytujete.', 'settings.notifications': 'Oznámení', 'settings.notifyTripInvite': 'Pozvánky na cesty', 'settings.notifyBookingChange': 'Změny rezervací', diff --git a/shared/src/i18n/de/collab.ts b/shared/src/i18n/de/collab.ts index 3fe9e737..06bc96af 100644 --- a/shared/src/i18n/de/collab.ts +++ b/shared/src/i18n/de/collab.ts @@ -41,6 +41,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Abbrechen', 'collab.notes.edit': 'Bearbeiten', 'collab.notes.delete': 'Löschen', + 'collab.notes.confirmDeleteTitle': 'Notiz löschen?', + 'collab.notes.confirmDeleteBody': 'Diese Notiz wird dauerhaft gelöscht.', 'collab.notes.pin': 'Anheften', 'collab.notes.unpin': 'Loslösen', 'collab.notes.daysAgo': 'vor {n} T.', diff --git a/shared/src/i18n/de/dayplan.ts b/shared/src/i18n/de/dayplan.ts index 1ad23de0..d84948cc 100644 --- a/shared/src/i18n/de/dayplan.ts +++ b/shared/src/i18n/de/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Dieser Ort hat eine feste Uhrzeit ({time}). Durch das Verschieben wird die Uhrzeit entfernt und der Ort kann frei sortiert werden.', 'dayplan.confirmRemoveTimeAction': 'Uhrzeit entfernen & verschieben', + 'dayplan.confirmDeleteNoteTitle': 'Notiz löschen?', + 'dayplan.confirmDeleteNoteBody': 'Diese Notiz wird dauerhaft gelöscht.', 'dayplan.cannotDropOnTimed': 'Orte können nicht zwischen zeitgebundene Einträge geschoben werden', 'dayplan.cannotBreakChronology': @@ -32,6 +34,7 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Mindestens zwei Orte für Routenoptimierung nötig', 'dayplan.toast.routeOptimized': 'Route optimiert', + 'dayplan.toast.routeOptimizedFromHotel': 'Route ab deiner Unterkunft optimiert', 'dayplan.toast.noGeoPlaces': 'Keine Orte mit Koordinaten für Routenberechnung gefunden', 'dayplan.confirmed': 'Bestätigt', diff --git a/shared/src/i18n/de/settings.ts b/shared/src/i18n/de/settings.ts index e1b433e2..d9bd1224 100644 --- a/shared/src/i18n/de/settings.ts +++ b/shared/src/i18n/de/settings.ts @@ -65,6 +65,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': 'Zeigt Bahnhofs-/Flughafennamen auf der Karte. Wenn aus, wird nur das Icon angezeigt.', 'settings.blurBookingCodes': 'Buchungscodes verbergen', + 'settings.optimizeFromAccommodation': 'Route ab der Unterkunft optimieren', + 'settings.optimizeFromAccommodationHint': + 'Beim Optimieren eines Tages startet die Route an der Unterkunft, in der du aufwachst, und endet an der, in die du am Abend eincheckst.', 'settings.notifications': 'Mitteilungen', 'settings.notifyTripInvite': 'Trip-Einladungen', 'settings.notifyBookingChange': 'Buchungsänderungen', diff --git a/shared/src/i18n/en/collab.ts b/shared/src/i18n/en/collab.ts index aee6b9a5..0d62294a 100644 --- a/shared/src/i18n/en/collab.ts +++ b/shared/src/i18n/en/collab.ts @@ -40,6 +40,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Cancel', 'collab.notes.edit': 'Edit', 'collab.notes.delete': 'Delete', + 'collab.notes.confirmDeleteTitle': 'Delete note?', + 'collab.notes.confirmDeleteBody': 'This note will be permanently deleted.', 'collab.notes.pin': 'Pin', 'collab.notes.unpin': 'Unpin', 'collab.notes.daysAgo': '{n}d ago', diff --git a/shared/src/i18n/en/dayplan.ts b/shared/src/i18n/en/dayplan.ts index 8f1825f6..28e537ab 100644 --- a/shared/src/i18n/en/dayplan.ts +++ b/shared/src/i18n/en/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'This place has a fixed time ({time}). Moving it will remove the time and allow free sorting.', 'dayplan.confirmRemoveTimeAction': 'Remove time & move', + 'dayplan.confirmDeleteNoteTitle': 'Delete note?', + 'dayplan.confirmDeleteNoteBody': 'This note will be permanently deleted.', 'dayplan.cannotDropOnTimed': 'Items cannot be placed between time-bound entries', 'dayplan.cannotBreakChronology': @@ -32,6 +34,7 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'At least two places needed for route optimization', 'dayplan.toast.routeOptimized': 'Route optimized', + 'dayplan.toast.routeOptimizedFromHotel': 'Route optimized from your accommodation', 'dayplan.toast.noGeoPlaces': 'No places with coordinates found for route calculation', 'dayplan.confirmed': 'Confirmed', diff --git a/shared/src/i18n/en/settings.ts b/shared/src/i18n/en/settings.ts index ae60ffad..62bc6cc9 100644 --- a/shared/src/i18n/en/settings.ts +++ b/shared/src/i18n/en/settings.ts @@ -64,6 +64,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': 'Show station / airport names on the map. When off, only the icon is shown.', 'settings.blurBookingCodes': 'Blur Booking Codes', + 'settings.optimizeFromAccommodation': 'Optimize route from accommodation', + 'settings.optimizeFromAccommodationHint': + 'When optimizing a day, start the route at the hotel you wake up in and end it at the one you check into that evening.', 'settings.notifications': 'Notifications', 'settings.notifyTripInvite': 'Trip invitations', 'settings.notifyBookingChange': 'Booking changes', diff --git a/shared/src/i18n/es/collab.ts b/shared/src/i18n/es/collab.ts index 5d3ccc09..24bf731f 100644 --- a/shared/src/i18n/es/collab.ts +++ b/shared/src/i18n/es/collab.ts @@ -41,6 +41,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Cancelar', 'collab.notes.edit': 'Editar', 'collab.notes.delete': 'Eliminar', + 'collab.notes.confirmDeleteTitle': '¿Eliminar nota?', + 'collab.notes.confirmDeleteBody': 'Esta nota se eliminará de forma permanente.', 'collab.notes.pin': 'Fijar', 'collab.notes.unpin': 'Desfijar', 'collab.notes.daysAgo': 'hace {n} d', diff --git a/shared/src/i18n/es/dayplan.ts b/shared/src/i18n/es/dayplan.ts index 25281557..e7f4d998 100644 --- a/shared/src/i18n/es/dayplan.ts +++ b/shared/src/i18n/es/dayplan.ts @@ -20,6 +20,7 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Se necesitan al menos dos lugares para optimizar la ruta', 'dayplan.toast.routeOptimized': 'Ruta optimizada', + 'dayplan.toast.routeOptimizedFromHotel': 'Ruta optimizada desde tu alojamiento', 'dayplan.toast.noGeoPlaces': 'No se encontraron lugares con coordenadas para calcular la ruta', 'dayplan.confirmed': 'Confirmado', @@ -33,6 +34,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Este lugar tiene una hora fija ({time}). Al moverlo se eliminará la hora y se permitirá el orden libre.', 'dayplan.confirmRemoveTimeAction': 'Eliminar hora y mover', + 'dayplan.confirmDeleteNoteTitle': '¿Eliminar nota?', + 'dayplan.confirmDeleteNoteBody': 'Esta nota se eliminará de forma permanente.', 'dayplan.cannotDropOnTimed': 'No se pueden colocar elementos entre entradas con hora fija', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/es/settings.ts b/shared/src/i18n/es/settings.ts index dfb68248..9f981e80 100644 --- a/shared/src/i18n/es/settings.ts +++ b/shared/src/i18n/es/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Unidad de temperatura', 'settings.timeFormat': 'Formato de hora', 'settings.blurBookingCodes': 'Difuminar códigos de reserva', + 'settings.optimizeFromAccommodation': 'Optimizar la ruta desde el alojamiento', + 'settings.optimizeFromAccommodationHint': + 'Al optimizar un día, comienza la ruta en el hotel donde despiertas y termínala en aquel en el que te registras esa noche.', 'settings.notifications': 'Notificaciones', 'settings.notifyTripInvite': 'Invitaciones de viaje', 'settings.notifyBookingChange': 'Cambios en reservas', diff --git a/shared/src/i18n/fr/collab.ts b/shared/src/i18n/fr/collab.ts index d771b15c..a14c8ead 100644 --- a/shared/src/i18n/fr/collab.ts +++ b/shared/src/i18n/fr/collab.ts @@ -42,6 +42,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Annuler', 'collab.notes.edit': 'Modifier', 'collab.notes.delete': 'Supprimer', + 'collab.notes.confirmDeleteTitle': 'Supprimer la note ?', + 'collab.notes.confirmDeleteBody': 'Cette note sera définitivement supprimée.', 'collab.notes.pin': 'Épingler', 'collab.notes.unpin': 'Désépingler', 'collab.notes.daysAgo': 'il y a {n} j', diff --git a/shared/src/i18n/fr/dayplan.ts b/shared/src/i18n/fr/dayplan.ts index 2066e978..1e77b5ab 100644 --- a/shared/src/i18n/fr/dayplan.ts +++ b/shared/src/i18n/fr/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': "Au moins deux lieux nécessaires pour optimiser l'itinéraire", 'dayplan.toast.routeOptimized': 'Itinéraire optimisé', + 'dayplan.toast.routeOptimizedFromHotel': + 'Itinéraire optimisé depuis votre hébergement', 'dayplan.toast.noGeoPlaces': "Aucun lieu avec des coordonnées trouvé pour le calcul d'itinéraire", 'dayplan.confirmed': 'Confirmé', @@ -33,6 +35,9 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': "Ce lieu a une heure fixe ({time}). Le déplacer supprimera l'heure et permettra un tri libre.", 'dayplan.confirmRemoveTimeAction': "Supprimer l'heure et déplacer", + 'dayplan.confirmDeleteNoteTitle': 'Supprimer la note ?', + 'dayplan.confirmDeleteNoteBody': + 'Cette note sera définitivement supprimée.', 'dayplan.cannotDropOnTimed': 'Les éléments ne peuvent pas être placés entre des entrées à heure fixe', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/fr/settings.ts b/shared/src/i18n/fr/settings.ts index fa55f294..e8041f02 100644 --- a/shared/src/i18n/fr/settings.ts +++ b/shared/src/i18n/fr/settings.ts @@ -62,6 +62,10 @@ const settings: TranslationStrings = { 'settings.temperature': 'Unité de température', 'settings.timeFormat': "Format de l'heure", 'settings.blurBookingCodes': 'Masquer les codes de réservation', + 'settings.optimizeFromAccommodation': + "Optimiser l'itinéraire depuis l'hébergement", + 'settings.optimizeFromAccommodationHint': + "Lors de l'optimisation d'une journée, commencez l'itinéraire à l'hôtel où vous vous réveillez et terminez-le à celui où vous arrivez le soir.", 'settings.notifications': 'Notifications', 'settings.notifyTripInvite': 'Invitations de voyage', 'settings.notifyBookingChange': 'Modifications de réservation', diff --git a/shared/src/i18n/gr/collab.ts b/shared/src/i18n/gr/collab.ts index ddc7dc94..5f9b762a 100644 --- a/shared/src/i18n/gr/collab.ts +++ b/shared/src/i18n/gr/collab.ts @@ -41,6 +41,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Ακύρωση', 'collab.notes.edit': 'Επεξεργασία', 'collab.notes.delete': 'Διαγραφή', + 'collab.notes.confirmDeleteTitle': 'Διαγραφή σημείωσης;', + 'collab.notes.confirmDeleteBody': 'Αυτή η σημείωση θα διαγραφεί οριστικά.', 'collab.notes.pin': 'Καρφίτσωμα', 'collab.notes.unpin': 'Ξεκαρφίτσωμα', 'collab.notes.daysAgo': '{n}η πριν', diff --git a/shared/src/i18n/gr/dayplan.ts b/shared/src/i18n/gr/dayplan.ts index 3d7631bc..58ef6253 100644 --- a/shared/src/i18n/gr/dayplan.ts +++ b/shared/src/i18n/gr/dayplan.ts @@ -9,6 +9,9 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Αυτό το μέρος έχει σταθερή ώρα ({time}). Η μετακίνησή του θα αφαιρέσει την ώρα και θα επιτρέψει ελεύθερη ταξινόμηση.', 'dayplan.confirmRemoveTimeAction': 'Αφαίρεση ώρας & μετακίνηση', + 'dayplan.confirmDeleteNoteTitle': 'Διαγραφή σημείωσης;', + 'dayplan.confirmDeleteNoteBody': + 'Αυτή η σημείωση θα διαγραφεί οριστικά.', 'dayplan.cannotDropOnTimed': 'Τα στοιχεία δεν μπορούν να τοποθετηθούν μεταξύ καταχωρήσεων με ώρα', 'dayplan.cannotBreakChronology': @@ -32,6 +35,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Χρειάζονται τουλάχιστον δύο μέρη για βελτιστοποίηση διαδρομής', 'dayplan.toast.routeOptimized': 'Η διαδρομή βελτιστοποιήθηκε', + 'dayplan.toast.routeOptimizedFromHotel': + 'Η διαδρομή βελτιστοποιήθηκε από το κατάλυμά σας', 'dayplan.toast.noGeoPlaces': 'Δεν βρέθηκαν μέρη με συντεταγμένες για τον υπολογισμό διαδρομής', 'dayplan.confirmed': 'Επιβεβαιωμένο', diff --git a/shared/src/i18n/gr/settings.ts b/shared/src/i18n/gr/settings.ts index 9a51c338..d11431c9 100644 --- a/shared/src/i18n/gr/settings.ts +++ b/shared/src/i18n/gr/settings.ts @@ -66,6 +66,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': 'Εμφάνιση ονομάτων σταθμών / αεροδρομίων στον χάρτη. Όταν είναι απενεργοποιημένο, εμφανίζεται μόνο το εικονίδιο.', 'settings.blurBookingCodes': 'Θόλωμα Κωδικών Κρατήσεων', + 'settings.optimizeFromAccommodation': 'Βελτιστοποίηση διαδρομής από το κατάλυμα', + 'settings.optimizeFromAccommodationHint': + 'Κατά τη βελτιστοποίηση μιας ημέρας, ξεκινήστε τη διαδρομή από το ξενοδοχείο στο οποίο ξυπνάτε και τερματίστε την σε αυτό στο οποίο κάνετε check-in το ίδιο βράδυ.', 'settings.notifications': 'Ειδοποιήσεις', 'settings.notifyTripInvite': 'Προσκλήσεις ταξιδιού', 'settings.notifyBookingChange': 'Αλλαγές κρατήσεων', diff --git a/shared/src/i18n/hu/collab.ts b/shared/src/i18n/hu/collab.ts index f5e35aaa..91504e26 100644 --- a/shared/src/i18n/hu/collab.ts +++ b/shared/src/i18n/hu/collab.ts @@ -42,6 +42,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Mégse', 'collab.notes.edit': 'Szerkesztés', 'collab.notes.delete': 'Törlés', + 'collab.notes.confirmDeleteTitle': 'Törli a jegyzetet?', + 'collab.notes.confirmDeleteBody': 'Ez a jegyzet véglegesen törlődik.', 'collab.notes.pin': 'Kitűzés', 'collab.notes.unpin': 'Kitűzés eltávolítása', 'collab.notes.daysAgo': '{n} napja', diff --git a/shared/src/i18n/hu/dayplan.ts b/shared/src/i18n/hu/dayplan.ts index 5742779d..2a407180 100644 --- a/shared/src/i18n/hu/dayplan.ts +++ b/shared/src/i18n/hu/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Legalább két hely szükséges az útvonal-optimalizáláshoz', 'dayplan.toast.routeOptimized': 'Útvonal optimalizálva', + 'dayplan.toast.routeOptimizedFromHotel': + 'Útvonal optimalizálva a szállásodtól', 'dayplan.toast.noGeoPlaces': 'Nem találhatók koordinátákkal rendelkező helyek az útvonalszámításhoz', 'dayplan.confirmed': 'Megerősítve', @@ -33,6 +35,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Ennek a helynek rögzített időpontja van ({time}). Az áthelyezéssel az időpont eltávolítódik és szabad rendezés válik lehetővé.', 'dayplan.confirmRemoveTimeAction': 'Időpont eltávolítása és áthelyezés', + 'dayplan.confirmDeleteNoteTitle': 'Törli a jegyzetet?', + 'dayplan.confirmDeleteNoteBody': 'Ez a jegyzet véglegesen törlődik.', 'dayplan.cannotDropOnTimed': 'Elemek nem helyezhetők rögzített időpontú bejegyzések közé', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/hu/settings.ts b/shared/src/i18n/hu/settings.ts index 6d82e28d..a4eb3b9b 100644 --- a/shared/src/i18n/hu/settings.ts +++ b/shared/src/i18n/hu/settings.ts @@ -63,6 +63,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Hőmérséklet egység', 'settings.timeFormat': 'Időformátum', 'settings.blurBookingCodes': 'Foglalási kódok elrejtése', + 'settings.optimizeFromAccommodation': 'Útvonal optimalizálása a szállástól', + 'settings.optimizeFromAccommodationHint': + 'A nap optimalizálásakor az útvonal annál a szállásnál kezdődjön, ahol felébredsz, és annál érjen véget, ahova este bejelentkezel.', 'settings.notifications': 'Értesítések', 'settings.notifyTripInvite': 'Utazási meghívók', 'settings.notifyBookingChange': 'Foglalási változások', diff --git a/shared/src/i18n/id/collab.ts b/shared/src/i18n/id/collab.ts index 04596587..cf91c8dd 100644 --- a/shared/src/i18n/id/collab.ts +++ b/shared/src/i18n/id/collab.ts @@ -40,6 +40,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Batal', 'collab.notes.edit': 'Edit', 'collab.notes.delete': 'Hapus', + 'collab.notes.confirmDeleteTitle': 'Hapus catatan?', + 'collab.notes.confirmDeleteBody': 'Catatan ini akan dihapus secara permanen.', 'collab.notes.pin': 'Sematkan', 'collab.notes.unpin': 'Lepas sematan', 'collab.notes.daysAgo': '{n}h lalu', diff --git a/shared/src/i18n/id/dayplan.ts b/shared/src/i18n/id/dayplan.ts index 3e6e6897..88905d7b 100644 --- a/shared/src/i18n/id/dayplan.ts +++ b/shared/src/i18n/id/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Tempat ini memiliki waktu tetap ({time}). Memindahkannya akan menghapus waktu dan mengizinkan pengurutan bebas.', 'dayplan.confirmRemoveTimeAction': 'Hapus waktu & pindahkan', + 'dayplan.confirmDeleteNoteTitle': 'Hapus catatan?', + 'dayplan.confirmDeleteNoteBody': 'Catatan ini akan dihapus secara permanen.', 'dayplan.cannotDropOnTimed': 'Item tidak dapat ditempatkan di antara entri yang terikat waktu', 'dayplan.cannotBreakChronology': @@ -30,6 +32,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Diperlukan minimal dua tempat untuk optimasi rute', 'dayplan.toast.routeOptimized': 'Rute dioptimalkan', + 'dayplan.toast.routeOptimizedFromHotel': + 'Rute dioptimalkan dari akomodasimu', 'dayplan.toast.noGeoPlaces': 'Tidak ditemukan tempat dengan koordinat untuk kalkulasi rute', 'dayplan.confirmed': 'Dikonfirmasi', diff --git a/shared/src/i18n/id/settings.ts b/shared/src/i18n/id/settings.ts index 6d953907..1288cc4c 100644 --- a/shared/src/i18n/id/settings.ts +++ b/shared/src/i18n/id/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Satuan Suhu', 'settings.timeFormat': 'Format Waktu', 'settings.blurBookingCodes': 'Sembunyikan Kode Pemesanan', + 'settings.optimizeFromAccommodation': 'Optimalkan rute dari akomodasi', + 'settings.optimizeFromAccommodationHint': + 'Saat mengoptimalkan suatu hari, mulai rute dari hotel tempatmu bangun pagi dan akhiri di hotel tempatmu check-in malam itu.', 'settings.notifications': 'Notifikasi', 'settings.notifyTripInvite': 'Undangan perjalanan', 'settings.notifyBookingChange': 'Perubahan pemesanan', diff --git a/shared/src/i18n/it/collab.ts b/shared/src/i18n/it/collab.ts index d339ec40..0bf522b9 100644 --- a/shared/src/i18n/it/collab.ts +++ b/shared/src/i18n/it/collab.ts @@ -41,6 +41,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Annulla', 'collab.notes.edit': 'Modifica', 'collab.notes.delete': 'Elimina', + 'collab.notes.confirmDeleteTitle': 'Eliminare la nota?', + 'collab.notes.confirmDeleteBody': 'Questa nota verrà eliminata definitivamente.', 'collab.notes.pin': 'Fissa', 'collab.notes.unpin': 'Rimuovi', 'collab.notes.daysAgo': '{n}g fa', diff --git a/shared/src/i18n/it/dayplan.ts b/shared/src/i18n/it/dayplan.ts index 312f52ef..f5625d61 100644 --- a/shared/src/i18n/it/dayplan.ts +++ b/shared/src/i18n/it/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': "Servono almeno due luoghi per l'ottimizzazione del percorso", 'dayplan.toast.routeOptimized': 'Percorso ottimizzato', + 'dayplan.toast.routeOptimizedFromHotel': + 'Percorso ottimizzato a partire dal tuo alloggio', 'dayplan.toast.noGeoPlaces': 'Nessun luogo con coordinate trovato per il calcolo del percorso', 'dayplan.confirmed': 'Confermata', @@ -33,6 +35,9 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': "Questo luogo ha un orario fisso ({time}). Spostarlo rimuoverà l'orario e consentirà l'ordinamento libero.", 'dayplan.confirmRemoveTimeAction': 'Rimuovi orario e sposta', + 'dayplan.confirmDeleteNoteTitle': 'Eliminare la nota?', + 'dayplan.confirmDeleteNoteBody': + 'Questa nota verrà eliminata definitivamente.', 'dayplan.cannotDropOnTimed': 'Gli elementi non possono essere posizionati tra voci con orario fisso', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/it/settings.ts b/shared/src/i18n/it/settings.ts index a1619c6d..cb7e8913 100644 --- a/shared/src/i18n/it/settings.ts +++ b/shared/src/i18n/it/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Unità di Temperatura', 'settings.timeFormat': 'Formato Ora', 'settings.blurBookingCodes': 'Nascondi codici di prenotazione', + 'settings.optimizeFromAccommodation': "Ottimizza il percorso dall'alloggio", + 'settings.optimizeFromAccommodationHint': + "Quando ottimizzi un giorno, fa iniziare il percorso dall'hotel in cui ti svegli e terminarlo in quello in cui fai il check-in quella sera.", 'settings.notifications': 'Notifiche', 'settings.notifyTripInvite': 'Inviti di viaggio', 'settings.notifyBookingChange': 'Modifiche alle prenotazioni', diff --git a/shared/src/i18n/ja/collab.ts b/shared/src/i18n/ja/collab.ts index 31abd110..4de8e5ef 100644 --- a/shared/src/i18n/ja/collab.ts +++ b/shared/src/i18n/ja/collab.ts @@ -40,6 +40,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'キャンセル', 'collab.notes.edit': '編集', 'collab.notes.delete': '削除', + 'collab.notes.confirmDeleteTitle': 'メモを削除しますか?', + 'collab.notes.confirmDeleteBody': 'このメモは完全に削除されます。', 'collab.notes.pin': '固定', 'collab.notes.unpin': '固定を解除', 'collab.notes.daysAgo': '{n}日前', diff --git a/shared/src/i18n/ja/dayplan.ts b/shared/src/i18n/ja/dayplan.ts index eca6d544..64f49fac 100644 --- a/shared/src/i18n/ja/dayplan.ts +++ b/shared/src/i18n/ja/dayplan.ts @@ -8,6 +8,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'この場所には固定時刻({time})があります。移動すると時刻が削除され、自由に並び替えできます。', 'dayplan.confirmRemoveTimeAction': '時刻を削除して移動', + 'dayplan.confirmDeleteNoteTitle': 'メモを削除しますか?', + 'dayplan.confirmDeleteNoteBody': 'このメモは完全に削除されます。', 'dayplan.cannotDropOnTimed': '時刻指定の項目の間には配置できません', 'dayplan.cannotBreakChronology': '時刻指定の項目や予約の時系列が崩れます', 'dayplan.addNote': 'メモを追加', @@ -28,6 +30,7 @@ const dayplan: TranslationStrings = { 'dayplan.routeError': 'ルートの計算に失敗しました', 'dayplan.toast.needTwoPlaces': 'ルート最適化には2つ以上の場所が必要です', 'dayplan.toast.routeOptimized': 'ルートを最適化しました', + 'dayplan.toast.routeOptimizedFromHotel': '宿泊先を起点にルートを最適化しました', 'dayplan.toast.noGeoPlaces': '座標付きの場所がありません', 'dayplan.confirmed': '確定', 'dayplan.pendingRes': '保留', diff --git a/shared/src/i18n/ja/settings.ts b/shared/src/i18n/ja/settings.ts index 4b88316a..38c59807 100644 --- a/shared/src/i18n/ja/settings.ts +++ b/shared/src/i18n/ja/settings.ts @@ -64,6 +64,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': '地図に駅・空港名を表示。オフ時はアイコンのみ。', 'settings.blurBookingCodes': '予約コードをぼかす', + 'settings.optimizeFromAccommodation': '宿泊先を起点にルートを最適化', + 'settings.optimizeFromAccommodationHint': + 'その日を最適化する際、朝に目覚める宿泊先を起点にし、その晩にチェックインする宿泊先を終点としてルートを組みます。', 'settings.notifications': '通知', 'settings.notifyTripInvite': '旅行の招待', 'settings.notifyBookingChange': '予約の変更', diff --git a/shared/src/i18n/ko/collab.ts b/shared/src/i18n/ko/collab.ts index c5d9ca44..e970e43f 100644 --- a/shared/src/i18n/ko/collab.ts +++ b/shared/src/i18n/ko/collab.ts @@ -39,6 +39,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': '취소', 'collab.notes.edit': '편집', 'collab.notes.delete': '삭제', + 'collab.notes.confirmDeleteTitle': '메모를 삭제할까요?', + 'collab.notes.confirmDeleteBody': '이 메모가 영구적으로 삭제됩니다.', 'collab.notes.pin': '고정', 'collab.notes.unpin': '고정 해제', 'collab.notes.daysAgo': '{n}일 전', diff --git a/shared/src/i18n/ko/dayplan.ts b/shared/src/i18n/ko/dayplan.ts index 20741475..b2095b22 100644 --- a/shared/src/i18n/ko/dayplan.ts +++ b/shared/src/i18n/ko/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': '이 장소에 고정된 시간 ({time})이 있습니다. 이동하면 시간이 제거되고 자유 정렬이 허용됩니다.', 'dayplan.confirmRemoveTimeAction': '시간 제거 및 이동', + 'dayplan.confirmDeleteNoteTitle': '메모를 삭제할까요?', + 'dayplan.confirmDeleteNoteBody': '이 메모가 영구적으로 삭제됩니다.', 'dayplan.cannotDropOnTimed': '시간이 고정된 항목 사이에 배치할 수 없습니다', 'dayplan.cannotBreakChronology': '이 작업은 시간 고정 항목과 예약의 시간 순서를 깨뜨립니다', @@ -31,6 +33,7 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': '경로 최적화에는 최소 두 개의 장소가 필요합니다', 'dayplan.toast.routeOptimized': '경로가 최적화되었습니다', + 'dayplan.toast.routeOptimizedFromHotel': '숙소를 기준으로 경로가 최적화되었습니다', 'dayplan.toast.noGeoPlaces': '경로 계산을 위한 좌표가 있는 장소가 없습니다', 'dayplan.confirmed': '확정됨', 'dayplan.pendingRes': '대기 중', diff --git a/shared/src/i18n/ko/settings.ts b/shared/src/i18n/ko/settings.ts index 705e1e04..6c75b2d1 100644 --- a/shared/src/i18n/ko/settings.ts +++ b/shared/src/i18n/ko/settings.ts @@ -65,6 +65,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': '지도에 역 / 공항 이름을 표시합니다. 끄면 아이콘만 표시됩니다.', 'settings.blurBookingCodes': '예약 코드 흐리게', + 'settings.optimizeFromAccommodation': '숙소 기준으로 경로 최적화', + 'settings.optimizeFromAccommodationHint': + '하루 일정을 최적화할 때, 아침에 머무는 숙소에서 경로를 시작하고 그날 저녁에 체크인하는 숙소에서 경로를 끝냅니다.', 'settings.notifications': '알림', 'settings.notifyTripInvite': '여행 초대', 'settings.notifyBookingChange': '예약 변경', diff --git a/shared/src/i18n/nl/collab.ts b/shared/src/i18n/nl/collab.ts index 61247230..bb09b6a4 100644 --- a/shared/src/i18n/nl/collab.ts +++ b/shared/src/i18n/nl/collab.ts @@ -39,6 +39,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Annuleren', 'collab.notes.edit': 'Bewerken', 'collab.notes.delete': 'Verwijderen', + 'collab.notes.confirmDeleteTitle': 'Notitie verwijderen?', + 'collab.notes.confirmDeleteBody': 'Deze notitie wordt definitief verwijderd.', 'collab.notes.pin': 'Vastpinnen', 'collab.notes.unpin': 'Losmaken', 'collab.notes.daysAgo': '{n}d geleden', diff --git a/shared/src/i18n/nl/dayplan.ts b/shared/src/i18n/nl/dayplan.ts index d721adf9..80ebfec9 100644 --- a/shared/src/i18n/nl/dayplan.ts +++ b/shared/src/i18n/nl/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Minimaal twee plaatsen nodig voor route-optimalisatie', 'dayplan.toast.routeOptimized': 'Route geoptimaliseerd', + 'dayplan.toast.routeOptimizedFromHotel': + 'Route geoptimaliseerd vanaf je accommodatie', 'dayplan.toast.noGeoPlaces': 'Geen plaatsen met coördinaten gevonden voor routeberekening', 'dayplan.confirmed': 'Bevestigd', @@ -33,6 +35,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Deze plek heeft een vast tijdstip ({time}). Verplaatsen verwijdert het tijdstip en maakt vrije sortering mogelijk.', 'dayplan.confirmRemoveTimeAction': 'Tijd verwijderen en verplaatsen', + 'dayplan.confirmDeleteNoteTitle': 'Notitie verwijderen?', + 'dayplan.confirmDeleteNoteBody': 'Deze notitie wordt definitief verwijderd.', 'dayplan.cannotDropOnTimed': 'Items kunnen niet tussen tijdgebonden items worden geplaatst', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/nl/settings.ts b/shared/src/i18n/nl/settings.ts index 33340e8b..513de913 100644 --- a/shared/src/i18n/nl/settings.ts +++ b/shared/src/i18n/nl/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Temperatuureenheid', 'settings.timeFormat': 'Tijdnotatie', 'settings.blurBookingCodes': 'Boekingscodes vervagen', + 'settings.optimizeFromAccommodation': 'Route optimaliseren vanaf accommodatie', + 'settings.optimizeFromAccommodationHint': + 'Begin bij het optimaliseren van een dag de route bij het hotel waar je wakker wordt en eindig bij het hotel waar je die avond incheckt.', 'settings.notifications': 'Meldingen', 'settings.notifyTripInvite': 'Reisuitnodigingen', 'settings.notifyBookingChange': 'Boekingswijzigingen', diff --git a/shared/src/i18n/pl/collab.ts b/shared/src/i18n/pl/collab.ts index 7dfbf806..2982b8cf 100644 --- a/shared/src/i18n/pl/collab.ts +++ b/shared/src/i18n/pl/collab.ts @@ -40,6 +40,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Anuluj', 'collab.notes.edit': 'Edytuj', 'collab.notes.delete': 'Usuń', + 'collab.notes.confirmDeleteTitle': 'Usunąć notatkę?', + 'collab.notes.confirmDeleteBody': 'Ta notatka zostanie trwale usunięta.', 'collab.notes.pin': 'Przypnij', 'collab.notes.unpin': 'Odepnij', 'collab.notes.daysAgo': '{n}d temu', diff --git a/shared/src/i18n/pl/dayplan.ts b/shared/src/i18n/pl/dayplan.ts index 23fe1a49..96f67d2e 100644 --- a/shared/src/i18n/pl/dayplan.ts +++ b/shared/src/i18n/pl/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'To miejsce ma określoną godzinę ({time}). Przeniesienie go usunie godzinę i umożliwi swobodne sortowanie.', 'dayplan.confirmRemoveTimeAction': 'Usuń godzinę i przenieś', + 'dayplan.confirmDeleteNoteTitle': 'Usunąć notatkę?', + 'dayplan.confirmDeleteNoteBody': 'Ta notatka zostanie trwale usunięta.', 'dayplan.cannotDropOnTimed': 'Nie można umieszczać elementów pomiędzy wpisami z określoną godziną', 'dayplan.cannotBreakChronology': @@ -30,6 +32,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Potrzeba co najmniej dwóch miejsc, aby zoptymalizować trasę', 'dayplan.toast.routeOptimized': 'Trasa została zoptymalizowana', + 'dayplan.toast.routeOptimizedFromHotel': + 'Trasa została zoptymalizowana względem Twojego zakwaterowania', 'dayplan.toast.noGeoPlaces': 'Nie znaleziono miejsc ze współrzędnymi do obliczenia trasy', 'dayplan.confirmed': 'Potwierdzono', diff --git a/shared/src/i18n/pl/settings.ts b/shared/src/i18n/pl/settings.ts index e55c15c5..88e9a176 100644 --- a/shared/src/i18n/pl/settings.ts +++ b/shared/src/i18n/pl/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Jednostka temperatury', 'settings.timeFormat': 'Format czasu', 'settings.blurBookingCodes': 'Rozmyj kody rezerwacji', + 'settings.optimizeFromAccommodation': 'Optymalizuj trasę od zakwaterowania', + 'settings.optimizeFromAccommodationHint': + 'Przy optymalizacji dnia rozpocznij trasę w hotelu, w którym się budzisz, a zakończ ją w tym, do którego się zameldujesz tego wieczoru.', 'settings.notifications': 'Powiadomienia', 'settings.notifyTripInvite': 'Zaproszenia do podróży', 'settings.notifyBookingChange': 'Zmiany w rezerwacjach', diff --git a/shared/src/i18n/ru/collab.ts b/shared/src/i18n/ru/collab.ts index 6111dc9c..88ada7b9 100644 --- a/shared/src/i18n/ru/collab.ts +++ b/shared/src/i18n/ru/collab.ts @@ -41,6 +41,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Отмена', 'collab.notes.edit': 'Редактировать', 'collab.notes.delete': 'Удалить', + 'collab.notes.confirmDeleteTitle': 'Удалить заметку?', + 'collab.notes.confirmDeleteBody': 'Эта заметка будет удалена безвозвратно.', 'collab.notes.pin': 'Закрепить', 'collab.notes.unpin': 'Открепить', 'collab.notes.daysAgo': '{n} дн. назад', diff --git a/shared/src/i18n/ru/dayplan.ts b/shared/src/i18n/ru/dayplan.ts index 1a7c2644..b64d1308 100644 --- a/shared/src/i18n/ru/dayplan.ts +++ b/shared/src/i18n/ru/dayplan.ts @@ -20,6 +20,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Для оптимизации маршрута нужно минимум два места', 'dayplan.toast.routeOptimized': 'Маршрут оптимизирован', + 'dayplan.toast.routeOptimizedFromHotel': + 'Маршрут оптимизирован от места проживания', 'dayplan.toast.noGeoPlaces': 'Не найдено мест с координатами для расчёта маршрута', 'dayplan.confirmed': 'Подтверждено', @@ -33,6 +35,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'У этого места фиксированное время ({time}). При перемещении время будет удалено, и станет доступна свободная сортировка.', 'dayplan.confirmRemoveTimeAction': 'Удалить время и переместить', + 'dayplan.confirmDeleteNoteTitle': 'Удалить заметку?', + 'dayplan.confirmDeleteNoteBody': 'Эта заметка будет удалена безвозвратно.', 'dayplan.cannotDropOnTimed': 'Элементы нельзя размещать между записями с фиксированным временем', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/ru/settings.ts b/shared/src/i18n/ru/settings.ts index 7359ddcc..77ee1e1c 100644 --- a/shared/src/i18n/ru/settings.ts +++ b/shared/src/i18n/ru/settings.ts @@ -62,6 +62,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Единица температуры', 'settings.timeFormat': 'Формат времени', 'settings.blurBookingCodes': 'Скрыть коды бронирования', + 'settings.optimizeFromAccommodation': 'Оптимизировать маршрут от места проживания', + 'settings.optimizeFromAccommodationHint': + 'При оптимизации дня маршрут начинается от отеля, в котором вы просыпаетесь, и заканчивается у того, в который вы заселяетесь вечером.', 'settings.notifications': 'Уведомления', 'settings.notifyTripInvite': 'Приглашения в поездку', 'settings.notifyBookingChange': 'Изменения бронирований', diff --git a/shared/src/i18n/tr/collab.ts b/shared/src/i18n/tr/collab.ts index 56eec3b0..f5ce49d0 100644 --- a/shared/src/i18n/tr/collab.ts +++ b/shared/src/i18n/tr/collab.ts @@ -40,6 +40,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'İptal etmek', 'collab.notes.edit': 'Düzenle', 'collab.notes.delete': 'Sil', + 'collab.notes.confirmDeleteTitle': 'Not silinsin mi?', + 'collab.notes.confirmDeleteBody': 'Bu not kalıcı olarak silinecek.', 'collab.notes.pin': 'Sabitle', 'collab.notes.unpin': 'Sabitlemeyi kaldır', 'collab.notes.daysAgo': '{n} gün önce', diff --git a/shared/src/i18n/tr/dayplan.ts b/shared/src/i18n/tr/dayplan.ts index b991441c..4b108330 100644 --- a/shared/src/i18n/tr/dayplan.ts +++ b/shared/src/i18n/tr/dayplan.ts @@ -9,6 +9,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'Bu yerin sabit bir saati var ({time}). Taşımak saati kaldırır ve serbest sıralamaya izin verir.', 'dayplan.confirmRemoveTimeAction': 'Saati kaldır ve taşı', + 'dayplan.confirmDeleteNoteTitle': 'Not silinsin mi?', + 'dayplan.confirmDeleteNoteBody': 'Bu not kalıcı olarak silinecek.', 'dayplan.cannotDropOnTimed': 'Öğeler saate bağlı girişler arasına yerleştirilemez', 'dayplan.cannotBreakChronology': @@ -32,6 +34,8 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Rota optimizasyonu için en az iki yer gerekli', 'dayplan.toast.routeOptimized': 'Rota optimize edildi', + 'dayplan.toast.routeOptimizedFromHotel': + 'Rota konakladığınız yerden optimize edildi', 'dayplan.toast.noGeoPlaces': 'Rota için koordinatlı yer bulunamadı', 'dayplan.confirmed': 'Onaylandı', 'dayplan.pendingRes': 'Beklemede', diff --git a/shared/src/i18n/tr/settings.ts b/shared/src/i18n/tr/settings.ts index f5d0a9d9..c451741a 100644 --- a/shared/src/i18n/tr/settings.ts +++ b/shared/src/i18n/tr/settings.ts @@ -66,6 +66,9 @@ const settings: TranslationStrings = { 'settings.bookingLabelsHint': 'Haritada istasyon / havalimanı adlarını göster. Kapalıyken yalnızca simge görünür.', 'settings.blurBookingCodes': 'Rezervasyon Kodlarını Bulanıklaştır', + 'settings.optimizeFromAccommodation': 'Rotayı konaklamadan optimize et', + 'settings.optimizeFromAccommodationHint': + 'Bir günü optimize ederken rotaya o sabah uyandığınız otelden başlayın ve akşam giriş yaptığınız otelde sonlandırın.', 'settings.notifications': 'Bildirimler', 'settings.notifyTripInvite': 'Seyahat davetleri', 'settings.notifyBookingChange': 'Rezervasyon değişiklikleri', diff --git a/shared/src/i18n/uk/collab.ts b/shared/src/i18n/uk/collab.ts index 710342f9..97692b15 100644 --- a/shared/src/i18n/uk/collab.ts +++ b/shared/src/i18n/uk/collab.ts @@ -40,6 +40,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': 'Скасувати', 'collab.notes.edit': 'Редагувати', 'collab.notes.delete': 'Видалити', + 'collab.notes.confirmDeleteTitle': 'Видалити нотатку?', + 'collab.notes.confirmDeleteBody': 'Цю нотатку буде видалено назавжди.', 'collab.notes.pin': 'Закріпити', 'collab.notes.unpin': 'Відкріпити', 'collab.notes.daysAgo': '{n} дн. тому', diff --git a/shared/src/i18n/uk/dayplan.ts b/shared/src/i18n/uk/dayplan.ts index 8185dfb6..c5ef0afa 100644 --- a/shared/src/i18n/uk/dayplan.ts +++ b/shared/src/i18n/uk/dayplan.ts @@ -22,6 +22,7 @@ const dayplan: TranslationStrings = { 'dayplan.toast.needTwoPlaces': 'Для оптимизации маршрута нужно минимум два места', 'dayplan.toast.routeOptimized': 'Маршрут оптимизирован', + 'dayplan.toast.routeOptimizedFromHotel': 'Маршрут оптимізовано від вашого житла', 'dayplan.toast.noGeoPlaces': 'Не знайдено місць з координатами для розрахунку маршруту', 'dayplan.confirmed': 'Подтверждено', @@ -35,6 +36,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': 'У цього місця фіксований час ({time}). При переміщенні час буде видалено, і стане доступне вільне сортування.', 'dayplan.confirmRemoveTimeAction': 'Видалити час і перемістити', + 'dayplan.confirmDeleteNoteTitle': 'Видалити нотатку?', + 'dayplan.confirmDeleteNoteBody': 'Цю нотатку буде видалено назавжди.', 'dayplan.cannotDropOnTimed': 'Елементи не можна розміщувати між записами з фіксованим часом', 'dayplan.cannotBreakChronology': diff --git a/shared/src/i18n/uk/settings.ts b/shared/src/i18n/uk/settings.ts index 33b660fa..c205f48e 100644 --- a/shared/src/i18n/uk/settings.ts +++ b/shared/src/i18n/uk/settings.ts @@ -63,6 +63,9 @@ const settings: TranslationStrings = { 'settings.temperature': 'Одиниця температури', 'settings.timeFormat': 'Формат часу', 'settings.blurBookingCodes': 'Приховати коди бронювання', + 'settings.optimizeFromAccommodation': 'Оптимізувати маршрут від житла', + 'settings.optimizeFromAccommodationHint': + 'Під час оптимізації дня починайте маршрут від готелю, у якому ви прокидаєтеся, і завершуйте його тим, у який ви заселяєтеся ввечері.', 'settings.notifications': 'Сповіщення', 'settings.notifyTripInvite': 'Запрошення до поїздки', 'settings.notifyBookingChange': 'Зміни бронювань', diff --git a/shared/src/i18n/zh-TW/collab.ts b/shared/src/i18n/zh-TW/collab.ts index 6c782810..58df7797 100644 --- a/shared/src/i18n/zh-TW/collab.ts +++ b/shared/src/i18n/zh-TW/collab.ts @@ -39,6 +39,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': '取消', 'collab.notes.edit': '編輯', 'collab.notes.delete': '刪除', + 'collab.notes.confirmDeleteTitle': '刪除筆記?', + 'collab.notes.confirmDeleteBody': '此筆記將被永久刪除。', 'collab.notes.pin': '置頂', 'collab.notes.unpin': '取消置頂', 'collab.notes.daysAgo': '{n} 天前', diff --git a/shared/src/i18n/zh-TW/dayplan.ts b/shared/src/i18n/zh-TW/dayplan.ts index 7cfd4a47..fa5ecce9 100644 --- a/shared/src/i18n/zh-TW/dayplan.ts +++ b/shared/src/i18n/zh-TW/dayplan.ts @@ -19,6 +19,7 @@ const dayplan: TranslationStrings = { 'dayplan.routeError': '路線計算失敗', 'dayplan.toast.needTwoPlaces': '路線最佳化至少需要兩個地點', 'dayplan.toast.routeOptimized': '路線已最佳化', + 'dayplan.toast.routeOptimizedFromHotel': '已從你的住宿地點最佳化路線', 'dayplan.toast.noGeoPlaces': '未找到有座標的地點用於路線計算', 'dayplan.confirmed': '已確認', 'dayplan.pendingRes': '待確認', @@ -30,6 +31,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': '此地點有固定時間({time})。移動後將移除時間並允許自由排序。', 'dayplan.confirmRemoveTimeAction': '移除時間並移動', + 'dayplan.confirmDeleteNoteTitle': '刪除筆記?', + 'dayplan.confirmDeleteNoteBody': '此筆記將被永久刪除。', 'dayplan.cannotDropOnTimed': '無法將專案放置在有固定時間的條目之間', 'dayplan.cannotBreakChronology': '這將打亂已計劃專案和預訂的時間順序', 'dayplan.mobile.addPlace': '新增地點', diff --git a/shared/src/i18n/zh-TW/settings.ts b/shared/src/i18n/zh-TW/settings.ts index e0ee4895..d291b79c 100644 --- a/shared/src/i18n/zh-TW/settings.ts +++ b/shared/src/i18n/zh-TW/settings.ts @@ -59,6 +59,9 @@ const settings: TranslationStrings = { 'settings.temperature': '溫度單位', 'settings.timeFormat': '時間格式', 'settings.blurBookingCodes': '模糊預訂程式碼', + 'settings.optimizeFromAccommodation': '從住宿地點最佳化路線', + 'settings.optimizeFromAccommodationHint': + '最佳化某一天的行程時,路線從你早上起床的飯店出發,並在你當晚入住的飯店結束。', 'settings.notifications': '通知', 'settings.notifyTripInvite': '旅行邀請', 'settings.notifyBookingChange': '預訂變更', diff --git a/shared/src/i18n/zh/collab.ts b/shared/src/i18n/zh/collab.ts index b682f4c8..3ce9689e 100644 --- a/shared/src/i18n/zh/collab.ts +++ b/shared/src/i18n/zh/collab.ts @@ -39,6 +39,8 @@ const collab: TranslationStrings = { 'collab.notes.cancel': '取消', 'collab.notes.edit': '编辑', 'collab.notes.delete': '删除', + 'collab.notes.confirmDeleteTitle': '删除备注?', + 'collab.notes.confirmDeleteBody': '此备注将被永久删除。', 'collab.notes.pin': '置顶', 'collab.notes.unpin': '取消置顶', 'collab.notes.daysAgo': '{n} 天前', diff --git a/shared/src/i18n/zh/dayplan.ts b/shared/src/i18n/zh/dayplan.ts index 6a6a64f3..d09d5a1a 100644 --- a/shared/src/i18n/zh/dayplan.ts +++ b/shared/src/i18n/zh/dayplan.ts @@ -19,6 +19,7 @@ const dayplan: TranslationStrings = { 'dayplan.routeError': '路线计算失败', 'dayplan.toast.needTwoPlaces': '路线优化至少需要两个地点', 'dayplan.toast.routeOptimized': '路线已优化', + 'dayplan.toast.routeOptimizedFromHotel': '已根据您的住宿优化路线', 'dayplan.toast.noGeoPlaces': '未找到有坐标的地点用于路线计算', 'dayplan.confirmed': '已确认', 'dayplan.pendingRes': '待确认', @@ -30,6 +31,8 @@ const dayplan: TranslationStrings = { 'dayplan.confirmRemoveTimeBody': '此地点有固定时间({time})。移动后将移除时间并允许自由排序。', 'dayplan.confirmRemoveTimeAction': '移除时间并移动', + 'dayplan.confirmDeleteNoteTitle': '删除备注?', + 'dayplan.confirmDeleteNoteBody': '此备注将被永久删除。', 'dayplan.cannotDropOnTimed': '无法将项目放置在有固定时间的条目之间', 'dayplan.cannotBreakChronology': '这将打乱已计划项目和预订的时间顺序', 'dayplan.mobile.addPlace': '添加地点', diff --git a/shared/src/i18n/zh/settings.ts b/shared/src/i18n/zh/settings.ts index 82374cd1..960aef2c 100644 --- a/shared/src/i18n/zh/settings.ts +++ b/shared/src/i18n/zh/settings.ts @@ -59,6 +59,9 @@ const settings: TranslationStrings = { 'settings.temperature': '温度单位', 'settings.timeFormat': '时间格式', 'settings.blurBookingCodes': '模糊预订代码', + 'settings.optimizeFromAccommodation': '从住宿地优化路线', + 'settings.optimizeFromAccommodationHint': + '优化某一天时,路线将从您醒来时所在的酒店出发,并在当晚入住的酒店结束。', 'settings.notifications': '通知', 'settings.notifyTripInvite': '旅行邀请', 'settings.notifyBookingChange': '预订变更',