mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
0497032ed7
BREAKING: Reservations have been completely rebuilt. Existing place-level reservations are no longer used. All reservations must be re-created via the Bookings tab. Your trips, places, and other data are unaffected. Reservation System (rebuilt from scratch): - Reservations now link to specific day assignments instead of places - Same place on different days can have independent reservations - New assignment picker in booking modal (grouped by day, searchable) - Removed day/place dropdowns from booking form - Reservation badges in day plan sidebar with type-specific icons - Reservation details in place inspector (only for selected assignment) - Reservation summary in day detail panel Day Detail Panel (new): - Opens on day click in the sidebar - Detailed weather: hourly forecast, precipitation, wind, sunrise/sunset - Historical climate averages for dates beyond 16 days - Accommodation management with check-in/check-out, confirmation number - Hotel assignment across multiple days with day range picker - Reservation overview for the day Places: - Places can now be assigned to the same day multiple times - Start time + end time fields (replaces single time field) - Map badges show multiple position numbers (e.g. "1 · 4") - Route optimization fixed for duplicate places - File attachments during place editing (not just creation) - Cover image upload during trip creation (not just editing) - Paste support (Ctrl+V) for images in trip, place, and file forms Internationalization: - 200+ hardcoded German strings translated to i18n (EN + DE) - Server error messages in English - Category seeds in English for new installations - All planner, register, photo, packing components translated UI/UX: - Auto dark mode (follows system preference, configurable in settings) - Navbar toggle switches light/dark (overrides auto) - Sidebar minimize buttons z-index fixed - Transport mode selector removed from day plan - CustomSelect supports grouped headers (isHeader option) - Optimistic updates for day notes (instant feedback) - Booking cards redesigned with type-colored headers and structured details Weather: - Wind speed in mph when using Fahrenheit setting - Weather description language matches app language Admin: - Weather info panel replaces OpenWeatherMap key input - "Recommended" badge styling updated
136 lines
4.8 KiB
JavaScript
136 lines
4.8 KiB
JavaScript
const express = require('express');
|
|
const { db, canAccessTrip } = require('../db/database');
|
|
const { authenticate } = require('../middleware/auth');
|
|
const { broadcast } = require('../websocket');
|
|
|
|
const router = express.Router({ mergeParams: true });
|
|
|
|
function verifyTripOwnership(tripId, userId) {
|
|
return canAccessTrip(tripId, userId);
|
|
}
|
|
|
|
// GET /api/trips/:tripId/reservations
|
|
router.get('/', authenticate, (req, res) => {
|
|
const { tripId } = req.params;
|
|
|
|
const trip = verifyTripOwnership(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
|
|
|
const reservations = db.prepare(`
|
|
SELECT r.*, d.day_number, p.name as place_name, r.assignment_id
|
|
FROM reservations r
|
|
LEFT JOIN days d ON r.day_id = d.id
|
|
LEFT JOIN places p ON r.place_id = p.id
|
|
WHERE r.trip_id = ?
|
|
ORDER BY r.reservation_time ASC, r.created_at ASC
|
|
`).all(tripId);
|
|
|
|
res.json({ reservations });
|
|
});
|
|
|
|
// 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 trip = verifyTripOwnership(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
|
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
tripId,
|
|
day_id || null,
|
|
place_id || null,
|
|
assignment_id || null,
|
|
title,
|
|
reservation_time || null,
|
|
location || null,
|
|
confirmation_number || null,
|
|
notes || null,
|
|
status || 'pending',
|
|
type || 'other'
|
|
);
|
|
|
|
const reservation = db.prepare(`
|
|
SELECT r.*, d.day_number, p.name as place_name, r.assignment_id
|
|
FROM reservations r
|
|
LEFT JOIN days d ON r.day_id = d.id
|
|
LEFT JOIN places p ON r.place_id = p.id
|
|
WHERE r.id = ?
|
|
`).get(result.lastInsertRowid);
|
|
|
|
res.status(201).json({ reservation });
|
|
broadcast(tripId, 'reservation:created', { reservation }, req.headers['x-socket-id']);
|
|
});
|
|
|
|
// 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 trip = verifyTripOwnership(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
|
|
|
const reservation = db.prepare('SELECT * FROM reservations WHERE id = ? AND trip_id = ?').get(id, tripId);
|
|
if (!reservation) return res.status(404).json({ error: 'Reservation not found' });
|
|
|
|
db.prepare(`
|
|
UPDATE reservations SET
|
|
title = COALESCE(?, title),
|
|
reservation_time = ?,
|
|
location = ?,
|
|
confirmation_number = ?,
|
|
notes = ?,
|
|
day_id = ?,
|
|
place_id = ?,
|
|
assignment_id = ?,
|
|
status = COALESCE(?, status),
|
|
type = COALESCE(?, type)
|
|
WHERE id = ?
|
|
`).run(
|
|
title || null,
|
|
reservation_time !== undefined ? (reservation_time || null) : reservation.reservation_time,
|
|
location !== undefined ? (location || null) : reservation.location,
|
|
confirmation_number !== undefined ? (confirmation_number || null) : reservation.confirmation_number,
|
|
notes !== undefined ? (notes || null) : reservation.notes,
|
|
day_id !== undefined ? (day_id || null) : reservation.day_id,
|
|
place_id !== undefined ? (place_id || null) : reservation.place_id,
|
|
assignment_id !== undefined ? (assignment_id || null) : reservation.assignment_id,
|
|
status || null,
|
|
type || null,
|
|
id
|
|
);
|
|
|
|
const updated = db.prepare(`
|
|
SELECT r.*, d.day_number, p.name as place_name, r.assignment_id
|
|
FROM reservations r
|
|
LEFT JOIN days d ON r.day_id = d.id
|
|
LEFT JOIN places p ON r.place_id = p.id
|
|
WHERE r.id = ?
|
|
`).get(id);
|
|
|
|
res.json({ reservation: updated });
|
|
broadcast(tripId, 'reservation:updated', { reservation: updated }, req.headers['x-socket-id']);
|
|
});
|
|
|
|
// DELETE /api/trips/:tripId/reservations/:id
|
|
router.delete('/:id', authenticate, (req, res) => {
|
|
const { tripId, id } = req.params;
|
|
|
|
const trip = verifyTripOwnership(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
|
|
|
const reservation = db.prepare('SELECT id FROM reservations WHERE id = ? AND trip_id = ?').get(id, tripId);
|
|
if (!reservation) return res.status(404).json({ error: 'Reservation not found' });
|
|
|
|
db.prepare('DELETE FROM reservations WHERE id = ?').run(id);
|
|
res.json({ success: true });
|
|
broadcast(tripId, 'reservation:deleted', { reservationId: Number(id) }, req.headers['x-socket-id']);
|
|
});
|
|
|
|
module.exports = router;
|