v2.5.7: Reservation overhaul, Day Detail Panel, i18n, paste support, auto dark mode

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
This commit is contained in:
Maurice
2026-03-24 20:10:45 +01:00
parent e4607e426c
commit 0497032ed7
67 changed files with 2390 additions and 1322 deletions

View File

@@ -107,6 +107,7 @@ function initDb() {
reservation_notes TEXT,
reservation_datetime TEXT,
place_time TEXT,
end_time TEXT,
duration_minutes INTEGER DEFAULT 60,
notes TEXT,
image_url TEXT,
@@ -130,6 +131,9 @@ function initDb() {
place_id INTEGER NOT NULL REFERENCES places(id) ON DELETE CASCADE,
order_index INTEGER DEFAULT 0,
notes TEXT,
reservation_status TEXT DEFAULT 'none',
reservation_notes TEXT,
reservation_datetime TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
@@ -175,6 +179,7 @@ function initDb() {
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
day_id INTEGER REFERENCES days(id) ON DELETE SET NULL,
place_id INTEGER REFERENCES places(id) ON DELETE SET NULL,
assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE SET NULL,
title TEXT NOT NULL,
reservation_time TEXT,
location TEXT,
@@ -213,7 +218,7 @@ function initDb() {
CREATE TABLE IF NOT EXISTS budget_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
category TEXT NOT NULL DEFAULT 'Sonstiges',
category TEXT NOT NULL DEFAULT 'Other',
name TEXT NOT NULL,
total_price REAL NOT NULL DEFAULT 0,
persons INTEGER DEFAULT NULL,
@@ -298,6 +303,19 @@ function initDb() {
note TEXT DEFAULT '',
UNIQUE(plan_id, date)
);
CREATE TABLE IF NOT EXISTS day_accommodations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
place_id INTEGER NOT NULL REFERENCES places(id) ON DELETE CASCADE,
start_day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
end_day_id INTEGER NOT NULL REFERENCES days(id) ON DELETE CASCADE,
check_in TEXT,
check_out TEXT,
confirmation TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// Create indexes for performance
@@ -318,6 +336,7 @@ function initDb() {
CREATE INDEX IF NOT EXISTS idx_day_notes_day_id ON day_notes(day_id);
CREATE INDEX IF NOT EXISTS idx_photos_trip_id ON photos(trip_id);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_day_accommodations_trip_id ON day_accommodations(trip_id);
`);
// Versioned migrations — each runs exactly once
@@ -369,7 +388,7 @@ function initDb() {
CREATE TABLE budget_items_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
category TEXT NOT NULL DEFAULT 'Sonstiges',
category TEXT NOT NULL DEFAULT 'Other',
name TEXT NOT NULL,
total_price REAL NOT NULL DEFAULT 0,
persons INTEGER DEFAULT NULL,
@@ -384,6 +403,41 @@ function initDb() {
`);
}
},
// 20: accommodation check-in/check-out/confirmation fields
() => {
try { _db.exec('ALTER TABLE day_accommodations ADD COLUMN check_in TEXT'); } catch {}
try { _db.exec('ALTER TABLE day_accommodations ADD COLUMN check_out TEXT'); } catch {}
try { _db.exec('ALTER TABLE day_accommodations ADD COLUMN confirmation TEXT'); } catch {}
},
// 21: places end_time field (place_time becomes start_time conceptually, end_time is new)
() => {
try { _db.exec('ALTER TABLE places ADD COLUMN end_time TEXT'); } catch {}
},
// 22: Move reservation fields from places to day_assignments
() => {
// Add new columns to day_assignments
try { _db.exec('ALTER TABLE day_assignments ADD COLUMN reservation_status TEXT DEFAULT \'none\''); } catch {}
try { _db.exec('ALTER TABLE day_assignments ADD COLUMN reservation_notes TEXT'); } catch {}
try { _db.exec('ALTER TABLE day_assignments ADD COLUMN reservation_datetime TEXT'); } catch {}
// Migrate existing data: copy reservation info from places to all their assignments
try {
_db.exec(`
UPDATE day_assignments SET
reservation_status = (SELECT reservation_status FROM places WHERE places.id = day_assignments.place_id),
reservation_notes = (SELECT reservation_notes FROM places WHERE places.id = day_assignments.place_id),
reservation_datetime = (SELECT reservation_datetime FROM places WHERE places.id = day_assignments.place_id)
WHERE place_id IN (SELECT id FROM places WHERE reservation_status IS NOT NULL AND reservation_status != 'none')
`);
console.log('[DB] Migrated reservation data from places to day_assignments');
} catch (e) {
console.error('[DB] Migration 22 data copy error:', e.message);
}
},
// 23: Add assignment_id to reservations table
() => {
try { _db.exec('ALTER TABLE reservations ADD COLUMN assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE SET NULL'); } catch {}
},
// Future migrations go here (append only, never reorder)
];
@@ -405,14 +459,14 @@ function initDb() {
const defaultCategories = [
{ name: 'Hotel', color: '#3b82f6', icon: '🏨' },
{ name: 'Restaurant', color: '#ef4444', icon: '🍽️' },
{ name: 'Sehenswürdigkeit', color: '#8b5cf6', icon: '🏛️' },
{ name: 'Attraction', color: '#8b5cf6', icon: '🏛️' },
{ name: 'Shopping', color: '#f59e0b', icon: '🛍️' },
{ name: 'Transport', color: '#6b7280', icon: '🚌' },
{ name: 'Aktivität', color: '#10b981', icon: '🎯' },
{ name: 'Bar/Café', color: '#f97316', icon: '☕' },
{ name: 'Strand', color: '#06b6d4', icon: '🏖️' },
{ name: 'Natur', color: '#84cc16', icon: '🌿' },
{ name: 'Sonstiges', color: '#6366f1', icon: '📍' },
{ name: 'Activity', color: '#10b981', icon: '🎯' },
{ name: 'Bar/Cafe', color: '#f97316', icon: '☕' },
{ name: 'Beach', color: '#06b6d4', icon: '🏖️' },
{ name: 'Nature', color: '#84cc16', icon: '🌿' },
{ name: 'Other', color: '#6366f1', icon: '📍' },
];
const insertCat = _db.prepare('INSERT INTO categories (name, color, icon) VALUES (?, ?, ?)');
for (const cat of defaultCategories) insertCat.run(cat.name, cat.color, cat.icon);

View File

@@ -57,6 +57,7 @@ app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
const authRoutes = require('./routes/auth');
const tripsRoutes = require('./routes/trips');
const daysRoutes = require('./routes/days');
const accommodationsRoutes = require('./routes/days').accommodationsRouter;
const placesRoutes = require('./routes/places');
const assignmentsRoutes = require('./routes/assignments');
const packingRoutes = require('./routes/packing');
@@ -77,6 +78,7 @@ app.use('/api/auth', authRoutes);
app.use('/api/auth/oidc', oidcRoutes);
app.use('/api/trips', tripsRoutes);
app.use('/api/trips/:tripId/days', daysRoutes);
app.use('/api/trips/:tripId/accommodations', accommodationsRoutes);
app.use('/api/trips/:tripId/places', placesRoutes);
app.use('/api/trips/:tripId/packing', packingRoutes);
app.use('/api/trips/:tripId/files', filesRoutes);

View File

@@ -30,18 +30,18 @@ router.post('/users', (req, res) => {
const { username, email, password, role } = req.body;
if (!username?.trim() || !email?.trim() || !password?.trim()) {
return res.status(400).json({ error: 'Benutzername, E-Mail und Passwort sind erforderlich' });
return res.status(400).json({ error: 'Username, email and password are required' });
}
if (role && !['user', 'admin'].includes(role)) {
return res.status(400).json({ error: 'Ungültige Rolle' });
return res.status(400).json({ error: 'Invalid role' });
}
const existingUsername = db.prepare('SELECT id FROM users WHERE username = ?').get(username.trim());
if (existingUsername) return res.status(409).json({ error: 'Benutzername bereits vergeben' });
if (existingUsername) return res.status(409).json({ error: 'Username already taken' });
const existingEmail = db.prepare('SELECT id FROM users WHERE email = ?').get(email.trim());
if (existingEmail) return res.status(409).json({ error: 'E-Mail bereits vergeben' });
if (existingEmail) return res.status(409).json({ error: 'Email already taken' });
const passwordHash = bcrypt.hashSync(password.trim(), 10);
@@ -61,19 +61,19 @@ router.put('/users/:id', (req, res) => {
const { username, email, role, password } = req.body;
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (!user) return res.status(404).json({ error: 'User not found' });
if (role && !['user', 'admin'].includes(role)) {
return res.status(400).json({ error: 'Ungültige Rolle' });
return res.status(400).json({ error: 'Invalid role' });
}
if (username && username !== user.username) {
const conflict = db.prepare('SELECT id FROM users WHERE username = ? AND id != ?').get(username, req.params.id);
if (conflict) return res.status(409).json({ error: 'Benutzername bereits vergeben' });
if (conflict) return res.status(409).json({ error: 'Username already taken' });
}
if (email && email !== user.email) {
const conflict = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?').get(email, req.params.id);
if (conflict) return res.status(409).json({ error: 'E-Mail bereits vergeben' });
if (conflict) return res.status(409).json({ error: 'Email already taken' });
}
const passwordHash = password ? bcrypt.hashSync(password, 10) : null;
@@ -98,11 +98,11 @@ router.put('/users/:id', (req, res) => {
// DELETE /api/admin/users/:id
router.delete('/users/:id', (req, res) => {
if (parseInt(req.params.id) === req.user.id) {
return res.status(400).json({ error: 'Eigenes Konto kann nicht gelöscht werden' });
return res.status(400).json({ error: 'Cannot delete own account' });
}
const user = db.prepare('SELECT id FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (!user) return res.status(404).json({ error: 'User not found' });
db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id);
res.json({ success: true });

View File

@@ -13,7 +13,7 @@ function getAssignmentWithPlace(assignmentId) {
const a = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.reservation_status, p.reservation_notes, p.reservation_datetime, p.place_time, p.duration_minutes, p.notes as place_notes,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -46,10 +46,8 @@ function getAssignmentWithPlace(assignmentId) {
category_id: a.category_id,
price: a.price,
currency: a.place_currency,
reservation_status: a.reservation_status,
reservation_notes: a.reservation_notes,
reservation_datetime: a.reservation_datetime,
place_time: a.place_time,
end_time: a.end_time,
duration_minutes: a.duration_minutes,
notes: a.place_notes,
image_url: a.image_url,
@@ -73,15 +71,15 @@ router.get('/trips/:tripId/days/:dayId/assignments', authenticate, (req, res) =>
const { tripId, dayId } = req.params;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const day = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId);
if (!day) return res.status(404).json({ error: 'Tag nicht gefunden' });
if (!day) return res.status(404).json({ error: 'Day not found' });
const assignments = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.reservation_status, p.reservation_notes, p.reservation_datetime, p.place_time, p.duration_minutes, p.notes as place_notes,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -124,9 +122,6 @@ router.get('/trips/:tripId/days/:dayId/assignments', authenticate, (req, res) =>
category_id: a.category_id,
price: a.price,
currency: a.place_currency,
reservation_status: a.reservation_status,
reservation_notes: a.reservation_notes,
reservation_datetime: a.reservation_datetime,
place_time: a.place_time,
duration_minutes: a.duration_minutes,
notes: a.place_notes,
@@ -155,16 +150,13 @@ router.post('/trips/:tripId/days/:dayId/assignments', authenticate, (req, res) =
const { place_id, notes } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const day = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId);
if (!day) return res.status(404).json({ error: 'Tag nicht gefunden' });
if (!day) return res.status(404).json({ error: 'Day not found' });
const place = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(place_id, tripId);
if (!place) return res.status(404).json({ error: 'Ort nicht gefunden' });
const existing = db.prepare('SELECT id FROM day_assignments WHERE day_id = ? AND place_id = ?').get(dayId, place_id);
if (existing) return res.status(409).json({ error: 'Ort ist bereits diesem Tag zugewiesen' });
if (!place) return res.status(404).json({ error: 'Place not found' });
const maxOrder = db.prepare('SELECT MAX(order_index) as max FROM day_assignments WHERE day_id = ?').get(dayId);
const orderIndex = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
@@ -183,13 +175,13 @@ router.delete('/trips/:tripId/days/:dayId/assignments/:id', authenticate, (req,
const { tripId, dayId, id } = req.params;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const assignment = db.prepare(
'SELECT da.id FROM day_assignments da JOIN days d ON da.day_id = d.id WHERE da.id = ? AND da.day_id = ? AND d.trip_id = ?'
).get(id, dayId, tripId);
if (!assignment) return res.status(404).json({ error: 'Zuweisung nicht gefunden' });
if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
db.prepare('DELETE FROM day_assignments WHERE id = ?').run(id);
res.json({ success: true });
@@ -202,10 +194,10 @@ router.put('/trips/:tripId/days/:dayId/assignments/reorder', authenticate, (req,
const { orderedIds } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const day = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId);
if (!day) return res.status(404).json({ error: 'Tag nicht gefunden' });
if (!day) return res.status(404).json({ error: 'Day not found' });
const update = db.prepare('UPDATE day_assignments SET order_index = ? WHERE id = ? AND day_id = ?');
db.exec('BEGIN');
@@ -228,7 +220,7 @@ router.put('/trips/:tripId/assignments/:id/move', authenticate, (req, res) => {
const { new_day_id, order_index } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const assignment = db.prepare(`
SELECT da.* FROM day_assignments da
@@ -236,10 +228,10 @@ router.put('/trips/:tripId/assignments/:id/move', authenticate, (req, res) => {
WHERE da.id = ? AND d.trip_id = ?
`).get(id, tripId);
if (!assignment) return res.status(404).json({ error: 'Zuweisung nicht gefunden' });
if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
const newDay = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(new_day_id, tripId);
if (!newDay) return res.status(404).json({ error: 'Zieltag nicht gefunden' });
if (!newDay) return res.status(404).json({ error: 'Target day not found' });
const oldDayId = assignment.day_id;
db.prepare('UPDATE day_assignments SET day_id = ?, order_index = ? WHERE id = ?').run(new_day_id, order_index || 0, id);

View File

@@ -145,7 +145,7 @@ router.post('/register', authLimiter, (req, res) => {
res.status(201).json({ token, user: { ...user, avatar_url: null } });
} catch (err) {
res.status(500).json({ error: 'Fehler beim Erstellen des Benutzers' });
res.status(500).json({ error: 'Error creating user' });
}
});
@@ -154,17 +154,17 @@ router.post('/login', authLimiter, (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' });
return res.status(400).json({ error: 'Email and password are required' });
}
const user = db.prepare('SELECT * FROM users WHERE LOWER(email) = LOWER(?)').get(email);
if (!user) {
return res.status(401).json({ error: 'Ungültige E-Mail oder Passwort' });
return res.status(401).json({ error: 'Invalid email or password' });
}
const validPassword = bcrypt.compareSync(password, user.password_hash);
if (!validPassword) {
return res.status(401).json({ error: 'Ungültige E-Mail oder Passwort' });
return res.status(401).json({ error: 'Invalid email or password' });
}
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(user.id);
@@ -181,7 +181,7 @@ router.get('/me', authenticate, (req, res) => {
).get(req.user.id);
if (!user) {
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
return res.status(404).json({ error: 'User not found' });
}
res.json({ user: { ...user, avatar_url: avatarUrl(user) } });

View File

@@ -48,7 +48,7 @@ router.get('/list', (req, res) => {
res.json({ backups: files });
} catch (err) {
res.status(500).json({ error: 'Fehler beim Laden der Backups' });
res.status(500).json({ error: 'Error loading backups' });
}
});
@@ -100,7 +100,7 @@ router.post('/create', async (req, res) => {
} catch (err) {
console.error('Backup error:', err);
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
res.status(500).json({ error: 'Fehler beim Erstellen des Backups' });
res.status(500).json({ error: 'Error creating backup' });
}
});
@@ -115,7 +115,7 @@ router.get('/download/:filename', (req, res) => {
const filePath = path.join(backupsDir, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Backup nicht gefunden' });
return res.status(404).json({ error: 'Backup not found' });
}
res.download(filePath, filename);
@@ -132,7 +132,7 @@ async function restoreFromZip(zipPath, res) {
const extractedDb = path.join(extractDir, 'travel.db');
if (!fs.existsSync(extractedDb)) {
fs.rmSync(extractDir, { recursive: true, force: true });
return res.status(400).json({ error: 'Ungültiges Backup: travel.db nicht gefunden' });
return res.status(400).json({ error: 'Invalid backup: travel.db not found' });
}
// Step 1: close DB connection BEFORE touching the file (required on Windows)
@@ -173,7 +173,7 @@ async function restoreFromZip(zipPath, res) {
} catch (err) {
console.error('Restore error:', err);
if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true });
if (!res.headersSent) res.status(500).json({ error: err.message || 'Fehler beim Wiederherstellen' });
if (!res.headersSent) res.status(500).json({ error: err.message || 'Error restoring backup' });
}
}
@@ -185,7 +185,7 @@ router.post('/restore/:filename', async (req, res) => {
}
const zipPath = path.join(backupsDir, filename);
if (!fs.existsSync(zipPath)) {
return res.status(404).json({ error: 'Backup nicht gefunden' });
return res.status(404).json({ error: 'Backup not found' });
}
await restoreFromZip(zipPath, res);
});
@@ -195,13 +195,13 @@ const uploadTmp = multer({
dest: path.join(dataDir, 'tmp/'),
fileFilter: (req, file, cb) => {
if (file.originalname.endsWith('.zip')) cb(null, true);
else cb(new Error('Nur ZIP-Dateien erlaubt'));
else cb(new Error('Only ZIP files allowed'));
},
limits: { fileSize: 500 * 1024 * 1024 },
});
router.post('/upload-restore', uploadTmp.single('backup'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'Keine Datei hochgeladen' });
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const zipPath = req.file.path;
await restoreFromZip(zipPath, res);
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
@@ -235,7 +235,7 @@ router.delete('/:filename', (req, res) => {
const filePath = path.join(backupsDir, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Backup nicht gefunden' });
return res.status(404).json({ error: 'Backup not found' });
}
fs.unlinkSync(filePath);

View File

@@ -14,7 +14,7 @@ router.get('/', authenticate, (req, res) => {
const { tripId } = req.params;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const items = db.prepare(
'SELECT * FROM budget_items WHERE trip_id = ? ORDER BY category ASC, created_at ASC'
@@ -29,9 +29,9 @@ router.post('/', authenticate, (req, res) => {
const { category, name, total_price, persons, days, note } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
if (!name) return res.status(400).json({ error: 'Name ist erforderlich' });
if (!name) return res.status(400).json({ error: 'Name is required' });
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM budget_items WHERE trip_id = ?').get(tripId);
const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
@@ -40,7 +40,7 @@ router.post('/', authenticate, (req, res) => {
'INSERT INTO budget_items (trip_id, category, name, total_price, persons, days, note, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
).run(
tripId,
category || 'Sonstiges',
category || 'Other',
name,
total_price || 0,
persons != null ? persons : null,
@@ -60,10 +60,10 @@ router.put('/:id', authenticate, (req, res) => {
const { category, name, total_price, persons, days, note, sort_order } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const item = db.prepare('SELECT * FROM budget_items WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!item) return res.status(404).json({ error: 'Budget-Eintrag nicht gefunden' });
if (!item) return res.status(404).json({ error: 'Budget item not found' });
db.prepare(`
UPDATE budget_items SET
@@ -96,10 +96,10 @@ 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: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const item = db.prepare('SELECT id FROM budget_items WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!item) return res.status(404).json({ error: 'Budget-Eintrag nicht gefunden' });
if (!item) return res.status(404).json({ error: 'Budget item not found' });
db.prepare('DELETE FROM budget_items WHERE id = ?').run(id);
res.json({ success: true });

View File

@@ -16,7 +16,7 @@ router.get('/', authenticate, (req, res) => {
router.post('/', authenticate, adminOnly, (req, res) => {
const { name, color, icon } = req.body;
if (!name) return res.status(400).json({ error: 'Kategoriename ist erforderlich' });
if (!name) return res.status(400).json({ error: 'Category name is required' });
const result = db.prepare(
'INSERT INTO categories (name, color, icon, user_id) VALUES (?, ?, ?, ?)'
@@ -31,7 +31,7 @@ router.put('/:id', authenticate, adminOnly, (req, res) => {
const { name, color, icon } = req.body;
const category = db.prepare('SELECT * FROM categories WHERE id = ?').get(req.params.id);
if (!category) return res.status(404).json({ error: 'Kategorie nicht gefunden' });
if (!category) return res.status(404).json({ error: 'Category not found' });
db.prepare(`
UPDATE categories SET
@@ -49,7 +49,7 @@ router.put('/:id', authenticate, adminOnly, (req, res) => {
router.delete('/:id', authenticate, adminOnly, (req, res) => {
const category = db.prepare('SELECT * FROM categories WHERE id = ?').get(req.params.id);
if (!category) return res.status(404).json({ error: 'Kategorie nicht gefunden' });
if (!category) return res.status(404).json({ error: 'Category not found' });
db.prepare('DELETE FROM categories WHERE id = ?').run(req.params.id);
res.json({ success: true });

View File

@@ -12,7 +12,7 @@ function verifyAccess(tripId, userId) {
// GET /api/trips/:tripId/days/:dayId/notes
router.get('/', authenticate, (req, res) => {
const { tripId, dayId } = req.params;
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Trip not found' });
const notes = db.prepare(
'SELECT * FROM day_notes WHERE day_id = ? AND trip_id = ? ORDER BY sort_order ASC, created_at ASC'
@@ -24,13 +24,13 @@ router.get('/', authenticate, (req, res) => {
// POST /api/trips/:tripId/days/:dayId/notes
router.post('/', authenticate, (req, res) => {
const { tripId, dayId } = req.params;
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Trip not found' });
const day = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId);
if (!day) return res.status(404).json({ error: 'Tag nicht gefunden' });
if (!day) return res.status(404).json({ error: 'Day not found' });
const { text, time, icon, sort_order } = req.body;
if (!text?.trim()) return res.status(400).json({ error: 'Text erforderlich' });
if (!text?.trim()) return res.status(400).json({ error: 'Text required' });
const result = db.prepare(
'INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order) VALUES (?, ?, ?, ?, ?, ?)'
@@ -44,10 +44,10 @@ router.post('/', authenticate, (req, res) => {
// PUT /api/trips/:tripId/days/:dayId/notes/:id
router.put('/:id', authenticate, (req, res) => {
const { tripId, dayId, id } = req.params;
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Trip not found' });
const note = db.prepare('SELECT * FROM day_notes WHERE id = ? AND day_id = ? AND trip_id = ?').get(id, dayId, tripId);
if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden' });
if (!note) return res.status(404).json({ error: 'Note not found' });
const { text, time, icon, sort_order } = req.body;
db.prepare(
@@ -68,10 +68,10 @@ router.put('/:id', authenticate, (req, res) => {
// DELETE /api/trips/:tripId/days/:dayId/notes/:id
router.delete('/:id', authenticate, (req, res) => {
const { tripId, dayId, id } = req.params;
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!verifyAccess(tripId, req.user.id)) return res.status(404).json({ error: 'Trip not found' });
const note = db.prepare('SELECT id FROM day_notes WHERE id = ? AND day_id = ? AND trip_id = ?').get(id, dayId, tripId);
if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden' });
if (!note) return res.status(404).json({ error: 'Note not found' });
db.prepare('DELETE FROM day_notes WHERE id = ?').run(id);
res.json({ success: true });

View File

@@ -13,7 +13,7 @@ function getAssignmentsForDay(dayId) {
const assignments = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.reservation_status, p.reservation_notes, p.reservation_datetime, p.place_time, p.duration_minutes, p.notes as place_notes,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -46,10 +46,8 @@ function getAssignmentsForDay(dayId) {
category_id: a.category_id,
price: a.price,
currency: a.place_currency,
reservation_status: a.reservation_status,
reservation_notes: a.reservation_notes,
reservation_datetime: a.reservation_datetime,
place_time: a.place_time,
end_time: a.end_time,
duration_minutes: a.duration_minutes,
notes: a.place_notes,
image_url: a.image_url,
@@ -75,7 +73,7 @@ router.get('/', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const days = db.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(tripId);
@@ -91,7 +89,7 @@ router.get('/', authenticate, (req, res) => {
const allAssignments = db.prepare(`
SELECT da.*, p.id as place_id, p.name as place_name, p.description as place_description,
p.lat, p.lng, p.address, p.category_id, p.price, p.currency as place_currency,
p.reservation_status, p.reservation_notes, p.reservation_datetime, p.place_time, p.duration_minutes, p.notes as place_notes,
p.place_time, p.end_time, p.duration_minutes, p.notes as place_notes,
p.image_url, p.transport_mode, p.google_place_id, p.website, p.phone,
c.name as category_name, c.color as category_color, c.icon as category_icon
FROM day_assignments da
@@ -137,10 +135,8 @@ router.get('/', authenticate, (req, res) => {
category_id: a.category_id,
price: a.price,
currency: a.place_currency,
reservation_status: a.reservation_status,
reservation_notes: a.reservation_notes,
reservation_datetime: a.reservation_datetime,
place_time: a.place_time,
end_time: a.end_time,
duration_minutes: a.duration_minutes,
notes: a.place_notes,
image_url: a.image_url,
@@ -184,7 +180,7 @@ router.post('/', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const { date, notes } = req.body;
@@ -209,12 +205,12 @@ router.put('/:id', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const day = db.prepare('SELECT * FROM days WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!day) {
return res.status(404).json({ error: 'Tag nicht gefunden' });
return res.status(404).json({ error: 'Day not found' });
}
const { notes, title } = req.body;
@@ -232,12 +228,12 @@ router.delete('/:id', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const day = db.prepare('SELECT * FROM days WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!day) {
return res.status(404).json({ error: 'Tag nicht gefunden' });
return res.status(404).json({ error: 'Day not found' });
}
db.prepare('DELETE FROM days WHERE id = ?').run(id);
@@ -245,4 +241,149 @@ router.delete('/:id', authenticate, (req, res) => {
broadcast(tripId, 'day:deleted', { dayId: Number(id) }, req.headers['x-socket-id']);
});
// === Accommodation routes ===
const accommodationsRouter = express.Router({ mergeParams: true });
function getAccommodationWithPlace(id) {
return db.prepare(`
SELECT a.*, p.name as place_name, p.address as place_address, p.image_url as place_image, p.lat as place_lat, p.lng as place_lng
FROM day_accommodations a
JOIN places p ON a.place_id = p.id
WHERE a.id = ?
`).get(id);
}
// GET /api/trips/:tripId/accommodations
accommodationsRouter.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 accommodations = db.prepare(`
SELECT a.*, p.name as place_name, p.address as place_address, p.image_url as place_image, p.lat as place_lat, p.lng as place_lng
FROM day_accommodations a
JOIN places p ON a.place_id = p.id
WHERE a.trip_id = ?
ORDER BY a.created_at ASC
`).all(tripId);
res.json({ accommodations });
});
// POST /api/trips/:tripId/accommodations
accommodationsRouter.post('/', 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 { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = req.body;
if (!place_id || !start_day_id || !end_day_id) {
return res.status(400).json({ error: 'place_id, start_day_id, and end_day_id are required' });
}
const place = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(place_id, tripId);
if (!place) {
return res.status(404).json({ error: 'Place not found' });
}
const startDay = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(start_day_id, tripId);
if (!startDay) {
return res.status(404).json({ error: 'Start day not found' });
}
const endDay = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(end_day_id, tripId);
if (!endDay) {
return res.status(404).json({ error: 'End day not found' });
}
const result = db.prepare(
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_out || null, confirmation || null, notes || null);
const accommodation = getAccommodationWithPlace(result.lastInsertRowid);
res.status(201).json({ accommodation });
broadcast(tripId, 'accommodation:created', { accommodation }, req.headers['x-socket-id']);
});
// PUT /api/trips/:tripId/accommodations/:id
accommodationsRouter.put('/: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 existing = db.prepare('SELECT * FROM day_accommodations WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!existing) {
return res.status(404).json({ error: 'Accommodation not found' });
}
const { place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes } = req.body;
const newPlaceId = place_id !== undefined ? place_id : existing.place_id;
const newStartDayId = start_day_id !== undefined ? start_day_id : existing.start_day_id;
const newEndDayId = end_day_id !== undefined ? end_day_id : existing.end_day_id;
const newCheckIn = check_in !== undefined ? check_in : existing.check_in;
const newCheckOut = check_out !== undefined ? check_out : existing.check_out;
const newConfirmation = confirmation !== undefined ? confirmation : existing.confirmation;
const newNotes = notes !== undefined ? notes : existing.notes;
if (place_id !== undefined) {
const place = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(place_id, tripId);
if (!place) {
return res.status(404).json({ error: 'Place not found' });
}
}
if (start_day_id !== undefined) {
const startDay = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(start_day_id, tripId);
if (!startDay) {
return res.status(404).json({ error: 'Start day not found' });
}
}
if (end_day_id !== undefined) {
const endDay = db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(end_day_id, tripId);
if (!endDay) {
return res.status(404).json({ error: 'End day not found' });
}
}
db.prepare(
'UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ?, check_in = ?, check_out = ?, confirmation = ?, notes = ? WHERE id = ?'
).run(newPlaceId, newStartDayId, newEndDayId, newCheckIn, newCheckOut, newConfirmation, newNotes, id);
const accommodation = getAccommodationWithPlace(id);
res.json({ accommodation });
broadcast(tripId, 'accommodation:updated', { accommodation }, req.headers['x-socket-id']);
});
// DELETE /api/trips/:tripId/accommodations/:id
accommodationsRouter.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 existing = db.prepare('SELECT * FROM day_accommodations WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!existing) {
return res.status(404).json({ error: 'Accommodation not found' });
}
db.prepare('DELETE FROM day_accommodations WHERE id = ?').run(id);
res.json({ success: true });
broadcast(tripId, 'accommodation:deleted', { accommodationId: Number(id) }, req.headers['x-socket-id']);
});
module.exports = router;
module.exports.accommodationsRouter = accommodationsRouter;

View File

@@ -43,7 +43,7 @@ const upload = multer({
if (allowed.includes(file.mimetype) || file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Dateityp nicht erlaubt'));
cb(new Error('File type not allowed'));
}
},
});
@@ -64,7 +64,7 @@ router.get('/', authenticate, (req, res) => {
const { tripId } = req.params;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const files = db.prepare(`
SELECT f.*, r.title as reservation_title
@@ -84,11 +84,11 @@ router.post('/', authenticate, demoUploadBlock, upload.single('file'), (req, res
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
if (req.file) fs.unlinkSync(req.file.path);
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
if (!req.file) {
return res.status(400).json({ error: 'Keine Datei hochgeladen' });
return res.status(400).json({ error: 'No file uploaded' });
}
const result = db.prepare(`
@@ -121,10 +121,10 @@ router.put('/:id', authenticate, (req, res) => {
const { description, place_id, reservation_id } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!file) return res.status(404).json({ error: 'Datei nicht gefunden' });
if (!file) return res.status(404).json({ error: 'File not found' });
db.prepare(`
UPDATE trip_files SET
@@ -154,10 +154,10 @@ 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: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!file) return res.status(404).json({ error: 'Datei nicht gefunden' });
if (!file) return res.status(404).json({ error: 'File not found' });
const filePath = path.join(filesDir, file.filename);
if (fs.existsSync(filePath)) {

View File

@@ -49,7 +49,7 @@ async function searchNominatim(query, lang) {
router.post('/search', authenticate, async (req, res) => {
const { query } = req.body;
if (!query) return res.status(400).json({ error: 'Suchanfrage ist erforderlich' });
if (!query) return res.status(400).json({ error: 'Search query is required' });
const apiKey = getMapsKey(req.user.id);
@@ -60,7 +60,7 @@ router.post('/search', authenticate, async (req, res) => {
return res.json({ places, source: 'openstreetmap' });
} catch (err) {
console.error('Nominatim search error:', err);
return res.status(500).json({ error: 'Fehler bei der OpenStreetMap Suche' });
return res.status(500).json({ error: 'OpenStreetMap search error' });
}
}
@@ -78,7 +78,7 @@ router.post('/search', authenticate, async (req, res) => {
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({ error: data.error?.message || 'Google Places API Fehler' });
return res.status(response.status).json({ error: data.error?.message || 'Google Places API error' });
}
const places = (data.places || []).map(p => ({
@@ -96,7 +96,7 @@ router.post('/search', authenticate, async (req, res) => {
res.json({ places, source: 'google' });
} catch (err) {
console.error('Maps search error:', err);
res.status(500).json({ error: 'Fehler bei der Google Places Suche' });
res.status(500).json({ error: 'Google Places search error' });
}
});
@@ -106,7 +106,7 @@ router.get('/details/:placeId', authenticate, async (req, res) => {
const apiKey = getMapsKey(req.user.id);
if (!apiKey) {
return res.status(400).json({ error: 'Google Maps API-Schlüssel nicht konfiguriert' });
return res.status(400).json({ error: 'Google Maps API key not configured' });
}
try {
@@ -122,7 +122,7 @@ router.get('/details/:placeId', authenticate, async (req, res) => {
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({ error: data.error?.message || 'Google Places API Fehler' });
return res.status(response.status).json({ error: data.error?.message || 'Google Places API error' });
}
const place = {
@@ -151,7 +151,7 @@ router.get('/details/:placeId', authenticate, async (req, res) => {
res.json({ place });
} catch (err) {
console.error('Maps details error:', err);
res.status(500).json({ error: 'Fehler beim Abrufen der Ortsdetails' });
res.status(500).json({ error: 'Error fetching place details' });
}
});
@@ -168,7 +168,7 @@ router.get('/place-photo/:placeId', authenticate, async (req, res) => {
const apiKey = getMapsKey(req.user.id);
if (!apiKey) {
return res.status(400).json({ error: 'Google Maps API-Schlüssel nicht konfiguriert' });
return res.status(400).json({ error: 'Google Maps API key not configured' });
}
try {
@@ -183,11 +183,11 @@ router.get('/place-photo/:placeId', authenticate, async (req, res) => {
if (!detailsRes.ok) {
console.error('Google Places photo details error:', details.error?.message || detailsRes.status);
return res.status(404).json({ error: 'Foto konnte nicht abgerufen werden' });
return res.status(404).json({ error: 'Photo could not be retrieved' });
}
if (!details.photos?.length) {
return res.status(404).json({ error: 'Kein Foto verfügbar' });
return res.status(404).json({ error: 'No photo available' });
}
const photo = details.photos[0];
@@ -202,7 +202,7 @@ router.get('/place-photo/:placeId', authenticate, async (req, res) => {
const photoUrl = mediaData.photoUri;
if (!photoUrl) {
return res.status(404).json({ error: 'Foto-URL nicht verfügbar' });
return res.status(404).json({ error: 'Photo URL not available' });
}
photoCache.set(placeId, { photoUrl, attribution, fetchedAt: Date.now() });
@@ -220,7 +220,7 @@ router.get('/place-photo/:placeId', authenticate, async (req, res) => {
res.json({ photoUrl, attribution });
} catch (err) {
console.error('Place photo error:', err);
res.status(500).json({ error: 'Fehler beim Abrufen des Fotos' });
res.status(500).json({ error: 'Error fetching photo' });
}
});

View File

@@ -14,7 +14,7 @@ router.get('/', authenticate, (req, res) => {
const { tripId } = req.params;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const items = db.prepare(
'SELECT * FROM packing_items WHERE trip_id = ? ORDER BY sort_order ASC, created_at ASC'
@@ -29,9 +29,9 @@ router.post('/', authenticate, (req, res) => {
const { name, category, checked } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
if (!name) return res.status(400).json({ error: 'Artikelname ist erforderlich' });
if (!name) return res.status(400).json({ error: 'Item name is required' });
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId);
const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
@@ -51,10 +51,10 @@ router.put('/:id', authenticate, (req, res) => {
const { name, checked, category } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const item = db.prepare('SELECT * FROM packing_items WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!item) return res.status(404).json({ error: 'Artikel nicht gefunden' });
if (!item) return res.status(404).json({ error: 'Item not found' });
db.prepare(`
UPDATE packing_items SET
@@ -80,10 +80,10 @@ 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: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const item = db.prepare('SELECT id FROM packing_items WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!item) return res.status(404).json({ error: 'Artikel nicht gefunden' });
if (!item) return res.status(404).json({ error: 'Item not found' });
db.prepare('DELETE FROM packing_items WHERE id = ?').run(id);
res.json({ success: true });
@@ -96,7 +96,7 @@ router.put('/reorder', authenticate, (req, res) => {
const { orderedIds } = req.body;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const update = db.prepare('UPDATE packing_items SET sort_order = ? WHERE id = ? AND trip_id = ?');
const updateMany = db.transaction((ids) => {

View File

@@ -48,7 +48,7 @@ router.get('/', authenticate, (req, res) => {
const { day_id, place_id } = req.query;
const trip = canAccessTrip(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
let query = 'SELECT * FROM photos WHERE trip_id = ?';
const params = [tripId];
@@ -78,11 +78,11 @@ router.post('/', authenticate, demoUploadBlock, upload.array('photos', 20), (req
if (!trip) {
// Delete uploaded files on auth failure
if (req.files) req.files.forEach(f => fs.unlinkSync(f.path));
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Keine Dateien hochgeladen' });
return res.status(400).json({ error: 'No files uploaded' });
}
const insertPhoto = db.prepare(`
@@ -122,10 +122,10 @@ router.put('/:id', authenticate, (req, res) => {
const { caption, day_id, place_id } = req.body;
const trip = canAccessTrip(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const photo = db.prepare('SELECT * FROM photos WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!photo) return res.status(404).json({ error: 'Foto nicht gefunden' });
if (!photo) return res.status(404).json({ error: 'Photo not found' });
db.prepare(`
UPDATE photos SET
@@ -149,10 +149,10 @@ router.delete('/:id', authenticate, (req, res) => {
const { tripId, id } = req.params;
const trip = canAccessTrip(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
const photo = db.prepare('SELECT * FROM photos WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!photo) return res.status(404).json({ error: 'Foto nicht gefunden' });
if (!photo) return res.status(404).json({ error: 'Photo not found' });
// Delete file
const filePath = path.join(photosDir, photo.filename);

View File

@@ -17,7 +17,7 @@ router.get('/', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
let query = `
@@ -89,30 +89,29 @@ router.post('/', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const {
name, description, lat, lng, address, category_id, price, currency,
reservation_status, reservation_notes, reservation_datetime, place_time,
place_time, end_time,
duration_minutes, notes, image_url, google_place_id, website, phone,
transport_mode, tags = []
} = req.body;
if (!name) {
return res.status(400).json({ error: 'Ortsname ist erforderlich' });
return res.status(400).json({ error: 'Place name is required' });
}
const result = db.prepare(`
INSERT INTO places (trip_id, name, description, lat, lng, address, category_id, price, currency,
reservation_status, reservation_notes, reservation_datetime, place_time,
place_time, end_time,
duration_minutes, notes, image_url, google_place_id, website, phone, transport_mode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
tripId, name, description || null, lat || null, lng || null, address || null,
category_id || null, price || null, currency || null,
reservation_status || 'none', reservation_notes || null, reservation_datetime || null,
place_time || null, duration_minutes || 60, notes || null, image_url || null,
place_time || null, end_time || null, duration_minutes || 60, notes || null, image_url || null,
google_place_id || null, website || null, phone || null, transport_mode || 'walking'
);
@@ -136,12 +135,12 @@ router.get('/:id', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const placeCheck = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!placeCheck) {
return res.status(404).json({ error: 'Ort nicht gefunden' });
return res.status(404).json({ error: 'Place not found' });
}
const place = getPlaceWithTags(id);
@@ -154,17 +153,17 @@ router.get('/:id/image', authenticate, async (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const place = db.prepare('SELECT * FROM places WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!place) {
return res.status(404).json({ error: 'Ort nicht gefunden' });
return res.status(404).json({ error: 'Place not found' });
}
const user = db.prepare('SELECT unsplash_api_key FROM users WHERE id = ?').get(req.user.id);
if (!user || !user.unsplash_api_key) {
return res.status(400).json({ error: 'Kein Unsplash API-Schlüssel konfiguriert' });
return res.status(400).json({ error: 'No Unsplash API key configured' });
}
try {
@@ -175,7 +174,7 @@ router.get('/:id/image', authenticate, async (req, res) => {
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({ error: data.errors?.[0] || 'Unsplash API Fehler' });
return res.status(response.status).json({ error: data.errors?.[0] || 'Unsplash API error' });
}
const photos = (data.results || []).map(p => ({
@@ -190,7 +189,7 @@ router.get('/:id/image', authenticate, async (req, res) => {
res.json({ photos });
} catch (err) {
console.error('Unsplash error:', err);
res.status(500).json({ error: 'Fehler beim Suchen des Bildes' });
res.status(500).json({ error: 'Error searching for image' });
}
});
@@ -200,17 +199,17 @@ router.put('/:id', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const existingPlace = db.prepare('SELECT * FROM places WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!existingPlace) {
return res.status(404).json({ error: 'Ort nicht gefunden' });
return res.status(404).json({ error: 'Place not found' });
}
const {
name, description, lat, lng, address, category_id, price, currency,
reservation_status, reservation_notes, reservation_datetime, place_time,
place_time, end_time,
duration_minutes, notes, image_url, google_place_id, website, phone,
transport_mode, tags
} = req.body;
@@ -225,10 +224,8 @@ router.put('/:id', authenticate, (req, res) => {
category_id = ?,
price = ?,
currency = COALESCE(?, currency),
reservation_status = COALESCE(?, reservation_status),
reservation_notes = ?,
reservation_datetime = ?,
place_time = ?,
end_time = ?,
duration_minutes = COALESCE(?, duration_minutes),
notes = ?,
image_url = ?,
@@ -247,10 +244,8 @@ router.put('/:id', authenticate, (req, res) => {
category_id !== undefined ? category_id : existingPlace.category_id,
price !== undefined ? price : existingPlace.price,
currency || null,
reservation_status || null,
reservation_notes !== undefined ? reservation_notes : existingPlace.reservation_notes,
reservation_datetime !== undefined ? reservation_datetime : existingPlace.reservation_datetime,
place_time !== undefined ? place_time : existingPlace.place_time,
end_time !== undefined ? end_time : existingPlace.end_time,
duration_minutes || null,
notes !== undefined ? notes : existingPlace.notes,
image_url !== undefined ? image_url : existingPlace.image_url,
@@ -282,12 +277,12 @@ router.delete('/:id', authenticate, (req, res) => {
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) {
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
}
const place = db.prepare('SELECT id FROM places WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!place) {
return res.status(404).json({ error: 'Ort nicht gefunden' });
return res.status(404).json({ error: 'Place not found' });
}
db.prepare('DELETE FROM places WHERE id = ?').run(id);

View File

@@ -14,10 +14,10 @@ router.get('/', authenticate, (req, res) => {
const { tripId } = req.params;
const trip = verifyTripOwnership(tripId, req.user.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
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
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
@@ -31,20 +31,21 @@ 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, status, type } = req.body;
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: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
if (!title) return res.status(400).json({ error: 'Titel ist erforderlich' });
if (!title) return res.status(400).json({ error: 'Title is required' });
const result = db.prepare(`
INSERT INTO reservations (trip_id, day_id, place_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, location, confirmation_number, notes, status, type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
tripId,
day_id || null,
place_id || null,
assignment_id || null,
title,
reservation_time || null,
location || null,
@@ -55,7 +56,7 @@ router.post('/', authenticate, (req, res) => {
);
const reservation = db.prepare(`
SELECT r.*, d.day_number, p.name as place_name
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
@@ -69,13 +70,13 @@ 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, status, type } = req.body;
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: 'Reise nicht gefunden' });
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: 'Reservierung nicht gefunden' });
if (!reservation) return res.status(404).json({ error: 'Reservation not found' });
db.prepare(`
UPDATE reservations SET
@@ -86,6 +87,7 @@ router.put('/:id', authenticate, (req, res) => {
notes = ?,
day_id = ?,
place_id = ?,
assignment_id = ?,
status = COALESCE(?, status),
type = COALESCE(?, type)
WHERE id = ?
@@ -97,13 +99,14 @@ router.put('/:id', authenticate, (req, res) => {
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
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
@@ -119,10 +122,10 @@ 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: 'Reise nicht gefunden' });
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: 'Reservierung nicht gefunden' });
if (!reservation) return res.status(404).json({ error: 'Reservation not found' });
db.prepare('DELETE FROM reservations WHERE id = ?').run(id);
res.json({ success: true });

View File

@@ -22,7 +22,7 @@ router.get('/', authenticate, (req, res) => {
router.put('/', authenticate, (req, res) => {
const { key, value } = req.body;
if (!key) return res.status(400).json({ error: 'Schlüssel ist erforderlich' });
if (!key) return res.status(400).json({ error: 'Key is required' });
const serialized = typeof value === 'object' ? JSON.stringify(value) : String(value !== undefined ? value : '');
@@ -39,7 +39,7 @@ router.post('/bulk', authenticate, (req, res) => {
const { settings } = req.body;
if (!settings || typeof settings !== 'object') {
return res.status(400).json({ error: 'Einstellungen-Objekt ist erforderlich' });
return res.status(400).json({ error: 'Settings object is required' });
}
const upsert = db.prepare(`
@@ -56,7 +56,7 @@ router.post('/bulk', authenticate, (req, res) => {
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
return res.status(500).json({ error: 'Fehler beim Speichern der Einstellungen', detail: err.message });
return res.status(500).json({ error: 'Error saving settings', detail: err.message });
}
res.json({ success: true, updated: Object.keys(settings).length });

View File

@@ -16,7 +16,7 @@ router.get('/', authenticate, (req, res) => {
router.post('/', authenticate, (req, res) => {
const { name, color } = req.body;
if (!name) return res.status(400).json({ error: 'Tag-Name ist erforderlich' });
if (!name) return res.status(400).json({ error: 'Tag name is required' });
const result = db.prepare(
'INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)'
@@ -31,7 +31,7 @@ router.put('/:id', authenticate, (req, res) => {
const { name, color } = req.body;
const tag = db.prepare('SELECT * FROM tags WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id);
if (!tag) return res.status(404).json({ error: 'Tag nicht gefunden' });
if (!tag) return res.status(404).json({ error: 'Tag not found' });
db.prepare('UPDATE tags SET name = COALESCE(?, name), color = COALESCE(?, color) WHERE id = ?')
.run(name || null, color || null, req.params.id);
@@ -43,7 +43,7 @@ router.put('/:id', authenticate, (req, res) => {
// DELETE /api/tags/:id
router.delete('/:id', authenticate, (req, res) => {
const tag = db.prepare('SELECT * FROM tags WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id);
if (!tag) return res.status(404).json({ error: 'Tag nicht gefunden' });
if (!tag) return res.status(404).json({ error: 'Tag not found' });
db.prepare('DELETE FROM tags WHERE id = ?').run(req.params.id);
res.json({ success: true });

View File

@@ -79,9 +79,9 @@ router.get('/', authenticate, (req, res) => {
// POST /api/trips
router.post('/', authenticate, (req, res) => {
const { title, description, start_date, end_date, currency } = req.body;
if (!title) return res.status(400).json({ error: 'Titel ist erforderlich' });
if (!title) return res.status(400).json({ error: 'Title is required' });
if (start_date && end_date && new Date(end_date) < new Date(start_date))
return res.status(400).json({ error: 'Enddatum muss nach dem Startdatum liegen' });
return res.status(400).json({ error: 'End date must be after start date' });
const result = db.prepare(`
INSERT INTO trips (user_id, title, description, start_date, end_date, currency)
@@ -102,24 +102,24 @@ router.get('/:id', authenticate, (req, res) => {
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
WHERE t.id = :tripId AND (t.user_id = :userId OR m.user_id IS NOT NULL)
`).get({ userId, tripId: req.params.id });
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
res.json({ trip });
});
// PUT /api/trips/:id — all members can edit; archive/cover owner-only
router.put('/:id', authenticate, (req, res) => {
const access = canAccessTrip(req.params.id, req.user.id);
if (!access) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!access) return res.status(404).json({ error: 'Trip not found' });
const ownerOnly = req.body.is_archived !== undefined || req.body.cover_image !== undefined;
if (ownerOnly && !isOwner(req.params.id, req.user.id))
return res.status(403).json({ error: 'Nur der Eigentümer kann diese Einstellung ändern' });
return res.status(403).json({ error: 'Only the owner can change this setting' });
const trip = db.prepare('SELECT * FROM trips WHERE id = ?').get(req.params.id);
const { title, description, start_date, end_date, currency, is_archived, cover_image } = req.body;
if (start_date && end_date && new Date(end_date) < new Date(start_date))
return res.status(400).json({ error: 'Enddatum muss nach dem Startdatum liegen' });
return res.status(400).json({ error: 'End date must be after start date' });
const newTitle = title || trip.title;
const newDesc = description !== undefined ? description : trip.description;
@@ -146,11 +146,11 @@ router.put('/:id', authenticate, (req, res) => {
// POST /api/trips/:id/cover
router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cover'), (req, res) => {
if (!isOwner(req.params.id, req.user.id))
return res.status(403).json({ error: 'Nur der Eigentümer kann das Titelbild ändern' });
return res.status(403).json({ error: 'Only the owner can change the cover image' });
const trip = db.prepare('SELECT * FROM trips WHERE id = ?').get(req.params.id);
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
if (!req.file) return res.status(400).json({ error: 'Kein Bild hochgeladen' });
if (!trip) return res.status(404).json({ error: 'Trip not found' });
if (!req.file) return res.status(400).json({ error: 'No image uploaded' });
if (trip.cover_image) {
const oldPath = path.join(__dirname, '../../', trip.cover_image.replace(/^\//, ''));
@@ -169,7 +169,7 @@ router.post('/:id/cover', authenticate, demoUploadBlock, uploadCover.single('cov
// DELETE /api/trips/:id — owner only
router.delete('/:id', authenticate, (req, res) => {
if (!isOwner(req.params.id, req.user.id))
return res.status(403).json({ error: 'Nur der Eigentümer kann die Reise löschen' });
return res.status(403).json({ error: 'Only the owner can delete the trip' });
const deletedTripId = Number(req.params.id);
db.prepare('DELETE FROM trips WHERE id = ?').run(req.params.id);
res.json({ success: true });
@@ -181,7 +181,7 @@ router.delete('/:id', authenticate, (req, res) => {
// GET /api/trips/:id/members
router.get('/:id/members', authenticate, (req, res) => {
if (!canAccessTrip(req.params.id, req.user.id))
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
const trip = db.prepare('SELECT user_id FROM trips WHERE id = ?').get(req.params.id);
const members = db.prepare(`
@@ -208,23 +208,23 @@ router.get('/:id/members', authenticate, (req, res) => {
// POST /api/trips/:id/members — add by email or username
router.post('/:id/members', authenticate, (req, res) => {
if (!canAccessTrip(req.params.id, req.user.id))
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
const { identifier } = req.body; // email or username
if (!identifier) return res.status(400).json({ error: 'E-Mail oder Benutzername erforderlich' });
if (!identifier) return res.status(400).json({ error: 'Email or username required' });
const target = db.prepare(
'SELECT id, username, email, avatar FROM users WHERE email = ? OR username = ?'
).get(identifier.trim(), identifier.trim());
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (!target) return res.status(404).json({ error: 'User not found' });
const trip = db.prepare('SELECT user_id FROM trips WHERE id = ?').get(req.params.id);
if (target.id === trip.user_id)
return res.status(400).json({ error: 'Der Eigentümer der Reise ist bereits Mitglied' });
return res.status(400).json({ error: 'Trip owner is already a member' });
const existing = db.prepare('SELECT id FROM trip_members WHERE trip_id = ? AND user_id = ?').get(req.params.id, target.id);
if (existing) return res.status(400).json({ error: 'Benutzer hat bereits Zugriff' });
if (existing) return res.status(400).json({ error: 'User already has access' });
db.prepare('INSERT INTO trip_members (trip_id, user_id, invited_by) VALUES (?, ?, ?)').run(req.params.id, target.id, req.user.id);
@@ -234,12 +234,12 @@ router.post('/:id/members', authenticate, (req, res) => {
// DELETE /api/trips/:id/members/:userId — owner removes anyone; member removes self
router.delete('/:id/members/:userId', authenticate, (req, res) => {
if (!canAccessTrip(req.params.id, req.user.id))
return res.status(404).json({ error: 'Reise nicht gefunden' });
return res.status(404).json({ error: 'Trip not found' });
const targetId = parseInt(req.params.userId);
const isSelf = targetId === req.user.id;
if (!isSelf && !isOwner(req.params.id, req.user.id))
return res.status(403).json({ error: 'Keine Berechtigung' });
return res.status(403).json({ error: 'No permission' });
db.prepare('DELETE FROM trip_members WHERE trip_id = ? AND user_id = ?').run(req.params.id, targetId);
res.json({ success: true });

View File

@@ -139,7 +139,7 @@ router.get('/', authenticate, async (req, res) => {
const { lat, lng, date, lang = 'de' } = req.query;
if (!lat || !lng) {
return res.status(400).json({ error: 'Breiten- und Längengrad sind erforderlich' });
return res.status(400).json({ error: 'Latitude and longitude are required' });
}
const ck = cacheKey(lat, lng, date);
@@ -161,7 +161,7 @@ router.get('/', authenticate, async (req, res) => {
const data = await response.json();
if (!response.ok || data.error) {
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo API Fehler' });
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo API error' });
}
const dateStr = targetDate.toISOString().slice(0, 10);
@@ -202,7 +202,7 @@ router.get('/', authenticate, async (req, res) => {
const data = await response.json();
if (!response.ok || data.error) {
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo Climate API Fehler' });
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo Climate API error' });
}
const daily = data.daily;
@@ -257,7 +257,7 @@ router.get('/', authenticate, async (req, res) => {
const data = await response.json();
if (!response.ok || data.error) {
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo API Fehler' });
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo API error' });
}
const code = data.current.weathercode;
@@ -274,7 +274,152 @@ router.get('/', authenticate, async (req, res) => {
res.json(result);
} catch (err) {
console.error('Weather error:', err);
res.status(500).json({ error: 'Fehler beim Abrufen der Wetterdaten' });
res.status(500).json({ error: 'Error fetching weather data' });
}
});
// GET /api/weather/detailed?lat=&lng=&date=&lang=de
router.get('/detailed', authenticate, async (req, res) => {
const { lat, lng, date, lang = 'de' } = req.query;
if (!lat || !lng || !date) {
return res.status(400).json({ error: 'Latitude, longitude, and date are required' });
}
const ck = `detailed_${cacheKey(lat, lng, date)}`;
try {
const cached = getCached(ck);
if (cached) return res.json(cached);
const targetDate = new Date(date);
const now = new Date();
const diffDays = (targetDate - now) / (1000 * 60 * 60 * 24);
const dateStr = targetDate.toISOString().slice(0, 10);
const descriptions = lang === 'de' ? WMO_DESCRIPTION_DE : WMO_DESCRIPTION_EN;
// Beyond 16-day forecast window → archive API (daily only, no hourly)
if (diffDays > 16) {
const refYear = targetDate.getFullYear() - 1;
const month = targetDate.getMonth() + 1;
const day = targetDate.getDate();
const startDate = new Date(refYear, month - 1, day - 2);
const endDate = new Date(refYear, month - 1, day + 2);
const startStr = startDate.toISOString().slice(0, 10);
const endStr = endDate.toISOString().slice(0, 10);
const url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lng}&start_date=${startStr}&end_date=${endStr}&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto`;
const response = await fetch(url);
const data = await response.json();
if (!response.ok || data.error) {
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo Climate API error' });
}
const daily = data.daily;
if (!daily || !daily.time || daily.time.length === 0) {
return res.json({ error: 'no_forecast' });
}
let sumMax = 0, sumMin = 0, sumPrecip = 0, count = 0;
for (let i = 0; i < daily.time.length; i++) {
if (daily.temperature_2m_max[i] != null && daily.temperature_2m_min[i] != null) {
sumMax += daily.temperature_2m_max[i];
sumMin += daily.temperature_2m_min[i];
sumPrecip += daily.precipitation_sum[i] || 0;
count++;
}
}
if (count === 0) {
return res.json({ error: 'no_forecast' });
}
const avgMax = sumMax / count;
const avgMin = sumMin / count;
const avgTemp = (avgMax + avgMin) / 2;
const avgPrecip = sumPrecip / count;
const result = {
type: 'climate',
temp: Math.round(avgTemp),
temp_max: Math.round(avgMax),
temp_min: Math.round(avgMin),
main: estimateCondition(avgTemp, avgPrecip),
precipitation_sum: Math.round(avgPrecip * 10) / 10,
};
setCache(ck, result, TTL_CLIMATE_MS);
return res.json(result);
}
// Within 16-day forecast window → full forecast with hourly data
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}`
+ `&hourly=temperature_2m,precipitation_probability,precipitation,weathercode,windspeed_10m,relativehumidity_2m`
+ `&daily=temperature_2m_max,temperature_2m_min,weathercode,sunrise,sunset,precipitation_probability_max,precipitation_sum,windspeed_10m_max`
+ `&timezone=auto&start_date=${dateStr}&end_date=${dateStr}`;
const response = await fetch(url);
const data = await response.json();
if (!response.ok || data.error) {
return res.status(response.status || 500).json({ error: data.reason || 'Open-Meteo API error' });
}
const daily = data.daily;
const hourly = data.hourly;
if (!daily || !daily.time || daily.time.length === 0) {
return res.json({ error: 'no_forecast' });
}
const dayIdx = 0; // We requested a single day
const code = daily.weathercode[dayIdx];
// Parse sunrise/sunset to HH:MM
const formatTime = (isoStr) => {
if (!isoStr) return '';
const d = new Date(isoStr);
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
};
// Build hourly array
const hourlyData = [];
if (hourly && hourly.time) {
for (let i = 0; i < hourly.time.length; i++) {
const h = new Date(hourly.time[i]).getHours();
hourlyData.push({
hour: h,
temp: Math.round(hourly.temperature_2m[i]),
precipitation_probability: hourly.precipitation_probability[i] || 0,
precipitation: hourly.precipitation[i] || 0,
main: WMO_MAP[hourly.weathercode[i]] || 'Clouds',
wind: Math.round(hourly.windspeed_10m[i] || 0),
humidity: Math.round(hourly.relativehumidity_2m[i] || 0),
});
}
}
const result = {
type: 'forecast',
temp: Math.round((daily.temperature_2m_max[dayIdx] + daily.temperature_2m_min[dayIdx]) / 2),
temp_max: Math.round(daily.temperature_2m_max[dayIdx]),
temp_min: Math.round(daily.temperature_2m_min[dayIdx]),
main: WMO_MAP[code] || 'Clouds',
description: descriptions[code] || '',
sunrise: formatTime(daily.sunrise[dayIdx]),
sunset: formatTime(daily.sunset[dayIdx]),
precipitation_sum: daily.precipitation_sum[dayIdx] || 0,
precipitation_probability_max: daily.precipitation_probability_max[dayIdx] || 0,
wind_max: Math.round(daily.windspeed_10m_max[dayIdx] || 0),
hourly: hourlyData,
};
setCache(ck, result, TTL_FORECAST_MS);
return res.json(result);
} catch (err) {
console.error('Detailed weather error:', err);
res.status(500).json({ error: 'Error fetching detailed weather data' });
}
});

View File

@@ -55,9 +55,9 @@ async function runBackup() {
if (fs.existsSync(uploadsDir)) archive.directory(uploadsDir, 'uploads');
archive.finalize();
});
console.log(`[Auto-Backup] Erstellt: ${filename}`);
console.log(`[Auto-Backup] Created: ${filename}`);
} catch (err) {
console.error('[Auto-Backup] Fehler:', err.message);
console.error('[Auto-Backup] Error:', err.message);
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
return;
}
@@ -77,11 +77,11 @@ function cleanupOldBackups(keepDays) {
const stat = fs.statSync(filePath);
if (stat.birthtimeMs < cutoff) {
fs.unlinkSync(filePath);
console.log(`[Auto-Backup] Altes Backup gelöscht: ${file}`);
console.log(`[Auto-Backup] Old backup deleted: ${file}`);
}
}
} catch (err) {
console.error('[Auto-Backup] Bereinigungsfehler:', err.message);
console.error('[Auto-Backup] Cleanup error:', err.message);
}
}
@@ -93,13 +93,13 @@ function start() {
const settings = loadSettings();
if (!settings.enabled) {
console.log('[Auto-Backup] Deaktiviert');
console.log('[Auto-Backup] Disabled');
return;
}
const expression = CRON_EXPRESSIONS[settings.interval] || CRON_EXPRESSIONS.daily;
currentTask = cron.schedule(expression, runBackup);
console.log(`[Auto-Backup] Geplant: ${settings.interval} (${expression}), Aufbewahrung: ${settings.keep_days === 0 ? 'immer' : settings.keep_days + ' Tage'}`);
console.log(`[Auto-Backup] Scheduled: ${settings.interval} (${expression}), retention: ${settings.keep_days === 0 ? 'forever' : settings.keep_days + ' days'}`);
}
// Demo mode: hourly reset of demo user data