- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
|
||
const GuideCalendarView = () => {
|
||
const [currentDate, setCurrentDate] = useState(new Date());
|
||
const [workingDays, setWorkingDays] = useState([]);
|
||
const [guides, setGuides] = useState([]);
|
||
const [selectedGuide, setSelectedGuide] = useState('');
|
||
const [loading, setLoading] = useState(true);
|
||
const [stats, setStats] = useState({ totalDays: 0, totalGuides: 0 });
|
||
|
||
useEffect(() => {
|
||
loadGuides();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadWorkingDays();
|
||
}, [currentDate, selectedGuide]);
|
||
|
||
const loadGuides = async () => {
|
||
try {
|
||
const response = await fetch('/api/guides');
|
||
const data = await response.json();
|
||
setGuides(data.success ? data.data : data);
|
||
} catch (error) {
|
||
console.error('Error loading guides:', error);
|
||
}
|
||
};
|
||
|
||
const loadWorkingDays = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const month = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
|
||
const url = selectedGuide
|
||
? `/api/guide-working-days?month=${month}&guide_id=${selectedGuide}`
|
||
: `/api/guide-working-days?month=${month}`;
|
||
|
||
const response = await fetch(url);
|
||
const data = await response.json();
|
||
setWorkingDays(data);
|
||
|
||
// Подсчет статистики
|
||
const uniqueGuides = new Set(data.map(d => d.guide_id));
|
||
setStats({
|
||
totalDays: data.length,
|
||
totalGuides: uniqueGuides.size
|
||
});
|
||
} catch (error) {
|
||
console.error('Error loading working days:', error);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
const getDaysInMonth = (date) => {
|
||
const year = date.getFullYear();
|
||
const month = date.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
const firstDayOfWeek = new Date(year, month, 1).getDay();
|
||
|
||
const days = [];
|
||
|
||
// Добавляем пустые дни в начале
|
||
for (let i = 0; i < (firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1); i++) {
|
||
days.push(null);
|
||
}
|
||
|
||
// Добавляем дни месяца
|
||
for (let day = 1; day <= daysInMonth; day++) {
|
||
days.push(day);
|
||
}
|
||
|
||
return days;
|
||
};
|
||
|
||
const getWorkingDaysForDate = (day) => {
|
||
if (!day) return [];
|
||
const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||
return workingDays.filter(wd => wd.work_date === dateStr);
|
||
};
|
||
|
||
const getGuideById = (id) => {
|
||
return guides.find(g => g.id === id);
|
||
};
|
||
|
||
const changeMonth = (delta) => {
|
||
const newDate = new Date(currentDate);
|
||
newDate.setMonth(newDate.getMonth() + delta);
|
||
setCurrentDate(newDate);
|
||
};
|
||
|
||
const monthNames = [
|
||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
|
||
];
|
||
|
||
const weekDays = ['ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ', 'ВС'];
|
||
|
||
if (loading && workingDays.length === 0) {
|
||
return (
|
||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||
Загрузка календаря...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{ padding: '20px', fontFamily: 'system-ui' }}>
|
||
{/* Заголовок и статистика */}
|
||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<h2>📅 Календарь рабочих дней гидов</h2>
|
||
<div style={{ display: 'flex', gap: '20px', fontSize: '14px' }}>
|
||
<div style={{ padding: '8px 12px', background: '#e3f2fd', borderRadius: '6px' }}>
|
||
<strong>{stats.totalDays}</strong> рабочих дней
|
||
</div>
|
||
<div style={{ padding: '8px 12px', background: '#f3e5f5', borderRadius: '6px' }}>
|
||
<strong>{stats.totalGuides}</strong> активных гидов
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Фильтр по гиду */}
|
||
<div style={{ marginBottom: '20px', display: 'flex', alignItems: 'center', gap: '15px' }}>
|
||
<label style={{ fontWeight: 'bold' }}>Фильтр по гиду:</label>
|
||
<select
|
||
value={selectedGuide}
|
||
onChange={(e) => setSelectedGuide(e.target.value)}
|
||
style={{
|
||
padding: '8px 12px',
|
||
borderRadius: '6px',
|
||
border: '1px solid #ddd',
|
||
minWidth: '200px',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
<option value="">Все гиды</option>
|
||
{guides.map(guide => (
|
||
<option key={guide.id} value={guide.id}>
|
||
{guide.name} ({guide.specialization})
|
||
</option>
|
||
))}
|
||
</select>
|
||
{selectedGuide && (
|
||
<button
|
||
onClick={() => setSelectedGuide('')}
|
||
style={{
|
||
padding: '6px 12px',
|
||
background: '#ff5722',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '12px'
|
||
}}
|
||
>
|
||
Очистить
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Навигация по месяцам */}
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
marginBottom: '20px',
|
||
padding: '15px',
|
||
background: '#f8f9fa',
|
||
borderRadius: '8px'
|
||
}}>
|
||
<button
|
||
onClick={() => changeMonth(-1)}
|
||
style={{
|
||
padding: '10px 20px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '6px',
|
||
background: 'white',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
fontWeight: 'bold'
|
||
}}
|
||
>
|
||
← Предыдущий
|
||
</button>
|
||
<h3 style={{ margin: 0, color: '#333' }}>
|
||
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||
</h3>
|
||
<button
|
||
onClick={() => changeMonth(1)}
|
||
style={{
|
||
padding: '10px 20px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '6px',
|
||
background: 'white',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
fontWeight: 'bold'
|
||
}}
|
||
>
|
||
Следующий →
|
||
</button>
|
||
</div>
|
||
|
||
{/* Календарная сетка */}
|
||
<div style={{
|
||
background: 'white',
|
||
borderRadius: '8px',
|
||
overflow: 'hidden',
|
||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||
}}>
|
||
{/* Заголовки дней недели */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
|
||
{weekDays.map(day => (
|
||
<div key={day} style={{
|
||
padding: '15px',
|
||
textAlign: 'center',
|
||
fontWeight: 'bold',
|
||
background: '#f5f5f5',
|
||
borderBottom: '1px solid #ddd',
|
||
color: '#333'
|
||
}}>
|
||
{day}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Дни месяца */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
|
||
{getDaysInMonth(currentDate).map((day, index) => {
|
||
const dayWorkingData = getWorkingDaysForDate(day);
|
||
const hasData = dayWorkingData.length > 0;
|
||
|
||
return (
|
||
<div
|
||
key={index}
|
||
style={{
|
||
padding: '10px',
|
||
minHeight: '120px',
|
||
border: '1px solid #e0e0e0',
|
||
background: day ? (hasData ? '#e8f5e8' : 'white') : '#f9f9f9',
|
||
color: day ? '#333' : '#ccc',
|
||
position: 'relative'
|
||
}}
|
||
>
|
||
{day && (
|
||
<>
|
||
<div style={{
|
||
fontWeight: 'bold',
|
||
marginBottom: '8px',
|
||
color: hasData ? '#2e7d32' : '#666'
|
||
}}>
|
||
{day}
|
||
</div>
|
||
|
||
{dayWorkingData.map((workDay, idx) => {
|
||
const guide = getGuideById(workDay.guide_id);
|
||
return (
|
||
<div
|
||
key={idx}
|
||
style={{
|
||
fontSize: '11px',
|
||
padding: '4px 6px',
|
||
margin: '2px 0',
|
||
background: guide?.specialization === 'city' ? '#bbdefb' :
|
||
guide?.specialization === 'mountain' ? '#c8e6c9' :
|
||
guide?.specialization === 'fishing' ? '#ffcdd2' : '#f0f0f0',
|
||
borderRadius: '4px',
|
||
color: '#333',
|
||
lineHeight: '1.2'
|
||
}}
|
||
title={workDay.notes}
|
||
>
|
||
<div style={{ fontWeight: 'bold' }}>
|
||
{guide?.name || `Гид #${workDay.guide_id}`}
|
||
</div>
|
||
{workDay.notes && (
|
||
<div style={{ opacity: 0.8, marginTop: '2px' }}>
|
||
{workDay.notes.length > 20 ? workDay.notes.substring(0, 20) + '...' : workDay.notes}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Легенда */}
|
||
<div style={{
|
||
marginTop: '20px',
|
||
display: 'flex',
|
||
gap: '20px',
|
||
fontSize: '14px',
|
||
flexWrap: 'wrap'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<div style={{ width: '20px', height: '20px', background: '#bbdefb', borderRadius: '4px' }}></div>
|
||
<span>Городские туры</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<div style={{ width: '20px', height: '20px', background: '#c8e6c9', borderRadius: '4px' }}></div>
|
||
<span>Горные туры</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<div style={{ width: '20px', height: '20px', background: '#ffcdd2', borderRadius: '4px' }}></div>
|
||
<span>Морская рыбалка</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<div style={{ width: '20px', height: '20px', background: '#e8f5e8', borderRadius: '4px' }}></div>
|
||
<span>Рабочий день</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Быстрые действия */}
|
||
<div style={{
|
||
marginTop: '20px',
|
||
padding: '15px',
|
||
background: '#f8f9fa',
|
||
borderRadius: '8px'
|
||
}}>
|
||
<h4 style={{ margin: '0 0 10px 0' }}>Быстрые действия:</h4>
|
||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||
<button
|
||
onClick={() => window.open('/admin/calendar-view', '_blank')}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#2196f3',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '6px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
📅 Полный календарь
|
||
</button>
|
||
<button
|
||
onClick={() => window.open('/admin/schedule-manager', '_blank')}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#4caf50',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '6px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
⚡ Планировщик смен
|
||
</button>
|
||
<button
|
||
onClick={() => loadWorkingDays()}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#ff9800',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '6px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
🔄 Обновить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default GuideCalendarView; |