Compare commits
	
		
			2 Commits
		
	
	
		
			2117a6836e
			...
			32a7cb44d1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 32a7cb44d1 | |||
| 481385c2f4 | 
| @@ -139,7 +139,7 @@ const router = createBrowserRouter([ | |||||||
|       // City |       // City | ||||||
|       { path: "city", element: <CityListPage /> }, |       { path: "city", element: <CityListPage /> }, | ||||||
|       { path: "city/create", element: <CityCreatePage /> }, |       { path: "city/create", element: <CityCreatePage /> }, | ||||||
|       { path: "city/:id", element: <CityPreviewPage /> }, |       // { path: "city/:id", element: <CityPreviewPage /> }, | ||||||
|       { path: "city/:id/edit", element: <CityEditPage /> }, |       { path: "city/:id/edit", element: <CityEditPage /> }, | ||||||
|       // Route |       // Route | ||||||
|       { path: "route", element: <RouteListPage /> }, |       { path: "route", element: <RouteListPage /> }, | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ interface NavigationItemProps { | |||||||
|   open: boolean; |   open: boolean; | ||||||
|   onClick?: () => void; |   onClick?: () => void; | ||||||
|   isNested?: boolean; |   isNested?: boolean; | ||||||
|  |   onDrawerOpen?: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | ||||||
| @@ -23,6 +24,7 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | |||||||
|   open, |   open, | ||||||
|   onClick, |   onClick, | ||||||
|   isNested = false, |   isNested = false, | ||||||
|  |   onDrawerOpen, | ||||||
| }) => { | }) => { | ||||||
|   const Icon = item.icon; |   const Icon = item.icon; | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
| @@ -32,6 +34,9 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | |||||||
|   const isActive = item.path ? location.pathname.startsWith(item.path) : false; |   const isActive = item.path ? location.pathname.startsWith(item.path) : false; | ||||||
|  |  | ||||||
|   const handleClick = () => { |   const handleClick = () => { | ||||||
|  |     if (item.id === "all" && !open) { | ||||||
|  |       onDrawerOpen?.(); | ||||||
|  |     } | ||||||
|     if (item.nestedItems) { |     if (item.nestedItems) { | ||||||
|       setIsExpanded(!isExpanded); |       setIsExpanded(!isExpanded); | ||||||
|     } else if (onClick) { |     } else if (onClick) { | ||||||
|   | |||||||
| @@ -3,7 +3,12 @@ import Divider from "@mui/material/Divider"; | |||||||
| import { NAVIGATION_ITEMS } from "@shared"; | import { NAVIGATION_ITEMS } from "@shared"; | ||||||
| import { NavigationItem, NavigationItemComponent } from "@entities"; | import { NavigationItem, NavigationItemComponent } from "@entities"; | ||||||
|  |  | ||||||
| export const NavigationList = ({ open }: { open: boolean }) => { | interface NavigationListProps { | ||||||
|  |   open: boolean; | ||||||
|  |   onDrawerOpen?: () => void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const NavigationList = ({ open, onDrawerOpen }: NavigationListProps) => { | ||||||
|   const primaryItems = NAVIGATION_ITEMS.primary; |   const primaryItems = NAVIGATION_ITEMS.primary; | ||||||
|   const secondaryItems = NAVIGATION_ITEMS.secondary; |   const secondaryItems = NAVIGATION_ITEMS.secondary; | ||||||
|  |  | ||||||
| @@ -15,6 +20,7 @@ export const NavigationList = ({ open }: { open: boolean }) => { | |||||||
|             key={item.id} |             key={item.id} | ||||||
|             item={item as NavigationItem} |             item={item as NavigationItem} | ||||||
|             open={open} |             open={open} | ||||||
|  |             onDrawerOpen={onDrawerOpen} | ||||||
|           /> |           /> | ||||||
|         ))} |         ))} | ||||||
|       </List> |       </List> | ||||||
| @@ -26,6 +32,7 @@ export const NavigationList = ({ open }: { open: boolean }) => { | |||||||
|             item={item as NavigationItem} |             item={item as NavigationItem} | ||||||
|             open={open} |             open={open} | ||||||
|             onClick={item.onClick ? item.onClick : undefined} |             onClick={item.onClick ? item.onClick : undefined} | ||||||
|  |             onDrawerOpen={onDrawerOpen} | ||||||
|           /> |           /> | ||||||
|         ))} |         ))} | ||||||
|       </List> |       </List> | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import { ArrowLeft, Save } from "lucide-react"; | |||||||
| import { Loader2 } from "lucide-react"; | import { Loader2 } from "lucide-react"; | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
| import { carrierStore, cityStore, mediaStore } from "@shared"; | import { carrierStore, cityStore, mediaStore, languageStore } from "@shared"; | ||||||
| import { useState, useEffect } from "react"; | import { useState, useEffect } from "react"; | ||||||
| import { ImageUploadCard, LanguageSwitcher } from "@widgets"; | import { ImageUploadCard, LanguageSwitcher } from "@widgets"; | ||||||
| import { | import { | ||||||
| @@ -23,11 +23,8 @@ import { | |||||||
|  |  | ||||||
| export const CarrierCreatePage = observer(() => { | export const CarrierCreatePage = observer(() => { | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|  |   const { createCarrierData, setCreateCarrierData } = carrierStore; | ||||||
|   const [fullName, setFullName] = useState(""); |   const { language } = languageStore; | ||||||
|   const [shortName, setShortName] = useState(""); |  | ||||||
|   const [cityId, setCityId] = useState<number | null>(null); |  | ||||||
|   const [slogan, setSlogan] = useState(""); |  | ||||||
|   const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null); |   const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null); | ||||||
|   const [isLoading, setIsLoading] = useState(false); |   const [isLoading, setIsLoading] = useState(false); | ||||||
|   const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); |   const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); | ||||||
| @@ -35,7 +32,7 @@ export const CarrierCreatePage = observer(() => { | |||||||
|   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); |   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); | ||||||
|   const [mediaId, setMediaId] = useState(""); |   const [mediaId, setMediaId] = useState(""); | ||||||
|   const [activeMenuType, setActiveMenuType] = useState< |   const [activeMenuType, setActiveMenuType] = useState< | ||||||
|     "thumbnail" | "watermark_lu" | "watermark_rd" | null |     "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null | ||||||
|   >(null); |   >(null); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @@ -46,13 +43,7 @@ export const CarrierCreatePage = observer(() => { | |||||||
|   const handleCreate = async () => { |   const handleCreate = async () => { | ||||||
|     try { |     try { | ||||||
|       setIsLoading(true); |       setIsLoading(true); | ||||||
|       await carrierStore.createCarrier( |       await carrierStore.createCarrier(); | ||||||
|         fullName, |  | ||||||
|         shortName, |  | ||||||
|         cityId!, |  | ||||||
|         slogan, |  | ||||||
|         selectedMediaId! |  | ||||||
|       ); |  | ||||||
|       toast.success("Перевозчик успешно создан"); |       toast.success("Перевозчик успешно создан"); | ||||||
|       navigate("/carrier"); |       navigate("/carrier"); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
| @@ -69,6 +60,14 @@ export const CarrierCreatePage = observer(() => { | |||||||
|     media_type: number; |     media_type: number; | ||||||
|   }) => { |   }) => { | ||||||
|     setSelectedMediaId(media.id); |     setSelectedMediaId(media.id); | ||||||
|  |     setCreateCarrierData( | ||||||
|  |       createCarrierData[language].full_name, | ||||||
|  |       createCarrierData[language].short_name, | ||||||
|  |       createCarrierData.city_id, | ||||||
|  |       createCarrierData[language].slogan, | ||||||
|  |       media.id, | ||||||
|  |       language | ||||||
|  |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const selectedMedia = selectedMediaId |   const selectedMedia = selectedMediaId | ||||||
| @@ -89,19 +88,28 @@ export const CarrierCreatePage = observer(() => { | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div className="flex flex-col gap-10 w-full items-end"> |       <div className="flex flex-col gap-10 w-full items-end"> | ||||||
|         <div className="flex gap-10 items-center mb-5 max-w-[80%]"> |         <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start"> | ||||||
|           <h1 className="text-3xl break-words">Создание перевозчика</h1> |           <h1 className="text-3xl break-words">Создание перевозчика</h1> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <FormControl fullWidth> |         <FormControl fullWidth> | ||||||
|           <InputLabel>Город</InputLabel> |           <InputLabel>Город</InputLabel> | ||||||
|           <Select |           <Select | ||||||
|             value={cityId || ""} |             value={createCarrierData.city_id || ""} | ||||||
|             label="Город" |             label="Город" | ||||||
|             required |             required | ||||||
|             onChange={(e) => setCityId(e.target.value as number)} |             onChange={(e) => | ||||||
|  |               setCreateCarrierData( | ||||||
|  |                 createCarrierData[language].full_name, | ||||||
|  |                 createCarrierData[language].short_name, | ||||||
|  |                 e.target.value as number, | ||||||
|  |                 createCarrierData[language].slogan, | ||||||
|  |                 selectedMediaId || "", | ||||||
|  |                 language | ||||||
|  |               ) | ||||||
|  |             } | ||||||
|           > |           > | ||||||
|             {cityStore.cities.ru.data.map((city) => ( |             {cityStore.cities["ru"].data.map((city) => ( | ||||||
|               <MenuItem key={city.id} value={city.id}> |               <MenuItem key={city.id} value={city.id}> | ||||||
|                 {city.name} |                 {city.name} | ||||||
|               </MenuItem> |               </MenuItem> | ||||||
| @@ -112,24 +120,51 @@ export const CarrierCreatePage = observer(() => { | |||||||
|         <TextField |         <TextField | ||||||
|           fullWidth |           fullWidth | ||||||
|           label="Полное название" |           label="Полное название" | ||||||
|           value={fullName} |           value={createCarrierData[language].full_name} | ||||||
|           required |           required | ||||||
|           onChange={(e) => setFullName(e.target.value)} |           onChange={(e) => | ||||||
|  |             setCreateCarrierData( | ||||||
|  |               e.target.value, | ||||||
|  |               createCarrierData[language].short_name, | ||||||
|  |               createCarrierData.city_id, | ||||||
|  |               createCarrierData[language].slogan, | ||||||
|  |               selectedMediaId || "", | ||||||
|  |               language | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|         <TextField |         <TextField | ||||||
|           fullWidth |           fullWidth | ||||||
|           label="Короткое название" |           label="Короткое название" | ||||||
|           value={shortName} |           value={createCarrierData[language].short_name} | ||||||
|           required |           required | ||||||
|           onChange={(e) => setShortName(e.target.value)} |           onChange={(e) => | ||||||
|  |             setCreateCarrierData( | ||||||
|  |               createCarrierData[language].full_name, | ||||||
|  |               e.target.value, | ||||||
|  |               createCarrierData.city_id, | ||||||
|  |               createCarrierData[language].slogan, | ||||||
|  |               selectedMediaId || "", | ||||||
|  |               language | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|         <TextField |         <TextField | ||||||
|           fullWidth |           fullWidth | ||||||
|           label="Слоган" |           label="Слоган" | ||||||
|           value={slogan} |           value={createCarrierData[language].slogan} | ||||||
|           onChange={(e) => setSlogan(e.target.value)} |           onChange={(e) => | ||||||
|  |             setCreateCarrierData( | ||||||
|  |               createCarrierData[language].full_name, | ||||||
|  |               createCarrierData[language].short_name, | ||||||
|  |               createCarrierData.city_id, | ||||||
|  |               e.target.value, | ||||||
|  |               selectedMediaId || "", | ||||||
|  |               language | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|         <div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> |         <div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> | ||||||
| @@ -144,14 +179,22 @@ export const CarrierCreatePage = observer(() => { | |||||||
|             onDeleteImageClick={() => { |             onDeleteImageClick={() => { | ||||||
|               setSelectedMediaId(null); |               setSelectedMediaId(null); | ||||||
|               setActiveMenuType(null); |               setActiveMenuType(null); | ||||||
|  |               setCreateCarrierData( | ||||||
|  |                 createCarrierData[language].full_name, | ||||||
|  |                 createCarrierData[language].short_name, | ||||||
|  |                 createCarrierData.city_id, | ||||||
|  |                 createCarrierData[language].slogan, | ||||||
|  |                 "", | ||||||
|  |                 language | ||||||
|  |               ); | ||||||
|             }} |             }} | ||||||
|             onSelectFileClick={() => { |             onSelectFileClick={() => { | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|               setIsSelectMediaOpen(true); |               setIsSelectMediaOpen(true); | ||||||
|             }} |             }} | ||||||
|             setUploadMediaOpen={() => { |             setUploadMediaOpen={() => { | ||||||
|               setIsUploadMediaOpen(true); |               setIsUploadMediaOpen(true); | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| @@ -162,7 +205,10 @@ export const CarrierCreatePage = observer(() => { | |||||||
|           startIcon={<Save size={20} />} |           startIcon={<Save size={20} />} | ||||||
|           onClick={handleCreate} |           onClick={handleCreate} | ||||||
|           disabled={ |           disabled={ | ||||||
|             isLoading || !fullName || !shortName || !cityId || !selectedMediaId |             isLoading || | ||||||
|  |             !createCarrierData[language].full_name || | ||||||
|  |             !createCarrierData[language].short_name || | ||||||
|  |             !createCarrierData.city_id | ||||||
|           } |           } | ||||||
|         > |         > | ||||||
|           {isLoading ? ( |           {isLoading ? ( | ||||||
| @@ -177,7 +223,7 @@ export const CarrierCreatePage = observer(() => { | |||||||
|         open={isSelectMediaOpen} |         open={isSelectMediaOpen} | ||||||
|         onClose={() => setIsSelectMediaOpen(false)} |         onClose={() => setIsSelectMediaOpen(false)} | ||||||
|         onSelectMedia={handleMediaSelect} |         onSelectMedia={handleMediaSelect} | ||||||
|         mediaType={3} |         mediaType={1} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <UploadMediaDialog |       <UploadMediaDialog | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ export const CarrierEditPage = observer(() => { | |||||||
|   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); |   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); | ||||||
|   const [mediaId, setMediaId] = useState(""); |   const [mediaId, setMediaId] = useState(""); | ||||||
|   const [activeMenuType, setActiveMenuType] = useState< |   const [activeMenuType, setActiveMenuType] = useState< | ||||||
|     "thumbnail" | "watermark_lu" | "watermark_rd" | null |     "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null | ||||||
|   >(null); |   >(null); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @@ -141,7 +141,7 @@ export const CarrierEditPage = observer(() => { | |||||||
|               ) |               ) | ||||||
|             } |             } | ||||||
|           > |           > | ||||||
|             {cityStore.cities[language].data?.map((city) => ( |             {cityStore.cities["ru"].data?.map((city) => ( | ||||||
|               <MenuItem key={city.id} value={city.id}> |               <MenuItem key={city.id} value={city.id}> | ||||||
|                 {city.name} |                 {city.name} | ||||||
|               </MenuItem> |               </MenuItem> | ||||||
| @@ -220,12 +220,12 @@ export const CarrierEditPage = observer(() => { | |||||||
|               setActiveMenuType(null); |               setActiveMenuType(null); | ||||||
|             }} |             }} | ||||||
|             onSelectFileClick={() => { |             onSelectFileClick={() => { | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|               setIsSelectMediaOpen(true); |               setIsSelectMediaOpen(true); | ||||||
|             }} |             }} | ||||||
|             setUploadMediaOpen={() => { |             setUploadMediaOpen={() => { | ||||||
|               setIsUploadMediaOpen(true); |               setIsUploadMediaOpen(true); | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| @@ -238,9 +238,7 @@ export const CarrierEditPage = observer(() => { | |||||||
|           disabled={ |           disabled={ | ||||||
|             isLoading || |             isLoading || | ||||||
|             !editCarrierData[language].full_name || |             !editCarrierData[language].full_name || | ||||||
|             !editCarrierData[language].short_name || |             !editCarrierData.city_id | ||||||
|             !editCarrierData.city_id || |  | ||||||
|             !editCarrierData.logo |  | ||||||
|           } |           } | ||||||
|         > |         > | ||||||
|           {isLoading ? ( |           {isLoading ? ( | ||||||
| @@ -255,7 +253,7 @@ export const CarrierEditPage = observer(() => { | |||||||
|         open={isSelectMediaOpen} |         open={isSelectMediaOpen} | ||||||
|         onClose={() => setIsSelectMediaOpen(false)} |         onClose={() => setIsSelectMediaOpen(false)} | ||||||
|         onSelectMedia={handleMediaSelect} |         onSelectMedia={handleMediaSelect} | ||||||
|         mediaType={3} |         mediaType={1} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <UploadMediaDialog |       <UploadMediaDialog | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | ||||||
| import { carrierStore, languageStore } from "@shared"; | import { carrierStore, cityStore, languageStore } from "@shared"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { Pencil, Trash2, Minus } from "lucide-react"; | import { Pencil, Trash2, Minus } from "lucide-react"; | ||||||
| @@ -8,6 +8,7 @@ import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets"; | |||||||
|  |  | ||||||
| export const CarrierListPage = observer(() => { | export const CarrierListPage = observer(() => { | ||||||
|   const { carriers, getCarriers, deleteCarrier } = carrierStore; |   const { carriers, getCarriers, deleteCarrier } = carrierStore; | ||||||
|  |   const { getCities, cities } = cityStore; | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); |   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | ||||||
|   const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); |   const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); | ||||||
| @@ -17,6 +18,9 @@ export const CarrierListPage = observer(() => { | |||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     (async () => { |     (async () => { | ||||||
|  |       await getCities("ru"); | ||||||
|  |       await getCities("en"); | ||||||
|  |       await getCities("zh"); | ||||||
|       await getCarriers(language); |       await getCarriers(language); | ||||||
|     })(); |     })(); | ||||||
|   }, [language]); |   }, [language]); | ||||||
| @@ -55,14 +59,15 @@ export const CarrierListPage = observer(() => { | |||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       field: "city", |       field: "city_id", | ||||||
|       headerName: "Город", |       headerName: "Город", | ||||||
|       flex: 1, |       flex: 1, | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|           <div className="w-full h-full flex items-center"> |           <div className="w-full h-full flex items-center"> | ||||||
|             {params.value ? ( |             {params.value ? ( | ||||||
|               params.value |               cities[language].data.find((city) => city.id == params.value) | ||||||
|  |                 ?.name | ||||||
|             ) : ( |             ) : ( | ||||||
|               <Minus size={20} className="text-red-500" /> |               <Minus size={20} className="text-red-500" /> | ||||||
|             )} |             )} | ||||||
| @@ -103,7 +108,7 @@ export const CarrierListPage = observer(() => { | |||||||
|     id: carrier.id, |     id: carrier.id, | ||||||
|     full_name: carrier.full_name, |     full_name: carrier.full_name, | ||||||
|     short_name: carrier.short_name, |     short_name: carrier.short_name, | ||||||
|     city: carrier.city, |     city_id: carrier.city_id, | ||||||
|   })); |   })); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ export const CityCreatePage = observer(() => { | |||||||
|   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); |   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); | ||||||
|   const [mediaId, setMediaId] = useState(""); |   const [mediaId, setMediaId] = useState(""); | ||||||
|   const [activeMenuType, setActiveMenuType] = useState< |   const [activeMenuType, setActiveMenuType] = useState< | ||||||
|     "thumbnail" | "watermark_lu" | "watermark_rd" | null |     "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null | ||||||
|   >(null); |   >(null); | ||||||
|   const { getCountries } = countryStore; |   const { getCountries } = countryStore; | ||||||
|   const { getMedia } = mediaStore; |   const { getMedia } = mediaStore; | ||||||
| @@ -132,15 +132,9 @@ export const CityCreatePage = observer(() => { | |||||||
|         </FormControl> |         </FormControl> | ||||||
|  |  | ||||||
|         <div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> |         <div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> | ||||||
|           {!selectedMedia && ( |  | ||||||
|             <div className="flex items-center gap-2 text-red-500"> |  | ||||||
|               <Minus size={20} /> |  | ||||||
|               <span className="text-sm">Герб города не выбран</span> |  | ||||||
|             </div> |  | ||||||
|           )} |  | ||||||
|           <ImageUploadCard |           <ImageUploadCard | ||||||
|             title="Герб города" |             title="Герб города" | ||||||
|             imageKey="thumbnail" |             imageKey="image" | ||||||
|             imageUrl={selectedMedia?.id} |             imageUrl={selectedMedia?.id} | ||||||
|             onImageClick={() => { |             onImageClick={() => { | ||||||
|               setIsPreviewMediaOpen(true); |               setIsPreviewMediaOpen(true); | ||||||
| @@ -156,12 +150,22 @@ export const CityCreatePage = observer(() => { | |||||||
|               setActiveMenuType(null); |               setActiveMenuType(null); | ||||||
|             }} |             }} | ||||||
|             onSelectFileClick={() => { |             onSelectFileClick={() => { | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|               setIsSelectMediaOpen(true); |               setIsSelectMediaOpen(true); | ||||||
|             }} |             }} | ||||||
|             setUploadMediaOpen={() => { |             setUploadMediaOpen={() => { | ||||||
|               setIsUploadMediaOpen(true); |               setIsUploadMediaOpen(true); | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|  |             }} | ||||||
|  |             setHardcodeType={(type) => { | ||||||
|  |               setActiveMenuType( | ||||||
|  |                 type as | ||||||
|  |                   | "thumbnail" | ||||||
|  |                   | "watermark_lu" | ||||||
|  |                   | "watermark_rd" | ||||||
|  |                   | "image" | ||||||
|  |                   | null | ||||||
|  |               ); | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| @@ -185,14 +189,16 @@ export const CityCreatePage = observer(() => { | |||||||
|         open={isSelectMediaOpen} |         open={isSelectMediaOpen} | ||||||
|         onClose={() => setIsSelectMediaOpen(false)} |         onClose={() => setIsSelectMediaOpen(false)} | ||||||
|         onSelectMedia={handleMediaSelect} |         onSelectMedia={handleMediaSelect} | ||||||
|         mediaType={3} // Тип медиа для иконок |         mediaType={1} // Тип медиа для иконок | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <UploadMediaDialog |       <UploadMediaDialog | ||||||
|         open={isUploadMediaOpen} |         open={isUploadMediaOpen} | ||||||
|         onClose={() => setIsUploadMediaOpen(false)} |         onClose={() => setIsUploadMediaOpen(false)} | ||||||
|         afterUpload={handleMediaSelect} |         afterUpload={handleMediaSelect} | ||||||
|         hardcodeType={activeMenuType} |         hardcodeType={ | ||||||
|  |           activeMenuType as "thumbnail" | "watermark_lu" | "watermark_rd" | null | ||||||
|  |         } | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <PreviewMediaDialog |       <PreviewMediaDialog | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ export const CityEditPage = observer(() => { | |||||||
|   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); |   const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); | ||||||
|   const [mediaId, setMediaId] = useState(""); |   const [mediaId, setMediaId] = useState(""); | ||||||
|   const [activeMenuType, setActiveMenuType] = useState< |   const [activeMenuType, setActiveMenuType] = useState< | ||||||
|     "thumbnail" | "watermark_lu" | "watermark_rd" | null |     "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null | ||||||
|   >(null); |   >(null); | ||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
|   const { id } = useParams(); |   const { id } = useParams(); | ||||||
| @@ -151,7 +151,7 @@ export const CityEditPage = observer(() => { | |||||||
|         <div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> |         <div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto"> | ||||||
|           <ImageUploadCard |           <ImageUploadCard | ||||||
|             title="Герб города" |             title="Герб города" | ||||||
|             imageKey="thumbnail" |             imageKey="image" | ||||||
|             imageUrl={selectedMedia?.id} |             imageUrl={selectedMedia?.id} | ||||||
|             onImageClick={() => { |             onImageClick={() => { | ||||||
|               setIsPreviewMediaOpen(true); |               setIsPreviewMediaOpen(true); | ||||||
| @@ -167,12 +167,22 @@ export const CityEditPage = observer(() => { | |||||||
|               setActiveMenuType(null); |               setActiveMenuType(null); | ||||||
|             }} |             }} | ||||||
|             onSelectFileClick={() => { |             onSelectFileClick={() => { | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|               setIsSelectMediaOpen(true); |               setIsSelectMediaOpen(true); | ||||||
|             }} |             }} | ||||||
|             setUploadMediaOpen={() => { |             setUploadMediaOpen={() => { | ||||||
|               setIsUploadMediaOpen(true); |               setIsUploadMediaOpen(true); | ||||||
|               setActiveMenuType("thumbnail"); |               setActiveMenuType("image"); | ||||||
|  |             }} | ||||||
|  |             setHardcodeType={(type) => { | ||||||
|  |               setActiveMenuType( | ||||||
|  |                 type as | ||||||
|  |                   | "thumbnail" | ||||||
|  |                   | "watermark_lu" | ||||||
|  |                   | "watermark_rd" | ||||||
|  |                   | "image" | ||||||
|  |                   | null | ||||||
|  |               ); | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| @@ -198,14 +208,21 @@ export const CityEditPage = observer(() => { | |||||||
|         open={isSelectMediaOpen} |         open={isSelectMediaOpen} | ||||||
|         onClose={() => setIsSelectMediaOpen(false)} |         onClose={() => setIsSelectMediaOpen(false)} | ||||||
|         onSelectMedia={handleMediaSelect} |         onSelectMedia={handleMediaSelect} | ||||||
|         mediaType={3} // Тип медиа для иконок |         mediaType={1} // Тип медиа для иконок | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <UploadMediaDialog |       <UploadMediaDialog | ||||||
|         open={isUploadMediaOpen} |         open={isUploadMediaOpen} | ||||||
|         onClose={() => setIsUploadMediaOpen(false)} |         onClose={() => setIsUploadMediaOpen(false)} | ||||||
|         afterUpload={handleMediaSelect} |         afterUpload={handleMediaSelect} | ||||||
|         hardcodeType={activeMenuType} |         hardcodeType={ | ||||||
|  |           activeMenuType as | ||||||
|  |             | "thumbnail" | ||||||
|  |             | "watermark_lu" | ||||||
|  |             | "watermark_rd" | ||||||
|  |             | "image" | ||||||
|  |             | null | ||||||
|  |         } | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <PreviewMediaDialog |       <PreviewMediaDialog | ||||||
|   | |||||||
| @@ -11,7 +11,9 @@ export const CityListPage = observer(() => { | |||||||
|   const { cities, getCities, deleteCity } = cityStore; |   const { cities, getCities, deleteCity } = cityStore; | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); |   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | ||||||
|  |   const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); | ||||||
|   const [rowId, setRowId] = useState<number | null>(null); |   const [rowId, setRowId] = useState<number | null>(null); | ||||||
|  |   const [ids, setIds] = useState<number[]>([]); | ||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @@ -57,18 +59,18 @@ export const CityListPage = observer(() => { | |||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|       width: 200, |       width: 200, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|           <div className="flex h-full gap-7 justify-center items-center"> |           <div className="flex h-full gap-7 justify-center items-center"> | ||||||
|             <button onClick={() => navigate(`/city/${params.row.id}/edit`)}> |             <button onClick={() => navigate(`/city/${params.row.id}/edit`)}> | ||||||
|               <Pencil size={20} className="text-blue-500" /> |               <Pencil size={20} className="text-blue-500" /> | ||||||
|             </button> |             </button> | ||||||
|             <button onClick={() => navigate(`/city/${params.row.id}`)}> |             {/* <button onClick={() => navigate(`/city/${params.row.id}`)}> | ||||||
|               <Eye size={20} className="text-green-500" /> |               <Eye size={20} className="text-green-500" /> | ||||||
|             </button> |             </button> */} | ||||||
|             <button |             <button | ||||||
|               onClick={() => { |               onClick={(e) => { | ||||||
|  |                 e.stopPropagation(); | ||||||
|                 setIsDeleteModalOpen(true); |                 setIsDeleteModalOpen(true); | ||||||
|                 setRowId(params.row.id); |                 setRowId(params.row.id); | ||||||
|               }} |               }} | ||||||
| @@ -96,11 +98,29 @@ export const CityListPage = observer(() => { | |||||||
|           <h1 className="text-2xl">Города</h1> |           <h1 className="text-2xl">Города</h1> | ||||||
|           <CreateButton label="Создать город" path="/city/create" /> |           <CreateButton label="Создать город" path="/city/create" /> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|  |         <div | ||||||
|  |           className="flex justify-end mb-5 duration-300" | ||||||
|  |           style={{ opacity: ids.length > 0 ? 1 : 0 }} | ||||||
|  |         > | ||||||
|  |           <button | ||||||
|  |             className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" | ||||||
|  |             onClick={() => setIsBulkDeleteModalOpen(true)} | ||||||
|  |           > | ||||||
|  |             <Trash2 size={20} className="text-white" /> Удалить выбранные ( | ||||||
|  |             {ids.length}) | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <DataGrid |         <DataGrid | ||||||
|           rows={rows} |           rows={rows} | ||||||
|           columns={columns} |           columns={columns} | ||||||
|           hideFooterPagination |           hideFooterPagination | ||||||
|           hideFooter |           hideFooter | ||||||
|  |           checkboxSelection | ||||||
|  |           onRowSelectionModelChange={(newSelection) => { | ||||||
|  |             setIds(Array.from(newSelection.ids as unknown as number[])); | ||||||
|  |           }} | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
| @@ -119,6 +139,20 @@ export const CityListPage = observer(() => { | |||||||
|           setRowId(null); |           setRowId(null); | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|  |       <DeleteModal | ||||||
|  |         open={isBulkDeleteModalOpen} | ||||||
|  |         onDelete={async () => { | ||||||
|  |           await Promise.all(ids.map((id) => deleteCity(id.toString()))); | ||||||
|  |           toast.success("Города успешно удалены"); | ||||||
|  |           getCities(language); | ||||||
|  |           setIsBulkDeleteModalOpen(false); | ||||||
|  |           setIds([]); | ||||||
|  |         }} | ||||||
|  |         onCancel={() => { | ||||||
|  |           setIsBulkDeleteModalOpen(false); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -10,7 +10,9 @@ export const CountryListPage = observer(() => { | |||||||
|   const { countries, getCountries, deleteCountry } = countryStore; |   const { countries, getCountries, deleteCountry } = countryStore; | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); |   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | ||||||
|  |   const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); | ||||||
|   const [rowId, setRowId] = useState<string | null>(null); |   const [rowId, setRowId] = useState<string | null>(null); | ||||||
|  |   const [ids, setIds] = useState<number[]>([]); | ||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @@ -52,7 +54,8 @@ export const CountryListPage = observer(() => { | |||||||
|               <Eye size={20} className="text-green-500" /> |               <Eye size={20} className="text-green-500" /> | ||||||
|             </button> */} |             </button> */} | ||||||
|             <button |             <button | ||||||
|               onClick={() => { |               onClick={(e) => { | ||||||
|  |                 e.stopPropagation(); | ||||||
|                 setIsDeleteModalOpen(true); |                 setIsDeleteModalOpen(true); | ||||||
|                 setRowId(params.row.code); |                 setRowId(params.row.code); | ||||||
|               }} |               }} | ||||||
| @@ -80,14 +83,37 @@ export const CountryListPage = observer(() => { | |||||||
|           <h1 className="text-2xl">Страны</h1> |           <h1 className="text-2xl">Страны</h1> | ||||||
|           <CreateButton label="Создать страну" path="/country/create" /> |           <CreateButton label="Создать страну" path="/country/create" /> | ||||||
|         </div> |         </div> | ||||||
|         <DataGrid rows={rows} columns={columns} hideFooter /> |  | ||||||
|  |         <div | ||||||
|  |           className="flex justify-end mb-5 duration-300" | ||||||
|  |           style={{ opacity: ids.length > 0 ? 1 : 0 }} | ||||||
|  |         > | ||||||
|  |           <button | ||||||
|  |             className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" | ||||||
|  |             onClick={() => setIsBulkDeleteModalOpen(true)} | ||||||
|  |           > | ||||||
|  |             <Trash2 size={20} className="text-white" /> Удалить выбранные ( | ||||||
|  |             {ids.length}) | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <DataGrid | ||||||
|  |           rows={rows} | ||||||
|  |           columns={columns} | ||||||
|  |           hideFooter | ||||||
|  |           checkboxSelection | ||||||
|  |           onRowSelectionModelChange={(newSelection) => { | ||||||
|  |             console.log(newSelection); | ||||||
|  |             setIds(Array.from(newSelection.ids as unknown as number[])); | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <DeleteModal |       <DeleteModal | ||||||
|         open={isDeleteModalOpen} |         open={isDeleteModalOpen} | ||||||
|         onDelete={async () => { |         onDelete={async () => { | ||||||
|           if (!rowId) return; |           if (!rowId) return; | ||||||
|           await deleteCountry(rowId, language); |           await deleteCountry(rowId); | ||||||
|           setRowId(null); |           setRowId(null); | ||||||
|           setIsDeleteModalOpen(false); |           setIsDeleteModalOpen(false); | ||||||
|         }} |         }} | ||||||
| @@ -96,6 +122,19 @@ export const CountryListPage = observer(() => { | |||||||
|           setIsDeleteModalOpen(false); |           setIsDeleteModalOpen(false); | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|  |       <DeleteModal | ||||||
|  |         open={isBulkDeleteModalOpen} | ||||||
|  |         onDelete={async () => { | ||||||
|  |           await Promise.all(ids.map((id) => deleteCountry(id.toString()))); | ||||||
|  |           getCountries(language); | ||||||
|  |           setIsBulkDeleteModalOpen(false); | ||||||
|  |           setIds([]); | ||||||
|  |         }} | ||||||
|  |         onCancel={() => { | ||||||
|  |           setIsBulkDeleteModalOpen(false); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -132,6 +132,8 @@ class MapStore { | |||||||
|       data = { |       data = { | ||||||
|         route_number: properties.name || "Новый маршрут", |         route_number: properties.name || "Новый маршрут", | ||||||
|         path: geometry.coordinates, |         path: geometry.coordinates, | ||||||
|  |         center_latitude: geometry.coordinates[0][1], | ||||||
|  |         center_longitude: geometry.coordinates[0][0], | ||||||
|       }; |       }; | ||||||
|     } else if (featureType === "sight") { |     } else if (featureType === "sight") { | ||||||
|       data = { |       data = { | ||||||
| @@ -192,17 +194,27 @@ class MapStore { | |||||||
|       oldData = this.sights.find((f) => f.id === numericId); |       oldData = this.sights.find((f) => f.id === numericId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     console.log(oldData); |     let response; | ||||||
|     console.log(data); |     if (featureType !== "route") { | ||||||
|  |       response = await languageInstance("ru").patch( | ||||||
|     const response = await languageInstance("ru").patch( |         `/${featureType}/${numericId}`, | ||||||
|       `/${featureType}/${numericId}`, |         { | ||||||
|       { |           ...oldData, | ||||||
|         ...oldData, |           latitude: data.latitude, | ||||||
|         latitude: data.latitude, |           longitude: data.longitude, | ||||||
|         longitude: data.longitude, |         } | ||||||
|       } |       ); | ||||||
|     ); |     } else { | ||||||
|  |       response = await languageInstance("ru").patch( | ||||||
|  |         `/${featureType}/${numericId}`, | ||||||
|  |         { | ||||||
|  |           ...oldData, | ||||||
|  |           path: data.path, | ||||||
|  |           center_latitude: data.path[0][1], | ||||||
|  |           center_longitude: data.path[0][0], | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (featureType === "route") { |     if (featureType === "route") { | ||||||
|       const index = this.routes.findIndex((f) => f.id === numericId); |       const index = this.routes.findIndex((f) => f.id === numericId); | ||||||
| @@ -1078,7 +1090,10 @@ class MapService { | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     if (!featureAtPixel) { |     if (!featureAtPixel) { | ||||||
|       if (!ctrlKey) this.unselect(); |       if (ctrlKey) { | ||||||
|  |         // При ctrl + клик вне сущности сбрасываем выбор | ||||||
|  |         this.setSelectedIds(new Set()); | ||||||
|  |       } | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1086,11 +1101,16 @@ class MapService { | |||||||
|     if (featureId === undefined) return; |     if (featureId === undefined) return; | ||||||
|  |  | ||||||
|     if (ctrlKey) { |     if (ctrlKey) { | ||||||
|  |       // При ctrl + клик на сущность добавляем/удаляем её из выбора | ||||||
|       const newSet = new Set(this.selectedIds); |       const newSet = new Set(this.selectedIds); | ||||||
|       if (newSet.has(featureId)) newSet.delete(featureId); |       if (newSet.has(featureId)) { | ||||||
|       else newSet.add(featureId); |         newSet.delete(featureId); | ||||||
|  |       } else { | ||||||
|  |         newSet.add(featureId); | ||||||
|  |       } | ||||||
|       this.setSelectedIds(newSet); |       this.setSelectedIds(newSet); | ||||||
|     } else { |     } else { | ||||||
|  |       // При обычном клике на сущность выбираем только её | ||||||
|       this.setSelectedIds(new Set([featureId])); |       this.setSelectedIds(new Set([featureId])); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -1153,14 +1173,12 @@ class MapService { | |||||||
|     mapStore |     mapStore | ||||||
|       .deleteFeature(recourse, numericId) |       .deleteFeature(recourse, numericId) | ||||||
|       .then(() => { |       .then(() => { | ||||||
|         toast.success("Объект успешно удален"); |  | ||||||
|         if (stateBeforeDelete) |         if (stateBeforeDelete) | ||||||
|           this.addStateToHistory("delete", stateBeforeDelete); |           this.addStateToHistory("delete", stateBeforeDelete); | ||||||
|         this.vectorSource.removeFeature(feature); |         this.vectorSource.removeFeature(feature); | ||||||
|         this.unselect(); |         this.unselect(); | ||||||
|       }) |       }) | ||||||
|       .catch((err) => { |       .catch((err) => { | ||||||
|         toast.error("Ошибка при удалении объекта"); |  | ||||||
|         console.error("Delete failed:", err); |         console.error("Delete failed:", err); | ||||||
|       }); |       }); | ||||||
|   } |   } | ||||||
| @@ -1196,7 +1214,6 @@ class MapService { | |||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       .catch((err) => { |       .catch((err) => { | ||||||
|         toast.error("Произошла ошибка при массовом удалении"); |  | ||||||
|         console.error("Bulk delete failed:", err); |         console.error("Bulk delete failed:", err); | ||||||
|       }); |       }); | ||||||
|   } |   } | ||||||
| @@ -1307,12 +1324,9 @@ class MapService { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       await mapStore.updateFeature(featureType, featureGeoJSON); |       await mapStore.updateFeature(featureType, featureGeoJSON); | ||||||
|       toast.success(`"${feature.get("name")}" успешно обновлен.`); |  | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error("Failed to update feature:", error); |       console.error("Failed to update feature:", error); | ||||||
|       toast.error( |  | ||||||
|         `Не удалось обновить "${feature.get("name")}". Отмена изменений...` |  | ||||||
|       ); |  | ||||||
|       this.undo(); |       this.undo(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -1338,7 +1352,6 @@ class MapService { | |||||||
|         featureType === "route" |         featureType === "route" | ||||||
|           ? createdFeatureData.route_number |           ? createdFeatureData.route_number | ||||||
|           : createdFeatureData.name; |           : createdFeatureData.name; | ||||||
|       toast.success(`"${newName}" создано.`); |  | ||||||
|  |  | ||||||
|       const newFeatureId = `${featureType}-${createdFeatureData.id}`; |       const newFeatureId = `${featureType}-${createdFeatureData.id}`; | ||||||
|       feature.setId(newFeatureId); |       feature.setId(newFeatureId); | ||||||
| @@ -1507,15 +1520,20 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const handleCheckboxChange = useCallback( |   const handleCheckboxChange = useCallback( | ||||||
|     // @ts-ignore |     (id: string | number | undefined) => { | ||||||
|     (id) => { |  | ||||||
|       if (id === undefined) return; |       if (id === undefined) return; | ||||||
|       const newSet = new Set(selectedIds); |       const newSet = new Set(selectedIds); | ||||||
|       if (newSet.has(id)) newSet.delete(id); |       if (newSet.has(id)) { | ||||||
|       else newSet.add(id); |         newSet.delete(id); | ||||||
|  |       } else { | ||||||
|  |         newSet.add(id); | ||||||
|  |       } | ||||||
|       setSelectedIds(newSet); |       setSelectedIds(newSet); | ||||||
|  |       if (mapService) { | ||||||
|  |         mapService.setSelectedIds(newSet); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     [selectedIds, setSelectedIds] |     [selectedIds, setSelectedIds, mapService] | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const handleBulkDelete = useCallback(() => { |   const handleBulkDelete = useCallback(() => { | ||||||
| @@ -1990,13 +2008,11 @@ export const MapPage: React.FC = () => { | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const handleMapClick = useCallback( |   const handleMapClick = useCallback( | ||||||
|     (event: any) => { |     (event: MapBrowserEvent<any>) => { | ||||||
|       if (!mapServiceInstance || isLassoActive) return; |       if (!mapServiceInstance) return; | ||||||
|       const ctrlKey = |       mapServiceInstance.handleMapClick(event, event.originalEvent.ctrlKey); | ||||||
|         event.originalEvent.ctrlKey || event.originalEvent.metaKey; |  | ||||||
|       mapServiceInstance.handleMapClick(event, ctrlKey); |  | ||||||
|     }, |     }, | ||||||
|     [mapServiceInstance, isLassoActive] |     [mapServiceInstance] | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|   | |||||||
| @@ -35,11 +35,49 @@ export const RouteCreatePage = observer(() => { | |||||||
|   const [centerLng, setCenterLng] = useState(""); |   const [centerLng, setCenterLng] = useState(""); | ||||||
|   const [isLoading, setIsLoading] = useState(false); |   const [isLoading, setIsLoading] = useState(false); | ||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     carrierStore.getCarriers(language); |     carrierStore.getCarriers(language); | ||||||
|     articlesStore.getArticleList(); |     articlesStore.getArticleList(); | ||||||
|   }, [language]); |   }, [language]); | ||||||
|  |  | ||||||
|  |   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 handleCreateRoute = async () => { |   const handleCreateRoute = async () => { | ||||||
|     try { |     try { | ||||||
|       setIsLoading(true); |       setIsLoading(true); | ||||||
| @@ -52,16 +90,24 @@ export const RouteCreatePage = observer(() => { | |||||||
|       const center_latitude = centerLat ? Number(centerLat) : undefined; |       const center_latitude = centerLat ? Number(centerLat) : undefined; | ||||||
|       const center_longitude = centerLng ? Number(centerLng) : undefined; |       const center_longitude = centerLng ? Number(centerLng) : undefined; | ||||||
|       const route_direction = direction === "forward"; |       const route_direction = direction === "forward"; | ||||||
|  |  | ||||||
|  |       const validationResult = validateCoordinates(routeCoords); | ||||||
|  |       if (validationResult !== true) { | ||||||
|  |         toast.error(validationResult); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       // Координаты маршрута как массив массивов чисел |       // Координаты маршрута как массив массивов чисел | ||||||
|       const path = routeCoords |       const path = routeCoords | ||||||
|  |         .trim() | ||||||
|         .split("\n") |         .split("\n") | ||||||
|         .map((line) => |         .map((line) => { | ||||||
|           line |           const [lat, lon] = line | ||||||
|             .split(" ") |             .trim() | ||||||
|             .map((coord) => Number(coord.trim())) |             .split(/[\s,]+/) | ||||||
|             .filter((n) => !isNaN(n)) |             .map(Number); | ||||||
|         ) |           return [lat, lon]; | ||||||
|         .filter((arr) => arr.length === 2); |         }); | ||||||
|  |  | ||||||
|       // Собираем объект маршрута |       // Собираем объект маршрута | ||||||
|       const newRoute: Partial<Route> = { |       const newRoute: Partial<Route> = { | ||||||
| @@ -141,9 +187,33 @@ export const RouteCreatePage = observer(() => { | |||||||
|             className="w-full" |             className="w-full" | ||||||
|             label="Координаты маршрута" |             label="Координаты маршрута" | ||||||
|             multiline |             multiline | ||||||
|             minRows={3} |             minRows={4} | ||||||
|             value={routeCoords} |             value={routeCoords} | ||||||
|             onChange={(e) => setRouteCoords(e.target.value)} |             onChange={(e) => { | ||||||
|  |               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" | ||||||
|           /> |           /> | ||||||
|           <TextField |           <TextField | ||||||
|             className="w-full" |             className="w-full" | ||||||
|   | |||||||
| @@ -27,6 +27,8 @@ export const RouteEditPage = observer(() => { | |||||||
|   const { editRouteData } = routeStore; |   const { editRouteData } = routeStore; | ||||||
|   const [isLoading, setIsLoading] = useState(false); |   const [isLoading, setIsLoading] = useState(false); | ||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
|  |   const [coordinates, setCoordinates] = useState<string>(""); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const fetchData = async () => { |     const fetchData = async () => { | ||||||
|       const response = await routeStore.getRoute(Number(id)); |       const response = await routeStore.getRoute(Number(id)); | ||||||
| @@ -37,6 +39,15 @@ export const RouteEditPage = observer(() => { | |||||||
|     fetchData(); |     fetchData(); | ||||||
|   }, [id, language]); |   }, [id, language]); | ||||||
|  |  | ||||||
|  |   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 () => { |   const handleSave = async () => { | ||||||
|     setIsLoading(true); |     setIsLoading(true); | ||||||
|     await routeStore.editRoute(Number(id)); |     await routeStore.editRoute(Number(id)); | ||||||
| @@ -44,6 +55,43 @@ export const RouteEditPage = observer(() => { | |||||||
|     setIsLoading(false); |     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 "Неверный формат координат"; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Paper className="w-full h-full p-3 flex flex-col gap-10"> |     <Paper className="w-full h-full p-3 flex flex-col gap-10"> | ||||||
|       <LanguageSwitcher /> |       <LanguageSwitcher /> | ||||||
| @@ -105,15 +153,46 @@ export const RouteEditPage = observer(() => { | |||||||
|             className="w-full" |             className="w-full" | ||||||
|             label="Координаты маршрута" |             label="Координаты маршрута" | ||||||
|             multiline |             multiline | ||||||
|             minRows={3} |             minRows={4} | ||||||
|             value={editRouteData.path.map((p) => p.join(" ")).join("\n") || ""} |             value={coordinates} | ||||||
|             onChange={(e) => |             onChange={(e) => { | ||||||
|               routeStore.setEditRouteData({ |               const newValue = e.target.value; | ||||||
|                 path: e.target.value |               setCoordinates(newValue); | ||||||
|                   .split("\n") |  | ||||||
|                   .map((line) => line.split(" ").map(Number)), |               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" | ||||||
|           /> |           /> | ||||||
|           <TextField |           <TextField | ||||||
|             className="w-full" |             className="w-full" | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | ||||||
| import { languageStore, routeStore } from "@shared"; | import { carrierStore, languageStore, routeStore } from "@shared"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { Map, Pencil, Trash2, Minus } from "lucide-react"; | import { Map, Pencil, Trash2, Minus } from "lucide-react"; | ||||||
| @@ -9,6 +9,7 @@ import { LanguageSwitcher } from "@widgets"; | |||||||
|  |  | ||||||
| export const RouteListPage = observer(() => { | export const RouteListPage = observer(() => { | ||||||
|   const { routes, getRoutes, deleteRoute } = routeStore; |   const { routes, getRoutes, deleteRoute } = routeStore; | ||||||
|  |   const { carriers, getCarriers } = carrierStore; | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); |   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | ||||||
|   const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); |   const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false); | ||||||
| @@ -17,19 +18,27 @@ export const RouteListPage = observer(() => { | |||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     getRoutes(); |     const fetchData = async () => { | ||||||
|  |       await getCarriers("ru"); | ||||||
|  |       await getCarriers("en"); | ||||||
|  |       await getCarriers("zh"); | ||||||
|  |       await getRoutes(); | ||||||
|  |     }; | ||||||
|  |     fetchData(); | ||||||
|   }, [language]); |   }, [language]); | ||||||
|  |  | ||||||
|   const columns: GridColDef[] = [ |   const columns: GridColDef[] = [ | ||||||
|     { |     { | ||||||
|       field: "carrier", |       field: "carrier_id", | ||||||
|       headerName: "Перевозчик", |       headerName: "Перевозчик", | ||||||
|       width: 250, |       width: 250, | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|           <div className="w-full h-full flex items-center"> |           <div className="w-full h-full flex items-center"> | ||||||
|             {params.value ? ( |             {params.value ? ( | ||||||
|               params.value |               carriers[language].data.find( | ||||||
|  |                 (carrier) => carrier.id == params.value | ||||||
|  |               )?.short_name | ||||||
|             ) : ( |             ) : ( | ||||||
|               <Minus size={20} className="text-red-500" /> |               <Minus size={20} className="text-red-500" /> | ||||||
|             )} |             )} | ||||||
| @@ -105,7 +114,7 @@ export const RouteListPage = observer(() => { | |||||||
|  |  | ||||||
|   const rows = routes.data.map((route) => ({ |   const rows = routes.data.map((route) => ({ | ||||||
|     id: route.id, |     id: route.id, | ||||||
|     carrier: route.carrier, |     carrier_id: route.carrier_id, | ||||||
|     route_number: route.route_number, |     route_number: route.route_number, | ||||||
|     route_direction: route.route_direction ? "Прямой" : "Обратный", |     route_direction: route.route_direction ? "Прямой" : "Обратный", | ||||||
|   })); |   })); | ||||||
|   | |||||||
| @@ -8,33 +8,62 @@ import { | |||||||
|   InputLabel, |   InputLabel, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { ArrowLeft, Loader2, Save } from "lucide-react"; | import { ArrowLeft, Save } from "lucide-react"; | ||||||
|  | import { Loader2 } from "lucide-react"; | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
| import { stationsStore } from "@shared"; | import { stationsStore, languageStore, cityStore } from "@shared"; | ||||||
| import { useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { LanguageSwitcher } from "@widgets"; | import { LanguageSwitcher } from "@widgets"; | ||||||
|  |  | ||||||
| export const StationCreatePage = observer(() => { | export const StationCreatePage = observer(() => { | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [name, setName] = useState(""); |  | ||||||
|   const [systemName, setSystemName] = useState(""); |  | ||||||
|   const [direction, setDirection] = useState(""); |  | ||||||
|   const [isLoading, setIsLoading] = useState(false); |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const { language } = languageStore; | ||||||
|  |   const { | ||||||
|  |     createStationData, | ||||||
|  |     setCreateCommonData, | ||||||
|  |     createStation, | ||||||
|  |     setLanguageCreateStationData, | ||||||
|  |   } = stationsStore; | ||||||
|  |   const { cities, getCities } = cityStore; | ||||||
|  |   const [coordinates, setCoordinates] = useState<string>(""); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if ( | ||||||
|  |       createStationData.common.latitude !== 0 || | ||||||
|  |       createStationData.common.longitude !== 0 | ||||||
|  |     ) { | ||||||
|  |       setCoordinates( | ||||||
|  |         `${createStationData.common.latitude}, ${createStationData.common.longitude}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, [createStationData.common.latitude, createStationData.common.longitude]); | ||||||
|  |  | ||||||
|   const handleCreate = async () => { |   const handleCreate = async () => { | ||||||
|     try { |     try { | ||||||
|       setIsLoading(true); |       setIsLoading(true); | ||||||
|       await stationsStore.createStation(name, systemName, direction); |       await createStation(); | ||||||
|       toast.success("Станция успешно создана"); |       toast.success("Станция успешно создана"); | ||||||
|       navigate("/station"); |       navigate("/station"); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |       console.error("Error creating station:", error); | ||||||
|       toast.error("Ошибка при создании станции"); |       toast.error("Ошибка при создании станции"); | ||||||
|     } finally { |     } finally { | ||||||
|       setIsLoading(false); |       setIsLoading(false); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const fetchCities = async () => { | ||||||
|  |       await getCities("ru"); | ||||||
|  |       await getCities("en"); | ||||||
|  |       await getCities("zh"); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     fetchCities(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Paper className="w-full h-full p-3 flex flex-col gap-10"> |     <Paper className="w-full h-full p-3 flex flex-col gap-10"> | ||||||
|       <LanguageSwitcher /> |       <LanguageSwitcher /> | ||||||
| @@ -47,44 +76,123 @@ export const StationCreatePage = observer(() => { | |||||||
|           Назад |           Назад | ||||||
|         </button> |         </button> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div className="flex flex-col gap-10 w-full items-end"> |       <div className="flex flex-col gap-10 w-full items-end"> | ||||||
|         <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start"> |         <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start"> | ||||||
|           <h1 className="text-3xl break-words">{name}</h1> |           <h1 className="text-3xl break-words">Создание станции</h1> | ||||||
|         </div> |         </div> | ||||||
|         <TextField |         <TextField | ||||||
|           className="w-full" |           fullWidth | ||||||
|           label="Название" |           label="Название" | ||||||
|  |           value={createStationData[language].name || ""} | ||||||
|           required |           required | ||||||
|           value={name} |           onChange={(e) => | ||||||
|           onChange={(e) => setName(e.target.value)} |             setLanguageCreateStationData(language, { | ||||||
|         /> |               name: e.target.value, | ||||||
|         <TextField |             }) | ||||||
|           className="w-full" |           } | ||||||
|           label="Системное название" |  | ||||||
|           required |  | ||||||
|           value={systemName} |  | ||||||
|           onChange={(e) => setSystemName(e.target.value)} |  | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|         <FormControl fullWidth> |         <FormControl fullWidth> | ||||||
|           <InputLabel>Направление</InputLabel> |           <InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel> | ||||||
|           <Select |           <Select | ||||||
|             value={direction} |             labelId="direction-label" | ||||||
|             label="Направление" |             value={createStationData.common.direction ? "Прямой" : "Обратный"} | ||||||
|             onChange={(e) => setDirection(e.target.value)} |             label="Прямой/обратный маршрут" | ||||||
|             required |             onChange={(e) => | ||||||
|  |               setCreateCommonData({ | ||||||
|  |                 direction: e.target.value === "Прямой", | ||||||
|  |               }) | ||||||
|  |             } | ||||||
|           > |           > | ||||||
|             <MenuItem value="forward">Прямое</MenuItem> |             <MenuItem value="Прямой">Прямой</MenuItem> | ||||||
|             <MenuItem value="backward">Обратное</MenuItem> |             <MenuItem value="Обратный">Обратный</MenuItem> | ||||||
|  |           </Select> | ||||||
|  |         </FormControl> | ||||||
|  |  | ||||||
|  |         <TextField | ||||||
|  |           fullWidth | ||||||
|  |           label="Описание" | ||||||
|  |           value={createStationData[language].description || ""} | ||||||
|  |           onChange={(e) => | ||||||
|  |             setLanguageCreateStationData(language, { | ||||||
|  |               description: e.target.value, | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <TextField | ||||||
|  |           fullWidth | ||||||
|  |           label="Адрес" | ||||||
|  |           value={createStationData[language].address || ""} | ||||||
|  |           onChange={(e) => | ||||||
|  |             setLanguageCreateStationData(language, { | ||||||
|  |               address: e.target.value, | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <TextField | ||||||
|  |           fullWidth | ||||||
|  |           label="Координаты" | ||||||
|  |           value={coordinates} | ||||||
|  |           onChange={(e) => { | ||||||
|  |             const newValue = e.target.value; | ||||||
|  |             setCoordinates(newValue); | ||||||
|  |  | ||||||
|  |             const input = newValue.replace(/,/g, " ").trim(); | ||||||
|  |             const [latStr, lonStr] = input.split(/\s+/); | ||||||
|  |  | ||||||
|  |             const lat = parseFloat(latStr); | ||||||
|  |             const lon = parseFloat(lonStr); | ||||||
|  |  | ||||||
|  |             const isValidLat = !isNaN(lat); | ||||||
|  |             const isValidLon = !isNaN(lon); | ||||||
|  |  | ||||||
|  |             if (isValidLat && isValidLon) { | ||||||
|  |               setCreateCommonData({ | ||||||
|  |                 latitude: lat, | ||||||
|  |                 longitude: lon, | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               setCreateCommonData({ | ||||||
|  |                 latitude: 0, | ||||||
|  |                 longitude: 0, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }} | ||||||
|  |           placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)" | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <FormControl fullWidth> | ||||||
|  |           <InputLabel>Город</InputLabel> | ||||||
|  |           <Select | ||||||
|  |             value={createStationData.common.city_id || ""} | ||||||
|  |             label="Город" | ||||||
|  |             onChange={(e) => { | ||||||
|  |               const selectedCity = cities["ru"].data.find( | ||||||
|  |                 (city) => city.id === e.target.value | ||||||
|  |               ); | ||||||
|  |               setCreateCommonData({ | ||||||
|  |                 city_id: e.target.value as number, | ||||||
|  |                 city: selectedCity?.name || "", | ||||||
|  |               }); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             {cities["ru"].data.map((city) => ( | ||||||
|  |               <MenuItem key={city.id} value={city.id}> | ||||||
|  |                 {city.name} | ||||||
|  |               </MenuItem> | ||||||
|  |             ))} | ||||||
|           </Select> |           </Select> | ||||||
|         </FormControl> |         </FormControl> | ||||||
|  |  | ||||||
|         <Button |         <Button | ||||||
|           variant="contained" |           variant="contained" | ||||||
|           color="primary" |  | ||||||
|           className="w-min flex gap-2 items-center" |           className="w-min flex gap-2 items-center" | ||||||
|           startIcon={<Save size={20} />} |           startIcon={<Save size={20} />} | ||||||
|           onClick={handleCreate} |           onClick={handleCreate} | ||||||
|           disabled={isLoading || !name || !systemName || !direction} |           disabled={isLoading || !createStationData[language]?.name} | ||||||
|         > |         > | ||||||
|           {isLoading ? ( |           {isLoading ? ( | ||||||
|             <Loader2 size={20} className="animate-spin" /> |             <Loader2 size={20} className="animate-spin" /> | ||||||
|   | |||||||
| @@ -29,6 +29,18 @@ export const StationEditPage = observer(() => { | |||||||
|     setLanguageEditStationData, |     setLanguageEditStationData, | ||||||
|   } = stationsStore; |   } = stationsStore; | ||||||
|   const { cities, getCities } = cityStore; |   const { cities, getCities } = cityStore; | ||||||
|  |   const [coordinates, setCoordinates] = useState<string>(""); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if ( | ||||||
|  |       editStationData.common.latitude !== 0 || | ||||||
|  |       editStationData.common.longitude !== 0 | ||||||
|  |     ) { | ||||||
|  |       setCoordinates( | ||||||
|  |         `${editStationData.common.latitude}, ${editStationData.common.longitude}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, [editStationData.common.latitude, editStationData.common.longitude]); | ||||||
|  |  | ||||||
|   const handleEdit = async () => { |   const handleEdit = async () => { | ||||||
|     try { |     try { | ||||||
| @@ -71,7 +83,7 @@ export const StationEditPage = observer(() => { | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div className="flex flex-col gap-10 w-full items-end"> |       <div className="flex flex-col gap-10 w-full items-end"> | ||||||
|         <div className="flex gap-10 items-center mb-5 max-w-[80%]"> |         <div className="flex gap-10 items-center mb-5 max-w-[80%] self-start"> | ||||||
|           <h1 className="text-3xl break-words">{editStationData.ru.name}</h1> |           <h1 className="text-3xl break-words">{editStationData.ru.name}</h1> | ||||||
|         </div> |         </div> | ||||||
|         <TextField |         <TextField | ||||||
| @@ -128,16 +140,33 @@ export const StationEditPage = observer(() => { | |||||||
|         <TextField |         <TextField | ||||||
|           fullWidth |           fullWidth | ||||||
|           label="Координаты" |           label="Координаты" | ||||||
|           value={`${editStationData.common.latitude} ${editStationData.common.longitude}`} |           value={coordinates} | ||||||
|           onChange={(e) => { |           onChange={(e) => { | ||||||
|             const [latitude, longitude] = e.target.value.split(" ").map(Number); |             const newValue = e.target.value; | ||||||
|             if (!isNaN(latitude) && !isNaN(longitude)) { |             setCoordinates(newValue); | ||||||
|  |  | ||||||
|  |             const input = newValue.replace(/,/g, " ").trim(); | ||||||
|  |             const [latStr, lonStr] = input.split(/\s+/); | ||||||
|  |  | ||||||
|  |             const lat = parseFloat(latStr); | ||||||
|  |             const lon = parseFloat(lonStr); | ||||||
|  |  | ||||||
|  |             const isValidLat = !isNaN(lat); | ||||||
|  |             const isValidLon = !isNaN(lon); | ||||||
|  |  | ||||||
|  |             if (isValidLat && isValidLon) { | ||||||
|               setEditCommonData({ |               setEditCommonData({ | ||||||
|                 latitude: latitude, |                 latitude: lat, | ||||||
|                 longitude: longitude, |                 longitude: lon, | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               setEditCommonData({ | ||||||
|  |                 latitude: 0, | ||||||
|  |                 longitude: 0, | ||||||
|               }); |               }); | ||||||
|             } |             } | ||||||
|           }} |           }} | ||||||
|  |           placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)" | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|         <FormControl fullWidth> |         <FormControl fullWidth> | ||||||
| @@ -146,7 +175,7 @@ export const StationEditPage = observer(() => { | |||||||
|             value={editStationData.common.city_id || ""} |             value={editStationData.common.city_id || ""} | ||||||
|             label="Город" |             label="Город" | ||||||
|             onChange={(e) => { |             onChange={(e) => { | ||||||
|               const selectedCity = cities[language].data.find( |               const selectedCity = cities["ru"].data.find( | ||||||
|                 (city) => city.id === e.target.value |                 (city) => city.id === e.target.value | ||||||
|               ); |               ); | ||||||
|               setEditCommonData({ |               setEditCommonData({ | ||||||
| @@ -155,7 +184,7 @@ export const StationEditPage = observer(() => { | |||||||
|               }); |               }); | ||||||
|             }} |             }} | ||||||
|           > |           > | ||||||
|             {cities[language].data.map((city) => ( |             {cities["ru"].data.map((city) => ( | ||||||
|               <MenuItem key={city.id} value={city.id}> |               <MenuItem key={city.id} value={city.id}> | ||||||
|                 {city.name} |                 {city.name} | ||||||
|               </MenuItem> |               </MenuItem> | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ export const StationPreviewPage = observer(() => { | |||||||
|   }, [id, language]); |   }, [id, language]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Paper className="w-full h-full p-3 flex flex-col gap-10"> |     <Paper className="w-full  p-3  py-5 flex flex-col gap-10"> | ||||||
|       <LanguageSwitcher /> |       <LanguageSwitcher /> | ||||||
|       <div className="flex justify-between items-center"> |       <div className="flex justify-between items-center"> | ||||||
|         <button |         <button | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import { | |||||||
|   GitBranch, |   GitBranch, | ||||||
|   // Car, |   // Car, | ||||||
|   Table, |   Table, | ||||||
|   Notebook, |  | ||||||
|   Split, |   Split, | ||||||
|   Newspaper, |   Newspaper, | ||||||
|   PersonStanding, |   PersonStanding, | ||||||
| @@ -36,9 +35,39 @@ export const NAVIGATION_ITEMS: { | |||||||
|   secondary: NavigationItem[]; |   secondary: NavigationItem[]; | ||||||
| } = { | } = { | ||||||
|   primary: [ |   primary: [ | ||||||
|  |     { | ||||||
|  |       id: "snapshots", | ||||||
|  |       label: "Снапшоты", | ||||||
|  |       icon: GitBranch, | ||||||
|  |       path: "/snapshot", | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       id: "map", | ||||||
|  |       label: "Карта", | ||||||
|  |       icon: Map, | ||||||
|  |       path: "/map", | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       id: "devices", | ||||||
|  |       label: "Устройства", | ||||||
|  |       icon: Cpu, | ||||||
|  |       path: "/devices", | ||||||
|  |     }, | ||||||
|  |     // { | ||||||
|  |     //   id: "vehicles", | ||||||
|  |     //   label: "Транспорт", | ||||||
|  |     //   icon: Car, | ||||||
|  |     //   path: "/vehicle", | ||||||
|  |     // }, | ||||||
|  |     { | ||||||
|  |       id: "users", | ||||||
|  |       label: "Пользователи", | ||||||
|  |       icon: Users, | ||||||
|  |       path: "/user", | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       id: "all", |       id: "all", | ||||||
|       label: "Все сущности", |       label: "Справочник", | ||||||
|       icon: Table, |       icon: Table, | ||||||
|       nestedItems: [ |       nestedItems: [ | ||||||
|         { |         { | ||||||
| @@ -71,64 +100,28 @@ export const NAVIGATION_ITEMS: { | |||||||
|           icon: Split, |           icon: Split, | ||||||
|           path: "/route", |           path: "/route", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         { |         { | ||||||
|           id: "reference", |           id: "countries", | ||||||
|           label: "Справочник", |           label: "Страны", | ||||||
|           icon: Notebook, |           icon: Earth, | ||||||
|           nestedItems: [ |           path: "/country", | ||||||
|             { |         }, | ||||||
|               id: "countries", |         { | ||||||
|               label: "Страны", |           id: "cities", | ||||||
|               icon: Earth, |           label: "Города", | ||||||
|               path: "/country", |           icon: Building2, | ||||||
|             }, |           path: "/city", | ||||||
|             { |         }, | ||||||
|               id: "cities", |         { | ||||||
|               label: "Города", |           id: "carriers", | ||||||
|               icon: Building2, |           label: "Перевозчики", | ||||||
|               path: "/city", |           // @ts-ignore | ||||||
|             }, |           icon: CarrierSvg, | ||||||
|             { |           path: "/carrier", | ||||||
|               id: "carriers", |  | ||||||
|               label: "Перевозчики", |  | ||||||
|               // @ts-ignore |  | ||||||
|               icon: CarrierSvg, |  | ||||||
|               path: "/carrier", |  | ||||||
|             }, |  | ||||||
|           ], |  | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|     { |  | ||||||
|       id: "snapshots", |  | ||||||
|       label: "Снапшоты", |  | ||||||
|       icon: GitBranch, |  | ||||||
|       path: "/snapshot", |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       id: "map", |  | ||||||
|       label: "Карта", |  | ||||||
|       icon: Map, |  | ||||||
|       path: "/map", |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       id: "devices", |  | ||||||
|       label: "Устройства", |  | ||||||
|       icon: Cpu, |  | ||||||
|       path: "/devices", |  | ||||||
|     }, |  | ||||||
|     // { |  | ||||||
|     //   id: "vehicles", |  | ||||||
|     //   label: "Транспорт", |  | ||||||
|     //   icon: Car, |  | ||||||
|     //   path: "/vehicle", |  | ||||||
|     // }, |  | ||||||
|     { |  | ||||||
|       id: "users", |  | ||||||
|       label: "Пользователи", |  | ||||||
|       icon: Users, |  | ||||||
|       path: "/user", |  | ||||||
|     }, |  | ||||||
|   ], |   ], | ||||||
|   secondary: [ |   secondary: [ | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ export const MEDIA_TYPE_LABELS = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const MEDIA_TYPE_VALUES = { | export const MEDIA_TYPE_VALUES = { | ||||||
|   photo: 1, |   image: 1, | ||||||
|   video: 2, |   video: 2, | ||||||
|   icon: 3, |   icon: 3, | ||||||
|   thumbnail: 3, |   thumbnail: 3, | ||||||
|   | |||||||
| @@ -132,7 +132,7 @@ export const PreviewMediaDialog = observer( | |||||||
|                 sx={{ width: "50%" }} |                 sx={{ width: "50%" }} | ||||||
|               /> |               /> | ||||||
|  |  | ||||||
|               <Box className="flex gap-4"> |               <Box className="flex gap-4 h-[40vh]"> | ||||||
|                 <Paper |                 <Paper | ||||||
|                   elevation={2} |                   elevation={2} | ||||||
|                   sx={{ |                   sx={{ | ||||||
| @@ -142,7 +142,6 @@ export const PreviewMediaDialog = observer( | |||||||
|                     alignItems: "center", |                     alignItems: "center", | ||||||
|                     justifyContent: "center", |                     justifyContent: "center", | ||||||
|                   }} |                   }} | ||||||
|                   className="max-h-[40vh]" |  | ||||||
|                 > |                 > | ||||||
|                   <MediaViewer |                   <MediaViewer | ||||||
|                     media={{ |                     media={{ | ||||||
| @@ -150,7 +149,6 @@ export const PreviewMediaDialog = observer( | |||||||
|                       media_type: media.media_type, |                       media_type: media.media_type, | ||||||
|                       filename: media.filename, |                       filename: media.filename, | ||||||
|                     }} |                     }} | ||||||
|                     fullHeight |  | ||||||
|                   /> |                   /> | ||||||
|                 </Paper> |                 </Paper> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -102,7 +102,6 @@ export const SelectMediaDialog = observer( | |||||||
|       filteredMedia = filteredMedia.filter( |       filteredMedia = filteredMedia.filter( | ||||||
|         (mediaItem) => mediaItem.media_type === mediaType |         (mediaItem) => mediaItem.media_type === mediaType | ||||||
|       ); |       ); | ||||||
|       console.log(filteredMedia); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
| @@ -163,7 +162,13 @@ export const SelectMediaDialog = observer( | |||||||
|                         }, |                         }, | ||||||
|                       }} |                       }} | ||||||
|                     > |                     > | ||||||
|                       <ListItemText primary={mediaItem.media_name} /> |                       <ListItemText | ||||||
|  |                         primary={ | ||||||
|  |                           mediaItem.media_name | ||||||
|  |                             ? mediaItem.media_name | ||||||
|  |                             : mediaItem.filename | ||||||
|  |                         } | ||||||
|  |                       /> | ||||||
|                     </ListItemButton> |                     </ListItemButton> | ||||||
|                   ) |                   ) | ||||||
|                 ) |                 ) | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ interface UploadMediaDialogProps { | |||||||
|     media_type: number; |     media_type: number; | ||||||
|   }) => void; |   }) => void; | ||||||
|   afterUploadSight?: (id: string) => void; |   afterUploadSight?: (id: string) => void; | ||||||
|   hardcodeType?: "thumbnail" | "watermark_lu" | "watermark_rd" | null; |   hardcodeType?: "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const UploadMediaDialog = observer( | export const UploadMediaDialog = observer( | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ class CarrierStore { | |||||||
|   getCarriers = async (language: Language) => { |   getCarriers = async (language: Language) => { | ||||||
|     if (this.carriers[language as keyof Carriers].loaded) return; |     if (this.carriers[language as keyof Carriers].loaded) return; | ||||||
|  |  | ||||||
|     const response = await authInstance.get("/carrier"); |     const response = await languageInstance(language).get("/carrier"); | ||||||
|  |  | ||||||
|     runInAction(() => { |     runInAction(() => { | ||||||
|       this.carriers[language as keyof Carriers].data = response.data; |       this.carriers[language as keyof Carriers].data = response.data; | ||||||
| @@ -108,46 +108,94 @@ class CarrierStore { | |||||||
|     return this.carrier[id]; |     return this.carrier[id]; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   createCarrier = async ( |   createCarrierData = { | ||||||
|  |     city_id: 0, | ||||||
|  |     logo: "", | ||||||
|  |     ru: { | ||||||
|  |       full_name: "", | ||||||
|  |       short_name: "", | ||||||
|  |       slogan: "", | ||||||
|  |     }, | ||||||
|  |     en: { | ||||||
|  |       full_name: "", | ||||||
|  |       short_name: "", | ||||||
|  |       slogan: "", | ||||||
|  |     }, | ||||||
|  |     zh: { | ||||||
|  |       full_name: "", | ||||||
|  |       short_name: "", | ||||||
|  |       slogan: "", | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   setCreateCarrierData = ( | ||||||
|     fullName: string, |     fullName: string, | ||||||
|     shortName: string, |     shortName: string, | ||||||
|     cityId: number, |     cityId: number, | ||||||
|     slogan: string, |     slogan: string, | ||||||
|     logoId: string |     logoId: string, | ||||||
|  |     language: Language | ||||||
|   ) => { |   ) => { | ||||||
|     const { language } = languageStore; |     this.createCarrierData.city_id = cityId; | ||||||
|     const cityName = |     this.createCarrierData.logo = logoId; | ||||||
|       cityStore.cities[language].data.find((city) => city.id === cityId) |     this.createCarrierData[language] = { | ||||||
|         ?.name || ""; |  | ||||||
|  |  | ||||||
|     const response = await languageInstance(language).post("/carrier", { |  | ||||||
|       full_name: fullName, |       full_name: fullName, | ||||||
|       short_name: shortName, |       short_name: shortName, | ||||||
|  |       slogan: slogan, | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   createCarrier = async () => { | ||||||
|  |     const { language } = languageStore; | ||||||
|  |     const cityName = | ||||||
|  |       cityStore.cities[language].data.find( | ||||||
|  |         (city) => city.id === this.createCarrierData.city_id | ||||||
|  |       )?.name || ""; | ||||||
|  |  | ||||||
|  |     const payload = { | ||||||
|  |       full_name: this.createCarrierData[language].full_name, | ||||||
|  |       short_name: this.createCarrierData[language].short_name, | ||||||
|       city: cityName, |       city: cityName, | ||||||
|       city_id: cityId, |       city_id: this.createCarrierData.city_id, | ||||||
|       slogan, |       slogan: this.createCarrierData[language].slogan, | ||||||
|       logo: logoId, |       ...(this.createCarrierData.logo | ||||||
|     }); |         ? { logo: this.createCarrierData.logo } | ||||||
|  |         : {}), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const response = await languageInstance(language).post("/carrier", payload); | ||||||
|  |  | ||||||
|     const carrierId = response.data.id; |     const carrierId = response.data.id; | ||||||
|  |  | ||||||
|  |     runInAction(() => { | ||||||
|  |       this.carriers[language].data.push(response.data); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     // Create translations for other languages |     // Create translations for other languages | ||||||
|     for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) { |     for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) { | ||||||
|       await languageInstance(lang as Language).patch(`/carrier/${carrierId}`, { |       const patchPayload = { | ||||||
|         full_name: fullName, |         // @ts-ignore | ||||||
|         short_name: shortName, |         full_name: this.createCarrierData[lang as any].full_name as string, | ||||||
|  |         // @ts-ignore | ||||||
|  |         short_name: this.createCarrierData[lang as any].short_name as string, | ||||||
|         city: cityName, |         city: cityName, | ||||||
|         city_id: cityId, |         city_id: this.createCarrierData.city_id, | ||||||
|         slogan, |         // @ts-ignore | ||||||
|         logo: logoId, |         slogan: this.createCarrierData[lang as any].slogan as string, | ||||||
|  |         ...(this.createCarrierData.logo | ||||||
|  |           ? { logo: this.createCarrierData.logo } | ||||||
|  |           : {}), | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       await languageInstance(lang as Language).patch( | ||||||
|  |         `/carrier/${carrierId}`, | ||||||
|  |         patchPayload | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       runInAction(() => { | ||||||
|  |         this.carriers[lang as keyof Carriers].data.push(response.data); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     runInAction(() => { |  | ||||||
|       for (const language of ["ru", "en", "zh"] as const) { |  | ||||||
|         this.carriers[language].data.push(response.data); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   editCarrierData = { |   editCarrierData = { | ||||||
| @@ -206,31 +254,29 @@ class CarrierStore { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   editCarrier = async (id: number) => { |   editCarrier = async (id: number) => { | ||||||
|  |     const { language } = languageStore; | ||||||
|     const cityName = |     const cityName = | ||||||
|       cityStore.cities[languageStore.language].data.find( |       cityStore.cities[language].data.find( | ||||||
|         (city) => city.id === this.editCarrierData.city_id |         (city) => city.id === this.editCarrierData.city_id | ||||||
|       )?.name || ""; |       )?.name || ""; | ||||||
|  |  | ||||||
|     for (const language of ["ru", "en", "zh"] as const) { |     for (const lang of ["ru", "en", "zh"] as const) { | ||||||
|       const response = await languageInstance(language).patch( |       const response = await languageInstance(lang).patch(`/carrier/${id}`, { | ||||||
|         `/carrier/${id}`, |         ...this.editCarrierData[lang], | ||||||
|         { |         city: cityName, | ||||||
|           ...this.editCarrierData[language], |         city_id: this.editCarrierData.city_id, | ||||||
|           city: cityName, |         logo: this.editCarrierData.logo, | ||||||
|           logo: this.editCarrierData.logo, |       }); | ||||||
|         } |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       runInAction(() => { |       runInAction(() => { | ||||||
|         if (this.carrier[id]) { |         if (this.carrier[id]) { | ||||||
|           this.carrier[id][language] = response.data; |           this.carrier[id][lang] = response.data; | ||||||
|         } |  | ||||||
|         for (const language of ["ru", "en", "zh"] as const) { |  | ||||||
|           this.carriers[language].data = this.carriers[language].data.map( |  | ||||||
|             (carrier: Carrier) => |  | ||||||
|               carrier.id === id ? { ...carrier, ...response.data } : carrier |  | ||||||
|           ); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         this.carriers[lang].data = this.carriers[lang].data.map( | ||||||
|  |           (carrier: Carrier) => | ||||||
|  |             carrier.id === id ? { ...carrier, ...response.data } : carrier | ||||||
|  |         ); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -85,7 +85,7 @@ class CityStore { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const response = await authInstance.get(`/city`); |     const response = await languageInstance(language).get(`/city`); | ||||||
|  |  | ||||||
|     runInAction(() => { |     runInAction(() => { | ||||||
|       this.cities[language].data = response.data; |       this.cities[language].data = response.data; | ||||||
| @@ -98,7 +98,7 @@ class CityStore { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const response = await authInstance.get(`/city/${code}`); |     const response = await languageInstance(language).get(`/city/${code}`); | ||||||
|  |  | ||||||
|     runInAction(() => { |     runInAction(() => { | ||||||
|       if (!this.city[code]) { |       if (!this.city[code]) { | ||||||
| @@ -170,15 +170,20 @@ class CityStore { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       // Create city in primary language |       // Create city in primary language | ||||||
|       const cityResponse = await languageInstance(language).post("/city", { |       const cityPayload = { | ||||||
|         name, |         name, | ||||||
|         country: |         country: | ||||||
|           countryStore.countries[language as keyof CashedCountries]?.data.find( |           countryStore.countries[language as keyof CashedCountries]?.data.find( | ||||||
|             (c) => c.code === country_code |             (c) => c.code === country_code | ||||||
|           )?.name || "", |           )?.name || "", | ||||||
|         country_code, |         country_code, | ||||||
|         arms: arms || "", |         ...(arms ? { arms } : {}), | ||||||
|       }); |       }; | ||||||
|  |  | ||||||
|  |       const cityResponse = await languageInstance(language).post( | ||||||
|  |         "/city", | ||||||
|  |         cityPayload | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       const cityId = cityResponse.data.id; |       const cityId = cityResponse.data.id; | ||||||
|  |  | ||||||
| @@ -194,14 +199,16 @@ class CityStore { | |||||||
|             (c) => c.code === country_code |             (c) => c.code === country_code | ||||||
|           )?.name || ""; |           )?.name || ""; | ||||||
|  |  | ||||||
|  |         const patchPayload = { | ||||||
|  |           name: secondaryName || "", | ||||||
|  |           country: countryName, | ||||||
|  |           country_code: country_code || "", | ||||||
|  |           ...(arms ? { arms } : {}), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         const patchResponse = await languageInstance(secondaryLanguage).patch( |         const patchResponse = await languageInstance(secondaryLanguage).patch( | ||||||
|           `/city/${cityId}`, |           `/city/${cityId}`, | ||||||
|           { |           patchPayload | ||||||
|             name: secondaryName || "", |  | ||||||
|             country: countryName, |  | ||||||
|             country_code: country_code || "", |  | ||||||
|             arms: arms || "", |  | ||||||
|           } |  | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         runInAction(() => { |         runInAction(() => { | ||||||
|   | |||||||
| @@ -91,14 +91,16 @@ class CountryStore { | |||||||
|     return response.data; |     return response.data; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   deleteCountry = async (code: string, language: keyof CashedCountries) => { |   deleteCountry = async (code: string) => { | ||||||
|     await authInstance.delete(`/country/${code}`); |     await authInstance.delete(`/country/${code}`); | ||||||
|  |  | ||||||
|     runInAction(() => { |     runInAction(() => { | ||||||
|       this.countries[language].data = this.countries[language].data.filter( |       for (const lang of ["ru", "en", "zh"]) { | ||||||
|         (country) => country.code !== code |         this.countries[lang as keyof CashedCountries].data = this.countries[ | ||||||
|       ); |           lang as keyof CashedCountries | ||||||
|       this.countries[language].loaded = true; |         ].data.filter((country) => country.code !== code); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       this.country[code] = { |       this.country[code] = { | ||||||
|         ru: null, |         ru: null, | ||||||
|         en: null, |         en: null, | ||||||
|   | |||||||
| @@ -64,6 +64,10 @@ type Station = { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | type CreateStationData = { | ||||||
|  |   [key in Language]: StationLanguageData; | ||||||
|  | } & { common: StationCommonData }; | ||||||
|  |  | ||||||
| class StationsStore { | class StationsStore { | ||||||
|   stations: Station[] = []; |   stations: Station[] = []; | ||||||
|   station: Station | null = null; |   station: Station | null = null; | ||||||
| @@ -139,6 +143,51 @@ class StationsStore { | |||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   createStationData: CreateStationData = { | ||||||
|  |     ru: { | ||||||
|  |       name: "", | ||||||
|  |       system_name: "", | ||||||
|  |       description: "", | ||||||
|  |       address: "", | ||||||
|  |       loaded: false, | ||||||
|  |     }, | ||||||
|  |     en: { | ||||||
|  |       name: "", | ||||||
|  |       system_name: "", | ||||||
|  |       description: "", | ||||||
|  |       address: "", | ||||||
|  |       loaded: false, | ||||||
|  |     }, | ||||||
|  |     zh: { | ||||||
|  |       name: "", | ||||||
|  |       system_name: "", | ||||||
|  |       description: "", | ||||||
|  |       address: "", | ||||||
|  |       loaded: false, | ||||||
|  |     }, | ||||||
|  |     common: { | ||||||
|  |       city: "", | ||||||
|  |       city_id: 0, | ||||||
|  |       direction: false, | ||||||
|  |       icon: "", | ||||||
|  |       latitude: 0, | ||||||
|  |       longitude: 0, | ||||||
|  |       offset_x: 0, | ||||||
|  |       offset_y: 0, | ||||||
|  |       transfers: { | ||||||
|  |         bus: "", | ||||||
|  |         metro_blue: "", | ||||||
|  |         metro_green: "", | ||||||
|  |         metro_orange: "", | ||||||
|  |         metro_purple: "", | ||||||
|  |         metro_red: "", | ||||||
|  |         train: "", | ||||||
|  |         tram: "", | ||||||
|  |         trolleybus: "", | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     makeAutoObservable(this); |     makeAutoObservable(this); | ||||||
|   } |   } | ||||||
| @@ -172,9 +221,6 @@ class StationsStore { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   getEditStation = async (id: number) => { |   getEditStation = async (id: number) => { | ||||||
|     if (this.editStationData.ru.loaded) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     const ruResponse = await languageInstance("ru").get(`/station/${id}`); |     const ruResponse = await languageInstance("ru").get(`/station/${id}`); | ||||||
|     const enResponse = await languageInstance("en").get(`/station/${id}`); |     const enResponse = await languageInstance("en").get(`/station/${id}`); | ||||||
|     const zhResponse = await languageInstance("zh").get(`/station/${id}`); |     const zhResponse = await languageInstance("zh").get(`/station/${id}`); | ||||||
| @@ -336,35 +382,125 @@ class StationsStore { | |||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   createStation = async ( |   setCreateCommonData = (data: Partial<StationCommonData>) => { | ||||||
|     name: string, |     this.createStationData.common = { | ||||||
|     systemName: string, |       ...this.createStationData.common, | ||||||
|     direction: string |       ...data, | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   setLanguageCreateStationData = ( | ||||||
|  |     language: Language, | ||||||
|  |     data: Partial<StationLanguageData> | ||||||
|   ) => { |   ) => { | ||||||
|     const response = await authInstance.post("/station", { |     this.createStationData[language] = { | ||||||
|       station_name: name, |       ...this.createStationData[language], | ||||||
|       system_name: systemName, |       ...data, | ||||||
|       direction, |     }; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   createStation = async () => { | ||||||
|  |     const { language } = languageStore; | ||||||
|  |     let commonDataPayload: Partial<StationCommonData> = { | ||||||
|  |       city_id: this.createStationData.common.city_id, | ||||||
|  |       direction: this.createStationData.common.direction, | ||||||
|  |       icon: this.createStationData.common.icon, | ||||||
|  |       latitude: this.createStationData.common.latitude, | ||||||
|  |       longitude: this.createStationData.common.longitude, | ||||||
|  |       offset_x: this.createStationData.common.offset_x, | ||||||
|  |       offset_y: this.createStationData.common.offset_y, | ||||||
|  |       transfers: this.createStationData.common.transfers, | ||||||
|  |       city: this.createStationData.common.city, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (this.createStationData.common.icon === "") { | ||||||
|  |       delete commonDataPayload.icon; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // First create station in Russian | ||||||
|  |     const { name, description, address } = this.createStationData[language]; | ||||||
|  |     const response = await languageInstance(language).post("/station", { | ||||||
|  |       name: name || "", | ||||||
|  |       system_name: name || "", // system_name is often derived from name | ||||||
|  |       description: description || "", | ||||||
|  |       address: address || "", | ||||||
|  |       ...commonDataPayload, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     runInAction(() => { |     runInAction(() => { | ||||||
|       this.stations.push(response.data); |       this.stationLists[language].data.push(response.data); | ||||||
|       const newStation = response.data as Station; |     }); | ||||||
|       if (!this.stationPreview[newStation.id]) { |  | ||||||
|         this.stationPreview[newStation.id] = { |     const stationId = response.data.id; | ||||||
|           ru: { loaded: false, data: newStation }, |  | ||||||
|           en: { loaded: false, data: newStation }, |     // Then update for other languages | ||||||
|           zh: { loaded: false, data: newStation }, |     for (const lang of ["ru", "en", "zh"].filter( | ||||||
|         }; |       (lang) => lang !== language | ||||||
|       } |     ) as Language[]) { | ||||||
|       this.stationPreview[newStation.id]["ru"] = { |       const { name, description, address } = this.createStationData[lang]; | ||||||
|         loaded: true, |       const response = await languageInstance(lang).patch( | ||||||
|         data: newStation, |         `/station/${stationId}`, | ||||||
|       }; |         { | ||||||
|       this.stationPreview[newStation.id]["en"] = { |           name: name || "", | ||||||
|         loaded: true, |           system_name: name || "", // system_name is often derived from name | ||||||
|         data: newStation, |           description: description || "", | ||||||
|  |           address: address || "", | ||||||
|  |           ...commonDataPayload, | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       runInAction(() => { | ||||||
|  |         this.stationLists[lang].data.push(response.data); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     runInAction(() => { | ||||||
|  |       this.createStationData = { | ||||||
|  |         ru: { | ||||||
|  |           name: "", | ||||||
|  |           system_name: "", | ||||||
|  |           description: "", | ||||||
|  |           address: "", | ||||||
|  |           loaded: false, | ||||||
|  |         }, | ||||||
|  |         en: { | ||||||
|  |           name: "", | ||||||
|  |           system_name: "", | ||||||
|  |           description: "", | ||||||
|  |           address: "", | ||||||
|  |           loaded: false, | ||||||
|  |         }, | ||||||
|  |         zh: { | ||||||
|  |           name: "", | ||||||
|  |           system_name: "", | ||||||
|  |           description: "", | ||||||
|  |           address: "", | ||||||
|  |           loaded: false, | ||||||
|  |         }, | ||||||
|  |         common: { | ||||||
|  |           city: "", | ||||||
|  |           city_id: 0, | ||||||
|  |           direction: false, | ||||||
|  |           icon: "", | ||||||
|  |           latitude: 0, | ||||||
|  |           longitude: 0, | ||||||
|  |           offset_x: 0, | ||||||
|  |           offset_y: 0, | ||||||
|  |           transfers: { | ||||||
|  |             bus: "", | ||||||
|  |             metro_blue: "", | ||||||
|  |             metro_green: "", | ||||||
|  |             metro_orange: "", | ||||||
|  |             metro_purple: "", | ||||||
|  |             metro_red: "", | ||||||
|  |             train: "", | ||||||
|  |             tram: "", | ||||||
|  |             trolleybus: "", | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
|  |     return response.data; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // Reset editStationData when navigating away or after saving |   // Reset editStationData when navigating away or after saving | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import { editSightStore } from "@shared"; | |||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
| interface ImageUploadCardProps { | interface ImageUploadCardProps { | ||||||
|   title: string; |   title: string; | ||||||
|   imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd"; |   imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd" | "image"; | ||||||
|   imageUrl: string | null | undefined; |   imageUrl: string | null | undefined; | ||||||
|   onImageClick: () => void; |   onImageClick: () => void; | ||||||
|   onDeleteImageClick: () => void; |   onDeleteImageClick: () => void; | ||||||
|   | |||||||
| @@ -55,10 +55,20 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => { | |||||||
|             )} |             )} | ||||||
|           </IconButton> |           </IconButton> | ||||||
|         </DrawerHeader> |         </DrawerHeader> | ||||||
|         <NavigationList open={open} /> |         <NavigationList open={open} onDrawerOpen={handleDrawerOpen} /> | ||||||
|       </Drawer> |       </Drawer> | ||||||
|       <Box component="main" sx={{ flexGrow: 1, p: 3 }}> |       <Box | ||||||
|  |         component="main" | ||||||
|  |         sx={{ | ||||||
|  |           width: "100%", | ||||||
|  |           flexGrow: 1, | ||||||
|  |           p: 3, | ||||||
|  |           overflow: "auto", | ||||||
|  |           maxWidth: "100vw", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|         <DrawerHeader /> |         <DrawerHeader /> | ||||||
|  |  | ||||||
|         {children} |         {children} | ||||||
|       </Box> |       </Box> | ||||||
|     </Box> |     </Box> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user