This commit is contained in:
@@ -94,7 +94,7 @@ const DEFAULT_ROOMS = [
|
|||||||
floors: [2],
|
floors: [2],
|
||||||
max_guests: 4,
|
max_guests: 4,
|
||||||
price_per_night: 4500,
|
price_per_night: 4500,
|
||||||
image_path: 'img/1eae46658-cfca-4b65-82f0-e5868af5541b.webp',
|
image_path: 'img/rooms/placeholder.webp',
|
||||||
extra_beds: 0,
|
extra_beds: 0,
|
||||||
extra_bed_price: 0
|
extra_bed_price: 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
const { validatePromocode } = require('../bookings');
|
const { validatePromocode, calculateNightPrices, checkAvailability } = require('../bookings');
|
||||||
|
|
||||||
const promocodeRateLimit = new Map();
|
const promocodeRateLimit = new Map();
|
||||||
const PROMOCODE_WINDOW = 60 * 1000;
|
const PROMOCODE_WINDOW = 60 * 1000;
|
||||||
@@ -141,10 +141,13 @@ function updateBookingRoom(req, res) {
|
|||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: 'Database error' });
|
||||||
if (!room) return res.status(400).json({ error: 'Room not found' });
|
if (!room) return res.status(400).json({ error: 'Room not found' });
|
||||||
|
|
||||||
|
function applyRoomChange() {
|
||||||
const oldValue = booking.room_name ? booking.room_type + ' — ' + booking.room_name : booking.room_type || 'Не указан';
|
const oldValue = booking.room_name ? booking.room_type + ' — ' + booking.room_name : booking.room_type || 'Не указан';
|
||||||
const newValue = room.type + ' — ' + room.name;
|
const newValue = room.type + ' — ' + room.name;
|
||||||
const totalGuests = (booking.adults || 0) + (booking.children || 0);
|
const totalGuests = (booking.adults || 0) + (booking.children || 0);
|
||||||
const basePrice = config.calculateBasePrice(room.type, booking.checkin_date, booking.checkout_date) * Math.max(1, totalGuests);
|
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 discountAmount = Math.round(basePrice * (booking.discount_percent || 0) / 100);
|
||||||
const totalPrice = basePrice - discountAmount;
|
const totalPrice = basePrice - discountAmount;
|
||||||
|
|
||||||
@@ -162,6 +165,22 @@ function updateBookingRoom(req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
values.push(bookingId);
|
||||||
db.run(`UPDATE bookings SET ${fields.join(', ')} WHERE id = ?`, values, function(err) {
|
db.run(`UPDATE bookings SET ${fields.join(', ')} WHERE id = ?`, values, function(err) {
|
||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: 'Database error' });
|
||||||
if (adults !== undefined && adults !== oldValues.adults) {
|
if (newAdults !== undefined && newAdults !== oldValues.adults) {
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), adults.toString());
|
logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), newAdults.toString());
|
||||||
}
|
}
|
||||||
if (children !== undefined && children !== oldValues.children) {
|
if (newChildren !== undefined && newChildren !== oldValues.children) {
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), children.toString());
|
logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), newChildren.toString());
|
||||||
}
|
}
|
||||||
if (checkin_date !== undefined && checkin_date !== oldValues.checkin_date) {
|
if (checkin_date !== undefined && checkin_date !== oldValues.checkin_date) {
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'checkin_date', oldValues.checkin_date, 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) {
|
if (checkout_date !== undefined && checkout_date !== oldValues.checkout_date) {
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'checkout_date', oldValues.checkout_date, 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 finalAdults = newAdults !== undefined ? newAdults : oldValues.adults;
|
||||||
const finalChildren = children !== undefined ? children : oldValues.children;
|
const finalChildren = newChildren !== undefined ? newChildren : oldValues.children;
|
||||||
const finalCheckin = checkin_date !== undefined ? checkin_date : oldValues.checkin_date;
|
const finalCheckin = checkin_date !== undefined ? checkin_date : oldValues.checkin_date;
|
||||||
const finalCheckout = checkout_date !== undefined ? checkout_date : oldValues.checkout_date;
|
const finalCheckout = checkout_date !== undefined ? checkout_date : oldValues.checkout_date;
|
||||||
const totalGuests = finalAdults + finalChildren;
|
const totalGuests = finalAdults + finalChildren;
|
||||||
@@ -288,11 +307,9 @@ function updateBookingDetails(req, res) {
|
|||||||
if (!roomType) {
|
if (!roomType) {
|
||||||
return finishUpdate();
|
return finishUpdate();
|
||||||
}
|
}
|
||||||
db.get(`SELECT price_per_night FROM rooms WHERE type = ? AND is_active = 1`, [roomType], (err, roomData) => {
|
calculateNightPrices(roomType, finalCheckin, finalCheckout, (err, baseSum) => {
|
||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: err.message });
|
||||||
const pricePerNight = roomData ? roomData.price_per_night : config.getRoomPrice(roomType);
|
const basePrice = baseSum * totalGuests;
|
||||||
const nights = config.calculateNights(finalCheckin, finalCheckout);
|
|
||||||
const basePrice = pricePerNight * totalGuests * nights;
|
|
||||||
const discountPercent = row.discount_percent || 0;
|
const discountPercent = row.discount_percent || 0;
|
||||||
const discountAmount = Math.round(basePrice * discountPercent / 100);
|
const discountAmount = Math.round(basePrice * discountPercent / 100);
|
||||||
const totalPrice = basePrice - discountAmount;
|
const totalPrice = basePrice - discountAmount;
|
||||||
@@ -336,7 +353,9 @@ function validatePromocodeAPI(req, res) {
|
|||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: 'Database error' });
|
||||||
if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' });
|
if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' });
|
||||||
const guestsCount = parseInt(guests) || 1;
|
const guestsCount = parseInt(guests) || 1;
|
||||||
const basePrice = config.calculateBasePrice(room_type, checkin, checkout) * guestsCount;
|
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 discountAmount = Math.round(basePrice * promo.discount_percent / 100);
|
||||||
const totalPrice = basePrice - discountAmount;
|
const totalPrice = basePrice - discountAmount;
|
||||||
res.json({
|
res.json({
|
||||||
@@ -348,6 +367,7 @@ function validatePromocodeAPI(req, res) {
|
|||||||
code: promo.code
|
code: promo.code
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportCSV(req, res) {
|
function exportCSV(req, res) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
const { getRoomBasePriceByType } = config;
|
const { getRoomBasePriceByType } = config;
|
||||||
|
const seasonalModule = require('../seasonalPrices');
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
|
|
||||||
@@ -8,7 +9,6 @@ function init(database) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEffectivePrice(roomType, date, callback) {
|
function getEffectivePrice(roomType, date, callback) {
|
||||||
const seasonalModule = require('../seasonalPrices');
|
|
||||||
seasonalModule.getSeasonalPrice(roomType, date, (err, seasonalPrice) => {
|
seasonalModule.getSeasonalPrice(roomType, date, (err, seasonalPrice) => {
|
||||||
if (err || !seasonalPrice) {
|
if (err || !seasonalPrice) {
|
||||||
const basePrice = getRoomBasePriceByType(roomType);
|
const basePrice = getRoomBasePriceByType(roomType);
|
||||||
@@ -52,11 +52,11 @@ function calculateNightPrices(roomType, checkin, checkout, callback) {
|
|||||||
|
|
||||||
function validatePromocode(promocode, callback) {
|
function validatePromocode(promocode, callback) {
|
||||||
if (!promocode) return callback(null, null);
|
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) => {
|
db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [promocode], (err, row) => {
|
||||||
if (err || !row) return callback(null, null);
|
if (err || !row) return callback(null, null);
|
||||||
if (row.valid_from && row.valid_from > now) return callback(null, null);
|
if (row.valid_from && new Date(row.valid_from) > now) return callback(null, null);
|
||||||
if (row.valid_to && row.valid_to < now) return callback(null, null);
|
if (row.valid_to && new Date(row.valid_to) < now) return callback(null, null);
|
||||||
if (row.valid_days) {
|
if (row.valid_days) {
|
||||||
const createdDate = new Date(row.created_at);
|
const createdDate = new Date(row.created_at);
|
||||||
const expireDate = new Date(createdDate.getTime() + row.valid_days * 24 * 60 * 60 * 1000);
|
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`;
|
FROM rooms r WHERE r.id = ? AND r.is_active = 1`;
|
||||||
params = [checkout, checkin, roomId];
|
params = [checkout, checkin, roomId];
|
||||||
} else {
|
} 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
|
(SELECT COUNT(b.id) FROM bookings b
|
||||||
WHERE b.room_type = r.type AND b.status IN ('новая','оплачена','зарезервирована','заселена')
|
WHERE b.room_type = ? AND b.status IN ('новая','оплачена','зарезервирована','заселена')
|
||||||
AND b.checkin_date < ? AND b.checkout_date > ?), 0) as booked
|
AND b.checkin_date < ? AND b.checkout_date > ?) as booked
|
||||||
FROM rooms r WHERE r.type = ? AND r.is_active = 1 LIMIT 1`;
|
FROM rooms r WHERE r.type = ? AND r.is_active = 1`;
|
||||||
params = [checkout, checkin, roomType];
|
params = [roomType, checkout, checkin, roomType];
|
||||||
}
|
}
|
||||||
|
|
||||||
db.get(sql, params, (err, row) => {
|
db.get(sql, params, (err, row) => {
|
||||||
@@ -96,7 +96,7 @@ function checkAvailability(roomId, roomType, checkin, checkout, requestedCount,
|
|||||||
|
|
||||||
function createBooking(req, res) {
|
function createBooking(req, res) {
|
||||||
const { name, phone, adults, children, checkin, checkout, wishes, room, room_id, promocode } = req.body;
|
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' });
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,18 +182,10 @@ function getBookingHistory(req, res) {
|
|||||||
res.json(rows);
|
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) {
|
function setupRoutes(app, authenticateToken, requireAdmin) {
|
||||||
app.post('/api/bookings', createBooking);
|
app.post('/api/bookings', createBooking);
|
||||||
app.get('/api/bookings', getBookings);
|
app.get('/api/bookings', getBookings);
|
||||||
app.get('/api/admin/bookings/:id/history', authenticateToken, getBookingHistory);
|
app.get('/api/admin/bookings/:id/history', authenticateToken, getBookingHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, setupRoutes, validatePromocode, logHistory, calculateNightPrices, checkAvailability };
|
module.exports = { init, setupRoutes, validatePromocode, calculateNightPrices, checkAvailability };
|
||||||
|
|||||||
@@ -252,34 +252,36 @@ function getAvailability(req, res) {
|
|||||||
return res.status(400).json({ error: 'checkin и checkout обязательны' });
|
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 (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;
|
let processed = 0;
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
rooms.forEach(room => {
|
typeGroups.forEach(group => {
|
||||||
db.get(
|
db.get(
|
||||||
`SELECT COUNT(b.id) as booked FROM bookings b
|
`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 > ?`,
|
AND b.checkin_date < ? AND b.checkout_date > ?`,
|
||||||
[room.id, checkout, checkin],
|
[group.type, checkout, checkin],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
const booked = err ? 0 : (row?.booked || 0);
|
const booked = err ? 0 : (row?.booked || 0);
|
||||||
result.push({
|
result.push({
|
||||||
type: room.type,
|
type: group.type,
|
||||||
name: room.name,
|
name: group.name,
|
||||||
id: room.id,
|
id: group.id,
|
||||||
total: room.rooms_count,
|
total: group.total,
|
||||||
booked: booked,
|
booked: booked,
|
||||||
available: Math.max(0, room.rooms_count - booked),
|
available: Math.max(0, group.total - booked),
|
||||||
price_per_night: room.price_per_night,
|
price_per_night: group.price_per_night,
|
||||||
max_guests: room.max_guests
|
max_guests: group.max_guests
|
||||||
});
|
});
|
||||||
processed++;
|
processed++;
|
||||||
if (processed === rooms.length) {
|
if (processed === typeGroups.length) {
|
||||||
result.sort((a, b) => a.price_per_night - b.price_per_night);
|
result.sort((a, b) => a.price_per_night - b.price_per_night);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -910,6 +910,7 @@
|
|||||||
try { localStorage.setItem('cookie_consent', '1'); } catch(e) {}
|
try { localStorage.setItem('cookie_consent', '1'); } catch(e) {}
|
||||||
document.getElementById('cookieBanner').classList.remove('show');
|
document.getElementById('cookieBanner').classList.remove('show');
|
||||||
}
|
}
|
||||||
|
window.acceptCookies = acceptCookies;
|
||||||
|
|
||||||
function initCookieBanner() {
|
function initCookieBanner() {
|
||||||
try {
|
try {
|
||||||
@@ -918,7 +919,11 @@
|
|||||||
document.getElementById('cookieBanner').classList.add('show');
|
document.getElementById('cookieBanner').classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initCookieBanner);
|
||||||
|
} else {
|
||||||
initCookieBanner();
|
initCookieBanner();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async function runTests() {
|
|||||||
});
|
});
|
||||||
assert(promoValidation1.status === 200, 'Валидация успешна');
|
assert(promoValidation1.status === 200, 'Валидация успешна');
|
||||||
const promo1 = promoValidation1.data;
|
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})`);
|
assert(promo1.base_price === 1500, `Базовая цена для 1 гостя = 1500 (получено: ${promo1.base_price})`);
|
||||||
|
|
||||||
console.log('\n--- Тест 3: Валидация промокода с 2 гостями ---');
|
console.log('\n--- Тест 3: Валидация промокода с 2 гостями ---');
|
||||||
@@ -84,7 +84,7 @@ async function runTests() {
|
|||||||
});
|
});
|
||||||
assert(promoValidation2.status === 200, 'Валидация успешна');
|
assert(promoValidation2.status === 200, 'Валидация успешна');
|
||||||
const promo2 = promoValidation2.data;
|
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})`);
|
assert(promo2.base_price === 3000, `Базовая цена для 2 гостей = 3000 (получено: ${promo2.base_price})`);
|
||||||
|
|
||||||
console.log('\n--- Тест 4: Валидация промокода с 3 гостями и 2 ночами ---');
|
console.log('\n--- Тест 4: Валидация промокода с 3 гостями и 2 ночами ---');
|
||||||
|
|||||||
Reference in New Issue
Block a user