let db; const path = require('path'); const fs = require('fs'); function init(database) { db = database; } function parseRoomFields(row) { if (!row) return null; try { row.furniture = JSON.parse(row.furniture || '[]'); } catch { row.furniture = []; } try { row.amenities = JSON.parse(row.amenities || '[]'); } catch { row.amenities = []; } try { row.floors = JSON.parse(row.floors || '[]'); } catch { row.floors = []; } return row; } function getAll(req, res) { db.all(`SELECT * FROM rooms WHERE is_active = 1 ORDER BY price_per_night ASC`, [], (err, rows) => { if (err) { console.error('Rooms API error:', err); return res.status(500).json({ error: 'Database error' }); } rows = rows.map(parseRoomFields); loadImages(rows, () => res.json(rows)); }); } function loadImages(rows, callback) { if (rows.length === 0) return callback(); let loaded = 0; rows.forEach(row => { db.all(`SELECT * FROM room_images WHERE room_id = ? ORDER BY sort_order ASC`, [row.id], (err, images) => { row.images = images || []; if (row.image_path && !images.some(i => i.image_path === row.image_path)) { row.images.unshift({ id: null, room_id: row.id, image_path: row.image_path, sort_order: -1, is_primary: 1 }); } loaded++; if (loaded === rows.length) callback(); }); }); } function getAllForAdmin(req, res) { db.all(`SELECT * FROM rooms ORDER BY price_per_night ASC`, [], (err, rows) => { if (err) { console.error('Admin rooms API error:', err); return res.status(500).json({ error: 'Database error' }); } rows = rows.map(parseRoomFields); loadImages(rows, () => res.json(rows)); }); } function createRoom(req, res) { const { type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, extra_beds, extra_bed_price, is_active } = req.body; if (!type || !name || !price_per_night) { return res.status(400).json({ error: 'type, name и price_per_night обязательны' }); } db.run(`INSERT INTO rooms (type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, extra_beds, extra_bed_price, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [type, name, description || '', rooms_count || 1, area_sqm || 20, max_guests || 2, JSON.stringify(furniture || []), JSON.stringify(amenities || []), JSON.stringify(floors || []), price_per_night, extra_beds || 0, extra_bed_price || 0, is_active !== undefined ? (is_active ? 1 : 0) : 1], function(err) { if (err) { console.error('Create room error:', err); return res.status(500).json({ error: 'Database error' }); } db.get(`SELECT * FROM rooms WHERE id = ?`, [this.lastID], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); res.status(201).json(parseRoomFields(row)); }); } ); } function updateRoom(req, res) { const { id } = req.params; const { type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, image_path, extra_beds, extra_bed_price, is_active } = req.body; db.get(`SELECT * FROM rooms WHERE id = ?`, [id], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); if (!row) return res.status(404).json({ error: 'Номер не найден' }); db.run(`UPDATE rooms SET type = ?, name = ?, description = ?, rooms_count = ?, area_sqm = ?, max_guests = ?, furniture = ?, amenities = ?, floors = ?, price_per_night = ?, image_path = ?, extra_beds = ?, extra_bed_price = ?, is_active = ? WHERE id = ?`, [ type ?? row.type, name ?? row.name, description ?? row.description, rooms_count ?? row.rooms_count, area_sqm ?? row.area_sqm, max_guests ?? row.max_guests, furniture ? JSON.stringify(furniture) : row.furniture, amenities ? JSON.stringify(amenities) : row.amenities, floors ? JSON.stringify(floors) : row.floors, price_per_night ?? row.price_per_night, image_path !== undefined ? image_path : row.image_path, extra_beds ?? row.extra_beds, extra_bed_price ?? row.extra_bed_price, is_active !== undefined ? (is_active ? 1 : 0) : row.is_active, id ], function(err) { if (err) { console.error('Update room error:', err); return res.status(500).json({ error: 'Database error' }); } db.get(`SELECT * FROM rooms WHERE id = ?`, [id], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); loadImages([parseRoomFields(row)], () => res.json(row)); }); } ); }); } function deleteRoom(req, res) { const { id } = req.params; db.get(`SELECT * FROM rooms WHERE id = ?`, [id], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); if (!row) return res.status(404).json({ error: 'Номер не найден' }); db.run(`UPDATE rooms SET is_active = 0 WHERE id = ?`, [id], function(err) { if (err) { console.error('Delete room error:', err); return res.status(500).json({ error: 'Database error' }); } res.json({ message: 'Номер удалён' }); }); }); } function uploadRoomImage(req, res) { if (!req.file) { return res.status(400).json({ error: 'Файл не загружен' }); } let imagePath = 'data/room_images/' + req.file.filename; if (req.file.mimetype !== 'image/webp') { const inputPath = req.file.path; const outputPath = path.join(path.dirname(inputPath), req.file.filename.replace(/\.[^.]+$/, '.webp')); require('sharp')(inputPath) .webp({ quality: 85 }) .toFile(outputPath) .then(() => { try { fs.unlinkSync(inputPath); } catch {} imagePath = 'data/room_images/' + path.basename(outputPath); res.json({ path: imagePath }); }) .catch(err => { console.error('Image conversion error:', err); res.json({ path: imagePath }); }); } else { res.json({ path: imagePath }); } } function uploadRoomImages(req, res) { const roomId = parseInt(req.params.id); if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'Файлы не загружены' }); } let completed = 0; const results = []; function processFile(index) { if (index >= req.files.length) return; const file = req.files[index]; let imagePath = 'data/room_images/' + file.filename; function saveByPath(finalPath) { db.get(`SELECT MAX(sort_order) as maxOrder FROM room_images WHERE room_id = ?`, [roomId], (err, row) => { const sortOrder = (row?.maxOrder || 0) + 1; db.run(`INSERT INTO room_images (room_id, image_path, sort_order) VALUES (?, ?, ?)`, [roomId, finalPath, sortOrder], function(insErr) { if (insErr) { console.error('Room image insert error:', insErr); } else results.push({ id: this.lastID, image_path: finalPath, sort_order: sortOrder }); completed++; if (completed === req.files.length) { res.json({ message: 'Изображения загружены', images: results }); } else { processFile(index + 1); } }); }); } if (file.mimetype !== 'image/webp') { const inputPath = file.path; const outputPath = path.join(path.dirname(inputPath), file.filename.replace(/\.[^.]+$/, '.webp')); require('sharp')(inputPath) .webp({ quality: 85 }) .toFile(outputPath) .then(() => { try { fs.unlinkSync(inputPath); } catch {} saveByPath('data/room_images/' + path.basename(outputPath)); }) .catch(() => { completed++; if (completed === req.files.length) { res.json({ message: 'Изображения загружены', images: results }); } else { processFile(index + 1); } }); } else { saveByPath(imagePath); } } processFile(0); } function deleteRoomImage(req, res) { const { imageId } = req.params; db.get(`SELECT * FROM room_images WHERE id = ?`, [imageId], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); if (!row) return res.status(404).json({ error: 'Изображение не найдено' }); const filePath = path.join(__dirname, '..', '..', row.image_path); try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch {} db.run(`DELETE FROM room_images WHERE id = ?`, [imageId], function(err) { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ message: 'Изображение удалено' }); }); }); } function setPrimaryImage(req, res) { const { imageId } = req.params; db.get(`SELECT * FROM room_images WHERE id = ?`, [imageId], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); if (!row) return res.status(404).json({ error: 'Изображение не найдено' }); db.run(`UPDATE room_images SET is_primary = 0 WHERE room_id = ?`, [row.room_id], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); db.run(`UPDATE room_images SET is_primary = 1 WHERE id = ?`, [imageId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); db.run(`UPDATE rooms SET image_path = ? WHERE id = ?`, [row.image_path, row.room_id], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ message: 'Главное изображение обновлено' }); }); }); }); }); } function reorderImages(req, res) { const { images } = req.body; if (!Array.isArray(images)) return res.status(400).json({ error: 'images must be an array' }); let done = 0; images.forEach((img) => { db.run(`UPDATE room_images SET sort_order = ? WHERE id = ?`, [img.sort_order, img.id], (err) => { if (err) console.error('Reorder error:', err); done++; if (done === images.length) res.json({ message: 'Порядок обновлён' }); }); }); } function getAvailability(req, res) { const { checkin, checkout } = req.query; if (!checkin || !checkout) { return res.status(400).json({ error: 'checkin и checkout обязательны' }); } db.all(`SELECT type, name, MIN(id) as id, SUM(rooms_count) as total, MIN(price_per_night) as price_per_night, MAX(max_guests) as max_guests FROM rooms WHERE is_active = 1 GROUP BY type ORDER BY price_per_night ASC`, [], (err, typeGroups) => { if (err) return res.status(500).json({ error: 'Database error' }); if (typeGroups.length === 0) return res.json([]); let processed = 0; const result = []; typeGroups.forEach(group => { db.get( `SELECT COUNT(b.id) as booked FROM bookings b WHERE b.room_type = ? AND b.status IN ('новая','оплачена','зарезервирована','заселена') AND b.checkin_date < ? AND b.checkout_date > ?`, [group.type, checkout, checkin], (err, row) => { const booked = err ? 0 : (row?.booked || 0); result.push({ type: group.type, name: group.name, id: group.id, total: group.total, booked: booked, available: Math.max(0, group.total - booked), price_per_night: group.price_per_night, max_guests: group.max_guests }); processed++; if (processed === typeGroups.length) { result.sort((a, b) => a.price_per_night - b.price_per_night); res.json(result); } } ); }); }); } function clearLegacyImage(req, res) { const { id } = req.params; db.get(`SELECT image_path FROM rooms WHERE id = ?`, [id], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); if (!row) return res.status(404).json({ error: 'Номер не найден' }); if (!row.image_path) return res.json({ message: 'Изображение уже отсутствует' }); const filePath = path.join(__dirname, '..', '..', row.image_path); try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch {} db.run(`UPDATE rooms SET image_path = NULL WHERE id = ?`, [id], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ message: 'Изображение удалено' }); }); }); } function setupRoutes(app, authenticateToken, requireAdmin, upload) { app.get('/api/rooms', getAll); app.get('/api/rooms/availability', getAvailability); app.get('/api/admin/rooms', authenticateToken, requireAdmin, getAllForAdmin); app.post('/api/admin/rooms', authenticateToken, requireAdmin, createRoom); app.put('/api/admin/rooms/:id', authenticateToken, requireAdmin, updateRoom); app.delete('/api/admin/rooms/:id', authenticateToken, requireAdmin, deleteRoom); app.post('/api/admin/rooms/upload', authenticateToken, requireAdmin, upload.single('image'), uploadRoomImage); app.post('/api/admin/rooms/:id/images', authenticateToken, requireAdmin, upload.array('images', 5), uploadRoomImages); app.delete('/api/admin/rooms/images/:imageId', authenticateToken, requireAdmin, deleteRoomImage); app.put('/api/admin/rooms/images/:imageId/primary', authenticateToken, requireAdmin, setPrimaryImage); app.put('/api/admin/rooms/images/reorder', authenticateToken, requireAdmin, reorderImages); app.delete('/api/admin/rooms/:id/image', authenticateToken, requireAdmin, clearLegacyImage); } module.exports = { init, setupRoutes };