import { Button, TextField, Select, MenuItem, FormControl, InputLabel, Typography, Box, Dialog, DialogTitle, DialogContent, DialogActions, } from "@mui/material"; import { MediaViewer, VideoPreviewCard, ImageUploadCard, } from "@widgets"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react"; import { useEffect, useState, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { carrierStore, articlesStore, routeStore, languageStore, mediaStore, isMediaIdEmpty, ArticleSelectOrCreateDialog, SelectMediaDialog, selectedCityStore, UploadMediaDialog, PreviewMediaDialog, } from "@shared"; import type { Route } from "@shared"; export const RouteCreatePage = observer(() => { const navigate = useNavigate(); useEffect(() => { selectedCityStore.setIsLocked(true); return () => selectedCityStore.setIsLocked(false); }, []); const [carrier, setCarrier] = useState(""); const [routeNumber, setRouteNumber] = useState(""); const [routeCoords, setRouteCoords] = useState(""); const [govRouteNumber, setGovRouteNumber] = useState(""); const [governorAppeal, setGovernorAppeal] = useState(""); const [direction, setDirection] = useState("backward"); const [scaleMin, setScaleMin] = useState("10"); const [scaleMax, setScaleMax] = useState("100"); const [routeName, setRouteName] = useState(""); const [turn, setTurn] = useState(""); const [centerLat, setCenterLat] = useState(""); const [centerLng, setCenterLng] = useState(""); const [videoTimer, setVideoTimer] = useState(420); const [videoPreview, setVideoPreview] = useState(""); const [icon, setIcon] = useState(""); const [isLoading, setIsLoading] = useState(false); 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 [previewIconId, setPreviewIconId] = useState(""); const [activeIconMenuType, setActiveIconMenuType] = useState< "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null >(null); const [fileToUpload, setFileToUpload] = useState(null); const { language } = languageStore; useEffect(() => { carrierStore.getCarriers(language); articlesStore.getArticleList(); mediaStore.getMedia(); }, [language]); const filteredCarriers = useMemo(() => { const carriers = carrierStore.carriers[language as keyof typeof carrierStore.carriers] .data || []; if (!selectedCityStore.selectedCityId) { return carriers; } return carriers.filter( (carrier: any) => carrier.city_id === selectedCityStore.selectedCityId ); }, [carrierStore.carriers, language, selectedCityStore.selectedCityId]); 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 handleArticleSelect = (articleId: number) => { setGovernorAppeal(articleId.toString()); setIsSelectArticleDialogOpen(false); articlesStore.getArticleList(); }; const handleVideoSelect = (media: { id: string; filename: string; media_name?: string; media_type: number; }) => { setVideoPreview(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; }) => { setVideoPreview(media.id); setIsUploadVideoDialogOpen(false); setFileToUpload(null); }; const handleVideoPreviewClick = () => { setIsVideoPreviewOpen(true); }; const handleIconSelect = (media: { id: string; filename: string; media_name?: string; media_type: number; }) => { setIcon(media.id); setIsSelectIconDialogOpen(false); }; const selectedIconMedia = icon && !isMediaIdEmpty(icon) ? mediaStore.media.find((m) => m.id === icon) : null; const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon; const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview; const handleCreateRoute = async () => { try { setIsLoading(true); if (!routeName.trim()) { toast.error("Заполните название маршрута"); setIsLoading(false); return; } if (!carrier) { toast.error("Выберите перевозчика"); setIsLoading(false); return; } if (!routeNumber.trim()) { toast.error("Заполните номер маршрута"); setIsLoading(false); return; } if (!govRouteNumber.trim()) { toast.error("Заполните номер маршрута в Говорящем Городе"); setIsLoading(false); return; } const validationResult = validateCoordinates(routeCoords); if (validationResult !== true) { toast.error(validationResult); setIsLoading(false); return; } const scale_min = scaleMin ? Number(scaleMin) : null; const scale_max = scaleMax ? Number(scaleMax) : null; if ( scale_min === 0 || scale_max === 0 || scale_min === null || scale_max === null ) { toast.error("Масштабы не могут быть равны 0"); setIsLoading(false); return; } if ( scale_min !== null && scale_max !== null && scale_max !== undefined && scale_min > scale_max ) { toast.error("Максимальный масштаб не может быть меньше минимального"); setIsLoading(false); return; } const carrier_id = Number(carrier); const governor_appeal = governorAppeal ? Number(governorAppeal) : undefined; const rotate = turn ? Number(turn) : undefined; const center_latitude = centerLat ? Number(centerLat) : undefined; const center_longitude = centerLng ? Number(centerLng) : undefined; const route_direction = direction === "forward"; const path = routeCoords .trim() .split("\n") .map((line) => { const [lat, lon] = line .trim() .split(/[\s,]+/) .map(Number); return [lat, lon]; }); const newRoute: Partial = { carrier: carrierStore.carriers[ language as keyof typeof carrierStore.carriers ].data?.find((c: any) => c.id === carrier_id)?.full_name || "", carrier_id, route_number: routeNumber.trim(), route_sys_number: govRouteNumber.trim(), route_name: routeName.trim(), route_direction, scale_min: scale_min !== null ? scale_min : 0, scale_max: scale_max !== null ? scale_max : 0, rotate, center_latitude, center_longitude, path, video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined, icon: !isMediaIdEmpty(icon) ? icon : undefined, video_timer: videoTimer, }; if (governor_appeal !== undefined) { newRoute.governor_appeal = governor_appeal; } const newId = await routeStore.createRoute(newRoute); toast.success("Маршрут успешно создан"); navigate(`/route/${newId}/edit`); } catch (error) { console.error(error); toast.error("Произошла ошибка при создании маршрута"); } finally { setIsLoading(false); } }; const selectedArticle = articlesStore.articleList.ru.data.find( (article) => article.id === Number(governorAppeal) ); return (
setRouteName(e.target.value)} /> Выберите перевозчика setRouteNumber(e.target.value)} /> { const newValue = e.target.value; setRouteCoords(newValue); }} onKeyDown={(e) => { if (e.key === "Enter") { const lines = routeCoords.split("\n"); const lastLine = lines[lines.length - 1]; if (lastLine && lastLine.trim()) { e.preventDefault(); const newValue = routeCoords + "\n"; setRouteCoords(newValue); } } }} error={validateCoordinates(routeCoords) !== true} helperText={ typeof validateCoordinates(routeCoords) === "string" ? validateCoordinates(routeCoords) : "Формат: широта долгота" } 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", }, }} /> setGovRouteNumber(e.target.value)} /> Обращение к пассажирам {selectedArticle && ( )}
{ setIsPreviewIconOpen(true); setPreviewIconId(selectedIconMedia?.id ?? icon ?? ""); }} onDeleteImageClick={() => { setIcon(""); setActiveIconMenuType(null); }} onSelectFileClick={() => { setActiveIconMenuType("image"); setIsSelectIconDialogOpen(true); }} setUploadMediaOpen={() => { setIsUploadIconDialogOpen(true); setActiveIconMenuType("image"); }} />
setVideoPreview("")} onSelectVideoClick={handleVideoFileSelect} className="w-full" />
Прямой/обратный маршрут { let value = e.target.value; if (Number(value) > 297) { value = "297"; } if (Number(value) < 10) { value = "10"; } setScaleMin(value); if (value && scaleMax && Number(value) > Number(scaleMax)) { setScaleMax(value); } }} error={ scaleMin !== "" && scaleMax !== "" && Number(scaleMin) > Number(scaleMax) } required helperText={ scaleMin !== "" && scaleMax !== "" && Number(scaleMin) > Number(scaleMax) ? "Минимальный масштаб не может быть больше максимального" : "" } /> { if (Number(e.target.value) > 300) { e.target.value = "300"; } const value = e.target.value; setScaleMax(value); }} /> setTurn(e.target.value)} /> setCenterLat(e.target.value)} /> setCenterLng(e.target.value)} /> { const val = Math.max(1, Math.round(Number(e.target.value))); if (Number.isFinite(val)) { setVideoTimer(val); } }} />
setIsSelectArticleDialogOpen(false)} onSelectArticle={handleArticleSelect} /> setIsSelectVideoDialogOpen(false)} onSelectMedia={handleVideoSelect} mediaType={2} /> {effectiveVideoId && ( setIsVideoPreviewOpen(false)} maxWidth="md" fullWidth > Предпросмотр видео )} { setIsUploadVideoDialogOpen(false); setFileToUpload(null); }} hardcodeType="video_preview" contextObjectName={routeName || "Маршрут"} contextType="sight" initialFile={fileToUpload || undefined} afterUpload={handleVideoUpload} /> setIsSelectIconDialogOpen(false)} onSelectMedia={handleIconSelect} mediaType={1} /> setIsUploadIconDialogOpen(false)} contextObjectName={routeName || "Маршрут"} contextType="route" afterUpload={handleIconSelect} hardcodeType={activeIconMenuType} /> setIsPreviewIconOpen(false)} mediaId={previewIconId} />
); });