This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -141,10 +141,13 @@ function updateBookingRoom(req, res) {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
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 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);
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
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,7 +353,9 @@ 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;
|
||||
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({
|
||||
@@ -348,6 +367,7 @@ function validatePromocodeAPI(req, res) {
|
||||
code: promo.code
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function exportCSV(req, res) {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCookieBanner);
|
||||
} else {
|
||||
initCookieBanner();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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 ночами ---');
|
||||
|
||||
Reference in New Issue
Block a user