статистика2
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 1m0s

This commit is contained in:
2026-07-26 22:29:38 +05:00
parent 3f318c2691
commit edbe54b915
6 changed files with 96 additions and 77 deletions

View File

@@ -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
}

View File

@@ -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,25 +141,44 @@ 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' });
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;
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 });
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
});
});
});
}

View File

@@ -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 };

View File

@@ -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);
}

View File

@@ -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();
}
})();
</script>

View File

@@ -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 ночами ---');