From cb080954c972102aab8c9bebe6cf0cb421b23fce Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 26 Mar 2026 22:32:15 +0100 Subject: [PATCH] Reservation end time, route perf overhaul, assignment search fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add reservation_end_time field (DB migration, API, UI) - Split reservation form: separate date, start time, end time, status fields - Fix DateTimePicker forcing 00:00 when no time selected - Show end time across all reservation displays - Link-to-assignment and date on same row (50/50 layout) - Assignment search now shows day headers for filtered results - Auto-fill date when selecting a day assignment - Route segments: single OSRM request instead of N separate calls (~6s → ~1s) - Route labels visible from zoom level 12 (was 16) - Fix stale route labels after place deletion (useEffect triggers recalc) - AbortController cancels outdated route calculations --- client/package-lock.json | 4 +- client/src/components/Map/MapView.jsx | 4 +- client/src/components/Map/RouteCalculator.js | 34 ++++++- .../src/components/Planner/DayDetailPanel.jsx | 3 +- .../src/components/Planner/DayPlanSidebar.jsx | 3 +- .../src/components/Planner/PlaceInspector.jsx | 7 +- .../src/components/Planner/PlannerSidebar.jsx | 1 + .../components/Planner/ReservationModal.jsx | 92 +++++++++++++------ .../components/Planner/ReservationsPanel.jsx | 6 +- client/src/components/Planner/RightPanel.jsx | 1 + .../shared/CustomDateTimePicker.jsx | 2 +- client/src/components/shared/CustomSelect.jsx | 24 ++++- client/src/i18n/translations/de.js | 2 + client/src/i18n/translations/en.js | 2 + client/src/pages/TripPlannerPage.jsx | 55 +++++------ server/package-lock.json | 4 +- server/src/db/database.js | 5 + server/src/routes/reservations.js | 11 ++- 18 files changed, 181 insertions(+), 79 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 76cbe57..af87f94 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "nomad-client", - "version": "2.5.5", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nomad-client", - "version": "2.5.5", + "version": "2.6.0", "dependencies": { "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", diff --git a/client/src/components/Map/MapView.jsx b/client/src/components/Map/MapView.jsx index 4424c16..1bac23e 100644 --- a/client/src/components/Map/MapView.jsx +++ b/client/src/components/Map/MapView.jsx @@ -162,11 +162,11 @@ function MapClickHandler({ onClick }) { // ── Route travel time label ── function RouteLabel({ midpoint, walkingText, drivingText }) { const map = useMap() - const [visible, setVisible] = useState(map ? map.getZoom() >= 16 : false) + const [visible, setVisible] = useState(map ? map.getZoom() >= 12 : false) useEffect(() => { if (!map) return - const check = () => setVisible(map.getZoom() >= 16) + const check = () => setVisible(map.getZoom() >= 12) check() map.on('zoomend', check) return () => map.off('zoomend', check) diff --git a/client/src/components/Map/RouteCalculator.js b/client/src/components/Map/RouteCalculator.js index b0b5438..5afb1bf 100644 --- a/client/src/components/Map/RouteCalculator.js +++ b/client/src/components/Map/RouteCalculator.js @@ -7,7 +7,7 @@ const OSRM_BASE = 'https://router.project-osrm.org/route/v1' * @param {string} profile - 'driving' | 'walking' | 'cycling' * @returns {Promise<{coordinates: Array<[number,number]>, distance: number, duration: number, distanceText: string, durationText: string}>} */ -export async function calculateRoute(waypoints, profile = 'driving') { +export async function calculateRoute(waypoints, profile = 'driving', { signal } = {}) { if (!waypoints || waypoints.length < 2) { throw new Error('At least 2 waypoints required') } @@ -16,7 +16,7 @@ export async function calculateRoute(waypoints, profile = 'driving') { // OSRM public API only supports driving; we override duration for other modes const url = `${OSRM_BASE}/driving/${coords}?overview=full&geometries=geojson&steps=false` - const response = await fetch(url) + const response = await fetch(url, { signal }) if (!response.ok) { throw new Error('Route could not be calculated') } @@ -100,6 +100,36 @@ export function optimizeRoute(places) { return result } +/** + * Calculate per-leg travel times in a single OSRM request + * Returns array of { mid, walkingText, drivingText } for each leg + */ +export async function calculateSegments(waypoints, { signal } = {}) { + if (!waypoints || waypoints.length < 2) return [] + + const coords = waypoints.map(p => `${p.lng},${p.lat}`).join(';') + const url = `${OSRM_BASE}/driving/${coords}?overview=false&geometries=geojson&steps=false&annotations=distance,duration` + + const response = await fetch(url, { signal }) + if (!response.ok) throw new Error('Route could not be calculated') + + const data = await response.json() + if (data.code !== 'Ok' || !data.routes?.[0]) throw new Error('No route found') + + const legs = data.routes[0].legs + return legs.map((leg, i) => { + const from = [waypoints[i].lat, waypoints[i].lng] + const to = [waypoints[i + 1].lat, waypoints[i + 1].lng] + const mid = [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2] + const walkingDuration = leg.distance / (5000 / 3600) // 5 km/h + return { + mid, from, to, + walkingText: formatDuration(walkingDuration), + drivingText: formatDuration(leg.duration), + } + }) +} + function formatDistance(meters) { if (meters < 1000) { return `${Math.round(meters)} m` diff --git a/client/src/components/Planner/DayDetailPanel.jsx b/client/src/components/Planner/DayDetailPanel.jsx index 8cf215f..3b21cfc 100644 --- a/client/src/components/Planner/DayDetailPanel.jsx +++ b/client/src/components/Planner/DayDetailPanel.jsx @@ -248,9 +248,10 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri {r.title} {linkedAssignment?.place && · {linkedAssignment.place.name}} - {r.reservation_time && ( + {r.reservation_time?.includes('T') && ( {new Date(r.reservation_time).toLocaleTimeString(language === 'de' ? 'de-DE' : 'en-US', { hour: '2-digit', minute: '2-digit', hour12: is12h })} + {r.reservation_end_time && ` – ${fmtTime(r.reservation_end_time)}`} )} diff --git a/client/src/components/Planner/DayPlanSidebar.jsx b/client/src/components/Planner/DayPlanSidebar.jsx index d9fccfd..7ae7889 100644 --- a/client/src/components/Planner/DayPlanSidebar.jsx +++ b/client/src/components/Planner/DayPlanSidebar.jsx @@ -762,9 +762,10 @@ export default function DayPlanSidebar({ }}> {(() => { const RI = RES_ICONS[res.type] || Ticket; return })()} {confirmed ? t('planner.resConfirmed') : t('planner.resPending')} - {res.reservation_time && ( + {res.reservation_time?.includes('T') && ( {new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })} + {res.reservation_end_time && ` – ${res.reservation_end_time}`} )} diff --git a/client/src/components/Planner/PlaceInspector.jsx b/client/src/components/Planner/PlaceInspector.jsx index 8175c12..1ee8c99 100644 --- a/client/src/components/Planner/PlaceInspector.jsx +++ b/client/src/components/Planner/PlaceInspector.jsx @@ -347,10 +347,13 @@ export default function PlaceInspector({
{new Date(res.reservation_time).toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })}
)} - {res.reservation_time && ( + {res.reservation_time?.includes('T') && (
{t('reservations.time')}
-
{new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}
+
+ {new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })} + {res.reservation_end_time && ` – ${res.reservation_end_time}`} +
)} {res.confirmation_number && ( diff --git a/client/src/components/Planner/PlannerSidebar.jsx b/client/src/components/Planner/PlannerSidebar.jsx index a7e1185..c0d6ef6 100644 --- a/client/src/components/Planner/PlannerSidebar.jsx +++ b/client/src/components/Planner/PlannerSidebar.jsx @@ -804,6 +804,7 @@ export default function PlannerSidebar({
{formatDateTime(r.reservation_time)} + {r.reservation_end_time && ` – ${r.reservation_end_time}`}
)} {r.location &&
📍 {r.location}
} diff --git a/client/src/components/Planner/ReservationModal.jsx b/client/src/components/Planner/ReservationModal.jsx index 40405af..1115f81 100644 --- a/client/src/components/Planner/ReservationModal.jsx +++ b/client/src/components/Planner/ReservationModal.jsx @@ -4,7 +4,8 @@ import CustomSelect from '../shared/CustomSelect' import { Plane, Hotel, Utensils, Train, Car, Ship, Ticket, FileText, Users, Paperclip, X, ExternalLink, Link2 } from 'lucide-react' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' -import { CustomDateTimePicker } from '../shared/CustomDateTimePicker' +import { CustomDatePicker } from '../shared/CustomDateTimePicker' +import CustomTimePicker from '../shared/CustomTimePicker' const TYPE_OPTIONS = [ { value: 'flight', labelKey: 'reservations.type.flight', Icon: Plane }, @@ -25,8 +26,9 @@ function buildAssignmentOptions(days, assignments, t, locale) { if (da.length === 0) continue const dayLabel = day.title || t('dayplan.dayN', { n: day.day_number }) const dateStr = day.date ? ` · ${formatDate(day.date, locale)}` : '' + const groupLabel = `${dayLabel}${dateStr}` // Group header (non-selectable) - options.push({ value: `_header_${day.id}`, label: `${dayLabel}${dateStr}`, disabled: true, isHeader: true }) + options.push({ value: `_header_${day.id}`, label: groupLabel, disabled: true, isHeader: true }) for (let i = 0; i < da.length; i++) { const place = da[i].place if (!place) continue @@ -34,6 +36,9 @@ function buildAssignmentOptions(days, assignments, t, locale) { options.push({ value: da[i].id, label: ` ${i + 1}. ${place.name}${timeStr}`, + searchLabel: place.name, + groupLabel, + dayDate: day.date || null, }) } } @@ -66,6 +71,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p type: reservation.type || 'other', status: reservation.status || 'pending', reservation_time: reservation.reservation_time ? reservation.reservation_time.slice(0, 16) : '', + reservation_end_time: reservation.reservation_end_time || '', location: reservation.location || '', confirmation_number: reservation.confirmation_number || '', notes: reservation.notes || '', @@ -74,7 +80,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p } else { setForm({ title: '', type: 'other', status: 'pending', - reservation_time: '', location: '', confirmation_number: '', + reservation_time: '', reservation_end_time: '', location: '', confirmation_number: '', notes: '', assignment_id: '', }) setPendingFiles([]) @@ -169,34 +175,66 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p placeholder={t('reservations.titlePlaceholder')} style={inputStyle} /> - {/* Assignment Picker */} - {assignmentOptions.length > 0 && ( -
- - set('assignment_id', value)} - placeholder={t('reservations.pickAssignment')} - options={[ - { value: '', label: t('reservations.noAssignment') }, - ...assignmentOptions, - ]} - searchable - size="sm" + {/* Assignment Picker + Date */} +
+ {assignmentOptions.length > 0 && ( +
+ + { + set('assignment_id', value) + const opt = assignmentOptions.find(o => o.value === value) + if (opt?.dayDate) { + setForm(prev => { + if (prev.reservation_time) return prev + return { ...prev, reservation_time: opt.dayDate } + }) + } + }} + placeholder={t('reservations.pickAssignment')} + options={[ + { value: '', label: t('reservations.noAssignment') }, + ...assignmentOptions, + ]} + searchable + size="sm" + /> +
+ )} +
+ + { const [d] = (form.reservation_time || '').split('T'); return d || '' })()} + onChange={d => { + const [, t] = (form.reservation_time || '').split('T') + set('reservation_time', d ? (t ? `${d}T${t}` : d) : '') + }} />
- )} +
- {/* Date/Time + Status */} -
-
- - set('reservation_time', v)} /> + {/* Start Time + End Time + Status */} +
+
+ + { const [, t] = (form.reservation_time || '').split('T'); return t || '' })()} + onChange={t => { + const [d] = (form.reservation_time || '').split('T') + const date = d || new Date().toISOString().split('T')[0] + set('reservation_time', t ? `${date}T${t}` : date) + }} + />
-
+
+ + set('reservation_end_time', v)} /> +
+
{fmtDate(r.reservation_time)}
)} - {r.reservation_time && ( + {r.reservation_time?.includes('T') && (
{t('reservations.time')}
-
{fmtTime(r.reservation_time)}
+
+ {fmtTime(r.reservation_time)}{r.reservation_end_time ? ` – ${r.reservation_end_time}` : ''} +
)} {r.confirmation_number && ( diff --git a/client/src/components/Planner/RightPanel.jsx b/client/src/components/Planner/RightPanel.jsx index a66e33f..0facf3d 100644 --- a/client/src/components/Planner/RightPanel.jsx +++ b/client/src/components/Planner/RightPanel.jsx @@ -514,6 +514,7 @@ export function RightPanel({
{formatDateTime(reservation.reservation_time)} + {reservation.reservation_end_time && ` – ${reservation.reservation_end_time}`}
)} {reservation.location && ( diff --git a/client/src/components/shared/CustomDateTimePicker.jsx b/client/src/components/shared/CustomDateTimePicker.jsx index c3d3f91..5391d23 100644 --- a/client/src/components/shared/CustomDateTimePicker.jsx +++ b/client/src/components/shared/CustomDateTimePicker.jsx @@ -172,7 +172,7 @@ export function CustomDateTimePicker({ value, onChange, placeholder, style = {} } const handleTimeChange = (t) => { const d = datePart || new Date().toISOString().split('T')[0] - onChange(t ? `${d}T${t}` : `${d}T00:00`) + onChange(t ? `${d}T${t}` : d) } return ( diff --git a/client/src/components/shared/CustomSelect.jsx b/client/src/components/shared/CustomSelect.jsx index 9b0ad73..d6d25db 100644 --- a/client/src/components/shared/CustomSelect.jsx +++ b/client/src/components/shared/CustomSelect.jsx @@ -33,7 +33,29 @@ export default function CustomSelect({ const selected = options.find(o => o.value === value) const filtered = searchable && search - ? options.filter(o => o.label.toLowerCase().includes(search.toLowerCase())) + ? (() => { + const q = search.toLowerCase() + const result = [] + let currentHeader = null + let headerAdded = false + for (const o of options) { + if (o.isHeader) { + currentHeader = o + headerAdded = false + continue + } + // Match against label, searchLabel, or groupLabel + const haystack = [o.label, o.searchLabel, o.groupLabel].filter(Boolean).join(' ').toLowerCase() + if (haystack.includes(q)) { + if (currentHeader && !headerAdded) { + result.push(currentHeader) + headerAdded = true + } + result.push(o) + } + } + return result + })() : options const sm = size === 'sm' diff --git a/client/src/i18n/translations/de.js b/client/src/i18n/translations/de.js index 8086965..3d7618a 100644 --- a/client/src/i18n/translations/de.js +++ b/client/src/i18n/translations/de.js @@ -577,6 +577,8 @@ const de = { 'reservations.editTitle': 'Reservierung bearbeiten', 'reservations.status': 'Status', 'reservations.datetime': 'Datum & Uhrzeit', + 'reservations.startTime': 'Startzeit', + 'reservations.endTime': 'Endzeit', 'reservations.date': 'Datum', 'reservations.time': 'Uhrzeit', 'reservations.timeAlt': 'Uhrzeit (alternativ, z.B. 19:30)', diff --git a/client/src/i18n/translations/en.js b/client/src/i18n/translations/en.js index d0d055b..d700f0a 100644 --- a/client/src/i18n/translations/en.js +++ b/client/src/i18n/translations/en.js @@ -577,6 +577,8 @@ const en = { 'reservations.editTitle': 'Edit Reservation', 'reservations.status': 'Status', 'reservations.datetime': 'Date & Time', + 'reservations.startTime': 'Start time', + 'reservations.endTime': 'End time', 'reservations.date': 'Date', 'reservations.time': 'Time', 'reservations.timeAlt': 'Time (alternative, e.g. 19:30)', diff --git a/client/src/pages/TripPlannerPage.jsx b/client/src/pages/TripPlannerPage.jsx index dcbcf48..4149d11 100644 --- a/client/src/pages/TripPlannerPage.jsx +++ b/client/src/pages/TripPlannerPage.jsx @@ -23,7 +23,7 @@ import { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen import { useTranslation } from '../i18n' import { joinTrip, leaveTrip, addListener, removeListener } from '../api/websocket' import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi } from '../api/client' -import { calculateRoute } from '../components/Map/RouteCalculator' +import { calculateRoute, calculateSegments } from '../components/Map/RouteCalculator' import ConfirmDialog from '../components/shared/ConfirmDialog' const MIN_SIDEBAR = 200 @@ -188,31 +188,29 @@ export default function TripPlannerPage() { }, [places]) const routeCalcEnabled = useSettingsStore(s => s.settings.route_calculation) !== false + const routeAbortRef = useRef(null) const updateRouteForDay = useCallback(async (dayId) => { + // Abort any previous calculation + if (routeAbortRef.current) routeAbortRef.current.abort() + if (!dayId) { setRoute(null); setRouteSegments([]); return } const da = (tripStore.assignments[String(dayId)] || []).slice().sort((a, b) => a.order_index - b.order_index) const waypoints = da.map(a => a.place).filter(p => p?.lat && p?.lng) - if (waypoints.length >= 2) { - setRoute(waypoints.map(p => [p.lat, p.lng])) - if (!routeCalcEnabled) { setRouteSegments([]); return } - // Calculate per-segment travel times - const segments = [] - for (let i = 0; i < waypoints.length - 1; i++) { - const from = [waypoints[i].lat, waypoints[i].lng] - const to = [waypoints[i + 1].lat, waypoints[i + 1].lng] - const mid = [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2] - try { - const result = await calculateRoute([{ lat: from[0], lng: from[1] }, { lat: to[0], lng: to[1] }], 'walking') - segments.push({ mid, from, to, walkingText: result.walkingText, drivingText: result.drivingText }) - } catch { - segments.push({ mid, from, to, walkingText: '?', drivingText: '?' }) - } - } - setRouteSegments(segments) - } else { - setRoute(null) - setRouteSegments([]) + if (waypoints.length < 2) { setRoute(null); setRouteSegments([]); return } + + setRoute(waypoints.map(p => [p.lat, p.lng])) + if (!routeCalcEnabled) { setRouteSegments([]); return } + + // Single OSRM request for all segments + const controller = new AbortController() + routeAbortRef.current = controller + + try { + const segments = await calculateSegments(waypoints, { signal: controller.signal }) + if (!controller.signal.aborted) setRouteSegments(segments) + } catch (err) { + if (err.name !== 'AbortError') setRouteSegments([]) } }, [tripStore, routeCalcEnabled]) @@ -231,8 +229,7 @@ export default function TripPlannerPage() { setSelectedPlaceId(placeId) } if (placeId) { setShowDayDetail(null); setLeftCollapsed(false); setRightCollapsed(false) } - updateRouteForDay(selectedDayId) - }, [selectedDayId, updateRouteForDay, selectAssignment, setSelectedPlaceId]) + }, [selectAssignment, setSelectedPlaceId]) const handleMarkerClick = useCallback((placeId) => { const opening = placeId !== undefined @@ -366,16 +363,10 @@ export default function TripPlannerPage() { return map }, [selectedDayId, assignments]) - // Auto-update route when assignments change + // Auto-update route + segments when assignments change useEffect(() => { - if (!selectedDayId) return - const da = (assignments[String(selectedDayId)] || []).slice().sort((a, b) => a.order_index - b.order_index) - const waypoints = da.map(a => a.place).filter(p => p?.lat && p?.lng) - if (waypoints.length >= 2) { - setRoute(waypoints.map(p => [p.lat, p.lng])) - } else { - setRoute(null) - } + if (!selectedDayId) { setRoute(null); setRouteSegments([]); return } + updateRouteForDay(selectedDayId) }, [selectedDayId, assignments]) // Places assigned to selected day (with coords) — used for map fitting diff --git a/server/package-lock.json b/server/package-lock.json index ca32792..a1818ac 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "nomad-server", - "version": "2.5.5", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nomad-server", - "version": "2.5.5", + "version": "2.6.0", "dependencies": { "archiver": "^6.0.1", "bcryptjs": "^2.4.3", diff --git a/server/src/db/database.js b/server/src/db/database.js index c65f5f9..ad25e44 100644 --- a/server/src/db/database.js +++ b/server/src/db/database.js @@ -182,6 +182,7 @@ function initDb() { assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE SET NULL, title TEXT NOT NULL, reservation_time TEXT, + reservation_end_time TEXT, location TEXT, confirmation_number TEXT, notes TEXT, @@ -606,6 +607,10 @@ function initDb() { try { _db.exec('ALTER TABLE trip_files ADD COLUMN note_id INTEGER REFERENCES collab_notes(id) ON DELETE SET NULL'); } catch {} try { _db.exec('ALTER TABLE collab_notes ADD COLUMN website TEXT'); } catch {} }, + // 28: Add end_time to reservations + () => { + try { _db.exec('ALTER TABLE reservations ADD COLUMN reservation_end_time TEXT'); } catch {} + }, // Future migrations go here (append only, never reorder) ]; diff --git a/server/src/routes/reservations.js b/server/src/routes/reservations.js index 1708cba..d97bc5f 100644 --- a/server/src/routes/reservations.js +++ b/server/src/routes/reservations.js @@ -31,7 +31,7 @@ router.get('/', authenticate, (req, res) => { // POST /api/trips/:tripId/reservations router.post('/', authenticate, (req, res) => { const { tripId } = req.params; - const { title, reservation_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type } = req.body; + const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type } = req.body; const trip = verifyTripOwnership(tripId, req.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); @@ -39,8 +39,8 @@ router.post('/', authenticate, (req, res) => { if (!title) return res.status(400).json({ error: 'Title is required' }); const result = db.prepare(` - INSERT INTO reservations (trip_id, day_id, place_id, assignment_id, title, reservation_time, location, confirmation_number, notes, status, type) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO reservations (trip_id, day_id, place_id, assignment_id, title, reservation_time, reservation_end_time, location, confirmation_number, notes, status, type) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( tripId, day_id || null, @@ -48,6 +48,7 @@ router.post('/', authenticate, (req, res) => { assignment_id || null, title, reservation_time || null, + reservation_end_time || null, location || null, confirmation_number || null, notes || null, @@ -70,7 +71,7 @@ router.post('/', authenticate, (req, res) => { // PUT /api/trips/:tripId/reservations/:id router.put('/:id', authenticate, (req, res) => { const { tripId, id } = req.params; - const { title, reservation_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type } = req.body; + const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type } = req.body; const trip = verifyTripOwnership(tripId, req.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); @@ -82,6 +83,7 @@ router.put('/:id', authenticate, (req, res) => { UPDATE reservations SET title = COALESCE(?, title), reservation_time = ?, + reservation_end_time = ?, location = ?, confirmation_number = ?, notes = ?, @@ -94,6 +96,7 @@ router.put('/:id', authenticate, (req, res) => { `).run( title || null, reservation_time !== undefined ? (reservation_time || null) : reservation.reservation_time, + reservation_end_time !== undefined ? (reservation_end_time || null) : reservation.reservation_end_time, location !== undefined ? (location || null) : reservation.location, confirmation_number !== undefined ? (confirmation_number || null) : reservation.confirmation_number, notes !== undefined ? (notes || null) : reservation.notes,