This commit is contained in:
2026-05-11 20:38:05 +05:00
parent 75247d9821
commit 144aedab99
7 changed files with 118 additions and 53 deletions

View File

@@ -68,9 +68,9 @@ function getBookingsForAdmin(req, res) {
const params = [];
if (search) {
whereClause += ' AND (b.name LIKE ? OR b.phone LIKE ? OR b.room_type LIKE ?)';
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);
params.push(searchTerm, searchTerm, searchTerm, searchTerm);
}
if (statusFilter && statusFilter !== 'all') {
@@ -78,7 +78,7 @@ function getBookingsForAdmin(req, res) {
params.push(statusFilter);
}
db.get(`SELECT COUNT(*) as total FROM bookings b WHERE ${whereClause}`, params, (err, countRow) => {
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' });
@@ -87,8 +87,10 @@ function getBookingsForAdmin(req, res) {
const total = countRow.total;
const totalPages = Math.ceil(total / limit);
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
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) => {
@@ -123,7 +125,11 @@ function updateBookingStatus(req, res) {
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 FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
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 });
});
@@ -133,28 +139,41 @@ function updateBookingStatus(req, res) {
function updateBookingRoom(req, res) {
const bookingId = parseInt(req.params.id);
const { room_type } = req.body;
const validRooms = config.ROOM_TYPE_LIST;
if (!room_type || !validRooms.includes(room_type)) {
return res.status(400).json({ error: 'Invalid room type. Valid: ' + validRooms.join(', ') });
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 room_type, checkin_date, checkout_date, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
db.get(`SELECT * FROM rooms WHERE id = ?`, [room_id], (err, room) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'Booking not found' });
const oldValue = row.room_type || 'Не указан';
const basePrice = config.calculateBasePrice(room_type, row.checkin_date, row.checkout_date);
const discountAmount = Math.round(basePrice * (row.discount_percent || 0) / 100);
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_type = ?, base_price = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
[room_type, basePrice, discountAmount, totalPrice, bookingId], (err) => {
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_type', oldValue, room_type);
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
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) {
@@ -167,7 +186,11 @@ function updateBookingComment(req, res) {
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 FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
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 });
});
@@ -192,7 +215,11 @@ function updateBookingDiscount(req, res) {
[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 FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
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 });
});
@@ -287,7 +314,11 @@ function updateBookingDetails(req, res) {
});
});
function finishUpdate() {
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
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 });
});

View File

