diff --git a/modules/adminBookings/index.js b/modules/adminBookings/index.js index d4539c3..dbb0c31 100644 --- a/modules/adminBookings/index.js +++ b/modules/adminBookings/index.js @@ -68,9 +68,9 @@ function getBookingsForAdmin(req, res) { const params = []; if (search) { - whereClause += ' AND (b.name LIKE ? OR b.phone LIKE ? OR b.room_type LIKE ?)'; + whereClause += ' AND (b.name LIKE ? OR b.phone LIKE ? OR b.room_type LIKE ? OR r.name LIKE ?)'; const searchTerm = `%${search}%`; - params.push(searchTerm, searchTerm, searchTerm); + params.push(searchTerm, searchTerm, searchTerm, searchTerm); } if (statusFilter && statusFilter !== 'all') { @@ -78,7 +78,7 @@ function getBookingsForAdmin(req, res) { params.push(statusFilter); } - db.get(`SELECT COUNT(*) as total FROM bookings b WHERE ${whereClause}`, params, (err, countRow) => { + db.get(`SELECT COUNT(*) as total FROM bookings b LEFT JOIN rooms r ON b.room_id = r.id WHERE ${whereClause}`, params, (err, countRow) => { if (err) { console.error(err); return res.status(500).json({ error: 'Database error' }); @@ -87,8 +87,10 @@ function getBookingsForAdmin(req, res) { const total = countRow.total; const totalPages = Math.ceil(total / limit); - db.all(`SELECT b.*, p.code as promocode_code FROM bookings b + db.all(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id + LEFT JOIN rooms r ON b.room_id = r.id WHERE ${whereClause} ORDER BY b.checkin_date ASC LIMIT ? OFFSET ?`, [...params, limit, offset], (err, rows) => { @@ -123,7 +125,11 @@ function updateBookingStatus(req, res) { db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status); - db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => { + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + FROM bookings b + LEFT JOIN promocodes p ON b.promocode_id = p.id + LEFT JOIN rooms r ON b.room_id = r.id + WHERE b.id = ?`, [bookingId], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ message: 'Status updated', booking: row }); }); @@ -133,25 +139,38 @@ function updateBookingStatus(req, res) { function updateBookingRoom(req, res) { const bookingId = parseInt(req.params.id); - const { room_type } = req.body; - const validRooms = config.ROOM_TYPE_LIST; - if (!room_type || !validRooms.includes(room_type)) { - return res.status(400).json({ error: 'Invalid room type. Valid: ' + validRooms.join(', ') }); - } - db.get(`SELECT room_type, checkin_date, checkout_date, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => { + const { room_id } = req.body; + + db.get(`SELECT * FROM bookings WHERE id = ?`, [bookingId], (err, booking) => { if (err) return res.status(500).json({ error: 'Database error' }); - if (!row) return res.status(404).json({ error: 'Booking not found' }); - const oldValue = row.room_type || 'Не указан'; - const basePrice = config.calculateBasePrice(room_type, row.checkin_date, row.checkout_date); - const discountAmount = Math.round(basePrice * (row.discount_percent || 0) / 100); - const totalPrice = basePrice - discountAmount; - db.run(`UPDATE bookings SET room_type = ?, base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`, - [room_type, basePrice, discountAmount, totalPrice, bookingId], (err) => { + if (!booking) return res.status(404).json({ error: 'Booking not found' }); + + if (!room_id) { + return res.status(400).json({ error: 'room_id is required' }); + } + + db.get(`SELECT * FROM rooms WHERE id = ?`, [room_id], (err, room) => { if (err) return res.status(500).json({ error: 'Database error' }); - logHistory(bookingId, req.user.id, req.user.login, 'room_type', oldValue, room_type); - db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => { + if (!room) return res.status(400).json({ error: 'Room not found' }); + + const oldValue = booking.room_name ? booking.room_type + ' — ' + booking.room_name : booking.room_type || 'Не указан'; + const newValue = room.type + ' — ' + room.name; + const basePrice = config.calculateBasePrice(room.type, booking.checkin_date, booking.checkout_date); + const discountAmount = Math.round(basePrice * (booking.discount_percent || 0) / 100); + const totalPrice = basePrice - discountAmount; + + db.run(`UPDATE bookings SET room_id = ?, room_type = ?, base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`, + [room.id, room.type, basePrice, discountAmount, totalPrice, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); - res.json({ message: 'Room updated', booking: row }); + logHistory(bookingId, req.user.id, req.user.login, 'room', oldValue, newValue); + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + FROM bookings b + LEFT JOIN promocodes p ON b.promocode_id = p.id + LEFT JOIN rooms r ON b.room_id = r.id + WHERE b.id = ?`, [bookingId], (err, row) => { + if (err) return res.status(500).json({ error: 'Database error' }); + res.json({ message: 'Room updated', booking: row }); + }); }); }); }); @@ -167,7 +186,11 @@ function updateBookingComment(req, res) { db.run(`UPDATE bookings SET comment = ? WHERE id = ?`, [comment || null, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'comment', oldValue, comment || 'Нет'); - db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => { + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + FROM bookings b + LEFT JOIN promocodes p ON b.promocode_id = p.id + LEFT JOIN rooms r ON b.room_id = r.id + WHERE b.id = ?`, [bookingId], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ message: 'Comment updated', booking: row }); }); @@ -192,10 +215,14 @@ function updateBookingDiscount(req, res) { [discount_percent, discountAmount, totalPrice, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'discount_percent', oldValue.toString(), discount_percent.toString()); - db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => { - if (err) return res.status(500).json({ error: 'Database error' }); - res.json({ message: 'Discount updated', booking: row }); - }); + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + FROM bookings b + LEFT JOIN promocodes p ON b.promocode_id = p.id + LEFT JOIN rooms r ON b.room_id = r.id + WHERE b.id = ?`, [bookingId], (err, row) => { + if (err) return res.status(500).json({ error: 'Database error' }); + res.json({ message: 'Discount updated', booking: row }); + }); }); }); } @@ -287,7 +314,11 @@ function updateBookingDetails(req, res) { }); }); function finishUpdate() { - db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => { + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + FROM bookings b + LEFT JOIN promocodes p ON b.promocode_id = p.id + LEFT JOIN rooms r ON b.room_id = r.id + WHERE b.id = ?`, [bookingId], (err, row) => { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ message: 'Booking details updated', booking: row }); }); diff --git a/modules/bookings/index.js b/modules/bookings/index.js index f86a215..c4c7b6c 100644 --- a/modules/bookings/index.js +++ b/modules/bookings/index.js @@ -30,7 +30,7 @@ function validatePromocode(promocode, callback) { } function createBooking(req, res) { - const { name, phone, adults, children, checkin, checkout, wishes, room, promocode } = req.body; + const { name, phone, adults, children, checkin, checkout, wishes, room, room_id, promocode } = req.body; if (!name || !phone || !adults || !checkin || !checkout) { return res.status(400).json({ error: 'Missing required fields' }); } @@ -46,9 +46,10 @@ function createBooking(req, res) { const safeBasePrice = basePrice || 0; const discountAmount = Math.round(safeBasePrice * discountPercent / 100); const totalPrice = safeBasePrice - discountAmount; - const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, base_price, discount_percent, discount_amount, total_price, promocode_id) - VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?)`); - stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, room || null, + const roomIdValue = room_id ? parseInt(room_id) : null; + const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, room_id, base_price, discount_percent, discount_amount, total_price, promocode_id) + VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?, ?)`); + stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, room || null, roomIdValue, safeBasePrice || null, discountPercent || 0, discountAmount || 0, totalPrice || null, promocodeId, function(err) { if (err) { console.error(err); diff --git a/public/admin.html b/public/admin.html index 1575e8a..a4110aa 100644 --- a/public/admin.html +++ b/public/admin.html @@ -626,7 +626,7 @@ function initTabs() { document.getElementById('tab-' + tab).classList.add('active'); if (tab === 'dashboard') loadDashboard(); if (tab === 'users') loadUsers(); - if (tab === 'bookings') { bookingsLoaded = false; loadBookings(); } + if (tab === 'bookings') { bookingsLoaded = false; loadRoomsForBookingEdit(); loadBookings(); } if (tab === 'rooms') loadRooms(); if (tab === 'promocodes') loadPromocodes(); if (tab === 'reviews') loadReviews(); @@ -701,14 +701,14 @@ async function loadDashboard() { document.getElementById('statAdmins').textContent = users.filter(u => u.role === 'admin').length; } catch(e) {} try { - const rows = await api('/api/admin/bookings'); - document.getElementById('statBookings').textContent = rows.length; - document.getElementById('statNew').textContent = rows.filter(r => r.status === 'новая').length; - document.getElementById('recentBookings').innerHTML = rows.filter(r => r.status !== 'отменена' && r.status !== 'выехала').slice(0, 5).map(r => '' + - '' + esc(r.name) + '' + esc(r.room_type || '—') + '' + + const rows = await api('/api/admin/bookings?limit=100'); + document.getElementById('statBookings').textContent = rows.total || rows.data.length; + document.getElementById('statNew').textContent = rows.data.filter(r => r.status === 'новая').length; + document.getElementById('recentBookings').innerHTML = rows.data.filter(r => r.status !== 'отменена' && r.status !== 'выехала').slice(0, 5).map(r => '' + + '' + esc(r.name) + '' + esc(r.room_name ? r.room_type + ' — ' + r.room_name : r.room_type || '—') + '' + '' + esc(r.checkin_date) + '' + esc(r.checkout_date) + '' + '' + r.status + '').join(''); - if (!rows.filter(r => r.status !== 'отменена' && r.status !== 'выехала').length) { + if (!rows.data.filter(r => r.status !== 'отменена' && r.status !== 'выехала').length) { document.getElementById('recentBookings').innerHTML = 'Нет данных'; } } catch(e) { @@ -739,12 +739,22 @@ async function loadUsers() { } let allBookingsData = []; +let allRoomsData = []; let sortDirection = 'asc'; let bookingsLoaded = false; let currentPage = 1; let totalPages = 1; let searchTimeout = null; +async function loadRoomsForBookingEdit() { + try { + const rooms = await api('/api/admin/rooms'); + allRoomsData = rooms; + } catch(e) { + console.error('Failed to load rooms:', e); + } +} + async function loadBookings(resetPage = true) { if (resetPage) currentPage = 1; try { @@ -858,7 +868,6 @@ function renderBookings() { }); const statuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена']; - const rooms = ['2x-местный', '3х-местный', 'Семейный', 'Люкс']; const tbody = document.getElementById('allBookings'); if (rows.length === 0) { @@ -873,14 +882,18 @@ function renderBookings() { if (checkoutDiff === 0 && r.status !== 'отменена' && r.status !== 'выехала') rowClass = 'row-checkout-today'; const selectOpts = statuses.map(s => '').join(''); - const roomOpts = '' + rooms.map(s => '').join(''); + + const roomDisplay = r.room_name ? r.room_type + ' — ' + r.room_name : r.room_type || '—'; + const roomOpts = '' + allRoomsData.map(room => + '' + ).join(''); const commentHtml = ''; const discountHtml = ''; return '' + '' + esc(r.name) + '' + esc(r.phone) + '' + - '' + + '' + '' + '' + '' + @@ -914,12 +927,12 @@ async function changeStatus(id, status) { } catch(err) { showToast(err.message, 'error'); } } -async function changeRoom(id, room_type) { +async function changeRoom(id, room_id) { try { - const data = await api('/api/admin/bookings/' + id + '/room', { method: 'PATCH', body: JSON.stringify({ room_type }) }); + const data = await api('/api/admin/bookings/' + id + '/room', { method: 'PATCH', body: JSON.stringify({ room_id: room_id || null }) }); allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b); renderBookings(); updateBookingStats(); loadDashboard(); - showToast('Номер обновлён: ' + (room_type || 'Не указан')); + showToast('Номер обновлён'); } catch(err) { showToast(err.message, 'error'); } } @@ -1544,7 +1557,7 @@ function renderRooms(rooms) { (extraBedsText ? '
' + extraBedsText + '
' : '') + '
' + '' + - '' + + '' + '
' + '' + ''; @@ -1663,11 +1676,13 @@ async function saveRoom() { } catch(err) { showToast(err.message, 'error'); } } -async function deleteRoom(id, name) { - if (!confirm('Удалить номер "' + name + '"? Это скроет номер из списка.')) return; +async function toggleRoomActive(id, isActive) { try { - await api('/api/admin/rooms/' + id, { method: 'DELETE' }); - showToast('Номер удалён'); + await api('/api/admin/rooms/' + id, { + method: 'PATCH', + body: JSON.stringify({ is_active: isActive ? 1 : 0 }) + }); + showToast(isActive ? 'Номер показан на сайте' : 'Номер скрыт с сайта'); loadRooms(); } catch(err) { showToast(err.message, 'error'); } } diff --git a/public/index.html b/public/index.html index 75a2f9c..8581dd5 100644 --- a/public/index.html +++ b/public/index.html @@ -461,6 +461,7 @@
+
diff --git a/public/js/main.js b/public/js/main.js index 66081bc..f5fbad2 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -269,6 +269,7 @@ document.getElementById('bookingForm').addEventListener('submit', async function checkout: this.querySelector('[name="checkout"]').value, wishes: this.querySelector('[name="wishes"]').value, room: document.getElementById('selectedRoom').value, + room_id: parseInt(document.getElementById('selectedRoomId').value) || null, promocode: document.getElementById('promocodeInput').value.trim() || null }; diff --git a/public/js/rooms-public.js b/public/js/rooms-public.js index 1ca8149..828929d 100644 --- a/public/js/rooms-public.js +++ b/public/js/rooms-public.js @@ -82,7 +82,7 @@ function renderRoomsPublic(rooms) { от ${room.price_per_night || 0} ₽ / ночь
- +
@@ -93,13 +93,28 @@ function renderRoomsPublic(rooms) { initRoomBookingHandlers(); } +function updateRoomPrices(rooms) { + const prices = {}; + const maxGuests = {}; + rooms.forEach(room => { + prices[room.type] = room.price_per_night; + maxGuests[room.type] = room.max_guests; + }); + if (window.updateRoomPricesData) { + window.updateRoomPricesData(prices, maxGuests); + } +} + function initRoomBookingHandlers() { document.querySelectorAll('.btn-book').forEach(btn => { btn.addEventListener('click', function() { - const room = this.getAttribute('data-room'); + const roomId = this.getAttribute('data-room-id'); + const roomType = this.getAttribute('data-room-type'); + const roomName = this.getAttribute('data-room-name'); const maxGuests = parseInt(this.getAttribute('data-max-guests')) || 4; - document.getElementById('selectedRoom').value = room; - updateGuestOptionsDynamic(room, maxGuests); + document.getElementById('selectedRoom').value = roomType + ' — ' + roomName; + document.getElementById('selectedRoomId').value = roomId; + updateGuestOptionsDynamic(roomType, maxGuests); hidePriceInfo(); }); }); diff --git a/server.js b/server.js index 699e022..a7cad58 100644 --- a/server.js +++ b/server.js @@ -195,6 +195,7 @@ db.serialize(() => { 'wishes TEXT', 'status TEXT DEFAULT "новая"', 'room_type TEXT', + 'room_id INTEGER', 'comment TEXT', 'base_price REAL', 'discount_percent INTEGER DEFAULT 0',