Files
tourrism_site/public/components/guide-calendar-view.jsx
Andrey K. Choi 13c752b93a feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
2025-11-30 21:57:58 +09:00

373 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;