From edbe54b915d6240e2c1f7b6f3b92f4ca02a3c7ea Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Sun, 26 Jul 2026 22:29:38 +0500 Subject: [PATCH] =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/index.js | 2 +- modules/adminBookings/index.js | 102 ++++++++++++++++++++------------- modules/bookings/index.js | 30 ++++------ modules/rooms/index.js | 28 ++++----- public/index.html | 7 ++- test_pricing.js | 4 +- 6 files changed, 96 insertions(+), 77 deletions(-) diff --git a/config/index.js b/config/index.js index a82572d..85e7d04 100644 --- a/config/index.js +++ b/config/index.js @@ -94,7 +94,7 @@ const DEFAULT_ROOMS = [ floors: [2], max_guests: 4, price_per_night: 4500, - image_path: 'img/1eae46658-cfca-4b65-82f0-e5868af5541b.webp', + image_path: 'img/rooms/placeholder.webp', extra_beds: 0, extra_bed_price: 0 } diff --git a/modules/adminBookings/index.js b/modules/adminBookings/index.js index 9cb6dbb..294d673 100644 --- a/modules/adminBookings/index.js +++ b/modules/adminBookings/index.js @@ -1,5 +1,5 @@ const config = require('../../config'); -const { validatePromocode } = require('../bookings'); +const { validatePromocode, calculateNightPrices, checkAvailability } = require('../bookings'); const promocodeRateLimit = new Map(); const PROMOCODE_WINDOW = 60 * 1000; @@ -140,26 +140,45 @@ function updateBookingRoom(req, res) { db.get(`SELECT * FROM rooms WHERE id = ?`, [room_id], (err, room) => { if (err) return res.status(500).json({ error: 'Database error' }); 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 totalGuests = (booking.adults || 0) + (booking.children || 0); - const basePrice = config.calculateBasePrice(room.type, booking.checkin_date, booking.checkout_date) * Math.max(1, totalGuests); - 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' }); - 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 }); + + function applyRoomChange() { + const oldValue = booking.room_name ? booking.room_type + ' — ' + booking.room_name : booking.room_type || 'Не указан'; + const newValue = room.type + ' — ' + room.name; + const totalGuests = (booking.adults || 0) + (booking.children || 0); + calculateNightPrices(room.type, booking.checkin_date, booking.checkout_date, (err, baseSum) => { + if (err) return res.status(400).json({ error: err.message }); + const basePrice = baseSum * Math.max(1, totalGuests); + 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' }); + 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 }); + }); + }); }); + } + + if (room.id === booking.room_id) { + return applyRoomChange(); + } + + checkAvailability(room.id, room.type, booking.checkin_date, booking.checkout_date, 1, (err, avail) => { + if (err) return res.status(500).json({ error: 'Database error' }); + if (avail.available < 1) { + return res.status(409).json({ + error: `Номер "${room.name}" занят на выбранные даты. Доступно: ${avail.available} из ${avail.total}` + }); + } + applyRoomChange(); }); }); }); @@ -267,11 +286,11 @@ function updateBookingDetails(req, res) { values.push(bookingId); db.run(`UPDATE bookings SET ${fields.join(', ')} WHERE id = ?`, values, function(err) { if (err) return res.status(500).json({ error: 'Database error' }); - if (adults !== undefined && adults !== oldValues.adults) { - logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), adults.toString()); + if (newAdults !== undefined && newAdults !== oldValues.adults) { + logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), newAdults.toString()); } - if (children !== undefined && children !== oldValues.children) { - logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), children.toString()); + if (newChildren !== undefined && newChildren !== oldValues.children) { + logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), newChildren.toString()); } if (checkin_date !== undefined && checkin_date !== oldValues.checkin_date) { logHistory(bookingId, req.user.id, req.user.login, 'checkin_date', oldValues.checkin_date, checkin_date); @@ -279,8 +298,8 @@ function updateBookingDetails(req, res) { if (checkout_date !== undefined && checkout_date !== oldValues.checkout_date) { logHistory(bookingId, req.user.id, req.user.login, 'checkout_date', oldValues.checkout_date, checkout_date); } - const finalAdults = adults !== undefined ? adults : oldValues.adults; - const finalChildren = children !== undefined ? children : oldValues.children; + const finalAdults = newAdults !== undefined ? newAdults : oldValues.adults; + const finalChildren = newChildren !== undefined ? newChildren : oldValues.children; const finalCheckin = checkin_date !== undefined ? checkin_date : oldValues.checkin_date; const finalCheckout = checkout_date !== undefined ? checkout_date : oldValues.checkout_date; const totalGuests = finalAdults + finalChildren; @@ -288,11 +307,9 @@ function updateBookingDetails(req, res) { if (!roomType) { return finishUpdate(); } - db.get(`SELECT price_per_night FROM rooms WHERE type = ? AND is_active = 1`, [roomType], (err, roomData) => { - if (err) return res.status(500).json({ error: 'Database error' }); - const pricePerNight = roomData ? roomData.price_per_night : config.getRoomPrice(roomType); - const nights = config.calculateNights(finalCheckin, finalCheckout); - const basePrice = pricePerNight * totalGuests * nights; + calculateNightPrices(roomType, finalCheckin, finalCheckout, (err, baseSum) => { + if (err) return res.status(500).json({ error: err.message }); + const basePrice = baseSum * totalGuests; const discountPercent = row.discount_percent || 0; const discountAmount = Math.round(basePrice * discountPercent / 100); const totalPrice = basePrice - discountAmount; @@ -336,16 +353,19 @@ function validatePromocodeAPI(req, res) { if (err) return res.status(500).json({ error: 'Database error' }); if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' }); const guestsCount = parseInt(guests) || 1; - const basePrice = config.calculateBasePrice(room_type, checkin, checkout) * guestsCount; - const discountAmount = Math.round(basePrice * promo.discount_percent / 100); - const totalPrice = basePrice - discountAmount; - res.json({ - valid: true, - discount_percent: promo.discount_percent, - base_price: basePrice, - discount_amount: discountAmount, - total_price: totalPrice, - code: promo.code + calculateNightPrices(room_type, checkin, checkout, (err, baseSum) => { + if (err) return res.status(400).json({ error: err.message }); + const basePrice = baseSum * guestsCount; + const discountAmount = Math.round(basePrice * promo.discount_percent / 100); + const totalPrice = basePrice - discountAmount; + res.json({ + valid: true, + discount_percent: promo.discount_percent, + base_price: basePrice, + discount_amount: discountAmount, + total_price: totalPrice, + code: promo.code + }); }); }); } diff --git a/modules/bookings/index.js b/modules/bookings/index.js index ef80851..5c31469 100644 --- a/modules/bookings/index.js +++ b/modules/bookings/index.js @@ -1,5 +1,6 @@ const config = require('../../config'); const { getRoomBasePriceByType } = config; +const seasonalModule = require('../seasonalPrices'); let db; @@ -8,7 +9,6 @@ function init(database) { } function getEffectivePrice(roomType, date, callback) { - const seasonalModule = require('../seasonalPrices'); seasonalModule.getSeasonalPrice(roomType, date, (err, seasonalPrice) => { if (err || !seasonalPrice) { const basePrice = getRoomBasePriceByType(roomType); @@ -52,11 +52,11 @@ function calculateNightPrices(roomType, checkin, checkout, callback) { function validatePromocode(promocode, callback) { if (!promocode) return callback(null, null); - const now = new Date().toISOString(); + const now = new Date(); db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [promocode], (err, row) => { if (err || !row) return callback(null, null); - if (row.valid_from && row.valid_from > now) return callback(null, null); - if (row.valid_to && row.valid_to < now) return callback(null, null); + if (row.valid_from && new Date(row.valid_from) > now) return callback(null, null); + if (row.valid_to && new Date(row.valid_to) < now) return callback(null, null); if (row.valid_days) { const createdDate = new Date(row.created_at); const expireDate = new Date(createdDate.getTime() + row.valid_days * 24 * 60 * 60 * 1000); @@ -78,12 +78,12 @@ function checkAvailability(roomId, roomType, checkin, checkout, requestedCount, FROM rooms r WHERE r.id = ? AND r.is_active = 1`; params = [checkout, checkin, roomId]; } else { - sql = `SELECT r.rooms_count, COALESCE( + sql = `SELECT COALESCE(SUM(r.rooms_count), 0) as rooms_count, (SELECT COUNT(b.id) FROM bookings b - WHERE b.room_type = r.type AND b.status IN ('новая','оплачена','зарезервирована','заселена') - AND b.checkin_date < ? AND b.checkout_date > ?), 0) as booked - FROM rooms r WHERE r.type = ? AND r.is_active = 1 LIMIT 1`; - params = [checkout, checkin, roomType]; + WHERE b.room_type = ? AND b.status IN ('новая','оплачена','зарезервирована','заселена') + AND b.checkin_date < ? AND b.checkout_date > ?) as booked + FROM rooms r WHERE r.type = ? AND r.is_active = 1`; + params = [roomType, checkout, checkin, roomType]; } db.get(sql, params, (err, row) => { @@ -96,7 +96,7 @@ function checkAvailability(roomId, roomType, checkin, checkout, requestedCount, function createBooking(req, res) { const { name, phone, adults, children, checkin, checkout, wishes, room, room_id, promocode } = req.body; - if (!name || !phone || !adults || !checkin || !checkout) { + if (!name || !phone || !room || !adults || !checkin || !checkout) { return res.status(400).json({ error: 'Missing required fields' }); } @@ -182,18 +182,10 @@ function getBookingHistory(req, res) { res.json(rows); }); } - -function logHistory(bookingId, userId, userLogin, field, oldValue, newValue) { - db.run(`INSERT INTO booking_history (booking_id, user_id, user_login, field, old_value, new_value) VALUES (?, ?, ?, ?, ?, ?)`, - [bookingId, userId, userLogin, field, oldValue, newValue], (err) => { - if (err) console.error('History log error:', err); - }); -} - function setupRoutes(app, authenticateToken, requireAdmin) { app.post('/api/bookings', createBooking); app.get('/api/bookings', getBookings); app.get('/api/admin/bookings/:id/history', authenticateToken, getBookingHistory); } -module.exports = { init, setupRoutes, validatePromocode, logHistory, calculateNightPrices, checkAvailability }; +module.exports = { init, setupRoutes, validatePromocode, calculateNightPrices, checkAvailability }; diff --git a/modules/rooms/index.js b/modules/rooms/index.js index 435503f..08b7841 100644 --- a/modules/rooms/index.js +++ b/modules/rooms/index.js @@ -252,34 +252,36 @@ function getAvailability(req, res) { return res.status(400).json({ error: 'checkin и checkout обязательны' }); } - db.all(`SELECT * FROM rooms WHERE is_active = 1 ORDER BY price_per_night ASC`, [], (err, rooms) => { + 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 (rooms.length === 0) return res.json([]); + if (typeGroups.length === 0) return res.json([]); let processed = 0; const result = []; - rooms.forEach(room => { + typeGroups.forEach(group => { db.get( `SELECT COUNT(b.id) as booked FROM bookings b - WHERE b.room_id = ? AND b.status IN ('новая','оплачена','зарезервирована','заселена') + WHERE b.room_type = ? AND b.status IN ('новая','оплачена','зарезервирована','заселена') AND b.checkin_date < ? AND b.checkout_date > ?`, - [room.id, checkout, checkin], + [group.type, checkout, checkin], (err, row) => { const booked = err ? 0 : (row?.booked || 0); result.push({ - type: room.type, - name: room.name, - id: room.id, - total: room.rooms_count, + type: group.type, + name: group.name, + id: group.id, + total: group.total, booked: booked, - available: Math.max(0, room.rooms_count - booked), - price_per_night: room.price_per_night, - max_guests: room.max_guests + available: Math.max(0, group.total - booked), + price_per_night: group.price_per_night, + max_guests: group.max_guests }); processed++; - if (processed === rooms.length) { + if (processed === typeGroups.length) { result.sort((a, b) => a.price_per_night - b.price_per_night); res.json(result); } diff --git a/public/index.html b/public/index.html index ba5a7ba..eeea914 100644 --- a/public/index.html +++ b/public/index.html @@ -910,6 +910,7 @@ try { localStorage.setItem('cookie_consent', '1'); } catch(e) {} document.getElementById('cookieBanner').classList.remove('show'); } + window.acceptCookies = acceptCookies; function initCookieBanner() { try { @@ -918,7 +919,11 @@ document.getElementById('cookieBanner').classList.add('show'); } - initCookieBanner(); + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCookieBanner); + } else { + initCookieBanner(); + } })(); diff --git a/test_pricing.js b/test_pricing.js index 28a5e93..e03413c 100644 --- a/test_pricing.js +++ b/test_pricing.js @@ -71,7 +71,7 @@ async function runTests() { }); assert(promoValidation1.status === 200, 'Валидация успешна'); const promo1 = promoValidation1.data; - console.log(` 1 гость, 1 ночь: ${promo1.base_price} ₽ (ожидалось: 1501)`); + console.log(` 1 гость, 1 ночь: ${promo1.base_price} ₽ (ожидалось: 1500)`); assert(promo1.base_price === 1500, `Базовая цена для 1 гостя = 1500 (получено: ${promo1.base_price})`); console.log('\n--- Тест 3: Валидация промокода с 2 гостями ---'); @@ -84,7 +84,7 @@ async function runTests() { }); assert(promoValidation2.status === 200, 'Валидация успешна'); const promo2 = promoValidation2.data; - console.log(` 2 гостя, 1 ночь: ${promo2.base_price} ₽ (ожидалось: 3002)`); + console.log(` 2 гостя, 1 ночь: ${promo2.base_price} ₽ (ожидалось: 3000)`); assert(promo2.base_price === 3000, `Базовая цена для 2 гостей = 3000 (получено: ${promo2.base_price})`); console.log('\n--- Тест 4: Валидация промокода с 3 гостями и 2 ночами ---');