Support schedule trip variants
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Calendar, Clock, Plus, Edit, Trash2, Copy, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react';
|
||||
import { routesApi, schedulesApi, holidaysApi } from '../services/api';
|
||||
import { Calendar, Plus, Edit, Trash2, ChevronDown, ChevronUp, AlertCircle } from 'lucide-react';
|
||||
import { routesApi, routeStopsApi, schedulesApi, holidaysApi } from '../services/api';
|
||||
import { PageLoader } from '../components/AnimatedComponents';
|
||||
|
||||
const DAY_TYPES = [
|
||||
@@ -11,9 +10,20 @@ const DAY_TYPES = [
|
||||
{ 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);
|
||||
@@ -29,6 +39,7 @@ export default function SchedulesPage() {
|
||||
useEffect(() => {
|
||||
if (selectedRoute) {
|
||||
loadSchedules(selectedRoute.id);
|
||||
loadRouteStops(selectedRoute.id);
|
||||
}
|
||||
}, [selectedRoute]);
|
||||
|
||||
@@ -56,6 +67,16 @@ export default function SchedulesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadRouteStops = async (routeId) => {
|
||||
try {
|
||||
const response = await routeStopsApi.getRouteStops(routeId);
|
||||
setRouteStops(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading route stops:', error);
|
||||
setRouteStops([]);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUpcomingHolidays = async () => {
|
||||
try {
|
||||
const response = await holidaysApi.getUpcoming(5);
|
||||
@@ -69,7 +90,8 @@ export default function SchedulesPage() {
|
||||
setEditingSchedule({
|
||||
routeId: selectedRoute.id,
|
||||
dayType: dayType,
|
||||
departureTimes: []
|
||||
departureTimes: [],
|
||||
trips: []
|
||||
});
|
||||
setShowEditor(true);
|
||||
};
|
||||
@@ -241,7 +263,7 @@ export default function SchedulesPage() {
|
||||
</div>
|
||||
{schedule ? (
|
||||
<span className="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">
|
||||
{schedule.departure_times?.length || 0} рейсов
|
||||
{getScheduleTrips(schedule).length} рейсов
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs">
|
||||
@@ -266,15 +288,25 @@ export default function SchedulesPage() {
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-2 p-3 bg-gray-50 rounded-lg max-h-48 overflow-y-auto animate-[fadeSlideIn_0.2s_ease-out]">
|
||||
<div className="grid grid-cols-4 gap-1 text-sm">
|
||||
{schedule.departure_times?.map((time, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="text-gray-700 list-item-animated"
|
||||
<div className="space-y-1 text-sm">
|
||||
{getScheduleTrips(schedule).map((trip, idx) => (
|
||||
<div
|
||||
key={`${trip.departureTime}-${idx}`}
|
||||
className="flex flex-wrap items-center gap-1 text-gray-700 list-item-animated"
|
||||
style={{ animationDelay: `${idx * 10}ms` }}
|
||||
>
|
||||
{time.substring(0, 5)}
|
||||
</span>
|
||||
<span className="font-mono font-medium w-12">
|
||||
{formatScheduleTime(trip.departureTime)}
|
||||
</span>
|
||||
{getTripLabels(trip, routeStops).map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="rounded bg-white px-1.5 py-0.5 text-xs text-gray-600 border border-gray-200"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,6 +377,7 @@ export default function SchedulesPage() {
|
||||
<ScheduleEditor
|
||||
schedule={editingSchedule}
|
||||
routeName={selectedRoute?.name}
|
||||
routeStops={routeStops}
|
||||
onSave={handleSaveSchedule}
|
||||
onCancel={() => {
|
||||
setShowEditor(false);
|
||||
@@ -356,10 +389,8 @@ export default function SchedulesPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduleEditor({ schedule, routeName, onSave, onCancel }) {
|
||||
const [times, setTimes] = useState(
|
||||
schedule.departure_times?.map(t => t.substring(0, 5)) || []
|
||||
);
|
||||
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);
|
||||
@@ -375,83 +406,92 @@ function ScheduleEditor({ schedule, routeName, onSave, onCancel }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (times.includes(newTime)) {
|
||||
alert('Это время уже добавлено');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTimes = [...times, newTime].sort();
|
||||
setTimes(newTimes);
|
||||
setTrips(sortTrips([...trips, createEditorTrip(newTime)]));
|
||||
setNewTime('');
|
||||
};
|
||||
|
||||
const handleRemoveTime = (index) => {
|
||||
const newTimes = times.filter((_, i) => i !== index);
|
||||
setTimes(newTimes);
|
||||
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 = () => {
|
||||
// Parse bulk input (comma, space, or newline separated)
|
||||
const inputTimes = bulkInput
|
||||
.split(/[,\s\n]+/)
|
||||
const inputTrips = bulkInput
|
||||
.split(/\s*\|\s*|[,;\n]+/)
|
||||
.map(t => t.trim())
|
||||
.filter(t => t);
|
||||
|
||||
const timeRegex = /^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/;
|
||||
const validTimes = [];
|
||||
const invalidTimes = [];
|
||||
const validTrips = [];
|
||||
const invalidItems = [];
|
||||
|
||||
for (const time of inputTimes) {
|
||||
if (timeRegex.test(time)) {
|
||||
// Normalize time format
|
||||
const parts = time.split(':');
|
||||
const normalized = `${parts[0].padStart(2, '0')}:${parts[1]}`;
|
||||
if (!times.includes(normalized) && !validTimes.includes(normalized)) {
|
||||
validTimes.push(normalized);
|
||||
}
|
||||
for (const item of inputTrips) {
|
||||
const parsedTrip = parseBulkTrip(item, routeStops);
|
||||
if (parsedTrip) {
|
||||
validTrips.push(parsedTrip);
|
||||
} else {
|
||||
invalidTimes.push(time);
|
||||
invalidItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidTimes.length > 0) {
|
||||
alert(`Неверный формат: ${invalidTimes.join(', ')}`);
|
||||
if (invalidItems.length > 0) {
|
||||
alert(`Неверный формат: ${invalidItems.join(', ')}`);
|
||||
}
|
||||
|
||||
if (validTimes.length > 0) {
|
||||
const newTimes = [...times, ...validTimes].sort();
|
||||
setTimes(newTimes);
|
||||
if (validTrips.length > 0) {
|
||||
setTrips(sortTrips([...trips, ...validTrips]));
|
||||
setBulkInput('');
|
||||
setShowBulkInput(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (times.length === 0) {
|
||||
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: times.map(t => `${t}:00`)
|
||||
departureTimes: apiTrips.map(trip => trip.departureTime),
|
||||
trips: apiTrips
|
||||
});
|
||||
};
|
||||
|
||||
// Generate quick add times
|
||||
const generateQuickTimes = (startHour, endHour, interval) => {
|
||||
const generatedTimes = [];
|
||||
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 (!times.includes(time)) {
|
||||
generatedTimes.push(time);
|
||||
if (!existingTimes.has(time)) {
|
||||
generatedTrips.push(createEditorTrip(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
const newTimes = [...times, ...generatedTimes].sort();
|
||||
setTimes(newTimes);
|
||||
setTrips(sortTrips([...trips, ...generatedTrips]));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -490,7 +530,7 @@ function ScheduleEditor({ schedule, routeName, onSave, onCancel }) {
|
||||
07-20 × 60мин
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTimes([])}
|
||||
onClick={() => setTrips([])}
|
||||
className="text-xs bg-red-50 text-red-600 border border-red-200 px-2 sm:px-3 py-1.5 rounded hover:bg-red-100"
|
||||
>
|
||||
Очистить
|
||||
@@ -526,7 +566,7 @@ function ScheduleEditor({ schedule, routeName, onSave, onCancel }) {
|
||||
<div className="mb-4 p-3 sm:p-4 bg-blue-50 rounded-lg">
|
||||
<label className="label">Массовый ввод</label>
|
||||
<p className="text-xs text-gray-600 mb-2">
|
||||
Введите время через запятую (ЧЧ:ММ)
|
||||
Введите рейсы через | или с новой строки: 15:10 по пятницам, 18:00 кроме пятницы, 06:05 через Мариненко
|
||||
</p>
|
||||
<textarea
|
||||
value={bulkInput}
|
||||
@@ -541,25 +581,134 @@ function ScheduleEditor({ schedule, routeName, onSave, onCancel }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Times Grid */}
|
||||
{/* Trips List */}
|
||||
<div className="mb-4">
|
||||
<label className="label">
|
||||
Время отправления ({times.length})
|
||||
Рейсы ({trips.length})
|
||||
</label>
|
||||
{times.length > 0 ? (
|
||||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-1.5 sm:gap-2 max-h-48 sm:max-h-64 overflow-y-auto p-2 bg-gray-50 rounded-lg">
|
||||
{times.map((time, index) => (
|
||||
{trips.length > 0 ? (
|
||||
<div className="space-y-3 max-h-[42vh] overflow-y-auto p-2 bg-gray-50 rounded-lg">
|
||||
{trips.map((trip, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-white border border-gray-200 rounded px-1.5 sm:px-2 py-1"
|
||||
key={`${trip.departureTime}-${index}`}
|
||||
className="bg-white border border-gray-200 rounded-lg p-3"
|
||||
>
|
||||
<span className="text-xs sm:text-sm font-mono">{time}</span>
|
||||
<button
|
||||
onClick={() => handleRemoveTime(index)}
|
||||
className="text-red-500 hover:text-red-700 ml-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<input
|
||||
type="time"
|
||||
value={trip.departureTime}
|
||||
onChange={(e) => handleUpdateTrip(index, { departureTime: e.target.value })}
|
||||
className="input max-w-[130px]"
|
||||
/>
|
||||
<div className="flex-1 flex flex-wrap gap-1">
|
||||
{getTripLabels(toApiTrip(trip), routeStops).map(label => (
|
||||
<span key={label} className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-700">
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRemoveTrip(index)}
|
||||
className="text-red-500 hover:text-red-700 px-2 py-1 text-lg leading-none"
|
||||
title="Удалить рейс"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<label className="text-xs text-gray-600">
|
||||
С остановки
|
||||
<select
|
||||
value={trip.startStopId || ''}
|
||||
onChange={(e) => handleUpdateTrip(index, { startStopId: e.target.value ? Number(e.target.value) : null })}
|
||||
className="input mt-1 text-sm"
|
||||
>
|
||||
<option value="">От начала маршрута</option>
|
||||
{routeStops.map(stop => (
|
||||
<option key={stop.stop_id} value={stop.stop_id}>{stop.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-gray-600">
|
||||
До остановки
|
||||
<select
|
||||
value={trip.endStopId || ''}
|
||||
onChange={(e) => handleUpdateTrip(index, { endStopId: e.target.value ? Number(e.target.value) : null })}
|
||||
className="input mt-1 text-sm"
|
||||
>
|
||||
<option value="">До конца маршрута</option>
|
||||
{routeStops.map(stop => (
|
||||
<option key={stop.stop_id} value={stop.stop_id}>{stop.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-gray-600">
|
||||
Через остановку
|
||||
<select
|
||||
value={trip.viaStopIds?.[0] || ''}
|
||||
onChange={(e) => handleUpdateTrip(index, { viaStopIds: e.target.value ? [Number(e.target.value)] : [] })}
|
||||
className="input mt-1 text-sm"
|
||||
>
|
||||
<option value="">Без отметки</option>
|
||||
{routeStops.map(stop => (
|
||||
<option key={stop.stop_id} value={stop.stop_id}>{stop.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 mb-1">Только по дням</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{WEEKDAYS.map(day => (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => handleToggleDay(index, 'daysOfWeek', day.value)}
|
||||
className={`px-2 py-1 rounded text-xs border ${
|
||||
trip.daysOfWeek?.includes(day.value)
|
||||
? 'bg-primary-600 text-white border-primary-600'
|
||||
: 'bg-white text-gray-700 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 mb-1">Кроме дней</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{WEEKDAYS.map(day => (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => handleToggleDay(index, 'exceptDaysOfWeek', day.value)}
|
||||
className={`px-2 py-1 rounded text-xs border ${
|
||||
trip.exceptDaysOfWeek?.includes(day.value)
|
||||
? 'bg-red-600 text-white border-red-600'
|
||||
: 'bg-white text-gray-700 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="text-xs text-gray-600 block mt-3">
|
||||
Примечание
|
||||
<input
|
||||
type="text"
|
||||
value={trip.note || ''}
|
||||
onChange={(e) => handleUpdateTrip(index, { note: e.target.value })}
|
||||
className="input mt-1 text-sm"
|
||||
placeholder="например: по пятницам, через Мариненко"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -582,4 +731,165 @@ function ScheduleEditor({ schedule, routeName, onSave, onCancel }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getScheduleTrips(schedule) {
|
||||
if (Array.isArray(schedule.trips) && schedule.trips.length > 0) {
|
||||
return schedule.trips;
|
||||
}
|
||||
|
||||
return (schedule.departure_times || []).map(departureTime => ({
|
||||
departureTime,
|
||||
startStopId: null,
|
||||
endStopId: null,
|
||||
viaStopIds: [],
|
||||
daysOfWeek: [],
|
||||
exceptDaysOfWeek: [],
|
||||
note: ''
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeTripsForEditor(schedule) {
|
||||
return getScheduleTrips(schedule).map(trip => ({
|
||||
departureTime: formatScheduleTime(trip.departureTime),
|
||||
startStopId: trip.startStopId || null,
|
||||
endStopId: trip.endStopId || null,
|
||||
viaStopIds: Array.isArray(trip.viaStopIds) ? trip.viaStopIds : [],
|
||||
daysOfWeek: Array.isArray(trip.daysOfWeek) ? trip.daysOfWeek.map(Number) : [],
|
||||
exceptDaysOfWeek: Array.isArray(trip.exceptDaysOfWeek) ? trip.exceptDaysOfWeek.map(Number) : [],
|
||||
note: trip.note || ''
|
||||
}));
|
||||
}
|
||||
|
||||
function createEditorTrip(departureTime) {
|
||||
return {
|
||||
departureTime,
|
||||
startStopId: null,
|
||||
endStopId: null,
|
||||
viaStopIds: [],
|
||||
daysOfWeek: [],
|
||||
exceptDaysOfWeek: [],
|
||||
note: ''
|
||||
};
|
||||
}
|
||||
|
||||
function toApiTrip(trip) {
|
||||
return {
|
||||
departureTime: `${formatScheduleTime(trip.departureTime)}:00`,
|
||||
startStopId: trip.startStopId || null,
|
||||
endStopId: trip.endStopId || null,
|
||||
viaStopIds: Array.isArray(trip.viaStopIds) ? trip.viaStopIds.filter(Boolean).map(Number) : [],
|
||||
daysOfWeek: Array.isArray(trip.daysOfWeek) ? trip.daysOfWeek.filter(Boolean).map(Number) : [],
|
||||
exceptDaysOfWeek: Array.isArray(trip.exceptDaysOfWeek) ? trip.exceptDaysOfWeek.filter(Boolean).map(Number) : [],
|
||||
note: (trip.note || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function sortTrips(items) {
|
||||
return [...items].sort((a, b) => a.departureTime.localeCompare(b.departureTime));
|
||||
}
|
||||
|
||||
function formatScheduleTime(time) {
|
||||
return (time || '').substring(0, 5);
|
||||
}
|
||||
|
||||
function getTripLabels(trip, routeStops) {
|
||||
const labels = [];
|
||||
const startStop = findRouteStop(routeStops, trip.startStopId);
|
||||
const endStop = findRouteStop(routeStops, trip.endStopId);
|
||||
const viaStops = (trip.viaStopIds || [])
|
||||
.map(stopId => findRouteStop(routeStops, stopId))
|
||||
.filter(Boolean);
|
||||
|
||||
if (startStop) labels.push(`с ${startStop.name}`);
|
||||
if (endStop) labels.push(`до ${endStop.name}`);
|
||||
viaStops.forEach(stop => labels.push(`через ${stop.name}`));
|
||||
|
||||
if (trip.daysOfWeek?.length > 0) {
|
||||
labels.push(`по ${trip.daysOfWeek.map(getWeekdayLabel).join(', ')}`);
|
||||
}
|
||||
|
||||
if (trip.exceptDaysOfWeek?.length > 0) {
|
||||
labels.push(`кроме ${trip.exceptDaysOfWeek.map(getWeekdayLabel).join(', ')}`);
|
||||
}
|
||||
|
||||
if (trip.note) labels.push(trip.note);
|
||||
return [...new Set(labels)];
|
||||
}
|
||||
|
||||
function getWeekdayLabel(value) {
|
||||
return WEEKDAYS.find(day => day.value === Number(value))?.label || value;
|
||||
}
|
||||
|
||||
function findRouteStop(routeStops, stopId) {
|
||||
if (!stopId) return null;
|
||||
return routeStops.find(stop => Number(stop.stop_id) === Number(stopId));
|
||||
}
|
||||
|
||||
function parseBulkTrip(value, routeStops) {
|
||||
const match = value.match(/^([0-1]?[0-9]|2[0-3]):([0-5][0-9])(?:\s+(.+))?$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const trip = createEditorTrip(`${match[1].padStart(2, '0')}:${match[2]}`);
|
||||
const note = (match[3] || '').trim();
|
||||
if (!note) return trip;
|
||||
|
||||
const normalizedNote = normalizeText(note);
|
||||
|
||||
if (/по\s+пятниц/.test(normalizedNote)) {
|
||||
trip.daysOfWeek = [5];
|
||||
}
|
||||
|
||||
if (/кроме\s+пятниц/.test(normalizedNote)) {
|
||||
trip.exceptDaysOfWeek = [5];
|
||||
}
|
||||
|
||||
const viaStopId = findStopIdInText(extractStopText(normalizedNote, 'через'), routeStops);
|
||||
const endStopId = findStopIdInText(extractStopText(normalizedNote, 'до'), routeStops);
|
||||
const startStopId = findStopIdInText(extractStopText(normalizedNote, 'с'), routeStops);
|
||||
|
||||
if (viaStopId) trip.viaStopIds = [viaStopId];
|
||||
if (endStopId) trip.endStopId = endStopId;
|
||||
if (startStopId) trip.startStopId = startStopId;
|
||||
|
||||
trip.note = note;
|
||||
return trip;
|
||||
}
|
||||
|
||||
function extractStopText(text, marker) {
|
||||
const match = text.match(new RegExp(`\\b${marker}\\s+(.+?)(?=\\s+(?:по|кроме)\\s+|$)`));
|
||||
return match?.[1] || '';
|
||||
}
|
||||
|
||||
function findStopIdInText(value, routeStops) {
|
||||
if (!value) return null;
|
||||
const normalizedValue = normalizeText(value);
|
||||
const looseValue = normalizeStopLookup(normalizedValue);
|
||||
|
||||
const match = routeStops.find(stop => {
|
||||
const stopName = normalizeText(stop.name);
|
||||
const looseStopName = normalizeStopLookup(stopName);
|
||||
return normalizedValue.includes(stopName) ||
|
||||
stopName.includes(normalizedValue) ||
|
||||
looseValue.includes(looseStopName) ||
|
||||
looseStopName.includes(looseValue);
|
||||
});
|
||||
|
||||
return match?.stop_id || null;
|
||||
}
|
||||
|
||||
function normalizeStopLookup(value) {
|
||||
return value
|
||||
.split(/\s+/)
|
||||
.map(word => word.replace(/[аяеуыиюь]$/u, ''))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[«»"']/g, '')
|
||||
.replace(/ё/g, 'е')
|
||||
.replace(/[.,]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user