commit 08.01

This commit is contained in:
Владимир
2026-01-08 12:38:09 +00:00
parent bbe639b604
commit f5c68bf0c7
13 changed files with 4444 additions and 542 deletions

430
public/admin-schedule.html Normal file
View 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>