commit 08.01
This commit is contained in:
413
public/admin-bookings.html
Normal file
413
public/admin-bookings.html
Normal file
@@ -0,0 +1,413 @@
|
||||
<!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()">×</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>
|
||||
430
public/admin-schedule.html
Normal file
430
public/admin-schedule.html
Normal file
@@ -0,0 +1,430 @@
|
||||
<!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: 1200px; 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: 1200px; margin: 40px auto; padding: 0 20px; }
|
||||
h1 { text-align: center; color: #333; margin-bottom: 40px; font-size: 32px; }
|
||||
|
||||
/* Выбор сотрудника */
|
||||
.employee-select {
|
||||
background: white; padding: 30px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 30px;
|
||||
display: flex; gap: 20px; align-items: center; flex-wrap: wrap;
|
||||
}
|
||||
@media (max-width: 768px) { .employee-select { flex-direction: column; text-align: center; } }
|
||||
|
||||
label { font-weight: bold; color: #555; font-size: 18px; }
|
||||
select {
|
||||
padding: 15px 20px; border: 2px solid #e9ecef;
|
||||
border-radius: 10px; font-size: 16px; min-width: 300px;
|
||||
}
|
||||
|
||||
/* Календарь */
|
||||
.calendar-section {
|
||||
background: white; padding: 40px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 30px;
|
||||
}
|
||||
.calendar-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 20px; flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.calendar-nav-btn {
|
||||
padding: 12px 20px; background: #667eea; color: white;
|
||||
border: none; border-radius: 10px; cursor: pointer; font-size: 16px;
|
||||
}
|
||||
.calendar-nav-btn:hover { background: #5a67d8; }
|
||||
.month-year { font-size: 24px; font-weight: bold; color: #333; }
|
||||
|
||||
.calendar-grid {
|
||||
display: grid; grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px; text-align: center;
|
||||
}
|
||||
.calendar-day {
|
||||
padding: 20px 10px; border: 2px solid #e9ecef;
|
||||
border-radius: 12px; cursor: pointer; transition: all 0.3s;
|
||||
font-weight: 500; min-height: 80px; display: flex; flex-direction: column;
|
||||
}
|
||||
.calendar-day:hover { border-color: #667eea; background: #f8f9ff; }
|
||||
.calendar-day.selected { border-color: #667eea; background: #e3f2fd; }
|
||||
.calendar-day.other-month { color: #ccc; }
|
||||
.weekdays { font-weight: bold; color: #555; padding: 15px 0; font-size: 16px; }
|
||||
|
||||
/* Интервалы времени */
|
||||
.time-intervals {
|
||||
background: white; padding: 40px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
}
|
||||
.interval-form {
|
||||
display: grid; grid-template-columns: 1fr 1fr auto;
|
||||
gap: 20px; align-items: end; margin-bottom: 20px;
|
||||
}
|
||||
@media (max-width: 768px) { .interval-form { grid-template-columns: 1fr; } }
|
||||
|
||||
input[type="time"] { padding: 15px; border: 2px solid #e9ecef; border-radius: 10px; }
|
||||
input[type="time"]:focus { border-color: #667eea; outline: none; }
|
||||
.interval-item {
|
||||
background: #f8f9fa; padding: 20px; border-radius: 15px;
|
||||
border-left: 4px solid #667eea; margin-bottom: 15px;
|
||||
}
|
||||
.delete-interval {
|
||||
background: #dc3545; color: white; border: none;
|
||||
border-radius: 50%; width: 35px; height: 35px;
|
||||
cursor: pointer; font-size: 18px;
|
||||
}
|
||||
.delete-interval:hover { background: #c82333; }
|
||||
|
||||
.no-intervals { text-align: center; color: #666; padding: 40px; font-style: italic; }
|
||||
</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-bookings.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="employee-select">
|
||||
<div>
|
||||
<label>Выберите сотрудника:</label>
|
||||
<select id="employeeSelect" onchange="loadEmployeeSchedule()">
|
||||
<option value="">— Выберите сотрудника —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="employeeInfo" style="display:none;">
|
||||
<strong id="selectedEmployeeName"></strong>
|
||||
<span style="color: #666; margin-left: 10px;">(ID: <span id="selectedEmployeeId"></span>)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Календарь -->
|
||||
<div class="calendar-section" id="calendarSection" style="display:none;">
|
||||
<div class="calendar-header">
|
||||
<button class="calendar-nav-btn" onclick="prevMonth()">← Пред. месяц</button>
|
||||
<div class="month-year" id="monthYear"></div>
|
||||
<button class="calendar-nav-btn" onclick="nextMonth()">След. месяц →</button>
|
||||
</div>
|
||||
<div class="calendar-grid">
|
||||
<div class="weekdays">Пн</div><div class="weekdays">Вт</div><div class="weekdays">Ср</div>
|
||||
<div class="weekdays">Чт</div><div class="weekdays">Пт</div><div class="weekdays">Сб</div><div class="weekdays">Вс</div>
|
||||
<div id="calendarDays"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Интервалы времени для выбранной даты -->
|
||||
<div class="time-intervals" id="timeIntervalsSection" style="display:none;">
|
||||
<h3 id="selectedDateTitle">Интервалы времени</h3>
|
||||
|
||||
<!-- Форма добавления интервала -->
|
||||
<form onsubmit="addInterval(event)" class="interval-form">
|
||||
<input type="time" id="startTime" required>
|
||||
<input type="time" id="endTime" required>
|
||||
<label>
|
||||
<input type="checkbox" id="isUnavailable"> Недоступен
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">➕ Добавить интервал</button>
|
||||
</form>
|
||||
|
||||
<!-- Список интервалов -->
|
||||
<div id="intervalsList">
|
||||
<div class="no-intervals">Выберите дату в календаре</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = localStorage.getItem('token');
|
||||
let selectedEmployeeId = null;
|
||||
let selectedDate = null;
|
||||
let currentMonth = new Date().getMonth();
|
||||
let currentYear = new Date().getFullYear();
|
||||
let employees = [];
|
||||
let intervals = [];
|
||||
|
||||
// Проверка авторизации
|
||||
if (!token) {
|
||||
window.location.href = 'register-login.html';
|
||||
}
|
||||
|
||||
// Загрузка сотрудников при старте
|
||||
window.onload = async function() {
|
||||
await loadEmployees();
|
||||
};
|
||||
|
||||
// Загрузить список сотрудников
|
||||
async function loadEmployees() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/users?role=employee', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const allUsers = await response.json();
|
||||
employees = allUsers.filter(user => user.role === 'employee' || user.role === 'admin');
|
||||
|
||||
const select = document.getElementById('employeeSelect');
|
||||
select.innerHTML = '<option value="">— Выберите сотрудника —</option>';
|
||||
|
||||
for (let employee of employees) {
|
||||
select.innerHTML += `<option value="${employee.id}">${employee.name}</option>`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка загрузки сотрудников');
|
||||
}
|
||||
}
|
||||
|
||||
// При смене сотрудника
|
||||
function loadEmployeeSchedule() {
|
||||
const select = document.getElementById('employeeSelect');
|
||||
selectedEmployeeId = select.value;
|
||||
|
||||
if (selectedEmployeeId) {
|
||||
document.getElementById('employeeInfo').style.display = 'block';
|
||||
document.getElementById('selectedEmployeeId').textContent = selectedEmployeeId;
|
||||
document.getElementById('selectedEmployeeName').textContent = select.options[select.selectedIndex].text;
|
||||
document.getElementById('calendarSection').style.display = 'block';
|
||||
renderCalendar();
|
||||
} else {
|
||||
document.getElementById('calendarSection').style.display = 'none';
|
||||
document.getElementById('timeIntervalsSection').style.display = 'none';
|
||||
document.getElementById('employeeInfo').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Простой календарь
|
||||
function renderCalendar() {
|
||||
const calendarDays = document.getElementById('calendarDays');
|
||||
const monthYear = document.getElementById('monthYear');
|
||||
|
||||
monthYear.textContent = new Date(currentYear, currentMonth).toLocaleDateString('ru', {
|
||||
year: 'numeric', month: 'long'
|
||||
});
|
||||
|
||||
calendarDays.innerHTML = '';
|
||||
const firstDay = new Date(currentYear, currentMonth, 1).getDay();
|
||||
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
||||
|
||||
let dayCounter = 1 - firstDay + 1;
|
||||
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const day = document.createElement('div');
|
||||
day.className = 'calendar-day';
|
||||
|
||||
if (dayCounter < 1) {
|
||||
day.textContent = new Date(currentYear, currentMonth, 0).getDate() + dayCounter;
|
||||
day.classList.add('other-month');
|
||||
dayCounter++;
|
||||
} else if (dayCounter <= daysInMonth) {
|
||||
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(dayCounter).padStart(2, '0')}`;
|
||||
day.textContent = dayCounter;
|
||||
day.onclick = () => selectDate(dateStr);
|
||||
dayCounter++;
|
||||
} else {
|
||||
day.textContent = dayCounter - daysInMonth;
|
||||
day.classList.add('other-month');
|
||||
dayCounter++;
|
||||
}
|
||||
|
||||
calendarDays.appendChild(day);
|
||||
}
|
||||
}
|
||||
|
||||
function selectDate(date) {
|
||||
selectedDate = date;
|
||||
document.querySelectorAll('.calendar-day').forEach(day => day.classList.remove('selected'));
|
||||
event.target.classList.add('selected');
|
||||
|
||||
document.getElementById('selectedDateTitle').textContent = `Интервалы на ${new Date(date).toLocaleDateString('ru')}`;
|
||||
loadIntervals();
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
currentMonth--;
|
||||
if (currentMonth < 0) {
|
||||
currentMonth = 11;
|
||||
currentYear--;
|
||||
}
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
currentMonth++;
|
||||
if (currentMonth > 11) {
|
||||
currentMonth = 0;
|
||||
currentYear++;
|
||||
}
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
// Загрузить интервалы для даты
|
||||
async function loadIntervals() {
|
||||
try {
|
||||
document.getElementById('timeIntervalsSection').style.display = 'block';
|
||||
const response = await fetch(`/api/admin/availabilities?employee_id=${selectedEmployeeId}&date=${selectedDate}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
intervals = await response.json();
|
||||
renderIntervals();
|
||||
} catch (error) {
|
||||
intervals = [];
|
||||
renderIntervals();
|
||||
}
|
||||
}
|
||||
|
||||
// Отобразить интервалы
|
||||
function renderIntervals() {
|
||||
const container = document.getElementById('intervalsList');
|
||||
|
||||
if (intervals.length === 0) {
|
||||
container.innerHTML = '<div class="no-intervals">Нет интервалов на эту дату</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
for (let interval of intervals) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'interval-item';
|
||||
item.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<strong>${interval.starttime.slice(0,5)} - ${interval.endtime.slice(0,5)}</strong>
|
||||
<div>
|
||||
<label style="margin-right: 15px;">
|
||||
<input type="checkbox" ${interval.isavailable ? '' : 'checked'} onchange="updateInterval(${interval.id}, this.checked)">
|
||||
Недоступен
|
||||
</label>
|
||||
<button class="delete-interval" onclick="deleteInterval(${interval.id})" title="Удалить">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавить интервал
|
||||
async function addInterval(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
const isUnavailable = document.getElementById('isUnavailable').checked;
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
alert('Заполните время начала и окончания');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/availabilities', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
employee_id: selectedEmployeeId,
|
||||
date: selectedDate,
|
||||
starttime: startTime + ':00',
|
||||
endtime: endTime + ':00',
|
||||
isavailable: !isUnavailable
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('startTime').value = '';
|
||||
document.getElementById('endTime').value = '';
|
||||
document.getElementById('isUnavailable').checked = false;
|
||||
loadIntervals();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка добавления интервала');
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить доступность
|
||||
async function updateInterval(id, isAvailable) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/availabilities/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ isavailable: isAvailable })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
alert('Ошибка обновления');
|
||||
loadIntervals(); // Вернуть как было
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка сети');
|
||||
loadIntervals();
|
||||
}
|
||||
}
|
||||
|
||||
// Удалить интервал
|
||||
async function deleteInterval(id) {
|
||||
if (confirm('Удалить интервал?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/availabilities/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadIntervals();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка удаления');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
355
public/admin-services.html
Normal file
355
public/admin-services.html
Normal file
@@ -0,0 +1,355 @@
|
||||
<!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: 1200px; 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: 1200px; margin: 40px auto; padding: 0 20px; }
|
||||
h1 { text-align: center; color: #333; margin-bottom: 40px; font-size: 32px; }
|
||||
|
||||
/* Форма добавления */
|
||||
.add-service-form {
|
||||
background: white; padding: 40px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 40px;
|
||||
}
|
||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; }
|
||||
label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
|
||||
input, textarea {
|
||||
width: 100%; padding: 15px; border: 2px solid #e9ecef;
|
||||
border-radius: 10px; font-size: 16px; transition: border-color 0.3s;
|
||||
}
|
||||
input:focus, textarea:focus { outline: none; border-color: #667eea; }
|
||||
textarea { resize: vertical; min-height: 100px; }
|
||||
|
||||
/* Таблица услуг */
|
||||
.services-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: 20px; text-align: left; border-bottom: 1px solid #e9ecef; }
|
||||
th { background: #667eea; color: white; font-weight: bold; }
|
||||
tr:hover { background: #f8f9fa; }
|
||||
|
||||
/* Статус */
|
||||
.status-badge { padding: 6px 12px; border-radius: 15px; font-size: 14px; font-weight: bold; }
|
||||
.status-active { background: #d4edda; color: #155724; }
|
||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
||||
|
||||
/* Кнопки действий */
|
||||
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.btn-small {
|
||||
padding: 8px 16px; font-size: 14px; border-radius: 15px;
|
||||
text-decoration: none; border: none; cursor: pointer;
|
||||
}
|
||||
.btn-edit { background: #ffc107; color: #212529; }
|
||||
.btn-edit:hover { background: #e0a800; }
|
||||
.btn-toggle { background: #17a2b8; color: white; }
|
||||
.btn-toggle:hover { background: #138496; }
|
||||
.btn-delete { background: #dc3545; color: white; }
|
||||
.btn-delete: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: 5% auto; padding: 40px;
|
||||
border-radius: 20px; max-width: 600px; width: 90%; position: relative;
|
||||
}
|
||||
.close {
|
||||
position: absolute; top: 15px; right: 20px; font-size: 28px;
|
||||
cursor: pointer; color: #999;
|
||||
}
|
||||
.close:hover { color: #333; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
.action-buttons { flex-direction: column; }
|
||||
th, td { padding: 15px 10px; font-size: 14px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Хедер -->
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<div class="logo">🧹 КлинСервис - Админка</div>
|
||||
<div class="header-nav">
|
||||
<a href="admin-bookings.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="add-service-form">
|
||||
<h3 style="margin-bottom: 25px; color: #333;">➕ Добавить новую услугу</h3>
|
||||
<form id="addServiceForm" onsubmit="addService(event)" class="form-grid">
|
||||
<div>
|
||||
<label>Название *</label>
|
||||
<input type="text" id="serviceName" required placeholder="Генеральная уборка">
|
||||
</div>
|
||||
<div>
|
||||
<label>Цена (₽) *</label>
|
||||
<input type="number" id="servicePrice" required min="0" placeholder="5000">
|
||||
</div>
|
||||
<div>
|
||||
<label>Длительность (минуты) *</label>
|
||||
<input type="number" id="serviceDuration" required min="30" placeholder="120">
|
||||
</div>
|
||||
<div style="grid-column: span 2;">
|
||||
<label>Описание</label>
|
||||
<textarea id="serviceDescription" placeholder="Полная уборка с мойкой окон..."></textarea>
|
||||
</div>
|
||||
<div style="grid-column: span 2;">
|
||||
<button type="submit" class="btn btn-primary" style="width: 200px; padding: 15px 30px;">
|
||||
➕ Создать услугу
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Таблица услуг -->
|
||||
<div class="services-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Длительность</th>
|
||||
<th>Цена</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="servicesTable">
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: 40px; color: #666;">
|
||||
Загрузка услуг...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно удаления -->
|
||||
<div id="deleteModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeModal()">×</span>
|
||||
<h3>Удалить услугу?</h3>
|
||||
<p>Услуга <strong id="deleteServiceName"></strong> будет удалена навсегда.</p>
|
||||
<div style="display: flex; gap: 15px; margin-top: 30px;">
|
||||
<button class="btn btn-secondary" onclick="closeModal()" style="flex: 1;">Отмена</button>
|
||||
<button class="btn btn-delete" onclick="confirmDelete()" style="flex: 1;">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = localStorage.getItem('token');
|
||||
let services = [];
|
||||
let selectedServiceId = null;
|
||||
|
||||
// Проверка авторизации и роли
|
||||
if (!token) {
|
||||
window.location.href = 'register-login.html';
|
||||
}
|
||||
|
||||
// Загрузка услуг при загрузке страницы
|
||||
window.onload = loadServices;
|
||||
|
||||
// Загрузить услуги из API
|
||||
async function loadServices() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/services', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
services = await response.json();
|
||||
renderServices();
|
||||
} else {
|
||||
alert('Ошибка доступа или загрузки услуг');
|
||||
window.location.href = 'register-login.html';
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка сети');
|
||||
}
|
||||
}
|
||||
|
||||
// Отобразить услуги в таблице
|
||||
function renderServices() {
|
||||
const tbody = document.getElementById('servicesTable');
|
||||
|
||||
if (services.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 40px; color: #666;">Нет услуг</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Простой цикл для каждой услуги
|
||||
for (let service of services) {
|
||||
const row = document.createElement('tr');
|
||||
const statusClass = service.isactive ? 'active' : 'inactive';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${service.id}</td>
|
||||
<td><strong>${service.name}</strong></td>
|
||||
<td>${service.description || '—'}</td>
|
||||
<td>${service.durationminutes} мин</td>
|
||||
<td>${service.price}₽</td>
|
||||
<td><span class="status-badge status-${statusClass}">${service.isactive ? 'Активна' : 'Неактивна'}</span></td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-small btn-edit" onclick="editService(${service.id})">
|
||||
✏️ Редактировать
|
||||
</button>
|
||||
<button class="btn-small btn-toggle" onclick="toggleService(${service.id}, ${service.isactive})">
|
||||
${service.isactive ? '❌ Деактивировать' : '✅ Активировать'}
|
||||
</button>
|
||||
<button class="btn-small btn-delete" onclick="showDeleteModal(${service.id}, '${service.name}')">
|
||||
🗑️ Удалить
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавить новую услугу
|
||||
async function addService(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = {
|
||||
name: document.getElementById('serviceName').value,
|
||||
description: document.getElementById('serviceDescription').value,
|
||||
durationminutes: parseInt(document.getElementById('serviceDuration').value),
|
||||
price: parseInt(document.getElementById('servicePrice').value),
|
||||
isactive: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/services', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('✅ Услуга создана!');
|
||||
document.getElementById('addServiceForm').reset();
|
||||
loadServices(); // Перезагрузить таблицу
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert('Ошибка: ' + error.error || error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка сети');
|
||||
}
|
||||
}
|
||||
|
||||
// Переключить статус (активна/неактивна)
|
||||
async function toggleService(id, isActive) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/services/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ isactive: !isActive })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ Услуга ${!isActive ? 'активирована' : 'деактивирована'}`);
|
||||
loadServices();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка сети');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать модальное окно удаления
|
||||
function showDeleteModal(id, name) {
|
||||
selectedServiceId = id;
|
||||
document.getElementById('deleteServiceName').textContent = name;
|
||||
document.getElementById('deleteModal').style.display = 'block';
|
||||
}
|
||||
|
||||
// Удалить услугу
|
||||
async function confirmDelete() {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/services/${selectedServiceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('✅ Услуга удалена');
|
||||
closeModal();
|
||||
loadServices();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка сети');
|
||||
}
|
||||
}
|
||||
|
||||
// Закрыть модальное окно
|
||||
function closeModal() {
|
||||
document.getElementById('deleteModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Выход
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// Закрытие модального окна по клику вне области
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('deleteModal');
|
||||
if (event.target === modal) closeModal();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
155
public/booking-confirm.html
Normal file
155
public/booking-confirm.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<!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: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 60px 40px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
.success-icon {
|
||||
font-size: 80px;
|
||||
color: #28a745;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #28a745;
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.booking-number {
|
||||
background: #d4edda;
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
margin: 30px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #155724;
|
||||
border: 3px solid #28a745;
|
||||
}
|
||||
.booking-details {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.detail-row:last-child { border-bottom: none; margin-bottom: 0; }
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
margin: 10px 0;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-primary { background: #667eea; color: white; }
|
||||
.btn-primary:hover { background: #5a67d8; transform: translateY(-2px); }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
.btn-secondary:hover { background: #5a6268; }
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Хедер -->
|
||||
<div class="header">
|
||||
<a href="index.html" style="font-size: 24px; font-weight: bold; color: #667eea;">← Главная</a>
|
||||
<a href="my-bookings.html" class="btn btn-secondary" style="width: auto; padding: 10px 20px;">Мои брони</a>
|
||||
</div>
|
||||
|
||||
<!-- Успешное подтверждение -->
|
||||
<div class="success-icon">✅</div>
|
||||
<h1>Бронь успешно создана!</h1>
|
||||
<p style="color: #666; margin-bottom: 30px;">Сохраните номер брони — он понадобится для отмены или связи</p>
|
||||
|
||||
<div class="booking-number" id="bookingNumber">CL-2025-0042</div>
|
||||
|
||||
<div class="booking-details" id="bookingDetails">
|
||||
<div class="detail-row">
|
||||
<span>🧹 Услуга:</span>
|
||||
<span>Генеральная уборка</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>👨💼 Сотрудник:</span>
|
||||
<span>Иван Петров</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>📅 Дата:</span>
|
||||
<span>15 июня 2025</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>🕒 Время:</span>
|
||||
<span>10:00–12:00</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>💰 Стоимость:</span>
|
||||
<span>5000 ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="services.html" class="btn btn-primary">📋 Забронировать еще</a>
|
||||
<a href="my-bookings.html" class="btn btn-secondary">📋 Мои брони</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Получить данные брони из localStorage (передается из services.html)
|
||||
const bookingData = JSON.parse(localStorage.getItem('lastBooking') || '{}');
|
||||
|
||||
if (bookingData.booking) {
|
||||
const booking = bookingData.booking;
|
||||
document.getElementById('bookingNumber').textContent = booking.bookingnumber;
|
||||
|
||||
// Заполнить детали (простые циклы для поиска)
|
||||
const services = JSON.parse(localStorage.getItem('services') || '[]');
|
||||
const service = services.find(s => s.id == booking.service_id);
|
||||
|
||||
document.querySelector('#bookingDetails .detail-row:nth-child(1) span:last-child').textContent = service ? service.name : 'Услуга';
|
||||
document.querySelector('#bookingDetails .detail-row:nth-child(4) span:last-child').textContent =
|
||||
`${booking.starttime.slice(0,5)}–${booking.endtime.slice(0,5)}`;
|
||||
document.querySelector('#bookingDetails .detail-row:nth-child(5) span:last-child').textContent =
|
||||
`${booking.service.price || '???'} ₽`;
|
||||
}
|
||||
|
||||
// Очистка localStorage через 30 секунд
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem('lastBooking');
|
||||
}, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
174
public/index.html
Normal file
174
public/index.html
Normal file
@@ -0,0 +1,174 @@
|
||||
<!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; line-height: 1.6; color: #333; }
|
||||
|
||||
/* Хедер */
|
||||
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: 28px; font-weight: bold; color: #667eea; }
|
||||
.auth-buttons { display: flex; gap: 15px; }
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-primary { background: #667eea; color: white; }
|
||||
.btn-primary:hover { background: #5a67d8; transform: translateY(-2px); }
|
||||
.btn-secondary { background: transparent; color: #667eea; border: 2px solid #667eea; }
|
||||
.btn-secondary:hover { background: #667eea; color: white; }
|
||||
|
||||
/* Hero секция */
|
||||
.hero {
|
||||
background: linear-gradient(rgba(102,126,234,0.9), rgba(118,75,162,0.9)),
|
||||
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600"><rect fill="%23f8f9fa" width="1200" height="600"/></svg>');
|
||||
padding: 100px 50px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
.hero-content { max-width: 800px; margin: 0 auto; }
|
||||
.hero h1 { font-size: 48px; margin-bottom: 20px; }
|
||||
.hero p { font-size: 20px; margin-bottom: 40px; opacity: 0.95; }
|
||||
.hero-buttons { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; }
|
||||
|
||||
/* Секция услуг */
|
||||
.services { padding: 80px 50px; max-width: 1200px; margin: 0 auto; }
|
||||
.section-title { text-align: center; font-size: 36px; margin-bottom: 60px; color: #333; }
|
||||
.services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 40px; }
|
||||
.service-card {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
.service-card:hover { transform: translateY(-10px); box-shadow: 0 30px 60px rgba(0,0,0,0.15); }
|
||||
.service-icon { font-size: 64px; margin-bottom: 20px; }
|
||||
.service-card h3 { font-size: 24px; margin-bottom: 15px; color: #333; }
|
||||
.service-price { font-size: 28px; font-weight: bold; color: #667eea; margin: 20px 0; }
|
||||
|
||||
/* Футер */
|
||||
footer {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 40px 50px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.footer-content { max-width: 1200px; margin: 0 auto; }
|
||||
.social-links { display: flex; justify-content: center; gap: 20px; margin: 20px 0; }
|
||||
.social-links a { color: white; font-size: 24px; transition: color 0.3s; }
|
||||
.social-links a:hover { color: #667eea; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header, .services { padding-left: 20px; padding-right: 20px; }
|
||||
.hero h1 { font-size: 36px; }
|
||||
.hero-buttons { flex-direction: column; align-items: center; }
|
||||
.services-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Хедер -->
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<div class="logo">🧹 КлинСервис</div>
|
||||
<div class="auth-buttons">
|
||||
<a href="register-login.html" class="btn btn-secondary">Войти</a>
|
||||
<a href="register-login.html" class="btn btn-primary">Регистрация</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero секция -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h1>Профессиональная уборка</h1>
|
||||
<p>Чистота и порядок в вашем доме за 2 часа. Быстро. Качественно. Надежно.</p>
|
||||
<div class="hero-buttons">
|
||||
<a href="services.html" class="btn btn-primary" style="padding: 18px 40px; font-size: 18px;">Заказать уборку</a>
|
||||
<a href="#services" class="btn btn-secondary" style="padding: 18px 40px; font-size: 18px;">Наши услуги</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Услуги -->
|
||||
<section class="services" id="services">
|
||||
<h2 class="section-title">Наши услуги</h2>
|
||||
<div class="services-grid">
|
||||
<div class="service-card">
|
||||
<div class="service-icon">🧹</div>
|
||||
<h3>Поддерживающая уборка</h3>
|
||||
<div class="service-price">от 2500 ₽</div>
|
||||
<p>Быстрая уборка помещений. Пыль, полы, сантехника. Идеально для ежедневной чистоты.</p>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
<div class="service-icon">✨</div>
|
||||
<h3>Генеральная уборка</h3>
|
||||
<div class="service-price">от 5000 ₽</div>
|
||||
<p>Полная уборка с мойкой окон, шкафов, техники. Как после ремонта.</p>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
<div class="service-icon">🪑</div>
|
||||
<h3>Химчистка мебели</h3>
|
||||
<div class="service-price">от 3500 ₽</div>
|
||||
<p>Профессиональная чистка диванов, кресел, ковров. Высокое давление + химия.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Футер -->
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<h3>🧹 КлинСервис</h3>
|
||||
<p>Профессиональная уборка для вашего комфорта</p>
|
||||
<div class="social-links">
|
||||
<a href="#">📱 Telegram</a>
|
||||
<a href="#">📧 WhatsApp</a>
|
||||
<a href="#">📞 +7 (999) 123-45-67</a>
|
||||
</div>
|
||||
<p style="margin-top: 30px; opacity: 0.8;">© 2026 КлинСервис. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Простая прокрутка к секции услуг
|
||||
document.querySelector('.hero-buttons .btn-secondary').onclick = function(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('services').scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
// Проверка авторизации
|
||||
if (localStorage.getItem('token')) {
|
||||
// Пользователь авторизован - можно добавить кнопку "Личный кабинет"
|
||||
console.log('Пользователь авторизован');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
291
public/my-bookings.html
Normal file
291
public/my-bookings.html
Normal file
@@ -0,0 +1,291 @@
|
||||
<!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; }
|
||||
.header-nav { display: flex; gap: 20px; align-items: center; }
|
||||
.btn {
|
||||
padding: 12px 25px; border: none; border-radius: 25px;
|
||||
cursor: pointer; font-size: 16px; text-decoration: none;
|
||||
display: inline-block; transition: all 0.3s;
|
||||
}
|
||||
.btn-primary { background: #667eea; color: white; }
|
||||
.btn-primary:hover { background: #5a67d8; }
|
||||
.btn-secondary { background: transparent; color: #667eea; border: 2px solid #667eea; }
|
||||
.btn-secondary:hover { background: #667eea; color: white; }
|
||||
|
||||
/* Контейнер */
|
||||
.container { max-width: 1200px; margin: 40px auto; padding: 0 20px; }
|
||||
h1 { text-align: center; color: #333; margin-bottom: 40px; font-size: 32px; }
|
||||
|
||||
/* Таблица броней */
|
||||
.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: 20px; text-align: left; border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
th {
|
||||
background: #667eea; color: white; font-weight: bold;
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
}
|
||||
tr:hover { background: #f8f9fa; }
|
||||
|
||||
/* Статусы */
|
||||
.status { padding: 8px 16px; border-radius: 20px; 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: 10px; align-items: center; }
|
||||
.btn-small {
|
||||
padding: 8px 16px; font-size: 14px; border-radius: 15px;
|
||||
text-decoration: none; display: inline-block;
|
||||
}
|
||||
.btn-cancel { background: #dc3545; color: white; }
|
||||
.btn-cancel:hover { background: #c82333; }
|
||||
.btn-cancel:disabled { background: #ccc; cursor: not-allowed; }
|
||||
|
||||
/* Модальное окно отмены */
|
||||
.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;
|
||||
}
|
||||
.modal-buttons { display: flex; gap: 15px; margin-top: 30px; }
|
||||
.btn-modal { flex: 1; padding: 15px; border: none; border-radius: 10px;
|
||||
font-size: 16px; font-weight: bold; cursor: pointer; }
|
||||
.confirm-cancel { background: #dc3545; color: white; }
|
||||
.confirm-cancel:hover { background: #c82333; }
|
||||
|
||||
.no-bookings {
|
||||
text-align: center; padding: 80px 40px; color: #666;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container, header { padding-left: 20px; padding-right: 20px; }
|
||||
.action-buttons { flex-direction: column; }
|
||||
th, td { padding: 15px 10px; font-size: 14px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Хедер -->
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<div class="logo">🧹 КлинСервис</div>
|
||||
<div class="header-nav">
|
||||
<a href="index.html" class="btn btn-secondary">Главная</a>
|
||||
<a href="services.html" class="btn btn-primary">Новая бронь</a>
|
||||
<button class="btn btn-secondary" onclick="logout()">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>📋 Мои бронирования</h1>
|
||||
|
||||
<!-- Таблица броней -->
|
||||
<div class="bookings-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>№ Брони</th>
|
||||
<th>Дата и время</th>
|
||||
<th>Услуга</th>
|
||||
<th>Сотрудник</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="bookingsTable">
|
||||
<tr>
|
||||
<td colspan="6" class="no-bookings">Загрузка броней...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно отмены -->
|
||||
<div id="cancelModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeModal()">×</span>
|
||||
<h3>Отменить бронь?</h3>
|
||||
<p>Укажите причину отмены (необязательно):</p>
|
||||
<textarea id="cancelReason" placeholder="Например: 'Перенесли отпуск', 'Заболел'"></textarea>
|
||||
<div class="modal-buttons">
|
||||
<button class="btn-modal" onclick="closeModal()">Отмена</button>
|
||||
<button class="btn-modal confirm-cancel" id="confirmCancelBtn" onclick="confirmCancel()">Отменить бронь</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = localStorage.getItem('token');
|
||||
let bookings = [];
|
||||
let selectedBookingId = null;
|
||||
|
||||
// Проверка авторизации
|
||||
if (!token) {
|
||||
window.location.href = 'register-login.html';
|
||||
}
|
||||
|
||||
// Загрузка броней при загрузке страницы
|
||||
window.onload = loadBookings;
|
||||
|
||||
// Загрузить список своих броней
|
||||
async function loadBookings() {
|
||||
try {
|
||||
const response = await fetch('/api/bookings', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
bookings = await response.json();
|
||||
renderBookings();
|
||||
} else {
|
||||
alert('Ошибка загрузки броней');
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('bookingsTable').innerHTML =
|
||||
'<tr><td colspan="6" class="no-bookings">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Отобразить брони в таблице
|
||||
function renderBookings() {
|
||||
const tbody = document.getElementById('bookingsTable');
|
||||
|
||||
if (bookings.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="no-bookings">У вас нет броней</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Простой цикл для каждой брони
|
||||
for (let booking of bookings) {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Статус badge
|
||||
let statusClass = '';
|
||||
if (booking.status === 'confirmed') statusClass = 'confirmed';
|
||||
else if (booking.status === 'completed') statusClass = 'completed';
|
||||
else if (booking.status === 'cancelled') statusClass = 'cancelled';
|
||||
|
||||
const canCancel = booking.status === 'confirmed';
|
||||
|
||||
row.innerHTML = `
|
||||
<td><strong>${booking.bookingnumber}</strong></td>
|
||||
<td>${booking.bookingdate} ${booking.starttime.slice(0,5)}–${booking.endtime.slice(0,5)}</td>
|
||||
<td>${booking.service ? booking.service.name : 'Удалена'}</td>
|
||||
<td>${booking.employee ? booking.employee.name : 'Не назначен'}</td>
|
||||
<td><span class="status ${statusClass}">${booking.status}</span></td>
|
||||
<td>
|
||||
${canCancel ?
|
||||
`<button class="btn-small btn-cancel" onclick="showCancelModal(${booking.id})">
|
||||
Отменить
|
||||
</button>` :
|
||||
'—'
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
// Показать модальное окно отмены
|
||||
function showCancelModal(bookingId) {
|
||||
selectedBookingId = bookingId;
|
||||
document.getElementById('cancelModal').style.display = 'block';
|
||||
document.getElementById('cancelReason').value = '';
|
||||
}
|
||||
|
||||
// Закрыть модальное окно
|
||||
function closeModal() {
|
||||
document.getElementById('cancelModal').style.display = 'none';
|
||||
selectedBookingId = null;
|
||||
}
|
||||
|
||||
// Подтвердить отмену
|
||||
async function confirmCancel() {
|
||||
const reason = document.getElementById('cancelReason').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/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('✅ Бронь отменена!');
|
||||
closeModal();
|
||||
loadBookings(); // Перезагрузить список
|
||||
} else {
|
||||
alert('Ошибка: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка сети');
|
||||
}
|
||||
}
|
||||
|
||||
// Выход
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// Закрытие модального окна по клику вне его
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('cancelModal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
218
public/services.html
Normal file
218
public/services.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!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; }
|
||||
.header-nav { display: flex; gap: 20px; align-items: center; }
|
||||
.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-primary:hover { background: #5a67d8; }
|
||||
.btn-secondary { background: transparent; color: #667eea; border: 2px solid #667eea; }
|
||||
.btn-secondary:hover { background: #667eea; color: white; }
|
||||
|
||||
/* Контейнер */
|
||||
.container { max-width: 1000px; margin: 40px auto; padding: 0 20px; }
|
||||
h1 { text-align: center; color: #333; margin-bottom: 40px; font-size: 32px; }
|
||||
|
||||
/* Форма выбора */
|
||||
.selection-form {
|
||||
background: white; padding: 40px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 40px;
|
||||
}
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 30px; }
|
||||
@media (max-width: 768px) { .form-row { grid-template-columns: 1fr; } }
|
||||
|
||||
label { display: block; margin-bottom: 10px; font-weight: bold; color: #555; }
|
||||
select, input {
|
||||
width: 100%; padding: 15px; border: 2px solid #e9ecef;
|
||||
border-radius: 10px; font-size: 16px; transition: border-color 0.3s;
|
||||
}
|
||||
select:focus, input:focus { outline: none; border-color: #667eea; }
|
||||
|
||||
/* Простой календарь */
|
||||
.calendar-section {
|
||||
background: white; padding: 40px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
}
|
||||
.calendar-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 20px; flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.calendar-nav-btn {
|
||||
padding: 10px 20px; background: #667eea; color: white;
|
||||
border: none; border-radius: 10px; cursor: pointer;
|
||||
}
|
||||
.calendar-nav-btn:hover { background: #5a67d8; }
|
||||
.calendar-nav-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
.month-year { font-size: 24px; font-weight: bold; color: #333; }
|
||||
|
||||
.calendar-grid {
|
||||
display: grid; grid-template-columns: repeat(7, 1fr);
|
||||
gap: 5px; text-align: center;
|
||||
}
|
||||
.calendar-day {
|
||||
padding: 15px; border: 1px solid #e9ecef;
|
||||
border-radius: 10px; cursor: pointer; transition: all 0.3s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.calendar-day:hover { background: #e3f2fd; border-color: #667eea; }
|
||||
.calendar-day.other-month { color: #ccc; }
|
||||
.calendar-day.selected {
|
||||
background: #667eea; color: white; box-shadow: 0 5px 15px rgba(102,126,234,0.4);
|
||||
}
|
||||
.calendar-day.disabled {
|
||||
background: #f8f9fa; color: #ccc; cursor: not-allowed;
|
||||
}
|
||||
.weekdays { font-weight: bold; color: #555; padding: 10px 0; }
|
||||
|
||||
/* Слоты */
|
||||
.slots-section {
|
||||
background: white; padding: 40px; border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-top: 30px;
|
||||
}
|
||||
.slots-grid { display: flex; flex-wrap: wrap; gap: 15px; }
|
||||
.slot {
|
||||
padding: 15px 25px; background: #f8f9fa;
|
||||
border: 2px solid transparent; border-radius: 25px;
|
||||
cursor: pointer; transition: all 0.3s; font-weight: 500;
|
||||
}
|
||||
.slot:hover { background: #e3f2fd; border-color: #667eea; }
|
||||
.slot.selected { background: #667eea; color: white; border-color: #5a67d8; }
|
||||
.slot.booked { background: #f8d7da; color: #721c24; cursor: not-allowed; }
|
||||
|
||||
.no-slots { text-align: center; color: #666; padding: 40px; font-style: italic; }
|
||||
|
||||
/* Кнопка забронировать */
|
||||
.book-btn {
|
||||
width: 100%; padding: 20px; background: #28a745;
|
||||
color: white; border: none; border-radius: 15px;
|
||||
font-size: 20px; font-weight: bold; cursor: pointer;
|
||||
margin-top: 30px; transition: all 0.3s;
|
||||
}
|
||||
.book-btn:hover:not(:disabled) { background: #218838; transform: translateY(-2px); }
|
||||
.book-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Хедер -->
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<div class="logo">🧹 КлинСервис</div>
|
||||
<div class="header-nav">
|
||||
<a href="index.html" class="btn btn-secondary">Главная</a>
|
||||
<a href="my-bookings.html" class="btn btn-secondary" id="profileBtn" style="display:none;">Мои брони</a>
|
||||
<button class="btn btn-primary" id="logoutBtn" style="display:none;" onclick="logout()">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>📅 Выберите дату и время</h1>
|
||||
|
||||
<!-- Выбор услуги -->
|
||||
<div class="selection-form">
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label>Услуга *</label>
|
||||
<select id="serviceSelect">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Длительность</label>
|
||||
<input type="text" id="duration" readonly placeholder="Выберите услугу">
|
||||
</div>
|
||||
</div>
|
||||
<button class="book-btn" id="bookBtn" onclick="bookService()" disabled>
|
||||
Выбрать время →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Календарь -->
|
||||
<div class="calendar-section">
|
||||
<div class="calendar-header">
|
||||
<button class="calendar-nav-btn" onclick="prevMonth()">← Месяц</button>
|
||||
<div class="month-year" id="monthYear"></div>
|
||||
<button class="calendar-nav-btn" onclick="nextMonth()">Месяц →</button>
|
||||
</div>
|
||||
<div class="calendar-grid">
|
||||
<div class="weekdays">Пн</div><div class="weekdays">Вт</div><div class="weekdays">Ср</div>
|
||||
<div class="weekdays">Чт</div><div class="weekdays">Пт</div><div class="weekdays">Сб</div><div class="weekdays">Вс</div>
|
||||
<div id="calendarDays"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Слоты времени -->
|
||||
<div class="slots-section" id="slotsSection" style="display:none;">
|
||||
<h3>Доступные слоты</h3>
|
||||
<div class="slots-grid" id="slotsGrid"></div>
|
||||
<button class="book-btn" id="confirmBookingBtn" onclick="confirmBooking()" style="display:none;">
|
||||
✅ Забронировать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let services = [];
|
||||
let selectedServiceId = null;
|
||||
let selectedDate = null;
|
||||
let currentMonth = new Date().getMonth();
|
||||
let currentYear = new Date().getFullYear();
|
||||
let availableSlots = [];
|
||||
let token = localStorage.getItem('token');
|
||||
|
||||
// Проверка авторизации
|
||||
if (!token) {
|
||||
window.location.href = 'register-login.html';
|
||||
throw new Error('Нужна авторизация');
|
||||
}
|
||||
updateHeader();
|
||||
|
||||
// 1. Загрузить услуги при загрузке страницы
|
||||
window.onload = async function() {
|
||||
await loadServices();
|
||||
initCalendar();
|
||||
};
|
||||
|
||||
// Загрузка услуг из API (только isactive=true)
|
||||
async function loadServices() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/services', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
services = data.filter(service => service.isactive);
|
||||
const select = document.getElementById('serviceSelect');
|
||||
|
||||
select.innerHTML = '<option value="">Выберите услугу</option>';
|
||||
for (let service of services) {
|
||||
select.innerHTML += `<option value="${service.id}" data-duration="${service.durationminutes}">${service.name} (${service.durationminutes} мин) - ${service.price}₽</option>`;
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка загрузки услуг');
|
||||
}
|
||||
}
|
||||
|
||||
// Изменение услуги
|
||||
//document.getElementById('serviceSelect').onchange = function()//
|
||||
Reference in New Issue
Block a user