import { useState, useEffect, useRef } from 'react'; import { Calendar, Plus, Edit, Trash2, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react'; import { routesApi, routeStopsApi, schedulesApi, holidaysApi } from '../services/api'; import { PageLoader } from '../components/AnimatedComponents'; import RouteBadge from '../components/RouteBadge'; import './SchedulesPage.css'; const DAY_TYPES = [ { value: 'weekday', label: 'Будние дни', description: 'Пн-Пт' }, { value: 'saturday', label: 'Суббота', description: 'Сб' }, { value: 'sunday', label: 'Воскресенье', description: 'Вс' }, { value: 'holiday', label: 'Праздники', description: 'По расписанию воскресенья' } ]; const WEEKDAYS = [ { value: 1, label: 'Пн' }, { value: 2, label: 'Вт' }, { value: 3, label: 'Ср' }, { value: 4, label: 'Чт' }, { value: 5, label: 'Пт' }, { value: 6, label: 'Сб' }, { value: 7, label: 'Вс' } ]; export default function SchedulesPage() { const [routes, setRoutes] = useState([]); const [selectedRoute, setSelectedRoute] = useState(null); const [routeStops, setRouteStops] = useState([]); const [schedules, setSchedules] = useState([]); const [loading, setLoading] = useState(true); const [showEditor, setShowEditor] = useState(false); const [editingSchedule, setEditingSchedule] = useState(null); const [upcomingHolidays, setUpcomingHolidays] = useState([]); const [expandedSchedule, setExpandedSchedule] = useState(null); const [detailsLoading, setDetailsLoading] = useState(false); const [detailsError, setDetailsError] = useState(''); const detailsRequest = useRef(0); const activeRouteId = useRef(null); useEffect(() => { loadRoutes(); loadUpcomingHolidays(); }, []); useEffect(() => { activeRouteId.current = selectedRoute?.id; setSchedules([]); setRouteStops([]); setExpandedSchedule(null); setShowEditor(false); if (selectedRoute) loadRouteDetails(selectedRoute.id); return () => { activeRouteId.current = null; detailsRequest.current += 1; }; }, [selectedRoute?.id]); const loadRoutes = async () => { try { const response = await routesApi.getAll(); setRoutes(response.data); if (response.data.length > 0) { setSelectedRoute(response.data[0]); } } catch (error) { console.error('Error loading routes:', error); } finally { setLoading(false); } }; const loadRouteDetails = async (routeId) => { if (routeId !== activeRouteId.current) return; const request = ++detailsRequest.current; setDetailsLoading(true); setDetailsError(''); try { const [scheduleResponse, stopsResponse] = await Promise.all([ schedulesApi.getRouteSchedules(routeId), routeStopsApi.getRouteStops(routeId), ]); if (request !== detailsRequest.current) return; setSchedules(scheduleResponse.data); setRouteStops(stopsResponse.data); } catch { if (request === detailsRequest.current) { setDetailsError('Не удалось загрузить расписание и остановки маршрута.'); } } finally { if (request === detailsRequest.current) setDetailsLoading(false); } }; const loadUpcomingHolidays = async () => { try { const response = await holidaysApi.getUpcoming(5); setUpcomingHolidays(response.data); } catch (error) { console.error('Error loading holidays:', error); } }; const handleCreateSchedule = (dayType) => { setEditingSchedule({ routeId: selectedRoute.id, dayType: dayType, departureTimes: [], trips: [] }); setShowEditor(true); }; const handleEditSchedule = (schedule) => { setEditingSchedule(schedule); setShowEditor(true); }; const handleDeleteSchedule = async (id) => { if (!confirm('Удалить это расписание?')) return; try { await schedulesApi.delete(id); loadRouteDetails(selectedRoute.id); } catch (error) { console.error('Error deleting schedule:', error); alert('Ошибка удаления расписания'); } }; const handleCopySchedule = async (fromDayType, toDayType) => { try { await schedulesApi.copy(selectedRoute.id, fromDayType, toDayType); loadRouteDetails(selectedRoute.id); } catch (error) { console.error('Error copying schedule:', error); alert('Ошибка копирования расписания'); } }; const handleSaveSchedule = async (data) => { try { if (editingSchedule.id) { await schedulesApi.update(editingSchedule.id, data); } else { await schedulesApi.create(data); } setShowEditor(false); setEditingSchedule(null); loadRouteDetails(selectedRoute.id); } catch (error) { console.error('Error saving schedule:', error); alert(error.response?.data?.message || 'Ошибка сохранения расписания'); } }; const getScheduleForDayType = (dayType) => { return schedules.find(s => s.day_type === dayType); }; if (loading) { return ; } return (

Расписания

{/* Upcoming Holidays Info */} {upcomingHolidays.length > 0 && (

Ближайшие праздники

Транспорт идет по расписанию воскресенья

{upcomingHolidays.slice(0, 3).map((holiday, idx) => ( {new Date(holiday.next_date).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })} — {holiday.name} ))}
)} {/* Mobile: Route selector dropdown */}

