Files
cleaning-company/public/admin-bookings.html
Владимир ae5ab2554b commit 12.01
2026-01-12 14:25:15 +00:00

272 lines
11 KiB
HTML
Raw Permalink 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: 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>