- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
865 lines
34 KiB
JavaScript
865 lines
34 KiB
JavaScript
/**
|
||
* GuideScheduleManager - Компонент для планирования рабочих смен гидов
|
||
*/
|
||
|
||
class GuideScheduleManager {
|
||
constructor(options = {}) {
|
||
this.container = options.container || document.body;
|
||
this.onScheduleChange = options.onScheduleChange || null;
|
||
this.allowMultiSelect = options.allowMultiSelect !== false;
|
||
|
||
this.currentDate = new Date();
|
||
this.currentDate.setDate(1); // Установить на первый день месяца
|
||
|
||
this.selectedGuides = new Set();
|
||
this.workingDays = new Map(); // guideId -> Set of dates
|
||
this.guides = [];
|
||
|
||
this.init();
|
||
}
|
||
|
||
async init() {
|
||
this.render();
|
||
await this.loadGuides();
|
||
this.bindEvents();
|
||
this.renderCalendar();
|
||
}
|
||
|
||
render() {
|
||
this.container.innerHTML = `
|
||
<div class="schedule-manager">
|
||
<div class="schedule-header">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h4><i class="fas fa-calendar-week me-2"></i>Планировщик рабочих смен</h4>
|
||
<div class="schedule-actions">
|
||
<button class="btn btn-outline-secondary btn-sm" id="copyPrevMonth">
|
||
<i class="fas fa-copy me-1"></i>Скопировать из прошлого месяца
|
||
</button>
|
||
<button class="btn btn-outline-secondary btn-sm" id="copyNextMonth">
|
||
<i class="fas fa-paste me-1"></i>Скопировать в следующий месяц
|
||
</button>
|
||
<button class="btn btn-primary btn-sm" id="saveSchedule">
|
||
<i class="fas fa-save me-1"></i>Сохранить изменения
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<!-- Панель выбора гидов -->
|
||
<div class="col-lg-3">
|
||
<div class="guides-panel">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">Выбор гидов</h6>
|
||
<small class="text-muted">Выберите гидов для планирования</small>
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div class="guide-selection-actions mb-3">
|
||
<button class="btn btn-sm btn-outline-primary w-100 mb-2" id="selectAllGuides">
|
||
<i class="fas fa-check-square me-1"></i>Выбрать всех
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-secondary w-100" id="clearGuideSelection">
|
||
<i class="fas fa-square me-1"></i>Очистить выбор
|
||
</button>
|
||
</div>
|
||
<div id="guidesList" class="guides-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Панель быстрых действий -->
|
||
<div class="card mt-3">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">Быстрые действия</h6>
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div class="d-grid gap-2">
|
||
<button class="btn btn-sm btn-success" id="markWeekdays">
|
||
<i class="fas fa-business-time me-1"></i>Отметить будни
|
||
</button>
|
||
<button class="btn btn-sm btn-warning" id="markWeekends">
|
||
<i class="fas fa-calendar-day me-1"></i>Отметить выходные
|
||
</button>
|
||
<button class="btn btn-sm btn-info" id="markFullMonth">
|
||
<i class="fas fa-calendar-check me-1"></i>Весь месяц
|
||
</button>
|
||
<button class="btn btn-sm btn-danger" id="clearMonth">
|
||
<i class="fas fa-calendar-times me-1"></i>Очистить месяц
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Календарь -->
|
||
<div class="col-lg-9">
|
||
<div class="calendar-container">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<button class="btn btn-outline-primary btn-sm" id="prevMonth">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</button>
|
||
<h5 class="mb-0" id="currentMonthLabel"></h5>
|
||
<button class="btn btn-outline-primary btn-sm" id="nextMonth">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div id="scheduleCalendar"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Статистика -->
|
||
<div class="schedule-stats mt-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">Статистика рабочих дней</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="scheduleStats" class="row g-3"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
this.injectStyles();
|
||
}
|
||
|
||
injectStyles() {
|
||
if (document.getElementById('schedule-manager-styles')) return;
|
||
|
||
const styles = `
|
||
<style id="schedule-manager-styles">
|
||
.schedule-manager {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
.guide-checkbox {
|
||
margin-bottom: 8px;
|
||
padding: 8px;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.guide-checkbox:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.guide-checkbox.selected {
|
||
background-color: #e3f2fd;
|
||
border-color: #2196f3;
|
||
}
|
||
|
||
.guide-checkbox input[type="checkbox"] {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.guide-info {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.guide-name {
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.guide-specialization {
|
||
font-size: 12px;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.schedule-calendar {
|
||
width: 100%;
|
||
}
|
||
|
||
.calendar-day {
|
||
min-height: 80px;
|
||
border: 1px solid #dee2e6;
|
||
position: relative;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
padding: 8px;
|
||
}
|
||
|
||
.calendar-day:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.calendar-day.selected {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.calendar-day.working {
|
||
background-color: #d4edda;
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.calendar-day.partial-working {
|
||
background: linear-gradient(45deg, #d4edda 50%, #fff3cd 50%);
|
||
border-color: #ffc107;
|
||
}
|
||
|
||
.calendar-day.weekend {
|
||
background-color: #fff3cd;
|
||
}
|
||
|
||
.calendar-day.other-month {
|
||
color: #6c757d;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.day-number {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.working-guides {
|
||
font-size: 10px;
|
||
color: #28a745;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.working-guides .guide-initial {
|
||
display: inline-block;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: #28a745;
|
||
color: white;
|
||
border-radius: 50%;
|
||
text-align: center;
|
||
line-height: 16px;
|
||
margin-right: 2px;
|
||
font-size: 9px;
|
||
}
|
||
|
||
.calendar-header {
|
||
background: #f8f9fa;
|
||
padding: 8px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.schedule-stats .stat-item {
|
||
text-align: center;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #6c757d;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.schedule-actions {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.calendar-day {
|
||
min-height: 60px;
|
||
padding: 4px;
|
||
}
|
||
|
||
.working-guides .guide-initial {
|
||
width: 12px;
|
||
height: 12px;
|
||
line-height: 12px;
|
||
font-size: 8px;
|
||
}
|
||
}
|
||
</style>
|
||
`;
|
||
|
||
document.head.insertAdjacentHTML('beforeend', styles);
|
||
}
|
||
|
||
async loadGuides() {
|
||
try {
|
||
const response = await fetch('/api/guides');
|
||
const data = await response.json();
|
||
this.guides = Array.isArray(data) ? data : (data.data || []);
|
||
this.renderGuidesList();
|
||
await this.loadSchedules();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки гидов:', error);
|
||
}
|
||
}
|
||
|
||
renderGuidesList() {
|
||
const guidesContainer = document.getElementById('guidesList');
|
||
|
||
guidesContainer.innerHTML = this.guides.map(guide => `
|
||
<div class="guide-checkbox" data-guide-id="${guide.id}">
|
||
<label class="d-flex align-items-center guide-info">
|
||
<input type="checkbox" value="${guide.id}">
|
||
<div class="flex-grow-1">
|
||
<div class="guide-name">${guide.name}</div>
|
||
<div class="guide-specialization">${guide.specialization || 'Универсальный'}</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
async loadSchedules() {
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth() + 1;
|
||
|
||
try {
|
||
const response = await fetch(`/api/guide-schedules?year=${year}&month=${month}`);
|
||
const data = await response.json();
|
||
const schedules = Array.isArray(data) ? data : (data.data || []);
|
||
|
||
this.workingDays.clear();
|
||
|
||
schedules.forEach(schedule => {
|
||
if (!this.workingDays.has(schedule.guide_id)) {
|
||
this.workingDays.set(schedule.guide_id, new Set());
|
||
}
|
||
this.workingDays.get(schedule.guide_id).add(schedule.work_date);
|
||
});
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки расписания:', error);
|
||
}
|
||
}
|
||
|
||
renderCalendar() {
|
||
const calendar = document.getElementById('scheduleCalendar');
|
||
const monthLabel = document.getElementById('currentMonthLabel');
|
||
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
|
||
monthLabel.textContent = this.currentDate.toLocaleDateString('ru-RU', {
|
||
year: 'numeric',
|
||
month: 'long'
|
||
});
|
||
|
||
const firstDay = new Date(year, month, 1);
|
||
const lastDay = new Date(year, month + 1, 0);
|
||
const startDate = new Date(firstDay);
|
||
startDate.setDate(startDate.getDate() - firstDay.getDay() + 1); // Начать с понедельника
|
||
|
||
const weeks = [];
|
||
let currentWeek = [];
|
||
let currentDate = new Date(startDate);
|
||
|
||
for (let i = 0; i < 42; i++) {
|
||
currentWeek.push(new Date(currentDate));
|
||
currentDate.setDate(currentDate.getDate() + 1);
|
||
|
||
if (currentWeek.length === 7) {
|
||
weeks.push(currentWeek);
|
||
currentWeek = [];
|
||
}
|
||
}
|
||
|
||
const calendarHTML = `
|
||
<table class="schedule-calendar table table-bordered mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th class="calendar-header">Пн</th>
|
||
<th class="calendar-header">Вт</th>
|
||
<th class="calendar-header">Ср</th>
|
||
<th class="calendar-header">Чт</th>
|
||
<th class="calendar-header">Пт</th>
|
||
<th class="calendar-header">Сб</th>
|
||
<th class="calendar-header">Вс</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${weeks.map(week => `
|
||
<tr>
|
||
${week.map(date => this.renderCalendarDay(date, month)).join('')}
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
calendar.innerHTML = calendarHTML;
|
||
}
|
||
|
||
renderCalendarDay(date, currentMonth) {
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
const dayOfWeek = date.getDay();
|
||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||
const isCurrentMonth = date.getMonth() === currentMonth;
|
||
|
||
let classes = ['calendar-day'];
|
||
|
||
if (!isCurrentMonth) {
|
||
classes.push('other-month');
|
||
} else if (isWeekend) {
|
||
classes.push('weekend');
|
||
}
|
||
|
||
// Проверить, работают ли гиды в этот день
|
||
const workingGuidesCount = this.getWorkingGuidesForDate(dateStr).length;
|
||
const selectedGuidesCount = this.selectedGuides.size;
|
||
|
||
if (workingGuidesCount > 0) {
|
||
if (selectedGuidesCount === 0 || workingGuidesCount === selectedGuidesCount) {
|
||
classes.push('working');
|
||
} else {
|
||
classes.push('partial-working');
|
||
}
|
||
}
|
||
|
||
const workingGuides = this.getWorkingGuidesForDate(dateStr);
|
||
const workingGuidesHTML = workingGuides.length > 0 ? `
|
||
<div class="working-guides">
|
||
${workingGuides.slice(0, 5).map(guide => `
|
||
<span class="guide-initial" title="${guide.name}">${guide.name.charAt(0)}</span>
|
||
`).join('')}
|
||
${workingGuides.length > 5 ? `<span title="+${workingGuides.length - 5} еще">+${workingGuides.length - 5}</span>` : ''}
|
||
</div>
|
||
` : '';
|
||
|
||
return `
|
||
<td class="${classes.join(' ')}" data-date="${dateStr}">
|
||
<div class="day-number">${date.getDate()}</div>
|
||
${workingGuidesHTML}
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
getWorkingGuidesForDate(dateStr) {
|
||
const working = [];
|
||
this.workingDays.forEach((dates, guideId) => {
|
||
if (dates.has(dateStr)) {
|
||
const guide = this.guides.find(g => g.id == guideId);
|
||
if (guide) working.push(guide);
|
||
}
|
||
});
|
||
return working;
|
||
}
|
||
|
||
bindEvents() {
|
||
// Навигация по месяцам
|
||
document.getElementById('prevMonth').addEventListener('click', () => {
|
||
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
|
||
this.loadSchedules();
|
||
});
|
||
|
||
document.getElementById('nextMonth').addEventListener('click', () => {
|
||
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
|
||
this.loadSchedules();
|
||
});
|
||
|
||
// Выбор гидов
|
||
document.getElementById('guidesList').addEventListener('change', (e) => {
|
||
if (e.target.type === 'checkbox') {
|
||
this.handleGuideSelection(e.target);
|
||
}
|
||
});
|
||
|
||
// Быстрые действия
|
||
document.getElementById('selectAllGuides').addEventListener('click', () => this.selectAllGuides());
|
||
document.getElementById('clearGuideSelection').addEventListener('click', () => this.clearGuideSelection());
|
||
|
||
// Быстрое планирование
|
||
document.getElementById('markWeekdays').addEventListener('click', () => this.markWeekdays());
|
||
document.getElementById('markWeekends').addEventListener('click', () => this.markWeekends());
|
||
document.getElementById('markFullMonth').addEventListener('click', () => this.markFullMonth());
|
||
document.getElementById('clearMonth').addEventListener('click', () => this.clearMonth());
|
||
|
||
// Копирование между месяцами
|
||
document.getElementById('copyPrevMonth').addEventListener('click', () => this.copyFromPreviousMonth());
|
||
document.getElementById('copyNextMonth').addEventListener('click', () => this.copyToNextMonth());
|
||
|
||
// Сохранение
|
||
document.getElementById('saveSchedule').addEventListener('click', () => this.saveSchedule());
|
||
|
||
// Клики по дням календаря
|
||
this.container.addEventListener('click', (e) => {
|
||
const calendarDay = e.target.closest('.calendar-day');
|
||
if (calendarDay && !calendarDay.classList.contains('other-month')) {
|
||
this.handleDayClick(calendarDay);
|
||
}
|
||
});
|
||
}
|
||
|
||
handleGuideSelection(checkbox) {
|
||
const guideId = parseInt(checkbox.value);
|
||
const guideCheckbox = checkbox.closest('.guide-checkbox');
|
||
|
||
if (checkbox.checked) {
|
||
this.selectedGuides.add(guideId);
|
||
guideCheckbox.classList.add('selected');
|
||
} else {
|
||
this.selectedGuides.delete(guideId);
|
||
guideCheckbox.classList.remove('selected');
|
||
}
|
||
|
||
this.renderCalendar();
|
||
}
|
||
|
||
selectAllGuides() {
|
||
const checkboxes = document.querySelectorAll('#guidesList input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
cb.checked = true;
|
||
this.handleGuideSelection(cb);
|
||
});
|
||
}
|
||
|
||
clearGuideSelection() {
|
||
const checkboxes = document.querySelectorAll('#guidesList input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
cb.checked = false;
|
||
this.handleGuideSelection(cb);
|
||
});
|
||
}
|
||
|
||
handleDayClick(dayElement) {
|
||
if (this.selectedGuides.size === 0) {
|
||
alert('Выберите хотя бы одного гида для планирования смен');
|
||
return;
|
||
}
|
||
|
||
const dateStr = dayElement.dataset.date;
|
||
const isWorking = dayElement.classList.contains('working') || dayElement.classList.contains('partial-working');
|
||
|
||
this.selectedGuides.forEach(guideId => {
|
||
if (!this.workingDays.has(guideId)) {
|
||
this.workingDays.set(guideId, new Set());
|
||
}
|
||
|
||
const guideDates = this.workingDays.get(guideId);
|
||
|
||
if (isWorking && this.allSelectedGuidesWorkingOnDate(dateStr)) {
|
||
// Если все выбранные гиды работают в этот день, убираем их
|
||
guideDates.delete(dateStr);
|
||
} else {
|
||
// Иначе добавляем день
|
||
guideDates.add(dateStr);
|
||
}
|
||
});
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
}
|
||
|
||
allSelectedGuidesWorkingOnDate(dateStr) {
|
||
for (let guideId of this.selectedGuides) {
|
||
if (!this.workingDays.has(guideId) || !this.workingDays.get(guideId).has(dateStr)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
markWeekdays() {
|
||
if (this.selectedGuides.size === 0) return;
|
||
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
for (let day = 1; day <= daysInMonth; day++) {
|
||
const date = new Date(year, month, day);
|
||
const dayOfWeek = date.getDay();
|
||
|
||
if (dayOfWeek >= 1 && dayOfWeek <= 5) { // Понедельник - Пятница
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
this.selectedGuides.forEach(guideId => {
|
||
if (!this.workingDays.has(guideId)) {
|
||
this.workingDays.set(guideId, new Set());
|
||
}
|
||
this.workingDays.get(guideId).add(dateStr);
|
||
});
|
||
}
|
||
}
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
}
|
||
|
||
markWeekends() {
|
||
if (this.selectedGuides.size === 0) return;
|
||
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
for (let day = 1; day <= daysInMonth; day++) {
|
||
const date = new Date(year, month, day);
|
||
const dayOfWeek = date.getDay();
|
||
|
||
if (dayOfWeek === 0 || dayOfWeek === 6) { // Суббота - Воскресенье
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
this.selectedGuides.forEach(guideId => {
|
||
if (!this.workingDays.has(guideId)) {
|
||
this.workingDays.set(guideId, new Set());
|
||
}
|
||
this.workingDays.get(guideId).add(dateStr);
|
||
});
|
||
}
|
||
}
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
}
|
||
|
||
markFullMonth() {
|
||
if (this.selectedGuides.size === 0) return;
|
||
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
for (let day = 1; day <= daysInMonth; day++) {
|
||
const date = new Date(year, month, day);
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
|
||
this.selectedGuides.forEach(guideId => {
|
||
if (!this.workingDays.has(guideId)) {
|
||
this.workingDays.set(guideId, new Set());
|
||
}
|
||
this.workingDays.get(guideId).add(dateStr);
|
||
});
|
||
}
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
}
|
||
|
||
clearMonth() {
|
||
if (this.selectedGuides.size === 0) return;
|
||
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
for (let day = 1; day <= daysInMonth; day++) {
|
||
const date = new Date(year, month, day);
|
||
const dateStr = date.toISOString().split('T')[0];
|
||
|
||
this.selectedGuides.forEach(guideId => {
|
||
if (this.workingDays.has(guideId)) {
|
||
this.workingDays.get(guideId).delete(dateStr);
|
||
}
|
||
});
|
||
}
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
}
|
||
|
||
async copyFromPreviousMonth() {
|
||
const prevMonth = new Date(this.currentDate);
|
||
prevMonth.setMonth(prevMonth.getMonth() - 1);
|
||
|
||
const year = prevMonth.getFullYear();
|
||
const month = prevMonth.getMonth() + 1;
|
||
|
||
try {
|
||
const response = await fetch(`/api/guide-schedules?year=${year}&month=${month}`);
|
||
const data = await response.json();
|
||
const schedules = Array.isArray(data) ? data : (data.data || []);
|
||
|
||
// Копируем расписание из предыдущего месяца в текущий
|
||
schedules.forEach(schedule => {
|
||
const prevDate = new Date(schedule.work_date);
|
||
const currentMonthDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), prevDate.getDate());
|
||
|
||
// Проверяем, что день существует в текущем месяце
|
||
if (currentMonthDate.getMonth() === this.currentDate.getMonth()) {
|
||
const dateStr = currentMonthDate.toISOString().split('T')[0];
|
||
|
||
if (!this.workingDays.has(schedule.guide_id)) {
|
||
this.workingDays.set(schedule.guide_id, new Set());
|
||
}
|
||
this.workingDays.get(schedule.guide_id).add(dateStr);
|
||
}
|
||
});
|
||
|
||
this.renderCalendar();
|
||
this.updateStats();
|
||
|
||
alert('Расписание скопировано из предыдущего месяца');
|
||
} catch (error) {
|
||
console.error('Ошибка копирования расписания:', error);
|
||
alert('Ошибка при копировании расписания');
|
||
}
|
||
}
|
||
|
||
async copyToNextMonth() {
|
||
// Сначала сохраняем текущие изменения
|
||
await this.saveSchedule(false);
|
||
|
||
const nextMonth = new Date(this.currentDate);
|
||
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
||
|
||
const scheduleData = [];
|
||
const year = nextMonth.getFullYear();
|
||
const month = nextMonth.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
// Создаем расписание для следующего месяца
|
||
this.workingDays.forEach((dates, guideId) => {
|
||
dates.forEach(dateStr => {
|
||
const currentDate = new Date(dateStr);
|
||
const day = currentDate.getDate();
|
||
|
||
// Проверяем, что день существует в следующем месяце
|
||
if (day <= daysInMonth) {
|
||
const nextMonthDate = new Date(year, month, day);
|
||
const nextDateStr = nextMonthDate.toISOString().split('T')[0];
|
||
|
||
scheduleData.push({
|
||
guide_id: guideId,
|
||
work_date: nextDateStr
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
try {
|
||
const response = await fetch('/api/guide-schedules/batch', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ schedules: scheduleData })
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert('Расписание скопировано в следующий месяц');
|
||
} else {
|
||
throw new Error('Ошибка сохранения');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка копирования расписания:', error);
|
||
alert('Ошибка при копировании расписания в следующий месяц');
|
||
}
|
||
}
|
||
|
||
async saveSchedule(showAlert = true) {
|
||
const scheduleData = [];
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth() + 1;
|
||
|
||
this.workingDays.forEach((dates, guideId) => {
|
||
dates.forEach(dateStr => {
|
||
const date = new Date(dateStr);
|
||
if (date.getFullYear() === year && date.getMonth() + 1 === month) {
|
||
scheduleData.push({
|
||
guide_id: guideId,
|
||
work_date: dateStr
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
try {
|
||
const response = await fetch(`/api/guide-schedules?year=${year}&month=${month}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ schedules: scheduleData })
|
||
});
|
||
|
||
if (response.ok) {
|
||
if (showAlert) {
|
||
alert('Расписание сохранено успешно');
|
||
}
|
||
if (this.onScheduleChange) {
|
||
this.onScheduleChange(scheduleData);
|
||
}
|
||
} else {
|
||
throw new Error('Ошибка сохранения');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка сохранения расписания:', error);
|
||
if (showAlert) {
|
||
alert('Ошибка при сохранении расписания');
|
||
}
|
||
}
|
||
}
|
||
|
||
updateStats() {
|
||
const statsContainer = document.getElementById('scheduleStats');
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
// Подсчет статистики
|
||
const stats = {
|
||
totalGuides: this.guides.length,
|
||
activeGuides: 0,
|
||
totalWorkingDays: 0,
|
||
averageWorkingDays: 0
|
||
};
|
||
|
||
const guideWorkingDays = new Map();
|
||
|
||
this.workingDays.forEach((dates, guideId) => {
|
||
const currentMonthDays = Array.from(dates).filter(dateStr => {
|
||
const date = new Date(dateStr);
|
||
return date.getFullYear() === year && date.getMonth() === month;
|
||
});
|
||
|
||
if (currentMonthDays.length > 0) {
|
||
stats.activeGuides++;
|
||
guideWorkingDays.set(guideId, currentMonthDays.length);
|
||
stats.totalWorkingDays += currentMonthDays.length;
|
||
}
|
||
});
|
||
|
||
stats.averageWorkingDays = stats.activeGuides > 0 ?
|
||
Math.round(stats.totalWorkingDays / stats.activeGuides * 10) / 10 : 0;
|
||
|
||
const coverage = stats.activeGuides > 0 ?
|
||
Math.round((stats.totalWorkingDays / (daysInMonth * stats.activeGuides)) * 100) : 0;
|
||
|
||
statsContainer.innerHTML = `
|
||
<div class="col-md-3">
|
||
<div class="stat-item">
|
||
<div class="stat-number text-primary">${stats.totalGuides}</div>
|
||
<div class="stat-label">Всего гидов</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-item">
|
||
<div class="stat-number text-success">${stats.activeGuides}</div>
|
||
<div class="stat-label">Активных гидов</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-item">
|
||
<div class="stat-number text-info">${stats.averageWorkingDays}</div>
|
||
<div class="stat-label">Ср. дней/гид</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-item">
|
||
<div class="stat-number text-warning">${coverage}%</div>
|
||
<div class="stat-label">Покрытие месяца</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
if (typeof window !== 'undefined') {
|
||
window.GuideScheduleManager = GuideScheduleManager;
|
||
} |