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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) } });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user