Files
cleaning-company/public/admin-bookings.html
Владимир f5c68bf0c7 commit 08.01
2026-01-08 12:38:09 +00:00

414 lines
18 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Все брони - КлинСервис Админка</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f8f9fa; }
/* Хедер */
header {
background: #2c3e50; color: white; padding: 20px 50px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); position: sticky; top: 0;
}
.header-content {
max-width: 1400px; margin: 0 auto;
display: flex; justify-content: space-between; align-items: center;
}
.logo { font-size: 24px; font-weight: bold; }
.header-nav { display: flex; gap: 20px; }
.btn {
padding: 12px 25px; border: none; border-radius: 25px;
cursor: pointer; font-size: 16px; text-decoration: none;
display: inline-block; transition: all 0.3s; color: white;
}
.btn-primary { background: #667eea; }
.btn-primary:hover { background: #5a67d8; }
.btn-secondary { background: #6c757d; }
.btn-secondary:hover { background: #5a6268; }
/* Контейнер */
.container { max-width: 1400px; margin: 40px auto; padding: 0 20px; }
h1 { text-align: center; color: #333; margin-bottom: 40px; font-size: 32px; }
/* Фильтры */
.filters {
background: white; padding: 30px; border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 30px;
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
@media (max-width: 768px) { .filters { grid-template-columns: 1fr; } }
.filter-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
.filter-group input, .filter-group select {
width: 100%; padding: 12px; border: 2px solid #e9ecef;
border-radius: 10px; font-size: 16px;
}
.filter-group input:focus, .filter-group select:focus { border-color: #667eea; outline: none; }
.filter-btn {
padding: 15px 30px; background: #667eea; color: white;
border: none; border-radius: 10px; font-size: 16px;
cursor: pointer; grid-column: span 2;
}
.filter-btn:hover { background: #5a67d8; }
/* Таблица */
.bookings-table {
background: white; border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1); overflow: hidden;
}
table { width: 100%; border-collapse: collapse; }
th, td { padding: 18px 15px; text-align: left; border-bottom: 1px solid #e9ecef; }
th { background: #667eea; color: white; font-weight: bold; position: sticky; top: 0; }
tr:hover { background: #f8f9fa; }
/* Статусы */
.status { padding: 6px 12px; border-radius: 15px; font-size: 14px; font-weight: bold; }
.status.confirmed { background: #d4edda; color: #155724; }
.status.completed { background: #d1ecf1; color: #0c5460; }
.status.cancelled { background: #f8d7da; color: #721c24; }
/* Действия */
.action-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-small {
padding: 8px 14px; font-size: 13px; border-radius: 12px;
text-decoration: none; border: none; cursor: pointer; font-weight: 500;
}
.btn-complete { background: #28a745; color: white; }
.btn-complete:hover { background: #218838; }
.btn-cancel-admin { background: #dc3545; color: white; }
.btn-cancel-admin:hover { background: #c82333; }
/* Модальное окно */
.modal {
display: none; position: fixed; top: 0; left: 0;
width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;
}
.modal-content {
background: white; margin: 10% auto; padding: 40px;
border-radius: 20px; max-width: 500px; width: 90%; position: relative;
}
.close {
position: absolute; top: 15px; right: 20px; font-size: 28px;
cursor: pointer; color: #999;
}
.close:hover { color: #333; }
.modal textarea {
width: 100%; height: 100px; padding: 15px;
border: 2px solid #e9ecef; border-radius: 10px;
font-family: Arial, sans-serif; resize: vertical; margin-bottom: 20px;
}
.modal-buttons { display: flex; gap: 15px; }
.btn-modal { flex: 1; padding: 15px; border: none; border-radius: 10px; font-size: 16px; cursor: pointer; }
.btn-confirm { background: #dc3545; color: white; }
.btn-confirm:hover { background: #c82333; }
.no-bookings { text-align: center; padding: 80px 40px; color: #666; font-size: 18px; }
@media (max-width: 768px) {
th, td { padding: 12px 8px; font-size: 14px; }
.action-buttons { flex-direction: column; }
}
</style>
</head>
<body>
<!-- Хедер -->
<header>
<div class="header-content">
<div class="logo">🧹 КлинСервис - Админка</div>
<div class="header-nav">
<a href="admin-services.html" class="btn btn-secondary">Услуги</a>
<a href="admin-schedule.html" class="btn btn-secondary">Расписание</a>
<a href="index.html" class="btn btn-primary">На главную</a>
<button class="btn btn-secondary" onclick="logout()">Выйти</button>
</div>
</div>
</header>
<div class="container">
<h1>📋 Все бронирования</h1>
<!-- Фильтры -->
<div class="filters">
<div class="filter-group">
<label>Дата:</label>
<input type="date" id="filterDate">
</div>
<div class="filter-group">
<label>Статус:</label>
<select id="filterStatus">
<option value="">Все</option>
<option value="confirmed">Подтверждена</option>
<option value="completed">Выполнена</option>
<option value="cancelled">Отменена</option>
</select>
</div>
<div class="filter-group">
<label>Сотрудник:</label>
<select id="filterEmployee">
<option value="">Все сотрудники</option>
</select>
</div>
<div class="filter-group">
<label>Клиент (email):</label>
<input type="email" id="filterClient" placeholder="ivan@example.com">
</div>
<button class="filter-btn" onclick="applyFilters()">🔍 Применить фильтры</button>
<button class="filter-btn" onclick="clearFilters()" style="background: #6c757d;">Очистить</button>
</div>
<!-- Таблица броней -->
<div class="bookings-table">
<table>
<thead>
<tr>
<th>Код брони</th>
<th>Клиент (email)</th>
<th>Услуга</th>
<th>Сотрудник</th>
<th>Дата и время</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="bookingsTable">
<tr>
<td colspan="7" class="no-bookings">Загрузка броней...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Модальное окно отмены -->
<div id="cancelModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeCancelModal()">&times;</span>
<h3>Отменить бронь администратором?</h3>
<p id="cancelBookingInfo"></p>
<p>Укажите причину отмены (необязательно):</p>
<textarea id="cancelReason" placeholder="Сотрудник заболел, клиент не вышел на связь..."></textarea>
<div class="modal-buttons">
<button class="btn-modal" onclick="closeCancelModal()">Отмена</button>
<button class="btn-modal btn-confirm" id="confirmCancelBtn" onclick="adminCancelBooking()">Отменить</button>
</div>
</div>
</div>
<script>
let token = localStorage.getItem('token');
let allBookings = [];
let filteredBookings = [];
let selectedBookingId = null;
// Проверка авторизации
if (!token) {
window.location.href = 'register-login.html';
}
// Загрузка при старте
window.onload = async function() {
await loadBookings();
await loadEmployeesForFilter();
};
// Загрузить все брони
async function loadBookings() {
try {
const response = await fetch('/api/admin/bookings', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
allBookings = await response.json();
filteredBookings = [...allBookings];
renderBookings();
} else {
alert('Ошибка доступа');
window.location.href = 'register-login.html';
}
} catch (error) {
document.getElementById('bookingsTable').innerHTML =
'<tr><td colspan="7" class="no-bookings">Ошибка загрузки</td></tr>';
}
}
// Загрузить сотрудников для фильтра
async function loadEmployeesForFilter() {
try {
const response = await fetch('/api/admin/users?role=employee', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const employees = await response.json();
const select = document.getElementById('filterEmployee');
for (let employee of employees.filter(e => e.role === 'employee')) {
select.innerHTML += `<option value="${employee.id}">${employee.name}</option>`;
}
}
} catch (error) {
console.error('Ошибка загрузки сотрудников');
}
}
// Применить фильтры
function applyFilters() {
const date = document.getElementById('filterDate').value;
const status = document.getElementById('filterStatus').value;
const employeeId = document.getElementById('filterEmployee').value;
const clientEmail = document.getElementById('filterClient').value.toLowerCase();
filteredBookings = allBookings.filter(booking => {
let match = true;
if (date && booking.bookingdate !== date) match = false;
if (status && booking.status !== status) match = false;
if (employeeId && booking.employee_id != employeeId) match = false;
if (clientEmail && (!booking.client || !booking.client.email.toLowerCase().includes(clientEmail))) match = false;
return match;
});
renderBookings();
}
// Очистить фильтры
function clearFilters() {
document.getElementById('filterDate').value = '';
document.getElementById('filterStatus').value = '';
document.getElementById('filterEmployee').value = '';
document.getElementById('filterClient').value = '';
filteredBookings = [...allBookings];
renderBookings();
}
// Отобразить брони
function renderBookings() {
const tbody = document.getElementById('bookingsTable');
if (filteredBookings.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="no-bookings">Нет броней по фильтрам</td></tr>';
return;
}
tbody.innerHTML = '';
for (let booking of filteredBookings) {
const row = document.createElement('tr');
const statusClass = booking.status;
row.innerHTML = `
<td><strong>${booking.bookingnumber}</strong></td>
<td>${booking.client ? booking.client.email : 'Неизвестно'}</td>
<td>${booking.service ? booking.service.name : 'Удалена'}</td>
<td>${booking.employee ? booking.employee.name : 'Не назначен'}</td>
<td>${booking.bookingdate}<br>${booking.starttime.slice(0,5)}${booking.endtime.slice(0,5)}</td>
<td><span class="status ${statusClass}">${booking.status}</span></td>
<td>
<div class="action-buttons">
${booking.status !== 'completed' ?
`<button class="btn-small btn-cancel-admin" onclick="showCancelModal(${booking.id}, '${booking.bookingnumber}')">
❌ Отменить
</button>` : ''}
${booking.status === 'confirmed' ?
`<button class="btn-small btn-complete" onclick="markAsCompleted(${booking.id})">
✅ Выполнено
</button>` : ''}
</div>
</td>
`;
tbody.appendChild(row);
}
}
// Показать модальное окно отмены
function showCancelModal(bookingId, bookingNumber) {
selectedBookingId = bookingId;
document.getElementById('cancelBookingInfo').innerHTML =
`Бронь <strong>${bookingNumber}</strong>`;
document.getElementById('cancelReason').value = '';
document.getElementById('cancelModal').style.display = 'block';
}
// Закрыть модальное окно
function closeCancelModal() {
document.getElementById('cancelModal').style.display = 'none';
}
// Админская отмена
async function adminCancelBooking() {
const reason = document.getElementById('cancelReason').value;
try {
const response = await fetch(`/api/admin/bookings/${selectedBookingId}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ reason: reason || null })
});
const data = await response.json();
if (response.ok) {
alert('✅ Бронь отменена администратором');
closeCancelModal();
loadBookings();
} else {
alert('Ошибка: ' + data.error);
}
} catch (error) {
alert('Ошибка сети');
}
}
// Пометить как выполнено
async function markAsCompleted(bookingId) {
if (confirm('Пометить бронь как выполненную?')) {
try {
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ status: 'completed' })
});
if (response.ok) {
alert('✅ Бронь отмечена как выполненная');
loadBookings();
}
} catch (error) {
alert('Ошибка');
}
}
}
function logout() {
localStorage.removeItem('token');
window.location.href = 'index.html';
}
// Enter для фильтров
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') applyFilters();
});
// Клик вне модального окна
window.onclick = function(event) {
const modal = document.getElementById('cancelModal');
if (event.target === modal) closeCancelModal();
}
</script>
</body>
</html>