Бронирования
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 49s

This commit is contained in:
2026-07-27 01:17:40 +05:00
parent 0277b46e33
commit 52d5b65fa3
2 changed files with 246 additions and 81 deletions

View File

@@ -45,6 +45,12 @@ function getBookingsForAdmin(req, res) {
const search = req.query.search || '';
const statusFilter = req.query.status || '';
const since = req.query.since || '';
const sort = req.query.sort || 'created_at';
const order = (req.query.order || 'desc').toLowerCase();
const sortCols = { created_at: 'b.created_at', checkin_date: 'b.checkin_date', total_price: 'b.total_price' };
const sortCol = sortCols[sort] || 'b.created_at';
const sortDir = order === 'asc' ? 'ASC' : 'DESC';
let whereClause = '1=1';
const params = [];
@@ -56,8 +62,9 @@ function getBookingsForAdmin(req, res) {
}
if (statusFilter && statusFilter !== 'all') {
whereClause += ' AND b.status = ?';
params.push(statusFilter);
const statuses = statusFilter.split(',');
whereClause += ' AND b.status IN (' + statuses.map(() => '?').join(',') + ')';
params.push(...statuses);
}
if (since) {
@@ -79,7 +86,7 @@ function getBookingsForAdmin(req, res) {
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
ORDER BY ${sortCol} ${sortDir}
LIMIT ? OFFSET ?`, [...params, limit, offset], (err, rows) => {
if (err) {
console.error(err);

View File

@@ -85,15 +85,43 @@ body { font-family: 'Inter', sans-serif; background: #f1f5f9; margin: 0; font-si
.review-text-full::-webkit-scrollbar-track { background: #e2e8f0; border-radius: 3px; }
.review-text-full::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 3px; }
tr.row-checkin-soon { background: #fffbeb !important; border-left: 4px solid #f59e0b; }
tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #ef4444; }
/* Booking cards */
.bookings-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.booking-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; }
.booking-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.1); }
.card-bar { width: 4px; flex-shrink: 0; }
.card-bar-новая { background: #3b82f6; }
.card-bar-оплачена { background: #22c55e; }
.card-bar-зарезервирована { background: #f59e0b; }
.card-bar-заселена { background: #a855f7; }
.card-bar-выехала { background: #94a3b8; }
.card-bar-отменена { background: #ef4444; }
.card-inner { flex: 1; padding: 14px; min-width: 0; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card-id { font-size: 0.7rem; color: #94a3b8; }
.card-name { font-size: 1rem; font-weight: 700; color: #0f172a; margin-bottom: 4px; }
.card-phone { font-size: 0.78rem; color: #64748b; margin-bottom: 8px; }
.card-meta { display: flex; gap: 14px; font-size: 0.78rem; color: #475569; margin-bottom: 6px; }
.card-dates { font-size: 0.82rem; color: #0f172a; margin-bottom: 8px; }
.card-nights { font-size: 0.72rem; color: #94a3b8; }
.card-price-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.card-disc-badge { background: #fef3c7; color: #92400e; padding: 1px 7px; border-radius: 10px; font-size: 0.72rem; font-weight: 600; }
.card-base-price { font-size: 0.78rem; color: #94a3b8; text-decoration: line-through; }
.card-total { font-size: 0.95rem; font-weight: 700; color: #0f172a; }
.card-promo { font-size: 0.72rem; color: #6366f1; margin-bottom: 4px; }
.card-comment { font-size: 0.72rem; color: #64748b; background: #f8fafc; padding: 3px 8px; border-radius: 6px; margin-bottom: 8px; }
.card-actions { display: flex; gap: 6px; align-items: center; padding-top: 8px; border-top: 1px solid #f1f5f9; }
.card-soon { background: #fffbeb; }
.card-today { background: #fef2f2; }
.status-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.status-tab { padding: 7px 18px; border-radius: 20px; border: 1px solid #e2e8f0; background: #fff; font-size: 0.82rem; font-weight: 500; cursor: pointer; transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; }
.status-tab:hover { border-color: var(--primary); color: var(--primary); }
.status-tab.active { background: var(--primary); color: #fff; border-color: var(--primary); }
.filter-bar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 20px; }
.filter-bar select, .filter-bar button { padding: 8px 14px; border-radius: 10px; border: 1px solid #e2e8f0; font-size: 0.85rem; background: #fff; cursor: pointer; }
.filter-bar select:focus { border-color: var(--primary); outline: none; }
.filter-bar .filter-label { font-weight: 600; font-size: 0.85rem; color: #64748b; }
.filter-bar .sort-btn { background: var(--primary); color: #fff; border: none; }
.filter-bar .sort-btn:hover { background: #1d4ed8; }
.history-timeline { border-left: 2px solid #e2e8f0; padding-left: 20px; }
.history-item { position: relative; padding: 12px 0; border-bottom: 1px solid #f1f5f9; }
@@ -308,20 +336,23 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
</div>
<div class="card-body-custom">
<div class="filter-bar">
<input type="text" id="searchBookings" class="form-control" style="max-width: 100%; width: 100%; font-size: 0.85rem;" placeholder="Поиск по имени/телефону..." onkeyup="debounceSearch()">
<span class="filter-label">Фильтр:</span>
<select id="filterStatus"><option value="all">Все статусы</option><option value="новая">Новая</option><option value="оплачена">Оплачена</option><option value="зарезервирована">Зарезервирована</option><option value="заселена">Заселена</option><option value="выехала">Выехала</option><option value="отменена">Отменена</option></select>
<select id="filterUrgent"><option value="all">Все записи</option><option value="checkin-soon">Заезд ≤ 3 дней</option><option value="checkout-today">Выезд сегодня</option></select>
<span class="filter-label" style="margin-left: 12px;">Сортировка:</span>
<button class="sort-btn" id="sortAsc" onclick="setSort('asc')"><i class="fas fa-sort-up"></i> Заезд ↑</button>
<button class="sort-btn" id="sortDesc" onclick="setSort('desc')"><i class="fas fa-sort-down"></i> Заезд ↓</button>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead><tr><th>Имя</th><th>Телефон</th><th>Номер</th><th>Взр.</th><th>Дет.</th><th>Заезд</th><th>Выезд</th><th>Пожелания</th><th>Комментарий</th><th>База (₽)</th><th>Скидка (%)</th><th>Сумма скидки (₽)</th><th>Итого (₽)</th><th>Промокод</th><th>Статус</th><th>Действия</th></tr></thead>
<tbody id="allBookings"></tbody>
</table>
<div style="display:flex;gap:8px;width:100%;flex-wrap:wrap;align-items:center;">
<form autocomplete="off" style="display:flex;gap:8px;flex:1;min-width:250px;" onsubmit="return false">
<input type="text" id="searchBookings" class="form-control" style="flex:1;font-size:0.85rem;" placeholder="Поиск по имени/телефону...">
<button type="button" class="btn" style="background:var(--primary);color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:0.85rem;white-space:nowrap;" onclick="applySearch()"><i class="fas fa-search"></i> Применить</button>
<button type="button" class="btn" style="background:#ef4444;color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:0.85rem;white-space:nowrap;" onclick="resetSearch()"><i class="fas fa-times"></i> Сброс</button>
</form>
<select id="filterStatus"><option value="all">Все статусы</option><option value="новая">Новая</option><option value="оплачена">Оплачена</option><option value="зарезервирована">Зарезервирована</option><option value="заселена">Заселена</option><option value="выехала">Выехала</option><option value="отменена">Отменена</option></select>
<select id="filterUrgent"><option value="all">Все записи</option><option value="checkin-soon">Заезд ≤ 3 дней</option><option value="checkout-today">Выезд сегодня</option></select>
<div class="status-tabs" style="margin-bottom:0;">
<button class="status-tab active" data-status="all">Все</button>
<button class="status-tab" data-status="новая">Новые</button>
<button class="status-tab" data-status="оплачена,зарезервирована,заселена">Активные</button>
<button class="status-tab" data-status="выехала,отменена">Завершённые</button>
</div>
</div>
</div>
<div id="bookingsGrid" class="bookings-grid"></div>
<div class="pagination-container" style="display: flex; justify-content: space-between; align-items: center; margin-top: 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">
<div id="paginationInfo" style="font-size: 0.85rem; color: #64748b;"></div>
<div class="pagination-controls" style="display: flex; gap: 8px;">
@@ -871,6 +902,49 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
</div>
</div>
<div class="modal-backdrop-custom" id="editBookingModal">
<div class="modal-custom" style="max-width: 650px;">
<div class="modal-header-custom">
<h3>Заявка #<span id="editBookingNum"></span></h3>
<button class="modal-close" onclick="hideEditBookingModal()">&times;</button>
</div>
<div class="modal-body-custom">
<form id="editBookingForm">
<input type="hidden" id="editBookingId">
<div class="row g-2">
<div class="col-6"><label class="form-label">Имя</label><input type="text" class="form-control" id="editName" readonly></div>
<div class="col-6"><label class="form-label">Телефон</label><input type="text" class="form-control" id="editPhone" readonly></div>
</div>
<div class="row g-2 mt-2">
<div class="col-8"><label class="form-label">Номер</label><select class="form-select" id="editRoom"></select></div>
<div class="col-4"><label class="form-label">Статус</label><select class="form-select" id="editStatus"></select></div>
</div>
<div class="row g-2 mt-2">
<div class="col-3"><label class="form-label">Взр.</label><input type="number" class="form-control" id="editAdults" min="1"></div>
<div class="col-3"><label class="form-label">Дет.</label><input type="number" class="form-control" id="editChildren" min="0"></div>
<div class="col-6"><label class="form-label">Скидка %</label><input type="number" class="form-control" id="editDiscount" min="0" max="99"></div>
</div>
<div class="row g-2 mt-2">
<div class="col-5"><label class="form-label">Заезд</label><input type="date" class="form-control" id="editCheckin"></div>
<div class="col-2 d-flex align-items-end"><span id="editNights" style="display:block;text-align:center;background:#f0f4f8;padding:6px 8px;border-radius:12px;font-size:0.8rem;"></span></div>
<div class="col-5"><label class="form-label">Выезд</label><input type="date" class="form-control" id="editCheckout"></div>
</div>
<div class="mt-2"><label class="form-label">Пожелания</label><input type="text" class="form-control" id="editWishes" readonly></div>
<div class="mt-2"><label class="form-label">Комментарий</label><input type="text" class="form-control" id="editComment"></div>
<div class="mt-2" style="background:#f8fafc;border-radius:8px;padding:12px;">
<div style="display:flex;justify-content:space-between;font-size:0.85rem;"><span>Базовая цена:</span><strong id="editBasePrice"></strong></div>
<div style="display:flex;justify-content:space-between;font-size:0.85rem;color:#16a34a;"><span>Скидка:</span><strong id="editDiscountAmount"></strong></div>
<div style="display:flex;justify-content:space-between;font-size:1.1rem;font-weight:700;border-top:1px solid #e2e8f0;margin-top:8px;padding-top:8px;"><span>Итого:</span><strong id="editTotalPrice" style="color:var(--primary);"></strong></div>
</div>
<div class="mt-3 d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="showHistoryForEdit()"><i class="fas fa-clock"></i> История</button>
<button type="submit" class="btn-primary-custom btn-sm ms-auto"><i class="fas fa-save"></i> Сохранить</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal-backdrop-custom" id="restoreModal">
<div class="modal-custom">
<div class="modal-header-custom">
@@ -1200,7 +1274,6 @@ async function loadUsers() {
let allBookingsData = [];
let allRoomsData = [];
let sortDirection = 'asc';
let bookingsLoaded = false;
let currentPage = 1;
let totalPages = 1;
@@ -1235,7 +1308,7 @@ async function loadBookings(resetPage = true) {
currentPage = response.pagination.page;
bookingsLoaded = true;
renderBookings();
renderBookingCards();
updateBookingStats();
updatePagination();
} catch(err) { showToast('Нет доступа к бронированиям: ' + err.message, 'error'); }
@@ -1305,6 +1378,16 @@ function debounceSearch() {
searchTimeout = setTimeout(() => loadBookings(true), 500);
}
function applySearch() {
if (searchTimeout) { clearTimeout(searchTimeout); searchTimeout = null; }
loadBookings(true);
}
function resetSearch() {
document.getElementById('searchBookings').value = '';
loadBookings(true);
}
function getDaysDiff(dateStr) {
const today = new Date(); today.setHours(0,0,0,0);
const d = new Date(dateStr); d.setHours(0,0,0,0);
@@ -1317,72 +1400,100 @@ function updateBookingStats() {
document.getElementById('bookingStats').innerHTML = Object.entries(stats).map(([k,v]) => '<span class="badge badge-status badge-status-' + k + '" style="margin-right: 4px;">' + v + ' ' + k + '</span>').join('');
}
function renderBookings() {
let rows = [...allBookingsData];
const filterStatus = document.getElementById('filterStatus').value;
const filterUrgent = document.getElementById('filterUrgent').value;
rows.sort((a, b) => {
const da = new Date(a.checkin_date), db = new Date(b.checkin_date);
return sortDirection === 'asc' ? da - db : db - da;
});
const statuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'];
const tbody = document.getElementById('allBookings');
function renderBookingCards() {
const grid = document.getElementById('bookingsGrid');
const rows = [...allBookingsData];
if (rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">Нет записей</td></tr>';
grid.innerHTML = '<div class="text-center text-muted" style="padding:40px;">Нет заявок</div>';
return;
}
tbody.innerHTML = rows.map(r => {
const checkinDiff = getDaysDiff(r.checkin_date);
const checkoutDiff = getDaysDiff(r.checkout_date);
let rowClass = '';
if (checkinDiff >= 0 && checkinDiff <= 3 && r.status !== 'отменена' && r.status !== 'выехала') rowClass = 'row-checkin-soon';
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 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="max-width: 100%; min-width: 80px; 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="max-width: 80px; min-width: 60px; 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="max-width: 100%; min-width: 120px; 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="max-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="max-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="max-width: 100%; min-width: 110px; font-size:0.8rem;" value="' + esc(r.checkin_date) + '" onchange="changeDetails(' + r.id + ', \'checkin_date\', this.value)"></td>' +
'<td><input type="date" class="form-control form-control-sm" style="max-width: 100%; min-width: 110px; font-size:0.8rem;" value="' + esc(r.checkout_date) + '" onchange="changeDetails(' + r.id + ', \'checkout_date\', this.value)"></td>' +
'<td>' + esc(r.wishes || '—') + '</td>' +
'<td>' + commentHtml + '</td>' +
'<td>' + (r.base_price || 0) + '</td>' +
'<td>' + discountHtml + '</td>' +
'<td>' + (r.discount_amount || 0) + '</td>' +
'<td><strong>' + (r.total_price || r.base_price || 0) + '</strong></td>' +
'<td><span class="badge bg-info" style="font-size: 0.7rem;">' + esc(r.promocode_code || '—') + '</span></td>' +
'<td><span class="badge badge-status badge-status-' + r.status + '">' + r.status + '</span></td>' +
'<td style="white-space: nowrap;"><select class="form-select form-select-sm" style="max-width: 100%; min-width: 120px; font-size: 0.8rem;" onchange="changeStatus(' + r.id + ', this.value)">' + selectOpts + '</select> <button class="btn btn-outline-secondary btn-sm" style="padding: 2px 8px; font-size: 0.7rem; margin-left: 4px;" onclick="showHistory(' + r.id + ')"><i class="fas fa-clock"></i></button></td>' +
'</tr>';
const statuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'];
grid.innerHTML = rows.map(r => {
const ciDiff = getDaysDiff(r.checkin_date);
const coDiff = getDaysDiff(r.checkout_date);
let cls = 'booking-card';
if (ciDiff >= 0 && ciDiff <= 3 && r.status !== 'отменена' && r.status !== 'выехала') cls += ' card-soon';
if (coDiff === 0 && r.status !== 'отменена' && r.status !== 'выехала') cls += ' card-today';
const n = calcNights(r.checkin_date, r.checkout_date);
const room = r.room_name ? r.room_type + ' — ' + r.room_name : (r.room_type || '—');
const created = r.created_at ? r.created_at.substring(0, 10) : '';
return '<div class="' + cls + '" onclick="editBooking(' + r.id + ')">' +
'<div class="card-bar card-bar-' + r.status + '"></div>' +
'<div class="card-inner">' +
'<div class="card-head">' +
'<span class="badge badge-status-' + r.status + '">' + r.status + '</span>' +
'<span class="card-id">#' + r.id + ' · ' + created + '</span>' +
'</div>' +
'<div class="card-name">' + esc(r.name) + '</div>' +
'<div class="card-phone"><i class="fas fa-phone fa-xs"></i> ' + esc(r.phone) + '</div>' +
'<div class="card-meta">' +
'<span><i class="fas fa-door-open fa-xs"></i> ' + esc(room) + '</span>' +
'<span><i class="fas fa-users fa-xs"></i> ' + r.adults + ' взр' + (r.children ? ' + ' + r.children + ' дет' : '') + '</span>' +
'</div>' +
'<div class="card-dates"><i class="fas fa-calendar-alt fa-xs"></i> ' + r.checkin_date + ' → ' + r.checkout_date + ' <span class="card-nights">· ' + n + ' ноч.</span></div>' +
'<div class="card-price-row">' +
(r.discount_percent ? '<span class="card-disc-badge">-' + r.discount_percent + '%</span>' : '') +
(r.base_price !== r.total_price ? '<span class="card-base-price">' + (r.base_price || 0) + ' ₽</span>' : '') +
'<span class="card-total">' + (r.total_price || r.base_price || 0) + ' ₽</span>' +
'</div>' +
(r.promocode_code ? '<div class="card-promo">🎫 ' + esc(r.promocode_code) + '</div>' : '') +
(r.comment ? '<div class="card-comment">💬 ' + esc(r.comment) + '</div>' : '') +
'<div class="card-actions" onclick="event.stopPropagation()">' +
'<select class="form-select form-select-sm card-status-select" onchange="changeStatus(' + r.id + ', this.value); event.stopPropagation();" style="max-width:120px;font-size:0.75rem;">' +
statuses.map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') +
'</select>' +
'<button class="btn btn-outline-secondary btn-sm" onclick="showHistory(' + r.id + '); event.stopPropagation();" title="История"><i class="fas fa-clock"></i></button>' +
'</div>' +
'</div>' +
'</div>';
}).join('');
}
function setSort(dir) {
sortDirection = dir;
document.getElementById('sortAsc').style.opacity = dir === 'asc' ? '1' : '0.5';
document.getElementById('sortDesc').style.opacity = dir === 'desc' ? '1' : '0.5';
renderBookings();
function calcNights(ci, co) { return Math.ceil((new Date(co) - new Date(ci)) / 86400000); }
let currentEditBooking = null;
function editBooking(id) {
currentEditBooking = allBookingsData.find(b => b.id === id);
var b = currentEditBooking;
if (!b) return;
document.getElementById('editBookingId').value = b.id;
document.getElementById('editBookingNum').textContent = b.id;
document.getElementById('editName').value = b.name;
document.getElementById('editPhone').value = b.phone;
document.getElementById('editAdults').value = b.adults;
document.getElementById('editChildren').value = b.children || 0;
document.getElementById('editCheckin').value = b.checkin_date;
document.getElementById('editCheckout').value = b.checkout_date;
document.getElementById('editNights').textContent = calcNights(b.checkin_date, b.checkout_date) + ' ноч.';
document.getElementById('editWishes').value = b.wishes || '';
document.getElementById('editComment').value = b.comment || '';
document.getElementById('editDiscount').value = b.discount_percent || 0;
document.getElementById('editBasePrice').textContent = (b.base_price || 0) + ' ₽';
document.getElementById('editDiscountAmount').textContent = (b.discount_amount || 0) + ' ₽';
document.getElementById('editTotalPrice').textContent = b.total_price || b.base_price || 0;
document.getElementById('editRoom').innerHTML = allRoomsData.map(r => '<option value="' + r.id + '"' + (b.room_id === r.id ? ' selected' : '') + '>' + r.type + ' — ' + r.name + '</option>').join('');
document.getElementById('editStatus').innerHTML = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена']
.map(s => '<option value="' + s + '"' + (b.status === s ? ' selected' : '') + '>' + s + '</option>').join('');
document.getElementById('editBookingModal').classList.add('show');
document.body.style.overflow = 'hidden';
}
function hideEditBookingModal() {
document.getElementById('editBookingModal').classList.remove('show');
document.body.style.overflow = '';
}
function showHistoryForEdit() {
if (currentEditBooking) showHistory(currentEditBooking.id);
}
document.getElementById('editBookingModal').addEventListener('click', function(e) { if (e.target === this) hideEditBookingModal(); });
async function changeStatus(id, status) {
try {
const data = await api('/api/admin/bookings/' + id, { method: 'PATCH', body: JSON.stringify({ status }) });
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
renderBookings(); updateBookingStats(); loadDashboard();
renderBookingCards(); updateBookingStats(); loadDashboard();
showToast('Статус обновлён: ' + status);
} catch(err) { showToast(err.message, 'error'); }
}
@@ -1391,7 +1502,7 @@ async function changeRoom(id, room_id) {
try {
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();
renderBookingCards(); updateBookingStats(); loadDashboard();
showToast('Номер обновлён');
} catch(err) { showToast(err.message, 'error'); }
}
@@ -1399,8 +1510,55 @@ async function changeRoom(id, room_id) {
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('filterStatus').addEventListener('change', loadBookings);
document.getElementById('filterUrgent').addEventListener('change', loadBookings);
document.getElementById('sortAsc').style.opacity = '1';
document.getElementById('sortDesc').style.opacity = '0.5';
document.querySelectorAll('.status-tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.status-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
document.getElementById('filterStatus').value = this.dataset.status;
loadBookings(true);
});
});
document.getElementById('editBookingForm').addEventListener('submit', async function(e) {
e.preventDefault();
var id = parseInt(document.getElementById('editBookingId').value);
var orig = currentEditBooking;
if (!orig) return;
try {
var roomId = parseInt(document.getElementById('editRoom').value) || null;
var status = document.getElementById('editStatus').value;
var comment = document.getElementById('editComment').value.trim();
var discount = parseInt(document.getElementById('editDiscount').value) || 0;
var adults = parseInt(document.getElementById('editAdults').value);
var children = parseInt(document.getElementById('editChildren').value);
var checkin = document.getElementById('editCheckin').value;
var checkout = document.getElementById('editCheckout').value;
if (roomId !== orig.room_id) {
var d = await api('/api/admin/bookings/' + id + '/room', { method: 'PATCH', body: JSON.stringify({ room_id: roomId }) });
orig = d.booking;
}
if (status !== orig.status) {
var d = await api('/api/admin/bookings/' + id, { method: 'PATCH', body: JSON.stringify({ status: status }) });
orig = d.booking;
}
if (comment !== (orig.comment || '')) {
var d = await api('/api/admin/bookings/' + id + '/comment', { method: 'PATCH', body: JSON.stringify({ comment: comment }) });
orig = d.booking;
}
if (discount !== (orig.discount_percent || 0)) {
var d = await api('/api/admin/bookings/' + id + '/discount', { method: 'PATCH', body: JSON.stringify({ discount_percent: discount }) });
orig = d.booking;
}
if (adults !== orig.adults || children !== orig.children || checkin !== orig.checkin_date || checkout !== orig.checkout_date) {
var d = await api('/api/admin/bookings/' + id + '/details', { method: 'PATCH', body: JSON.stringify({ adults: adults, children: children, checkin_date: checkin, checkout_date: checkout }) });
orig = d.booking;
}
allBookingsData = allBookingsData.map(b => b.id === id ? orig : b);
hideEditBookingModal();
renderBookingCards();
updateBookingStats();
showToast('Заявка обновлена');
} catch(err) { showToast(err.message, 'error'); }
});
});
function loadProfile() {
@@ -1508,7 +1666,7 @@ async function changeDiscount(id, discount_percent) {
try {
const data = await api('/api/admin/bookings/' + id + '/discount', { method: 'PATCH', body: JSON.stringify({ discount_percent: parseInt(discount_percent) }) });
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
renderBookings();
renderBookingCards();
showToast('Скидка обновлена: ' + discount_percent + '%');
} catch(err) { showToast(err.message, 'error'); }
}
@@ -1628,7 +1786,7 @@ async function changeDetails(id, field, value) {
}
const data = await api('/api/admin/bookings/' + id + '/details', { method: 'PATCH', body: JSON.stringify(body) });
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
renderBookings(); updateBookingStats(); loadDashboard();
renderBookingCards(); updateBookingStats(); loadDashboard();
showToast('Данные обновлены');
} catch(err) {
showToast(err.message || 'Ошибка при сохранении', 'error');