commit 08.01
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user