@@ -30,7 +30,7 @@ function validatePromocode(promocode, callback) {
}
function createBooking(req, res) {
const { name, phone, adults, children, checkin, checkout, wishes, room, promocode } = req.body;
const { name, phone, adults, children, checkin, checkout, wishes, room, room_id, promocode } = req.body;
if (!name || !phone || !adults || !checkin || !checkout) {
return res.status(400).json({ error: 'Missing required fields' });
}
@@ -46,9 +46,10 @@ function createBooking(req, res) {
const safeBasePrice = basePrice || 0;
const discountAmount = Math.round(safeBasePrice * discountPercent / 100);
const totalPrice = safeBasePrice - discountAmount;
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, base_price, discount_percent, discount_amount, total_price, promocode_id)
VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?)`);
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, room || null,
const roomIdValue = room_id ? parseInt(room_id) : null;
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, room_id, base_price, discount_percent, discount_amount, total_price, promocode_id)
VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?, ?)`);
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, room || null, roomIdValue,
safeBasePrice || null, discountPercent || 0, discountAmount || 0, totalPrice || null, promocodeId, function(err) {
if (err) {
console.error(err);

View File

@@ -626,7 +626,7 @@ function initTabs() {
document.getElementById('tab-' + tab).classList.add('active');
if (tab === 'dashboard') loadDashboard();
if (tab === 'users') loadUsers();
if (tab === 'bookings') { bookingsLoaded = false; loadBookings(); }
if (tab === 'bookings') { bookingsLoaded = false; loadRoomsForBookingEdit(); loadBookings(); }
if (tab === 'rooms') loadRooms();
if (tab === 'promocodes') loadPromocodes();
if (tab === 'reviews') loadReviews();
@@ -701,14 +701,14 @@ async function loadDashboard() {
document.getElementById('statAdmins').textContent = users.filter(u => u.role === 'admin').length;
} catch(e) {}
try {
const rows = await api('/api/admin/bookings');
document.getElementById('statBookings').textContent = rows.length;
document.getElementById('statNew').textContent = rows.filter(r => r.status === 'новая').length;
document.getElementById('recentBookings').innerHTML = rows.filter(r => r.status !== 'отменена' && r.status !== 'выехала').slice(0, 5).map(r => '<tr>' +
'<td>' + esc(r.name) + '</td><td>' + esc(r.room_type || '—') + '</td>' +
const rows = await api('/api/admin/bookings?limit=100');
document.getElementById('statBookings').textContent = rows.total || rows.data.length;
document.getElementById('statNew').textContent = rows.data.filter(r => r.status === 'новая').length;
document.getElementById('recentBookings').innerHTML = rows.data.filter(r => r.status !== 'отменена' && r.status !== 'выехала').slice(0, 5).map(r => '<tr>' +
'<td>' + esc(r.name) + '</td><td>' + esc(r.room_name ? r.room_type + ' — ' + r.room_name : r.room_type || '—') + '</td>' +
'<td>' + esc(r.checkin_date) + '</td><td>' + esc(r.checkout_date) + '</td>' +
'<td><span class="badge badge-status badge-status-' + r.status + '">' + r.status + '</span></td></tr>').join('');
if (!rows.filter(r => r.status !== 'отменена' && r.status !== 'выехала').length) {
if (!rows.data.filter(r => r.status !== 'отменена' && r.status !== 'выехала').length) {
document.getElementById('recentBookings').innerHTML = '<tr><td colspan="5" class="text-center text-muted">Нет данных</td></tr>';
}
} catch(e) {
@@ -739,12 +739,22 @@ async function loadUsers() {
}
let allBookingsData = [];
let allRoomsData = [];
let sortDirection = 'asc';
let bookingsLoaded = false;
let currentPage = 1;
let totalPages = 1;
let searchTimeout = null;
async function loadRoomsForBookingEdit() {
try {
const rooms = await api('/api/admin/rooms');
allRoomsData = rooms;
} catch(e) {
console.error('Failed to load rooms:', e);
}
}
async function loadBookings(resetPage = true) {
if (resetPage) currentPage = 1;
try {
@@ -858,7 +868,6 @@ function renderBookings() {
});
const statuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'];
const rooms = ['2x-местный', '3х-местный', 'Семейный', 'Люкс'];
const tbody = document.getElementById('allBookings');
if (rows.length === 0) {
@@ -873,14 +882,18 @@ function renderBookings() {
if (checkoutDiff === 0 && r.status !== 'отменена' && r.status !== 'выехала') rowClass = 'row-checkout-today';
const selectOpts = statuses.map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s.charAt(0).toUpperCase() + s.slice(1) + '</option>').join('');
const roomOpts = '<option value="">—</option>' + rooms.map(s => '<option value="' + s + '"' + (r.room_type === s ? ' selected' : '') + '>' + s + '</option>').join('');
const roomDisplay = r.room_name ? r.room_type + ' — ' + r.room_name : r.room_type || '—';
const roomOpts = '<option value="">—</option>' + allRoomsData.map(room =>
'<option value="' + room.id + '"' + (r.room_id === room.id ? ' selected' : '') + '>' + room.type + ' — ' + room.name + '</option>'
).join('');
const commentHtml = '<input type="text" class="form-control form-control-sm" style="width: 120px; font-size: 0.8rem;" value="' + esc(r.comment || '') + '" onchange="changeComment(' + r.id + ', this.value)" placeholder="Комментарий">';
const discountHtml = '<input type="number" min="0" max="99" class="form-control form-control-sm" style="width: 70px; font-size: 0.8rem;" value="' + (r.discount_percent || 0) + '" onchange="changeDiscount(' + r.id + ', this.value)">';
return '<tr class="' + rowClass + '">' +
'<td><strong>' + esc(r.name) + '</strong></td><td>' + esc(r.phone) + '</td>' +
'<td><select class="form-select form-select-sm room-select" style="width: 120px; font-size: 0.8rem;" data-booking-id="' + r.id + '" onchange="changeRoom(' + r.id + ', this.value)">' + roomOpts + '</select></td>' +
'<td><select class="form-select form-select-sm room-select" style="width: 160px; font-size: 0.75rem;" data-booking-id="' + r.id + '" onchange="changeRoom(' + r.id + ', this.value)">' + roomOpts + '</select></td>' +
'<td><input type="number" min="0" class="form-control form-control-sm" style="width:60px; font-size:0.8rem;" value="' + r.adults + '" onchange="changeDetails(' + r.id + ', \'adults\', this.value)"></td>' +
'<td><input type="number" min="0" class="form-control form-control-sm" style="width:60px; font-size:0.8rem;" value="' + r.children + '" onchange="changeDetails(' + r.id + ', \'children\', this.value)"></td>' +
'<td><input type="date" class="form-control form-control-sm" style="width:130px; font-size:0.8rem;" value="' + esc(r.checkin_date) + '" onchange="changeDetails(' + r.id + ', \'checkin_date\', this.value)"></td>' +
@@ -914,12 +927,12 @@ async function changeStatus(id, status) {
} catch(err) { showToast(err.message, 'error'); }
}
async function changeRoom(id, room_type) {
async function changeRoom(id, room_id) {
try {
const data = await api('/api/admin/bookings/' + id + '/room', { method: 'PATCH', body: JSON.stringify({ room_type }) });
const data = await api('/api/admin/bookings/' + id + '/room', { method: 'PATCH', body: JSON.stringify({ room_id: room_id || null }) });
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
renderBookings(); updateBookingStats(); loadDashboard();
showToast('Номер обновлён: ' + (room_type || 'Не указан'));
showToast('Номер обновлён');
} catch(err) { showToast(err.message, 'error'); }
}
@@ -1544,7 +1557,7 @@ function renderRooms(rooms) {
(extraBedsText ? '<div class="room-admin-extra">' + extraBedsText + '</div>' : '') +
'<div class="room-admin-actions">' +
'<button class="btn-primary-custom btn-sm" onclick="showRoomModal(' + r.id + ')"><i class="fas fa-edit"></i> Редактировать</button>' +
'<button class="btn-danger-custom btn-sm" onclick="deleteRoom(' + r.id + ', \'' + escAttr(r.name) + '\')"><i class="fas fa-trash"></i></button>' +
'<button class="btn-' + (r.is_active ? 'warning' : 'success') + ' btn-sm" onclick="toggleRoomActive(' + r.id + ', ' + !r.is_active + ')"><i class="fas fa-' + (r.is_active ? 'eye-slash' : 'eye') + '"></i> ' + (r.is_active ? 'Скрыть' : 'Показать') + '</button>' +
'</div>' +
'</div>' +
'</div>';
@@ -1663,11 +1676,13 @@ async function saveRoom() {
} catch(err) { showToast(err.message, 'error'); }
}
async function deleteRoom(id, name) {
if (!confirm('Удалить номер "' + name + '"? Это скроет номер из списка.')) return;
async function toggleRoomActive(id, isActive) {
try {
await api('/api/admin/rooms/' + id, { method: 'DELETE' });
showToast('Номер удалён');
await api('/api/admin/rooms/' + id, {
method: 'PATCH',
body: JSON.stringify({ is_active: isActive ? 1 : 0 })
});
showToast(isActive ? 'Номер показан на сайте' : 'Номер скрыт с сайта');
loadRooms();
} catch(err) { showToast(err.message, 'error'); }
}

View File

@@ -461,6 +461,7 @@
<div class="mb-3">
<label class="form-label">Выбранный номер</label>
<input type="text" class="form-control" id="selectedRoom" name="room" readonly style="background: #f0f4f8; font-weight: 600;">
<input type="hidden" id="selectedRoomId" name="room_id">
</div>
<div class="row g-3">
<div class="col-md-6">

View File

@@ -269,6 +269,7 @@ document.getElementById('bookingForm').addEventListener('submit', async function
checkout: this.querySelector('[name="checkout"]').value,
wishes: this.querySelector('[name="wishes"]').value,
room: document.getElementById('selectedRoom').value,
room_id: parseInt(document.getElementById('selectedRoomId').value) || null,
promocode: document.getElementById('promocodeInput').value.trim() || null
};

View File

@@ -82,7 +82,7 @@ function renderRoomsPublic(rooms) {
<span class="amount">от ${room.price_per_night || 0} ₽</span>
<span class="period"> / ночь</span>
</div>
<button class="btn-book" data-bs-toggle="modal" data-bs-target="#bookingModal" data-room="${room.type}" data-max-guests="${room.max_guests}">Забронировать</button>
<button class="btn-book" data-bs-toggle="modal" data-bs-target="#bookingModal" data-room-id="${room.id}" data-room-type="${room.type}" data-room-name="${escapeHtml(room.name)}" data-max-guests="${room.max_guests}">Забронировать</button>
</div>
</div>
</div>
@@ -93,13 +93,28 @@ function renderRoomsPublic(rooms) {
initRoomBookingHandlers();
}
function updateRoomPrices(rooms) {
const prices = {};
const maxGuests = {};
rooms.forEach(room => {
prices[room.type] = room.price_per_night;
maxGuests[room.type] = room.max_guests;
});
if (window.updateRoomPricesData) {
window.updateRoomPricesData(prices, maxGuests);
}
}
function initRoomBookingHandlers() {
document.querySelectorAll('.btn-book').forEach(btn => {
btn.addEventListener('click', function() {
const room = this.getAttribute('data-room');
const roomId = this.getAttribute('data-room-id');
const roomType = this.getAttribute('data-room-type');
const roomName = this.getAttribute('data-room-name');
const maxGuests = parseInt(this.getAttribute('data-max-guests')) || 4;
document.getElementById('selectedRoom').value = room;
updateGuestOptionsDynamic(room, maxGuests);
document.getElementById('selectedRoom').value = roomType + ' — ' + roomName;
document.getElementById('selectedRoomId').value = roomId;
updateGuestOptionsDynamic(roomType, maxGuests);
hidePriceInfo();
});
});

View File

@@ -195,6 +195,7 @@ db.serialize(() => {
'wishes TEXT',
'status TEXT DEFAULT "новая"',
'room_type TEXT',
'room_id INTEGER',
'comment TEXT',
'base_price REAL',
'discount_percent INTEGER DEFAULT 0',