This commit is contained in:
2026-05-09 11:20:17 +05:00
parent 3e91ebd083
commit 67b7008bf3
2 changed files with 180 additions and 23 deletions

View File

@@ -236,6 +236,7 @@ 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="width: 200px; 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>
@@ -249,6 +250,14 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
<tbody id="allBookings"></tbody>
</table>
</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;">
<button class="btn btn-outline-secondary btn-sm" id="prevPage" onclick="changePage(-1)" disabled>← Назад</button>
<span id="pageNumbers" style="display: flex; gap: 4px;"></span>
<button class="btn btn-outline-secondary btn-sm" id="nextPage" onclick="changePage(1)" disabled>Вперёд →</button>
</div>
</div>
</div>
</div>
</div>
@@ -560,15 +569,98 @@ async function loadUsers() {
let allBookingsData = [];
let sortDirection = 'asc';
let bookingsLoaded = false;
let currentPage = 1;
let totalPages = 1;
let searchTimeout = null;
async function loadBookings() {
if (bookingsLoaded) { renderBookings(); return; }
async function loadBookings(resetPage = true) {
if (resetPage) currentPage = 1;
try {
allBookingsData = await api('/api/admin/bookings');
const search = document.getElementById('searchBookings').value;
const status = document.getElementById('filterStatus').value;
const limit = 20;
const params = new URLSearchParams({
page: currentPage,
limit: limit,
search: search,
status: status
});
const response = await api('/api/admin/bookings?' + params.toString());
allBookingsData = response.data;
totalPages = response.pagination.totalPages;
currentPage = response.pagination.page;
bookingsLoaded = true;
renderBookings();
updateBookingStats();
} catch(err) { showToast('Нет доступа к бронированиям', 'error'); }
updatePagination();
} catch(err) { showToast('Нет доступа к бронированиям: ' + err.message, 'error'); }
}
function updatePagination() {
const info = document.getElementById('paginationInfo');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
const pageNumbers = document.getElementById('pageNumbers');
const total = allBookingsData.length;
const from = (currentPage - 1) * 20 + 1;
const to = Math.min(currentPage * 20, total);
info.textContent = total > 0 ? `Показано ${from}-${to} из ${total}` : 'Нет записей';
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
// Page numbers
let pagesHtml = '';
const maxVisible = 5;
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let end = Math.min(totalPages, start + maxVisible - 1);
if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
if (start > 1) {
pagesHtml += `<button class="btn btn-outline-secondary btn-sm" onclick="goToPage(1)">1</button>`;
if (start > 2) pagesHtml += `<span style="padding: 0 4px;">...</span>`;
}
for (let i = start; i <= end; i++) {
if (i === currentPage) {
pagesHtml += `<button class="btn btn-primary btn-sm" onclick="goToPage(${i})">${i}</button>`;
} else {
pagesHtml += `<button class="btn btn-outline-secondary btn-sm" onclick="goToPage(${i})">${i}</button>`;
}
}
if (end < totalPages) {
if (end < totalPages - 1) pagesHtml += `<span style="padding: 0 4px;">...</span>`;
pagesHtml += `<button class="btn btn-outline-secondary btn-sm" onclick="goToPage(${totalPages})">${totalPages}</button>`;
}
pageNumbers.innerHTML = pagesHtml;
}
function changePage(delta) {
const newPage = currentPage + delta;
if (newPage >= 1 && newPage <= totalPages) {
currentPage = newPage;
loadBookings(false);
}
}
function goToPage(page) {
if (page >= 1 && page <= totalPages) {
currentPage = page;
loadBookings(false);
}
}
function debounceSearch() {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => loadBookings(true), 500);
}
function getDaysDiff(dateStr) {
@@ -588,14 +680,6 @@ function renderBookings() {
const filterStatus = document.getElementById('filterStatus').value;
const filterUrgent = document.getElementById('filterUrgent').value;
if (filterStatus !== 'all') rows = rows.filter(r => r.status === filterStatus);
if (filterUrgent === 'checkin-soon') {
rows = rows.filter(r => { const diff = getDaysDiff(r.checkin_date); return diff >= 0 && diff <= 3 && r.status !== 'отменена' && r.status !== 'выехала'; });
} else if (filterUrgent === 'checkout-today') {
rows = rows.filter(r => getDaysDiff(r.checkout_date) === 0 && r.status !== 'отменена' && r.status !== 'выехала');
}
rows.sort((a, b) => {
const da = new Date(a.checkin_date), db = new Date(b.checkin_date);
return sortDirection === 'asc' ? da - db : db - da;
@@ -668,8 +752,8 @@ async function changeRoom(id, room_type) {
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('filterStatus').addEventListener('change', renderBookings);
document.getElementById('filterUrgent').addEventListener('change', renderBookings);
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';
});
@@ -889,6 +973,10 @@ async function changeDetails(id, field, value) {
try {
const body = {};
if (field === 'adults' || field === 'children') {
if (value === '' || isNaN(parseInt(value))) {
showToast('Введите корректное число', 'error');
return;
}
body[field] = parseInt(value);
} else {
body[field] = value;
@@ -897,7 +985,10 @@ async function changeDetails(id, field, value) {
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
renderBookings(); updateBookingStats(); loadDashboard();
showToast('Данные обновлены');
} catch(err) { showToast(err.message, 'error'); }
} catch(err) {
showToast(err.message || 'Ошибка при сохранении', 'error');
loadBookings(false);
}
}
checkAuth();

View File

@@ -301,14 +301,54 @@ app.post('/api/bookings', (req, res) => {
});
app.get('/api/admin/bookings', authenticateToken, (req, res) => {
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
ORDER BY b.checkin_date ASC`, [], (err, rows) => {
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 ?)';
const searchTerm = `%${search}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
if (statusFilter && statusFilter !== 'all') {
whereClause += ' AND b.status = ?';
params.push(statusFilter);
}
db.get(`SELECT COUNT(*) as total FROM bookings b WHERE ${whereClause}`, params, (err, countRow) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
const total = countRow.total;
const totalPages = Math.ceil(total / limit);
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
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
}
});
});
});
});
@@ -409,6 +449,32 @@ app.patch('/api/admin/bookings/:id/details', authenticateToken, requireAdmin, (r
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' });
@@ -420,10 +486,10 @@ app.patch('/api/admin/bookings/:id/details', authenticateToken, requireAdmin, (r
};
let fields = [];
let values = [];
if (adults !== undefined) { fields.push('adults = ?'); values.push(adults); }
if (children !== undefined) { fields.push('children = ?'); values.push(children); }
if (checkin_date !== undefined) { fields.push('checkin_date = ?'); values.push(checkin_date); }
if (checkout_date !== undefined) { fields.push('checkout_date = ?'); values.push(checkout_date); }
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) {