Files
hotell777_260507/modules/adminBookings/index.js
2026-05-11 23:44:01 +05:00

374 lines
18 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 bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('../../config');
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;
let JWT_SECRET;
function init(database, jwtSecret) {
db = database;
JWT_SECRET = jwtSecret;
}
function validatePromocode(promocode, callback) {
if (!promocode) return callback(null, null);
const now = new Date().toISOString();
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_days) {
const createdDate = new Date(row.created_at);
const expireDate = new Date(createdDate.getTime() + row.valid_days * 24 * 60 * 60 * 1000);
if (expireDate < new Date()) return callback(null, null);
}
callback(null, row);
});
}
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 || '';
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);
}
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' });
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' });
const oldValue = booking.room_name ? booking.room_type + ' — ' + booking.room_name : booking.room_type || 'Не указан';
const newValue = room.type + ' — ' + room.name;
const basePrice = config.calculateBasePrice(room.type, booking.checkin_date, booking.checkout_date);
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 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 (adults !== undefined && adults !== oldValues.adults) {
logHistory(bookingId, req.user.id, req.user.login, 'adults', oldValues.adults.toString(), adults.toString());
}
if (children !== undefined && children !== oldValues.children) {
logHistory(bookingId, req.user.id, req.user.login, 'children', oldValues.children.toString(), children.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 = adults !== undefined ? adults : oldValues.adults;
const finalChildren = children !== undefined ? children : 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();
}
db.get(`SELECT price_per_guest FROM rooms WHERE type = ? AND is_active = 1`, [roomType], (err, roomData) => {
if (err) return res.status(500).json({ error: 'Database error' });
const pricePerGuest = roomData ? roomData.price_per_guest : config.getRoomPrice(roomType);
const nights = config.calculateNights(finalCheckin, finalCheckout);
const basePrice = pricePerGuest * totalGuests * nights;
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;
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
});
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.get('/api/admin/bookings', authenticateToken, 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);
}
module.exports = { init, setupRoutes };