Маршруты

{routes.map(route => ( ))}
{selectedRoute ? ( <>

{selectedRoute.name}

Расписание движения

{detailsLoading ? : detailsError ? (

{detailsError}

) : (
{DAY_TYPES.map((dayType, index) => { const schedule = getScheduleForDayType(dayType.value); const isExpanded = expandedSchedule === dayType.value; return (

{dayType.label}

{dayType.description}

{schedule ? ( {getScheduleTrips(schedule).length} рейсов ) : ( Не задано )}
{schedule ? ( <> {isExpanded && (
{getScheduleTrips(schedule).map((trip, idx) => (
{formatScheduleTime(trip.departureTime)} {getTripLabels(trip, routeStops).map((label) => ( {label} ))}
))}
)}
{/* Copy to other day types */} {dayType.value !== 'holiday' && (

Копировать в:

{DAY_TYPES.filter(dt => dt.value !== dayType.value && !getScheduleForDayType(dt.value) ).map(dt => ( ))}
)} ) : ( )}
); })}
)} ) : (

Выберите маршрут для просмотра расписания

)}
{/* Schedule Editor Modal */} {showEditor && editingSchedule && ( { setShowEditor(false); setEditingSchedule(null); }} /> )}
); } function ScheduleEditor({ schedule, routeName, routeStops, onSave, onCancel }) { const [trips, setTrips] = useState(() => normalizeTripsForEditor(schedule)); const [newTime, setNewTime] = useState(''); const [bulkInput, setBulkInput] = useState(''); const [showBulkInput, setShowBulkInput] = useState(false); const dayTypeLabel = DAY_TYPES.find(dt => dt.value === schedule.dayType || dt.value === schedule.day_type)?.label; const handleAddTime = () => { if (!newTime) return; const timeRegex = /^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/; if (!timeRegex.test(newTime)) { alert('Неверный формат времени. Используйте ЧЧ:ММ'); return; } setTrips(sortTrips([...trips, createEditorTrip(newTime)])); setNewTime(''); }; const handleRemoveTrip = (index) => { setTrips(trips.filter((_, i) => i !== index)); }; const handleUpdateTrip = (index, patch) => { setTrips(trips.map((trip, i) => ( i === index ? { ...trip, ...patch } : trip ))); }; const handleToggleDay = (index, field, day) => { const trip = trips[index]; const current = trip[field] || []; const next = current.includes(day) ? current.filter(value => value !== day) : [...current, day].sort(); const oppositeField = field === 'daysOfWeek' ? 'exceptDaysOfWeek' : 'daysOfWeek'; handleUpdateTrip(index, { [field]: next, [oppositeField]: (trip[oppositeField] || []).filter(value => value !== day) }); }; const handleBulkAdd = () => { const inputTrips = bulkInput .split(/\s*\|\s*|[,;\n]+/) .map(t => t.trim()) .filter(t => t); const validTrips = []; const invalidItems = []; for (const item of inputTrips) { const parsedTrip = parseBulkTrip(item, routeStops); if (parsedTrip) { validTrips.push(parsedTrip); } else { invalidItems.push(item); } } if (invalidItems.length > 0) { alert(`Неверный формат: ${invalidItems.join(', ')}`); } if (validTrips.length > 0) { setTrips(sortTrips([...trips, ...validTrips])); setBulkInput(''); setShowBulkInput(false); } }; const handleSave = () => { if (trips.length === 0) { alert('Добавьте хотя бы одно время отправления'); return; } const apiTrips = sortTrips(trips).map(toApiTrip); onSave({ routeId: schedule.routeId || schedule.route_id, dayType: schedule.dayType || schedule.day_type, departureTimes: apiTrips.map(trip => trip.departureTime), trips: apiTrips }); }; // Generate quick add times const generateQuickTimes = (startHour, endHour, interval) => { const existingTimes = new Set(trips.map(trip => trip.departureTime)); const generatedTrips = []; for (let h = startHour; h <= endHour; h++) { for (let m = 0; m < 60; m += interval) { const time = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; if (!existingTimes.has(time)) { generatedTrips.push(createEditorTrip(time)); } } } setTrips(sortTrips([...trips, ...generatedTrips])); }; return (

{schedule.id ? 'Редактировать' : 'Создать'} расписание

{routeName} — {dayTypeLabel}

{/* Quick Generation */}

Быстрое создание

{/* Add Single Time */}
setNewTime(e.target.value)} className="input flex-1" />
{/* Bulk Input */} {showBulkInput && (

Введите рейсы через | или с новой строки: 15:10 по пятницам, 18:00 кроме пятницы, 06:05 через Мариненко