feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
865
public/components/guide-schedule-manager.js
Normal file
865
public/components/guide-schedule-manager.js
Normal file
@@ -0,0 +1,865 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user