Files
hotell777_260507/modules/adminBookings/index.js
kalugin66 edbe54b915
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 1m0s
статистика2
2026-07-26 22:29:38 +05:00

494 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const config = require('../../config');
const { validatePromocode, calculateNightPrices, checkAvailability } = require('../bookings');
const promocodeRateLimit = new Map();
const PROMOCODE_WINDOW = 60 * 1000;
const MAX_PROMOCODE_REQUESTS = 20;
function checkPromocodeRateLimit(ip) {
const now = Date.now();
const record = promocodeRateLimit.get(ip);
if (!record) {
promocodeRateLimit.set(ip, { count: 1, firstRequest: now });
return true;
}
if (now - record.firstRequest > PROMOCODE_WINDOW) {
promocodeRateLimit.set(ip, { count: 1, firstRequest: now });
return true;
}
if (record.count >= MAX_PROMOCODE_REQUESTS) {
return false;
}
record.count++;
return true;
}
setInterval(() => {
const now = Date.now();
for (const [ip, record] of promocodeRateLimit.entries()) {
if (now - record.firstRequest > PROMOCODE_WINDOW) {
promocodeRateLimit.delete(ip);
}
}
}, PROMOCODE_WINDOW);
let db;
function init(database) {
db = database;
}
function getBookingsForAdmin(req, res) {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const search = req.query.search || '';
const statusFilter = req.query.status || '';
const since = req.query.since || '';
let whereClause = '1=1';
const params = [];
if (search) {
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, searchTerm);
}
if (statusFilter && statusFilter !== 'all') {
whereClause += ' AND b.status = ?';
params.push(statusFilter);
}
if (since) {
whereClause += ' AND b.created_at >= ?';
params.push(since);
}
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' });
}
const total = countRow.total;
const totalPages = Math.ceil(total / limit);
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) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json({
data: rows,
pagination: {
page,
limit,
total,
totalPages
}
});
});
});
}
function updateBookingStatus(req, res) {
const bookingId = parseInt(req.params.id);
const { status } = req.body;
const validStatuses = config.STATUS_LIST;
if (!status || !validStatuses.includes(status)) {
return res.status(400).json({ error: 'Invalid status. Valid: ' + validStatuses.join(', ') });
}
db.get(`SELECT status FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.status;
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, 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' });
try { require('../email').sendStatusChange(row, oldValue, status); } catch {}
res.json({ message: 'Status updated', booking: row });
});
});
});
}
function updateBookingRoom(req, res) {
const bookingId = parseInt(req.params.id);
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 (!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' });
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);
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();
});
});
});
}
function updateBookingComment(req, res) {
const bookingId = parseInt(req.params.id);
const { comment } = req.body;
db.get(`SELECT comment FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.comment || 'Нет';
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, 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 });
});
});
});
}
function updateBookingDiscount(req, res) {
const bookingId = parseInt(req.params.id);
const { discount_percent } = req.body;
if (discount_percent === undefined || discount_percent < 0 || discount_percent > 99) {
return res.status(400).json({ error: 'Discount percent must be between 0 and 99' });
}
db.get(`SELECT base_price, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.discount_percent || 0;
const basePrice = row.base_price || 0;
const discountAmount = Math.round(basePrice * discount_percent / 100);
const totalPrice = basePrice - discountAmount;
db.run(`UPDATE bookings SET discount_percent = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[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, 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 });
});
});
});
}
function updateBookingDetails(req, res) {
const bookingId = parseInt(req.params.id);
const { adults, children, checkin_date, checkout_date } = req.body;
if (adults === undefined && children === undefined && checkin_date === undefined && checkout_date === undefined) {
return res.status(400).json({ error: 'No fields to update' });
}
const newAdults = adults !== undefined ? parseInt(adults) : undefined;
const newChildren = children !== undefined ? parseInt(children) : undefined;
const newCheckin = checkin_date || undefined;
const newCheckout = checkout_date || undefined;
if (newAdults !== undefined && (isNaN(newAdults) || newAdults < 1)) {
return res.status(400).json({ error: 'Количество взрослых должно быть минимум 1' });
}
if (newChildren !== undefined && (isNaN(newChildren) || newChildren < 0)) {
return res.status(400).json({ error: 'Количество детей не может быть отрицательным' });
}
const checkinDate = newCheckin ? new Date(newCheckin) : new Date();
const checkoutDate = newCheckout ? new Date(newCheckout) : new Date();
if (newCheckin && isNaN(checkinDate.getTime())) {
return res.status(400).json({ error: 'Некорректная дата заезда' });
}
if (newCheckout && isNaN(checkoutDate.getTime())) {
return res.status(400).json({ error: 'Некорректная дата выезда' });
}
if (newCheckin && newCheckout && checkoutDate <= checkinDate) {
return res.status(400).json({ error: 'Дата выезда должна быть позже даты заезда' });
}
db.get(`SELECT * FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValues = {
adults: row.adults,
children: row.children,
checkin_date: row.checkin_date,
checkout_date: row.checkout_date
};
let fields = [];
let values = [];
if (newAdults !== undefined) { fields.push('adults = ?'); values.push(newAdults); }
if (newChildren !== undefined) { fields.push('children = ?'); values.push(newChildren); }
if (newCheckin !== undefined) { fields.push('checkin_date = ?'); values.push(newCheckin); }
if (newCheckout !== undefined) { fields.push('checkout_date = ?'); values.push(newCheckout); }
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
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 (newAdults !== undefined && newAdults !== oldValues.adults) {
logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), newAdults.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);
}
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 = 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;
const roomType = row.room_type;
if (!roomType) {
return finishUpdate();
}
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;
db.run(`UPDATE bookings SET base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[basePrice, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
finishUpdate();
});
});
function finishUpdate() {
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 });
});
}
});
});
}
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 validatePromocodeAPI(req, res) {
const clientIp = req.ip || req.connection.remoteAddress || 'unknown';
if (!checkPromocodeRateLimit(clientIp)) {
return res.status(429).json({ error: 'Too many requests. Please try again later.' });
}
const { code, room_type, checkin, checkout, guests } = req.body;
if (!code) return res.status(400).json({ error: 'Promocode required' });
validatePromocode(code, (err, promo) => {
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;
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
});
});
});
}
function exportCSV(req, res) {
const statusFilter = req.query.status || '';
const from = req.query.from || '';
const to = req.query.to || '';
let whereClause = '1=1';
const params = [];
if (statusFilter && statusFilter !== 'all') {
whereClause += ' AND b.status = ?';
params.push(statusFilter);
}
if (from) {
whereClause += ' AND b.checkin_date >= ?';
params.push(from);
}
if (to) {
whereClause += ' AND b.checkin_date <= ?';
params.push(to);
}
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE ${whereClause} ORDER BY b.checkin_date ASC`, params, (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' });
const headers = ['ID', 'Имя', 'Телефон', 'Взрослых', 'Детей', 'Заезд', 'Выезд', 'Тип номера', 'Комментарий', 'Базовая цена', 'Скидка %', 'Сумма скидки', 'Итого', 'Промокод', 'Статус', 'Пожелания'];
function escapeCsv(val) {
if (val === null || val === undefined) return '';
const str = String(val);
if (str.includes(';') || str.includes('"') || str.includes('\n')) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
}
const csvRows = ['\uFEFF' + headers.join(';')];
rows.forEach(r => {
csvRows.push([
r.id, r.name, r.phone, r.adults, r.children,
r.checkin_date, r.checkout_date, r.room_type, r.comment,
r.base_price, r.discount_percent, r.discount_amount, r.total_price,
r.promocode_code, r.status, r.wishes
].map(escapeCsv).join(';'));
});
const dateStr = new Date().toISOString().split('T')[0];
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="bookings_${dateStr}.csv"`);
res.send(csvRows.join('\n'));
});
}
function getCalendar(req, res) {
const month = req.query.month;
if (!month) return res.status(400).json({ error: 'month parameter required (YYYY-MM)' });
const startDate = month + '-01';
const [y, m] = month.split('-').map(Number);
const endDate = new Date(y, m, 0).toISOString().split('T')[0];
db.all(`SELECT r.type, r.name, r.id as room_id, r.rooms_count, r.price_per_night, r.image_path FROM rooms r WHERE r.is_active = 1 ORDER BY r.price_per_night ASC`, [], (err, rooms) => {
if (err) return res.status(500).json({ error: 'Database error' });
db.all(`SELECT b.id, b.name, b.status, b.room_type, b.room_id, b.checkin_date, b.checkout_date, b.phone
FROM bookings b
WHERE b.status NOT IN ('отменена','выехала')
AND b.checkin_date <= ? AND b.checkout_date > ?
ORDER BY b.checkin_date ASC`,
[endDate, startDate], (err, bookings) => {
if (err) return res.status(500).json({ error: 'Database error' });
const days = {};
const currentDate = new Date(startDate);
const end = new Date(endDate);
while (currentDate <= end) {
const dateStr = currentDate.toISOString().split('T')[0];
days[dateStr] = {};
rooms.forEach(r => {
days[dateStr][r.type] = { total: r.rooms_count, booked: 0, bookings: [] };
});
currentDate.setDate(currentDate.getDate() + 1);
}
bookings.forEach(b => {
let d = new Date(Math.max(new Date(b.checkin_date).getTime(), new Date(startDate).getTime()));
const endD = new Date(Math.min(new Date(b.checkout_date).getTime(), new Date(endDate).getTime()));
while (d < endD) {
const dateStr = d.toISOString().split('T')[0];
const type = b.room_type || 'Не указан';
if (days[dateStr]) {
if (!days[dateStr][type]) {
days[dateStr][type] = { total: 0, booked: 0, bookings: [] };
}
days[dateStr][type].booked++;
if (days[dateStr][type].bookings.length < 10) {
days[dateStr][type].bookings.push({ id: b.id, name: b.name, status: b.status, phone: b.phone });
}
}
d.setDate(d.getDate() + 1);
}
});
res.json({ month, rooms: rooms.map(r => ({ type: r.type, name: r.name, id: r.room_id, rooms_count: r.rooms_count, price_per_night: r.price_per_night })), days });
});
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.get('/api/admin/bookings', authenticateToken, requireAdmin, getBookingsForAdmin);
app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, updateBookingStatus);
app.patch('/api/admin/bookings/:id/room', authenticateToken, requireAdmin, updateBookingRoom);
app.patch('/api/admin/bookings/:id/comment', authenticateToken, requireAdmin, updateBookingComment);
app.patch('/api/admin/bookings/:id/discount', authenticateToken, requireAdmin, updateBookingDiscount);
app.patch('/api/admin/bookings/:id/details', authenticateToken, requireAdmin, updateBookingDetails);
app.post('/api/promocodes/validate', validatePromocodeAPI);
app.get('/api/admin/export/bookings', authenticateToken, exportCSV);
app.get('/api/admin/calendar', authenticateToken, getCalendar);
}
module.exports = { init, setupRoutes };