272 lines
11 KiB
HTML
272 lines
11 KiB
HTML
<!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: white; padding: 20px 50px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
position: sticky; top: 0; z-index: 100;
|
||
}
|
||
.header-content {
|
||
max-width: 1200px; margin: 0 auto;
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
}
|
||
.logo { font-size: 24px; font-weight: bold; color: #667eea; }
|
||
.btn {
|
||
padding: 10px 20px; border: none; border-radius: 25px;
|
||
cursor: pointer; font-size: 14px; text-decoration: none;
|
||
display: inline-block; transition: all 0.3s;
|
||
}
|
||
.btn-primary { background: #667eea; color: white; }
|
||
.btn-secondary { background: transparent; color: #667eea; border: 2px solid #667eea; }
|
||
|
||
.container { max-width: 1200px; margin: 40px auto; padding: 0 20px; }
|
||
h1 { text-align: center; margin-bottom: 30px; color: #333; }
|
||
|
||
table { width: 100%; border-collapse: collapse; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
|
||
th, td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; }
|
||
th { background: #667eea; color: white; }
|
||
tr:hover { background: #f9f9f9; }
|
||
|
||
select, input { padding: 8px; border: 1px solid #ccc; border-radius: 5px; }
|
||
.actions { display: flex; gap: 10px; margin-top: 10px; }
|
||
.btn-sm { padding: 5px 10px; font-size: 12px; }
|
||
.status { padding: 4px 8px; border-radius: 10px; font-size: 12px; }
|
||
.confirmed { background: #d4edda; color: #155724; }
|
||
.cancelled { background: #f8d7da; color: #721c24; }
|
||
.completed { background: #d1ecf1; color: #0c5460; }
|
||
|
||
.no-data { text-align: center; padding: 40px; color: #666; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="header-content">
|
||
<div class="logo">🧹 Админка — КлинСервис</div>
|
||
<div>
|
||
<a href="index.html" class="btn btn-secondary">Главная</a>
|
||
<button class="btn btn-primary" onclick="logout()">Выйти</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="container">
|
||
<h1>📋 Все бронирования</h1>
|
||
<table id="bookingsTable">
|
||
<thead>
|
||
<tr>
|
||
<th>№</th>
|
||
<th>Клиент</th>
|
||
<th>Услуга</th>
|
||
<th>Дата и время</th>
|
||
<th>Сотрудник</th>
|
||
<th>Статус</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td colspan="7" class="no-data">Загрузка...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<script>
|
||
let token = localStorage.getItem('token');
|
||
let bookings = [];
|
||
let employees = [];
|
||
|
||
if (!token) {
|
||
window.location.href = 'register-login.html';
|
||
}
|
||
|
||
// Загрузка данных при старте
|
||
window.onload = async function() {
|
||
try {
|
||
await loadEmployees();
|
||
await loadBookings();
|
||
} catch (err) {
|
||
alert('Ошибка загрузки данных: ' + err.message);
|
||
}
|
||
};
|
||
|
||
// Загрузить сотрудников
|
||
async function loadEmployees() {
|
||
try {
|
||
const res = await fetch('/api/users?role=employee', {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
// Проверяем тип ответа
|
||
const contentType = res.headers.get('content-type');
|
||
if (!contentType || !contentType.includes('application/json')) {
|
||
throw new Error('Сервер вернул HTML вместо JSON (проверьте роль пользователя)');
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`HTTP ${res.status}: ${errorText}`);
|
||
}
|
||
|
||
employees = await res.json();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки сотрудников:', error);
|
||
alert('Ошибка загрузки сотрудников: ' + error.message);
|
||
employees = [];
|
||
}
|
||
}
|
||
|
||
// Загрузить все брони
|
||
async function loadBookings() {
|
||
try {
|
||
const res = await fetch('/api/admin/bookings', {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
const contentType = res.headers.get('content-type');
|
||
if (!contentType || !contentType.includes('application/json')) {
|
||
throw new Error('Сервер вернул HTML вместо JSON (проверьте роль пользователя)');
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`HTTP ${res.status}: ${errorText}`);
|
||
}
|
||
|
||
bookings = await res.json();
|
||
renderBookings();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки бронирований:', error);
|
||
document.querySelector('#bookingsTable tbody').innerHTML =
|
||
`<tr><td colspan="7" class="no-data">Ошибка: ${error.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// Отобразить таблицу
|
||
function renderBookings() {
|
||
const tbody = document.querySelector('#bookingsTable tbody');
|
||
if (bookings.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="no-data">Нет бронирований</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = '';
|
||
bookings.forEach(b => {
|
||
const employeeSelect = document.createElement('select');
|
||
employeeSelect.className = 'employee-select';
|
||
employeeSelect.dataset.bookingId = b.id;
|
||
|
||
if (!employees.length) {
|
||
const option = document.createElement('option');
|
||
option.value = '';
|
||
option.textContent = '—';
|
||
employeeSelect.appendChild(option);
|
||
} else {
|
||
employees.forEach(emp => {
|
||
const option = document.createElement('option');
|
||
option.value = emp.id;
|
||
option.textContent = emp.name;
|
||
if ((b.employee && b.employee.id == emp.id) || b.employee_id == emp.id) {
|
||
option.selected = true;
|
||
}
|
||
employeeSelect.appendChild(option);
|
||
});
|
||
}
|
||
|
||
const status = b.status || 'confirmed';
|
||
const statusClass = status;
|
||
|
||
const row = `
|
||
<tr>
|
||
<td>${b.booking_number || '—'}</td>
|
||
<td>${(b.client && b.client.name) ? b.client.name : '—'}</td>
|
||
<td>${(b.service && b.service.name) ? b.service.name : '—'}</td>
|
||
<td>${b.booking_date || '—'} ${b.start_time ? b.start_time.slice(0,5) : '—'}</td>
|
||
<td>
|
||
${employeeSelect.outerHTML}
|
||
<div class="actions">
|
||
<button class="btn-sm btn-primary" onclick="saveEmployee(${b.id})">Сохранить</button>
|
||
</div>
|
||
</td>
|
||
<td><span class="status ${statusClass}">${status}</span></td>
|
||
<td>
|
||
${status === 'confirmed' ?
|
||
`<button class="btn-sm btn-danger" onclick="cancelBooking(${b.id})">Отменить</button>` :
|
||
`<button class="btn-sm btn-secondary" disabled>Отменено</button>`
|
||
}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
tbody.innerHTML += row;
|
||
});
|
||
}
|
||
|
||
// Сохранить сотрудника
|
||
async function saveEmployee(bookingId) {
|
||
const select = document.querySelector(`.employee-select[data-booking-id="${bookingId}"]`);
|
||
if (!select || !select.value) {
|
||
alert('Выберите сотрудника');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`/api/admin/bookings/${bookingId}/assign`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ employee_id: select.value })
|
||
});
|
||
|
||
if (res.ok) {
|
||
alert('✅ Сотрудник назначен!');
|
||
loadBookings();
|
||
} else {
|
||
const err = await res.json().catch(() => ({ message: 'Неизвестная ошибка' }));
|
||
alert('Ошибка: ' + (err.message || 'Не удалось назначить сотрудника'));
|
||
}
|
||
} catch (error) {
|
||
alert('Ошибка сети: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Отменить бронь
|
||
async function cancelBooking(bookingId) {
|
||
const reason = prompt('Причина отмены:');
|
||
if (reason === null) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/admin/bookings/${bookingId}/cancel`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({ reason: reason.trim() || null })
|
||
});
|
||
|
||
if (res.ok) {
|
||
alert('✅ Бронь отменена!');
|
||
loadBookings();
|
||
} else {
|
||
const err = await res.json().catch(() => ({ message: 'Неизвестная ошибка' }));
|
||
alert('Ошибка: ' + (err.message || 'Не удалось отменить бронь'));
|
||
}
|
||
} catch (error) {
|
||
alert('Ошибка сети: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('token');
|
||
window.location.href = 'index.html';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |