import { Button, TextField, Select, MenuItem, FormControl, InputLabel, Typography, Box, Dialog, DialogTitle, DialogContent, DialogActions, } from "@mui/material"; import { MediaViewer, VideoPreviewCard, ImageUploadCard, DeleteModal, } from "@widgets"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Copy, Save, Plus, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { carrierStore, articlesStore, routeStore, languageStore, mediaStore, isMediaIdEmpty, stationsStore, ArticleSelectOrCreateDialog, SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, LoadingSpinner, selectedCityStore, } from "@shared"; import { LinkedItems } from "../LinekedStations"; export const RouteEditPage = observer(() => { const navigate = useNavigate(); useEffect(() => { selectedCityStore.setIsLocked(true); return () => selectedCityStore.setIsLocked(false); }, []); const { id } = useParams(); const { editRouteData, copyRouteAction } = routeStore; const [isLoading, setIsLoading] = useState(false); const [isLoadingData, setIsLoadingData] = useState(true); const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = useState(false); const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false); const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false); const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false); const [previewIconId, setPreviewIconId] = useState(""); const [activeIconMenuType, setActiveIconMenuType] = useState< "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null >(null); const [fileToUpload, setFileToUpload] = useState(null); const { language } = languageStore; const [coordinates, setCoordinates] = useState(""); useEffect(() => { const fetchData = async () => { if (!id) { setIsLoadingData(false); return; } setIsLoadingData(true); try { const response = await routeStore.getRoute(Number(id)); routeStore.setEditRouteData(response); languageStore.setLanguage("ru"); } finally { setIsLoadingData(false); } }; fetchData(); }, [id]); useEffect(() => { const fetchData = async () => { await carrierStore.getCarriers(language); await stationsStore.getStations(); await articlesStore.getArticleList(); await mediaStore.getMedia(); }; fetchData(); }, [id, language]); const handleIconSelect = (media: { id: string; filename: string; media_name?: string; media_type: number; }) => { routeStore.setEditRouteData({ icon: media.id }); setIsSelectIconDialogOpen(false); }; const selectedIconMedia = editRouteData.icon && !isMediaIdEmpty(editRouteData.icon) ? mediaStore.media.find((m) => m.id === editRouteData.icon) : null; const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon) ? null : (selectedIconMedia?.id ?? editRouteData.icon); const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview) ? null : editRouteData.video_preview; useEffect(() => { if (editRouteData.path && editRouteData.path.length > 0) { const formattedPath = editRouteData.path .map((coords) => coords.join(" ")) .join("\n"); setCoordinates(formattedPath); } }, [editRouteData.path]); const handleSave = async () => { // Валидация обязательных полей if (!editRouteData.route_name?.trim()) { toast.error("Заполните название маршрута"); return; } if (!editRouteData.carrier_id) { toast.error("Выберите перевозчика"); return; } if (!editRouteData.route_number?.trim()) { toast.error("Заполните номер маршрута"); return; } if (!editRouteData.route_sys_number?.trim()) { toast.error("Заполните номер маршрута в Говорящем Городе"); return; } const validationResult = validateCoordinates(coordinates); if (validationResult !== true) { toast.error(validationResult); return; } // Валидация масштабов if ( editRouteData.scale_min !== null && editRouteData.scale_min !== undefined && editRouteData.scale_max !== null && editRouteData.scale_max !== undefined && editRouteData.scale_min > editRouteData.scale_max ) { toast.error("Максимальный масштаб не может быть меньше минимального"); return; } if ( editRouteData.scale_min === 0 || editRouteData.scale_max === 0 || editRouteData.scale_min === null || editRouteData.scale_max === null ) { toast.error("Масштабы не могут быть равны 0"); setIsLoading(false); return; } setIsLoading(true); try { await routeStore.editRoute(Number(id)); toast.success("Маршрут успешно сохранен"); } catch (error) { console.error(error); toast.error("Произошла ошибка при сохранении маршрута"); } finally { setIsLoading(false); } }; const validateCoordinates = (value: string) => { try { const lines = value.trim().split("\n"); const coordinates = lines.map((line) => { const [lat, lon] = line .trim() .split(/[\s,]+/) .map(Number); return [lat, lon]; }); if (coordinates.length === 0) { return "Введите хотя бы одну пару координат"; } if ( !coordinates.every( (point) => Array.isArray(point) && point.length === 2 ) ) { return "Каждая строка должна содержать две координаты"; } if ( !coordinates.every((point) => point.every((coord) => !isNaN(coord) && typeof coord === "number") ) ) { return "Координаты должны быть числами"; } return true; } catch { return "Неверный формат координат"; } }; const handleCopy = async () => { await copyRouteAction(Number(id)); toast.success("Маршрут успешно скопирован"); }; const handleArticleSelect = (articleId: number) => { routeStore.setEditRouteData({ governor_appeal: articleId, }); setIsSelectArticleDialogOpen(false); // Обновляем список статей после создания новой articlesStore.getArticleList(); }; const handleVideoSelect = (media: { id: string; filename: string; media_name?: string; media_type: number; }) => { routeStore.setEditRouteData({ video_preview: media.id, }); setIsSelectVideoDialogOpen(false); }; const handleVideoFileSelect = (file?: File) => { if (file) { setFileToUpload(file); setIsUploadVideoDialogOpen(true); } else { setIsSelectVideoDialogOpen(true); } }; const handleVideoUpload = (media: { id: string; filename: string; media_name?: string; media_type: number; }) => { routeStore.setEditRouteData({ video_preview: media.id, }); setIsUploadVideoDialogOpen(false); setFileToUpload(null); }; const handleVideoPreviewClick = () => { if (editRouteData.video_preview && editRouteData.video_preview !== "") { setIsVideoPreviewOpen(true); } }; // Получаем название выбранной статьи для отображения const selectedArticle = articlesStore.articleList.ru.data.find( (article) => article.id === editRouteData.governor_appeal ); if (isLoadingData) { return ( ); } return (
routeStore.setEditRouteData({ route_name: e.target.value, }) } /> Выберите перевозчика routeStore.setEditRouteData({ route_number: e.target.value, }) } /> { const newValue = e.target.value; setCoordinates(newValue); const validationResult = validateCoordinates(newValue); if (validationResult === true) { const lines = newValue.trim().split("\n"); const path = lines.map((line) => { const [lat, lon] = line .trim() .split(/[\s,]+/) .map(Number); return [lat, lon]; }); routeStore.setEditRouteData({ path }); } }} onKeyDown={(e) => { if (e.key === "Enter") { const lines = coordinates.split("\n"); const lastLine = lines[lines.length - 1]; if (lastLine && lastLine.trim()) { e.preventDefault(); const newValue = coordinates + "\n"; setCoordinates(newValue); } } }} error={validateCoordinates(coordinates) !== true} helperText={ typeof validateCoordinates(coordinates) === "string" ? validateCoordinates(coordinates) : "Формат: широта долгота" } placeholder="55.7558 37.6173 55.7539 37.6208" sx={{ mt: 1, "& .MuiInputBase-root": { maxHeight: "500px", overflow: "auto", }, "& .MuiInputBase-input": { fontFamily: "monospace", fontSize: "0.8rem", lineHeight: "1.2", }, "& .MuiFormHelperText-root": { fontSize: "0.75rem", marginTop: "2px", }, }} /> routeStore.setEditRouteData({ route_sys_number: e.target.value, }) } /> Прямой/обратный маршрут { let value = e.target.value === "" ? null : e.target.value; if (value && Number(value) > 297) { value = "297"; } if (value && Number(value) < 10) { value = "10"; } routeStore.setEditRouteData({ scale_min: value ? Number(value) : null, }); // Если максимальный масштаб стал меньше минимального, обновляем его if ( value !== null && editRouteData.scale_max !== null && editRouteData.scale_max !== undefined && value && Number(value) > (editRouteData.scale_max ?? 0) ) { routeStore.setEditRouteData({ scale_max: value ? Number(value) : null, }); } }} required /> { let value = e.target.value; if (Number(value) > 300) { value = "300"; } routeStore.setEditRouteData({ scale_max: value === "" ? null : parseFloat(value), }); }} error={ editRouteData.scale_min !== null && editRouteData.scale_min !== undefined && editRouteData.scale_max !== null && editRouteData.scale_max !== undefined && editRouteData.scale_max < editRouteData.scale_min } helperText={ editRouteData.scale_min !== null && editRouteData.scale_min !== undefined && editRouteData.scale_max !== null && editRouteData.scale_max !== undefined && editRouteData.scale_max < editRouteData.scale_min ? "Максимальный масштаб не может быть меньше минимального" : "" } /> routeStore.setEditRouteData({ rotate: e.target.value === "" ? null : parseFloat(e.target.value), }) } /> routeStore.setEditRouteData({ center_latitude: e.target.value, }) } /> routeStore.setEditRouteData({ center_longitude: e.target.value, }) } /> { const val = Math.max(1, Math.round(Number(e.target.value))); if (Number.isFinite(val)) { routeStore.setEditRouteData({ video_timer: val }); } }} /> Текст кнопки обращения routeStore.setEditRouteData({ button_text: e.target.value, }) } placeholder="Обращение губернатора" fullWidth size="small" helperText="Если пусто, будет использован текст по умолчанию" /> Обращение к пассажирам {selectedArticle && ( )}
{ setIsPreviewIconOpen(true); setPreviewIconId( selectedIconMedia?.id ?? editRouteData.icon ?? "" ); }} onDeleteImageClick={() => setIsDeleteIconModalOpen(true)} onSelectFileClick={() => { setActiveIconMenuType("image"); setIsSelectIconDialogOpen(true); }} setUploadMediaOpen={() => { setIsUploadIconDialogOpen(true); setActiveIconMenuType("image"); }} />
{ routeStore.setEditRouteData({ video_preview: "" }); }} onSelectVideoClick={handleVideoFileSelect} className="w-full" />
{ routeStore.getRoute(Number(id)); }} />
setIsSelectArticleDialogOpen(false)} onSelectArticle={handleArticleSelect} /> setIsSelectVideoDialogOpen(false)} onSelectMedia={handleVideoSelect} mediaType={2} /> setIsVideoPreviewOpen(false)} maxWidth="md" fullWidth > Предпросмотр видео {effectiveVideoId && ( )} { setIsUploadVideoDialogOpen(false); setFileToUpload(null); }} hardcodeType="video_preview" contextObjectName={editRouteData.route_name || "Маршрут"} contextType="sight" initialFile={fileToUpload || undefined} afterUpload={handleVideoUpload} /> setIsSelectIconDialogOpen(false)} onSelectMedia={handleIconSelect} mediaType={1} /> setIsUploadIconDialogOpen(false)} contextObjectName={editRouteData.route_name || "Маршрут"} contextType="route" afterUpload={handleIconSelect} hardcodeType={activeIconMenuType} /> setIsPreviewIconOpen(false)} mediaId={previewIconId} /> { routeStore.setEditRouteData({ icon: "" }); setIsDeleteIconModalOpen(false); }} onCancel={() => setIsDeleteIconModalOpen(false)} edit />
); });