diff --git a/client/src/components/Planner/PlacesSidebar.tsx b/client/src/components/Planner/PlacesSidebar.tsx index 17cbbe8..ef7db09 100644 --- a/client/src/components/Planner/PlacesSidebar.tsx +++ b/client/src/components/Planner/PlacesSidebar.tsx @@ -2,7 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom' import { useState, useRef, useMemo, useCallback } from 'react' import DOM from 'react-dom' -import { Search, Plus, X, CalendarDays, Pencil, Trash2, ExternalLink, Navigation, Upload, ChevronDown, Check, MapPin } from 'lucide-react' +import { Search, Plus, X, CalendarDays, Pencil, Trash2, ExternalLink, Navigation, Upload, ChevronDown, Check, MapPin, Eye } from 'lucide-react' import PlaceAvatar from '../shared/PlaceAvatar' import { getCategoryIcon } from '../shared/categoryIcons' import { useTranslation } from '../../i18n' @@ -92,6 +92,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({ } const [dayPickerPlace, setDayPickerPlace] = useState(null) const [catDropOpen, setCatDropOpen] = useState(false) + const [mobileShowDays, setMobileShowDays] = useState(false) // Alle geplanten Ort-IDs abrufen (einem Tag zugewiesen) const plannedIds = useMemo(() => new Set( @@ -286,7 +287,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({ window.__dragData = { placeId: String(place.id) } }} onClick={() => { - if (isMobile && days?.length > 0) { + if (isMobile) { setDayPickerPlace(place) } else { onPlaceClick(isSelected ? null : place.id) @@ -353,49 +354,75 @@ const PlacesSidebar = React.memo(function PlacesSidebar({ )} - {dayPickerPlace && days?.length > 0 && ReactDOM.createPortal( + {dayPickerPlace && ReactDOM.createPortal(
setDayPickerPlace(null)} + onClick={() => { setDayPickerPlace(null); setMobileShowDays(false) }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 99999, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }} >
e.stopPropagation()} - style={{ background: 'var(--bg-card)', borderRadius: '20px 20px 0 0', width: '100%', maxWidth: 500, maxHeight: '60vh', display: 'flex', flexDirection: 'column', overflow: 'hidden', paddingBottom: 'env(safe-area-inset-bottom)' }} + style={{ background: 'var(--bg-card)', borderRadius: '20px 20px 0 0', width: '100%', maxWidth: 500, maxHeight: '70vh', display: 'flex', flexDirection: 'column', overflow: 'hidden', paddingBottom: 'env(safe-area-inset-bottom)' }} >
{dayPickerPlace.name}
-
{t('places.assignToDay')}
+ {dayPickerPlace.address &&
{dayPickerPlace.address}
}
-
- {days.map((day, i) => { - return ( +
+ {/* View details */} + + {/* Edit */} + {canEditPlaces && ( + + )} + {/* Assign to day */} + {days?.length > 0 && ( + <> - ) - })} + {mobileShowDays && ( +
+ {days.map((day, i) => ( + + ))} +
+ )} + + )} + {/* Delete */} + {canEditPlaces && ( + + )}
, diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index cd3fe11..a68fe29 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -791,6 +791,7 @@ const ar: Record = { 'places.googleListHint': 'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.', 'places.googleListImported': 'تم استيراد {count} أماكن من "{list}"', 'places.googleListError': 'فشل استيراد قائمة Google Maps', + 'places.viewDetails': 'عرض التفاصيل', 'places.urlResolved': 'تم استيراد المكان من الرابط', 'places.assignToDay': 'إلى أي يوم تريد الإضافة؟', 'places.all': 'الكل', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index a0ba58d..044c39f 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -771,6 +771,7 @@ const br: Record = { 'places.googleListHint': 'Cole um link compartilhado de uma lista do Google Maps para importar todos os lugares.', 'places.googleListImported': '{count} lugares importados de "{list}"', 'places.googleListError': 'Falha ao importar lista do Google Maps', + 'places.viewDetails': 'Ver detalhes', 'places.urlResolved': 'Lugar importado da URL', 'places.assignToDay': 'Adicionar a qual dia?', 'places.all': 'Todos', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 4d23467..996edb4 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -792,6 +792,7 @@ const cs: Record = { 'places.googleListHint': 'Vložte sdílený odkaz na seznam Google Maps pro import všech míst.', 'places.googleListImported': '{count} míst importováno ze seznamu "{list}"', 'places.googleListError': 'Import seznamu Google Maps se nezdařil', + 'places.viewDetails': 'Zobrazit detaily', 'places.assignToDay': 'Přidat do kterého dne?', 'places.all': 'Vše', 'places.unplanned': 'Nezařazené', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index c2d13be..09139b3 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -790,6 +790,7 @@ const de: Record = { 'places.googleListHint': 'Geteilten Google Maps Listen-Link einfügen, um alle Orte zu importieren.', 'places.googleListImported': '{count} Orte aus "{list}" importiert', 'places.googleListError': 'Google Maps Liste konnte nicht importiert werden', + 'places.viewDetails': 'Details anzeigen', 'places.assignToDay': 'Zu welchem Tag hinzufügen?', 'places.all': 'Alle', 'places.unplanned': 'Ungeplant', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index e0b7087..2f2231f 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -786,6 +786,7 @@ const en: Record = { 'places.googleListHint': 'Paste a shared Google Maps list link to import all places.', 'places.googleListImported': '{count} places imported from "{list}"', 'places.googleListError': 'Failed to import Google Maps list', + 'places.viewDetails': 'View Details', 'places.assignToDay': 'Add to which day?', 'places.all': 'All', 'places.unplanned': 'Unplanned', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 102e343..be7cf01 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -765,6 +765,7 @@ const es: Record = { 'places.googleListHint': 'Pega un enlace compartido de una lista de Google Maps para importar todos los lugares.', 'places.googleListImported': '{count} lugares importados de "{list}"', 'places.googleListError': 'Error al importar la lista de Google Maps', + 'places.viewDetails': 'Ver detalles', 'places.urlResolved': 'Lugar importado desde URL', 'places.assignToDay': '¿A qué día añadirlo?', 'places.all': 'Todo', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 9bcb2d3..eb7d0ad 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -788,6 +788,7 @@ const fr: Record = { 'places.googleListHint': 'Collez un lien de liste Google Maps partagée pour importer tous les lieux.', 'places.googleListImported': '{count} lieux importés depuis "{list}"', 'places.googleListError': 'Impossible d\'importer la liste Google Maps', + 'places.viewDetails': 'Voir les détails', 'places.urlResolved': 'Lieu importé depuis l\'URL', 'places.assignToDay': 'Ajouter à quel jour ?', 'places.all': 'Tous', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index fc64a99..1cbb682 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -788,6 +788,7 @@ const hu: Record = { 'places.googleListHint': 'Illessz be egy megosztott Google Maps lista linket az osszes hely importalasahoz.', 'places.googleListImported': '{count} hely importalva a(z) "{list}" listabol', 'places.googleListError': 'Google Maps lista importalasa sikertelen', + 'places.viewDetails': 'Részletek megtekintése', 'places.assignToDay': 'Melyik naphoz adod?', 'places.all': 'Összes', 'places.unplanned': 'Nem tervezett', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 629db66..46fe748 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -788,6 +788,7 @@ const it: Record = { 'places.googleListHint': 'Incolla un link condiviso di una lista Google Maps per importare tutti i luoghi.', 'places.googleListImported': '{count} luoghi importati da "{list}"', 'places.googleListError': 'Importazione lista Google Maps non riuscita', + 'places.viewDetails': 'Visualizza dettagli', 'places.assignToDay': 'A quale giorno aggiungere?', 'places.all': 'Tutti', 'places.unplanned': 'Non pianificati', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index e87e4eb..c99fee7 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -788,6 +788,7 @@ const nl: Record = { 'places.googleListHint': 'Plak een gedeelde Google Maps lijstlink om alle plaatsen te importeren.', 'places.googleListImported': '{count} plaatsen geimporteerd uit "{list}"', 'places.googleListError': 'Google Maps lijst importeren mislukt', + 'places.viewDetails': 'Details bekijken', 'places.urlResolved': 'Plaats geïmporteerd van URL', 'places.assignToDay': 'Aan welke dag toevoegen?', 'places.all': 'Alle', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 20886f2..2ea67fa 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -788,6 +788,7 @@ const ru: Record = { 'places.googleListHint': 'Вставьте ссылку на общий список Google Maps для импорта всех мест.', 'places.googleListImported': '{count} мест импортировано из "{list}"', 'places.googleListError': 'Не удалось импортировать список Google Maps', + 'places.viewDetails': 'Подробности', 'places.urlResolved': 'Место импортировано из URL', 'places.assignToDay': 'Добавить в какой день?', 'places.all': 'Все', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index bd613cf..5fc6a1f 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -788,6 +788,7 @@ const zh: Record = { 'places.googleListHint': '粘贴共享的 Google Maps 列表链接以导入所有地点。', 'places.googleListImported': '已从"{list}"导入 {count} 个地点', 'places.googleListError': 'Google Maps 列表导入失败', + 'places.viewDetails': '查看详情', 'places.urlResolved': '已从 URL 导入地点', 'places.assignToDay': '添加到哪一天?', 'places.all': '全部', diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index eff748f..5bc9055 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -562,7 +562,7 @@ export default function TripPlannerPage(): React.ReactElement | null { ) })()} - {selectedPlace && ( + {selectedPlace && !isMobile && ( setSelectedPlaceId(null)} onEdit={() => { - // When editing from assignment context, use assignment-level times if (selectedAssignmentId) { const assignmentObj = Object.values(assignments).flat().find(a => a.id === selectedAssignmentId) const placeWithAssignmentTimes = assignmentObj?.place ? { ...selectedPlace, place_time: assignmentObj.place.place_time, end_time: assignmentObj.place.end_time } : selectedPlace @@ -609,6 +608,58 @@ export default function TripPlannerPage(): React.ReactElement | null { /> )} + {selectedPlace && isMobile && ReactDOM.createPortal( +
setSelectedPlaceId(null)}> +
e.stopPropagation()}> + setSelectedPlaceId(null)} + onEdit={() => { + if (selectedAssignmentId) { + const assignmentObj = Object.values(assignments).flat().find(a => a.id === selectedAssignmentId) + const placeWithAssignmentTimes = assignmentObj?.place ? { ...selectedPlace, place_time: assignmentObj.place.place_time, end_time: assignmentObj.place.end_time } : selectedPlace + setEditingPlace(placeWithAssignmentTimes) + } else { + setEditingPlace(selectedPlace) + } + setEditingAssignmentId(selectedAssignmentId || null) + setShowPlaceForm(true) + setSelectedPlaceId(null) + }} + onDelete={() => { handleDeletePlace(selectedPlace.id); setSelectedPlaceId(null) }} + onAssignToDay={handleAssignToDay} + onRemoveAssignment={handleRemoveAssignment} + files={files} + onFileUpload={canUploadFiles ? (fd) => tripStore.addFile(tripId, fd) : undefined} + tripMembers={tripMembers} + onSetParticipants={async (assignmentId, dayId, userIds) => { + try { + const data = await assignmentsApi.setParticipants(tripId, assignmentId, userIds) + useTripStore.setState(state => ({ + assignments: { + ...state.assignments, + [String(dayId)]: (state.assignments[String(dayId)] || []).map(a => + a.id === assignmentId ? { ...a, participants: data.participants } : a + ), + } + })) + } catch {} + }} + onUpdatePlace={async (placeId, data) => { try { await tripStore.updatePlace(tripId, placeId, data) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Unknown error') } }} + leftWidth={0} + rightWidth={0} + /> +
+
, + document.body + )} + {mobileSidebarOpen && ReactDOM.createPortal(
setMobileSidebarOpen(null)}>
e.stopPropagation()}> @@ -620,8 +671,8 @@ export default function TripPlannerPage(): React.ReactElement | null {
{mobileSidebarOpen === 'left' - ? { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={handlePlaceClick} onReorder={handleReorder} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute(r.coordinates); setRouteInfo({ distance: r.distanceText, duration: r.durationText }) } }} reservations={reservations} onAddReservation={(dayId) => { setEditingReservation(null); tripStore.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); setSelectedAssignmentId(null); setMobileSidebarOpen(null) }} accommodations={tripAccommodations} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} /> - : { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} days={days} isMobile onCategoryFilterChange={setMapCategoryFilter} /> + ? { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId); setMobileSidebarOpen(null) }} onReorder={handleReorder} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute(r.coordinates); setRouteInfo({ distance: r.distanceText, duration: r.durationText }) } }} reservations={reservations} onAddReservation={(dayId) => { setEditingReservation(null); tripStore.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); setSelectedAssignmentId(null); setMobileSidebarOpen(null) }} accommodations={tripAccommodations} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} /> + : { handlePlaceClick(placeId); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} onEditPlace={(place) => { setEditingPlace(place); setEditingAssignmentId(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} days={days} isMobile onCategoryFilterChange={setMapCategoryFilter} /> }