хз
This commit is contained in:
@@ -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();
|
||||
|
||||
82
server.js
82
server.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user