feat: Add checkbox for sightbar entity + fix build errors
				
					
				
			This commit is contained in:
		| @@ -6,7 +6,6 @@ import { | ||||
|   MenuItem, | ||||
|   FormControl, | ||||
|   InputLabel, | ||||
|   Box, | ||||
| } from "@mui/material"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { ArrowLeft, Save } from "lucide-react"; | ||||
| @@ -15,8 +14,7 @@ import { useNavigate } from "react-router-dom"; | ||||
| import { toast } from "react-toastify"; | ||||
| import { carrierStore, cityStore, mediaStore } from "@shared"; | ||||
| import { useState, useEffect } from "react"; | ||||
| import { LanguageSwitcher, MediaViewer } from "@widgets"; | ||||
| import { HexColorPicker } from "react-colorful"; | ||||
| import { MediaViewer } from "@widgets"; | ||||
|  | ||||
| export const CarrierCreatePage = observer(() => { | ||||
|   const navigate = useNavigate(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { Paper } from "@mui/material"; | ||||
| import { carrierStore, languageStore, mediaStore } from "@shared"; | ||||
| import { carrierStore, mediaStore } from "@shared"; | ||||
| import { MediaViewer } from "@widgets"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { ArrowLeft } from "lucide-react"; | ||||
|   | ||||
| @@ -32,7 +32,7 @@ export const CityEditPage = observer(() => { | ||||
|   const { id } = useParams(); | ||||
|   const { editCityData, editCity, getCity, setEditCityData } = cityStore; | ||||
|   const { getCountries } = countryStore; | ||||
|   const { getMedia, getOneMedia, oneMedia } = mediaStore; | ||||
|   const { getMedia, getOneMedia } = mediaStore; | ||||
|  | ||||
|   const handleEdit = async () => { | ||||
|     try { | ||||
|   | ||||
| @@ -40,7 +40,7 @@ export const CreateSightPage = observer(() => { | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|       await getCities(); | ||||
|       await getCities("ru"); | ||||
|       await getArticles(languageStore.language); | ||||
|     }; | ||||
|     fetchData(); | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import VectorLayer from "ol/layer/Vector"; | ||||
| import VectorSource, { VectorSourceEvent } from "ol/source/Vector"; | ||||
| import { Draw, Modify, Select } from "ol/interaction"; | ||||
| import { DrawEvent } from "ol/interaction/Draw"; | ||||
| import { ModifyEvent } from "ol/interaction/Modify"; | ||||
| import { SelectEvent } from "ol/interaction/Select"; | ||||
| import { | ||||
|   Style, | ||||
| @@ -34,7 +33,6 @@ import { | ||||
|   Landmark, | ||||
|   Pencil, | ||||
|   Save, | ||||
|   Plus, | ||||
|   Loader2, | ||||
| } from "lucide-react"; | ||||
| import { toast } from "react-toastify"; | ||||
| @@ -382,7 +380,8 @@ class MapService { | ||||
|  | ||||
|     this.modifyInteraction = new Modify({ | ||||
|       source: this.vectorSource, | ||||
|       style: (feature) => { | ||||
|       // @ts-ignore | ||||
|       style: (feature: FeatureLike) => { | ||||
|         const originalFeature = feature.get("features")[0]; | ||||
|         if ( | ||||
|           originalFeature && | ||||
| @@ -475,7 +474,7 @@ class MapService { | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     this.modifyInteraction.on("modifyend", (event: ModifyEvent) => { | ||||
|     this.modifyInteraction.on("modifyend", () => { | ||||
|       if (this.beforeModifyState) { | ||||
|         this.addStateToHistory("modify-before", this.beforeModifyState); | ||||
|         this.beforeModifyState = null; | ||||
| @@ -976,7 +975,54 @@ class MapService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // --- ИСПРАВЛЕННЫЙ МЕТОД --- | ||||
|   // --- НОВОЕ --- | ||||
|   // Метод для множественного удаления объектов по их ID | ||||
|   public deleteMultipleFeatures(featureIds: (string | number)[]): void { | ||||
|     if (!featureIds || featureIds.length === 0) return; | ||||
|  | ||||
|     // Вывод в консоль по требованию | ||||
|     console.log("Запрос на множественное удаление. ID объектов:", featureIds); | ||||
|  | ||||
|     const currentState = this.getCurrentStateAsGeoJSON(); | ||||
|     if (currentState) { | ||||
|       this.addStateToHistory("multiple delete", currentState); | ||||
|     } | ||||
|  | ||||
|     const selectedFeaturesCollection = this.selectInteraction?.getFeatures(); | ||||
|     let deletedCount = 0; | ||||
|  | ||||
|     featureIds.forEach((id) => { | ||||
|       const feature = this.vectorSource.getFeatureById(id); | ||||
|       if (feature) { | ||||
|         // Удаление из "бэкенда"/стора для каждого объекта | ||||
|         const recourse = String(id).split("-")[0]; | ||||
|         const numericId = String(id).split("-")[1]; | ||||
|         if (recourse && numericId) { | ||||
|           mapStore.deleteRecourse(recourse, Number(numericId)); | ||||
|         } | ||||
|  | ||||
|         // Если удаляемый объект выбран для редактирования, убираем его из выделения | ||||
|         if (selectedFeaturesCollection?.getArray().includes(feature)) { | ||||
|           selectedFeaturesCollection.remove(feature); | ||||
|         } | ||||
|  | ||||
|         // Удаляем объект с карты | ||||
|         this.vectorSource.removeFeature(feature); | ||||
|         deletedCount++; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     if (deletedCount > 0) { | ||||
|       // Если основное выделение стало пустым, оповещаем React | ||||
|       if (selectedFeaturesCollection?.getLength() === 0) { | ||||
|         this.onFeatureSelect(null); | ||||
|       } | ||||
|       toast.success(`Удалено ${deletedCount} объект(ов).`); | ||||
|     } else { | ||||
|       toast.warn("Не найдено объектов для удаления."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public getAllFeaturesAsGeoJSON(): string | null { | ||||
|     if (!this.vectorSource || !this.map) return null; | ||||
|     const feats = this.vectorSource.getFeatures(); | ||||
| @@ -984,11 +1030,9 @@ class MapService { | ||||
|  | ||||
|     const geoJSONFmt = new GeoJSON(); | ||||
|  | ||||
|     // Просто передаем опции трансформации в метод writeFeatures. | ||||
|     // Он сам всё сделает правильно, не трогая оригинальные объекты. | ||||
|     return geoJSONFmt.writeFeatures(feats, { | ||||
|       dataProjection: "EPSG:4326", // В какую проекцию конвертировать (стандарт для GeoJSON) | ||||
|       featureProjection: this.map.getView().getProjection(), // В какой проекции находятся объекты на карте | ||||
|       dataProjection: "EPSG:4326", | ||||
|       featureProjection: this.map.getView().getProjection(), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -1111,6 +1155,11 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|   const [activeSection, setActiveSection] = useState<string | null>("layers"); | ||||
|   const navigate = useNavigate(); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   // --- НОВОЕ --- | ||||
|   // Состояние для хранения ID объектов, выбранных для удаления | ||||
|   const [selectedForDeletion, setSelectedForDeletion] = useState< | ||||
|     Set<string | number> | ||||
|   >(new Set()); | ||||
|  | ||||
|   const toggleSection = (id: string) => | ||||
|     setActiveSection(activeSection === id ? null : id); | ||||
| @@ -1135,6 +1184,40 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|     [mapService] | ||||
|   ); | ||||
|  | ||||
|   // --- НОВОЕ --- | ||||
|   // Обработчик изменения состояния чекбокса | ||||
|   const handleCheckboxChange = useCallback( | ||||
|     (id: string | number | undefined) => { | ||||
|       if (id === undefined) return; | ||||
|       setSelectedForDeletion((prev) => { | ||||
|         const newSet = new Set(prev); | ||||
|         if (newSet.has(id)) { | ||||
|           newSet.delete(id); | ||||
|         } else { | ||||
|           newSet.add(id); | ||||
|         } | ||||
|         return newSet; | ||||
|       }); | ||||
|     }, | ||||
|     [] | ||||
|   ); | ||||
|  | ||||
|   // --- НОВОЕ --- | ||||
|   // Обработчик для запуска множественного удаления | ||||
|   const handleBulkDelete = useCallback(() => { | ||||
|     if (!mapService || selectedForDeletion.size === 0) return; | ||||
|  | ||||
|     if ( | ||||
|       window.confirm( | ||||
|         `Вы уверены, что хотите удалить ${selectedForDeletion.size} объект(ов)? Это действие нельзя отменить.` | ||||
|       ) | ||||
|     ) { | ||||
|       const idsToDelete = Array.from(selectedForDeletion); | ||||
|       mapService.deleteMultipleFeatures(idsToDelete); | ||||
|       setSelectedForDeletion(new Set()); // Очищаем выбор после удаления | ||||
|     } | ||||
|   }, [mapService, selectedForDeletion]); | ||||
|  | ||||
|   const handleEditFeature = useCallback( | ||||
|     (featureType: string | undefined, fullId: string | number | undefined) => { | ||||
|       if (!featureType || !fullId) return; | ||||
| @@ -1191,18 +1274,35 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|               const sId = s.getId(); | ||||
|               const sName = (s.get("name") as string) || "Без названия"; | ||||
|               const isSelected = selectedFeature?.getId() === sId; | ||||
|               // --- ИЗМЕНЕНИЕ --- | ||||
|               const isCheckedForDeletion = | ||||
|                 sId !== undefined && selectedForDeletion.has(sId); | ||||
|  | ||||
|               return ( | ||||
|                 <div | ||||
|                   key={String(sId)} | ||||
|                   className={`flex items-start justify-between p-2 rounded-md cursor-pointer group transition-colors duration-150 ${ | ||||
|                   className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${ | ||||
|                     isSelected | ||||
|                       ? "bg-orange-100 border border-orange-300 hover:bg-orange-200" | ||||
|                       ? "bg-orange-100 border border-orange-300" | ||||
|                       : "hover:bg-blue-50" | ||||
|                   }`} | ||||
|                   onClick={() => handleFeatureClick(sId)} | ||||
|                 > | ||||
|                   <div className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0"> | ||||
|                   {/* --- НОВОЕ: Чекбокс для множественного выбора --- */} | ||||
|                   <div className="flex-shrink-0 pr-2 pt-1"> | ||||
|                     <input | ||||
|                       type="checkbox" | ||||
|                       className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" | ||||
|                       checked={!!isCheckedForDeletion} | ||||
|                       onChange={() => handleCheckboxChange(sId)} | ||||
|                       onClick={(e) => e.stopPropagation()} | ||||
|                       aria-label={`Выбрать ${sName} для удаления`} | ||||
|                     /> | ||||
|                   </div> | ||||
|  | ||||
|                   <div | ||||
|                     className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer" | ||||
|                     onClick={() => handleFeatureClick(sId)} | ||||
|                   > | ||||
|                     <div className="flex items-center"> | ||||
|                       <MapPin | ||||
|                         size={16} | ||||
| @@ -1266,6 +1366,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|               const lId = l.getId(); | ||||
|               const lName = (l.get("name") as string) || "Без названия"; | ||||
|               const isSelected = selectedFeature?.getId() === lId; | ||||
|               const isCheckedForDeletion = | ||||
|                 lId !== undefined && selectedForDeletion.has(lId); | ||||
|               const lGeom = l.getGeometry(); | ||||
|               let lineLengthText: string | null = null; | ||||
|               if (lGeom instanceof LineString) { | ||||
| @@ -1276,14 +1378,26 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|               return ( | ||||
|                 <div | ||||
|                   key={String(lId)} | ||||
|                   className={`flex items-start justify-between p-2 rounded-md cursor-pointer group transition-colors duration-150 ${ | ||||
|                   className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${ | ||||
|                     isSelected | ||||
|                       ? "bg-orange-100 border border-orange-300 hover:bg-orange-200" | ||||
|                       ? "bg-orange-100 border border-orange-300" | ||||
|                       : "hover:bg-blue-50" | ||||
|                   }`} | ||||
|                   onClick={() => handleFeatureClick(lId)} | ||||
|                 > | ||||
|                   <div className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0"> | ||||
|                   <div className="flex-shrink-0 pr-2 pt-1"> | ||||
|                     <input | ||||
|                       type="checkbox" | ||||
|                       className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" | ||||
|                       checked={!!isCheckedForDeletion} | ||||
|                       onChange={() => handleCheckboxChange(lId)} | ||||
|                       onClick={(e) => e.stopPropagation()} | ||||
|                       aria-label={`Выбрать ${lName} для удаления`} | ||||
|                     /> | ||||
|                   </div> | ||||
|                   <div | ||||
|                     className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer" | ||||
|                     onClick={() => handleFeatureClick(lId)} | ||||
|                   > | ||||
|                     <div className="flex items-center mb-0.5"> | ||||
|                       <ArrowRightLeft | ||||
|                         size={16} | ||||
| @@ -1352,17 +1466,31 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|               const sId = s.getId(); | ||||
|               const sName = (s.get("name") as string) || "Без названия"; | ||||
|               const isSelected = selectedFeature?.getId() === sId; | ||||
|               const isCheckedForDeletion = | ||||
|                 sId !== undefined && selectedForDeletion.has(sId); | ||||
|               return ( | ||||
|                 <div | ||||
|                   key={String(sId)} | ||||
|                   className={`flex items-center justify-between p-2 rounded-md cursor-pointer group transition-colors duration-150 ${ | ||||
|                   className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${ | ||||
|                     isSelected | ||||
|                       ? "bg-orange-100 border border-orange-300 hover:bg-orange-200" | ||||
|                       ? "bg-orange-100 border border-orange-300" | ||||
|                       : "hover:bg-blue-50" | ||||
|                   }`} | ||||
|                   onClick={() => handleFeatureClick(sId)} | ||||
|                 > | ||||
|                   <div className="flex items-center text-gray-800 text-sm flex-grow mr-2 min-w-0"> | ||||
|                   <div className="flex-shrink-0 pr-2 pt-1"> | ||||
|                     <input | ||||
|                       type="checkbox" | ||||
|                       className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" | ||||
|                       checked={!!isCheckedForDeletion} | ||||
|                       onChange={() => handleCheckboxChange(sId)} | ||||
|                       onClick={(e) => e.stopPropagation()} | ||||
|                       aria-label={`Выбрать ${sName} для удаления`} | ||||
|                     /> | ||||
|                   </div> | ||||
|                   <div | ||||
|                     className="flex items-center text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer" | ||||
|                     onClick={() => handleFeatureClick(sId)} | ||||
|                   > | ||||
|                     <Landmark | ||||
|                       size={16} | ||||
|                       className={`mr-1.5 flex-shrink-0 ${ | ||||
| @@ -1427,12 +1555,13 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     // --- ИЗМЕНЕНИЕ: Реструктуризация для футера с кнопками --- | ||||
|     <div className="w-72 relative md:w-80 bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]"> | ||||
|       <div className="p-4 bg-gray-700 text-white"> | ||||
|         <h2 className="text-lg font-semibold">Панель управления</h2> | ||||
|       </div> | ||||
|       <div className="flex-1 flex flex-col"> | ||||
|         <div className="flex-1 overflow-y-auto max-h-[70%]"> | ||||
|       <div className="flex-1 flex flex-col min-h-0"> | ||||
|         <div className="flex-1 overflow-y-auto"> | ||||
|           {sections.map((s) => ( | ||||
|             <div | ||||
|               key={s.id} | ||||
| @@ -1480,18 +1609,30 @@ const MapSightbar: React.FC<MapSightbarProps> = ({ | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <button | ||||
|         onClick={handleSave} | ||||
|         className="m-3 w-[90%] h-[40px] flex items-center justify-center px-4 py-2 bg-blue-500 disabled:bg-blue-300 text-white rounded-md hover:bg-blue-600 transition-colors" | ||||
|         disabled={isLoading} | ||||
|       > | ||||
|         <Save size={16} className="mr-2" /> | ||||
|         {isLoading ? ( | ||||
|           <Loader2 size={16} className="animate-spin" /> | ||||
|         ) : ( | ||||
|           "Сохранить изменения" | ||||
|       {/* --- НОВОЕ: Футер сайдбара с кнопками действий --- */} | ||||
|       <div className="p-3 border-t border-gray-200 bg-gray-50/95 space-y-2"> | ||||
|         {selectedForDeletion.size > 0 && ( | ||||
|           <button | ||||
|             onClick={handleBulkDelete} | ||||
|             className="w-full flex items-center justify-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors" | ||||
|           > | ||||
|             <Trash2 size={16} className="mr-2" /> | ||||
|             Удалить выбранное ({selectedForDeletion.size}) | ||||
|           </button> | ||||
|         )} | ||||
|       </button> | ||||
|         <button | ||||
|           onClick={handleSave} | ||||
|           className="w-full h-[40px] flex items-center justify-center px-4 py-2 bg-blue-500 disabled:bg-blue-300 text-white rounded-md hover:bg-blue-600 transition-colors" | ||||
|           disabled={isLoading} | ||||
|         > | ||||
|           <Save size={16} className="mr-2" /> | ||||
|           {isLoading ? ( | ||||
|             <Loader2 size={16} className="animate-spin" /> | ||||
|           ) : ( | ||||
|             "Сохранить изменения" | ||||
|           )} | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1502,9 +1643,8 @@ export const MapPage: React.FC = () => { | ||||
|   const tooltipRef = useRef<HTMLDivElement | null>(null); | ||||
|   const [mapServiceInstance, setMapServiceInstance] = | ||||
|     useState<MapService | null>(null); | ||||
|   // --- ИЗМЕНЕНИЕ: Разделение состояния загрузки --- | ||||
|   const [isMapLoading, setIsMapLoading] = useState<boolean>(true); // Для рендеринга карты | ||||
|   const [isDataLoading, setIsDataLoading] = useState<boolean>(true); // Для загрузки данных с API | ||||
|   const [isMapLoading, setIsMapLoading] = useState<boolean>(true); | ||||
|   const [isDataLoading, setIsDataLoading] = useState<boolean>(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [currentMapMode, setCurrentMapMode] = useState<string>("edit"); | ||||
|   const [mapFeatures, setMapFeatures] = useState<Feature<Geometry>[]>([]); | ||||
| @@ -1525,12 +1665,10 @@ export const MapPage: React.FC = () => { | ||||
|   useEffect(() => { | ||||
|     let service: MapService | null = null; | ||||
|     if (mapRef.current && tooltipRef.current && !mapServiceInstance) { | ||||
|       // Изначально оба процесса загрузки активны | ||||
|       setIsMapLoading(true); | ||||
|       setIsDataLoading(true); | ||||
|       setError(null); | ||||
|  | ||||
|       // --- ИЗМЕНЕНИЕ: Логика загрузки данных вынесена и управляет своим состоянием --- | ||||
|       const loadInitialData = async (mapService: MapService) => { | ||||
|         console.log("Starting data load..."); | ||||
|         try { | ||||
| @@ -1548,7 +1686,6 @@ export const MapPage: React.FC = () => { | ||||
|           console.error("Failed to load initial map data:", e); | ||||
|           setError("Не удалось загрузить данные для карты."); | ||||
|         } finally { | ||||
|           // Завершаем состояние загрузки данных независимо от результата | ||||
|           setIsDataLoading(false); | ||||
|         } | ||||
|       }; | ||||
| @@ -1556,7 +1693,7 @@ export const MapPage: React.FC = () => { | ||||
|       try { | ||||
|         service = new MapService( | ||||
|           { ...mapConfig, target: mapRef.current }, | ||||
|           setIsMapLoading, // MapService теперь управляет только состоянием загрузки карты | ||||
|           setIsMapLoading, | ||||
|           setError, | ||||
|           setCurrentMapMode, | ||||
|           handleFeaturesChange, | ||||
| @@ -1565,7 +1702,6 @@ export const MapPage: React.FC = () => { | ||||
|         ); | ||||
|         setMapServiceInstance(service); | ||||
|  | ||||
|         // Запускаем загрузку данных | ||||
|         loadInitialData(service); | ||||
|       } catch (e: any) { | ||||
|         console.error("MapPage useEffect error:", e); | ||||
| @@ -1574,7 +1710,6 @@ export const MapPage: React.FC = () => { | ||||
|             e.message || "Неизвестная ошибка" | ||||
|           }. Пожалуйста, проверьте консоль.` | ||||
|         ); | ||||
|         // В случае критической ошибки инициализации, завершаем все загрузки | ||||
|         setIsMapLoading(false); | ||||
|         setIsDataLoading(false); | ||||
|       } | ||||
| @@ -1607,7 +1742,6 @@ export const MapPage: React.FC = () => { | ||||
|               pointerEvents: "none", | ||||
|             }} | ||||
|           ></div> | ||||
|           {/* --- ИЗМЕНЕНИЕ: Обновленный лоадер --- */} | ||||
|           {showLoader && ( | ||||
|             <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-500 bg-opacity-50 z-[1001]"> | ||||
|               <div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mb-3"></div> | ||||
| @@ -1629,7 +1763,6 @@ export const MapPage: React.FC = () => { | ||||
|             </div> | ||||
|           )} | ||||
|         </div> | ||||
|         {/* --- ИЗМЕНЕНИЕ: Условие для отображения контента --- */} | ||||
|         {showContent && ( | ||||
|           <MapControls | ||||
|             mapService={mapServiceInstance} | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { Eye, Trash2 } from "lucide-react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets"; | ||||
| import { CreateButton, DeleteModal } from "@widgets"; | ||||
|  | ||||
| export const MediaListPage = observer(() => { | ||||
|   const { media, getMedia, deleteMedia } = mediaStore; | ||||
|   | ||||
| @@ -80,9 +80,6 @@ export const RouteCreatePage = observer(() => { | ||||
|         path, | ||||
|       }; | ||||
|  | ||||
|       // Отправка на сервер (пример, если есть routeStore.createRoute) | ||||
|       let createdRoute: Route | null = null; | ||||
|  | ||||
|       await routeStore.createRoute(newRoute); | ||||
|       toast.success("Маршрут успешно создан"); | ||||
|       navigate(-1); | ||||
|   | ||||
| @@ -2,9 +2,9 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | ||||
| import { languageStore, routeStore } from "@shared"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { Eye, Map, Pencil, Trash2 } from "lucide-react"; | ||||
| import { Map, Pencil, Trash2 } from "lucide-react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets"; | ||||
| import { CreateButton, DeleteModal } from "@widgets"; | ||||
|  | ||||
| export const RouteListPage = observer(() => { | ||||
|   const { routes, getRoutes, deleteRoute } = routeStore; | ||||
|   | ||||
| @@ -171,20 +171,14 @@ export const MapDataProvider = observer( | ||||
|  | ||||
|     async function saveStationChanges() { | ||||
|       for (const station of stationChanges) { | ||||
|         const response = await authInstance.patch( | ||||
|           `/route/${routeId}/station`, | ||||
|           station | ||||
|         ); | ||||
|         await authInstance.patch(`/route/${routeId}/station`, station); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     async function saveSightChanges() { | ||||
|       console.log("sightChanges", sightChanges); | ||||
|       for (const sight of sightChanges) { | ||||
|         const response = await authInstance.patch( | ||||
|           `/route/${routeId}/sight`, | ||||
|           sight | ||||
|         ); | ||||
|         await authInstance.patch(`/route/${routeId}/sight`, sight); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -2,118 +2,106 @@ import { useEffect, useState } from "react"; | ||||
| import { useTransform } from "./TransformContext"; | ||||
| import { SightData } from "./types"; | ||||
| import { Assets, FederatedMouseEvent, Graphics, Texture } from "pixi.js"; | ||||
| import { COLORS } from "../../contexts/color-mode/theme"; | ||||
|  | ||||
| import { SIGHT_SIZE, UP_SCALE } from "./Constants"; | ||||
| import { coordinatesToLocal, localToCoordinates } from "./utils"; | ||||
| import { useMapData } from "./MapDataContext"; | ||||
|  | ||||
| interface SightProps { | ||||
| 	sight: SightData; | ||||
| 	id: number; | ||||
|   sight: SightData; | ||||
|   id: number; | ||||
| } | ||||
|  | ||||
| export function Sight({ | ||||
| 	sight, id | ||||
| }: Readonly<SightProps>) { | ||||
| 	const { rotation, scale } = useTransform(); | ||||
| 	const { setSightCoordinates } = useMapData(); | ||||
| export function Sight({ sight, id }: Readonly<SightProps>) { | ||||
|   const { rotation, scale } = useTransform(); | ||||
|   const { setSightCoordinates } = useMapData(); | ||||
|  | ||||
| 	const [position, setPosition] = useState(coordinatesToLocal(sight.latitude, sight.longitude)); | ||||
| 	const [isDragging, setIsDragging] = useState(false); | ||||
|     const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); | ||||
|     const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 }); | ||||
|   const [position, setPosition] = useState( | ||||
|     coordinatesToLocal(sight.latitude, sight.longitude) | ||||
|   ); | ||||
|   const [isDragging, setIsDragging] = useState(false); | ||||
|   const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); | ||||
|   const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 }); | ||||
|  | ||||
|     const handlePointerDown = (e: FederatedMouseEvent) => { | ||||
|         setIsDragging(true); | ||||
|         setStartPosition({ | ||||
|             x: position.x, | ||||
|             y: position.y | ||||
|         }); | ||||
| 		setStartMousePosition({ | ||||
| 			x: e.globalX, | ||||
| 			y: e.globalY | ||||
| 		}); | ||||
|   const handlePointerDown = (e: FederatedMouseEvent) => { | ||||
|     setIsDragging(true); | ||||
|     setStartPosition({ | ||||
|       x: position.x, | ||||
|       y: position.y, | ||||
|     }); | ||||
|     setStartMousePosition({ | ||||
|       x: e.globalX, | ||||
|       y: e.globalY, | ||||
|     }); | ||||
|  | ||||
|         e.stopPropagation(); | ||||
|     }; | ||||
|     const handlePointerMove = (e: FederatedMouseEvent) => { | ||||
|         if (!isDragging) return; | ||||
| 		const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE; | ||||
| 		const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE; | ||||
| 		const cos = Math.cos(rotation); | ||||
| 		const sin = Math.sin(rotation); | ||||
| 		const newPosition = { | ||||
| 			x: startPosition.x + dx * cos + dy * sin, | ||||
| 			y: startPosition.y - dx * sin + dy * cos | ||||
| 		}; | ||||
| 		setPosition(newPosition); | ||||
| 		const coordinates = localToCoordinates(newPosition.x, newPosition.y); | ||||
| 		setSightCoordinates(sight.id, coordinates.latitude, coordinates.longitude); | ||||
|         e.stopPropagation(); | ||||
|     e.stopPropagation(); | ||||
|   }; | ||||
|   const handlePointerMove = (e: FederatedMouseEvent) => { | ||||
|     if (!isDragging) return; | ||||
|     const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE; | ||||
|     const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE; | ||||
|     const cos = Math.cos(rotation); | ||||
|     const sin = Math.sin(rotation); | ||||
|     const newPosition = { | ||||
|       x: startPosition.x + dx * cos + dy * sin, | ||||
|       y: startPosition.y - dx * sin + dy * cos, | ||||
|     }; | ||||
|     setPosition(newPosition); | ||||
|     const coordinates = localToCoordinates(newPosition.x, newPosition.y); | ||||
|     setSightCoordinates(sight.id, coordinates.latitude, coordinates.longitude); | ||||
|     e.stopPropagation(); | ||||
|   }; | ||||
|  | ||||
|     const handlePointerUp = (e: FederatedMouseEvent) => { | ||||
|         setIsDragging(false); | ||||
|         e.stopPropagation(); | ||||
|     }; | ||||
|   const handlePointerUp = (e: FederatedMouseEvent) => { | ||||
|     setIsDragging(false); | ||||
|     e.stopPropagation(); | ||||
|   }; | ||||
|  | ||||
| 	const [texture, setTexture] = useState(Texture.EMPTY); | ||||
| 	useEffect(() => { | ||||
|         if (texture === Texture.EMPTY) { | ||||
|             Assets | ||||
|                 .load('/SightIcon.png') | ||||
|                 .then((result) => { | ||||
|                     setTexture(result) | ||||
|                 }); | ||||
|         } | ||||
|     }, [texture]); | ||||
|   const [texture, setTexture] = useState(Texture.EMPTY); | ||||
|   useEffect(() => { | ||||
|     if (texture === Texture.EMPTY) { | ||||
|       Assets.load("/SightIcon.png").then((result) => { | ||||
|         setTexture(result); | ||||
|       }); | ||||
|     } | ||||
|   }, [texture]); | ||||
|  | ||||
| 	function draw(g: Graphics) { | ||||
| 		g.clear(); | ||||
| 		g.circle(0, 0, 20); | ||||
| 		g.fill({color: COLORS.primary}); // Fill circle with primary color | ||||
| 	} | ||||
|   function draw(g: Graphics) { | ||||
|     g.clear(); | ||||
|     g.circle(0, 0, 20); | ||||
|     g.fill({ color: "#000" }); // Fill circle with primary color | ||||
|   } | ||||
|  | ||||
| 	if(!sight) { | ||||
| 		console.error("sight is null"); | ||||
| 		return null; | ||||
| 	} | ||||
|   if (!sight) { | ||||
|     console.error("sight is null"); | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| 	const coordinates = coordinatesToLocal(sight.latitude, sight.longitude); | ||||
|  | ||||
| 	return ( | ||||
| 		<pixiContainer rotation={-rotation} | ||||
| 			eventMode='static' | ||||
| 			interactive | ||||
| 			onPointerDown={handlePointerDown} | ||||
| 			onGlobalPointerMove={handlePointerMove} | ||||
| 			onPointerUp={handlePointerUp} | ||||
| 			onPointerUpOutside={handlePointerUp} | ||||
| 			x={position.x * UP_SCALE - SIGHT_SIZE/2} // Offset by half width to center | ||||
| 			y={position.y * UP_SCALE - SIGHT_SIZE/2} // Offset by half height to center | ||||
| 		> | ||||
| 			<pixiSprite | ||||
| 				texture={texture}  | ||||
| 				width={SIGHT_SIZE} | ||||
| 				height={SIGHT_SIZE} | ||||
| 			/> | ||||
| 			<pixiGraphics | ||||
| 				draw={draw} | ||||
| 				x={SIGHT_SIZE} | ||||
| 				y={0} | ||||
| 			/> | ||||
| 			<pixiText | ||||
| 				text={`${id+1}`} | ||||
| 				x={SIGHT_SIZE+1} | ||||
| 				y={0} | ||||
| 				anchor={0.5} | ||||
| 				 | ||||
| 				style={{ | ||||
| 					fontSize: 24, | ||||
| 					fontWeight: 'bold', | ||||
| 					fill: "#ffffff", | ||||
| 				}} | ||||
| 			/> | ||||
| 		</pixiContainer> | ||||
| 	); | ||||
|   return ( | ||||
|     <pixiContainer | ||||
|       rotation={-rotation} | ||||
|       eventMode="static" | ||||
|       interactive | ||||
|       onPointerDown={handlePointerDown} | ||||
|       onGlobalPointerMove={handlePointerMove} | ||||
|       onPointerUp={handlePointerUp} | ||||
|       onPointerUpOutside={handlePointerUp} | ||||
|       x={position.x * UP_SCALE - SIGHT_SIZE / 2} // Offset by half width to center | ||||
|       y={position.y * UP_SCALE - SIGHT_SIZE / 2} // Offset by half height to center | ||||
|     > | ||||
|       <pixiSprite texture={texture} width={SIGHT_SIZE} height={SIGHT_SIZE} /> | ||||
|       <pixiGraphics draw={draw} x={SIGHT_SIZE} y={0} /> | ||||
|       <pixiText | ||||
|         text={`${id + 1}`} | ||||
|         x={SIGHT_SIZE + 1} | ||||
|         y={0} | ||||
|         anchor={0.5} | ||||
|         style={{ | ||||
|           fontSize: 24, | ||||
|           fontWeight: "bold", | ||||
|           fill: "#ffffff", | ||||
|         }} | ||||
|       /> | ||||
|     </pixiContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -7,12 +7,11 @@ import { | ||||
|   UP_SCALE, | ||||
| } from "./Constants"; | ||||
| import { useTransform } from "./TransformContext"; | ||||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||||
| import { useCallback, useState } from "react"; | ||||
| import { StationData } from "./types"; | ||||
| import { useMapData } from "./MapDataContext"; | ||||
| import { coordinatesToLocal } from "./utils"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { languageStore } from "@shared"; | ||||
|  | ||||
| interface StationProps { | ||||
|   station: StationData; | ||||
| @@ -47,7 +46,6 @@ export const Station = observer( | ||||
|  | ||||
| export const StationLabel = observer( | ||||
|   ({ station, ruLabel }: Readonly<StationProps>) => { | ||||
|     const { language } = languageStore; | ||||
|     const { rotation, scale } = useTransform(); | ||||
|     const { setStationOffset } = useMapData(); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { useRef, useEffect, useState } from "react"; | ||||
|  | ||||
| import { Application, ApplicationRef, extend } from "@pixi/react"; | ||||
| import { Application, extend } from "@pixi/react"; | ||||
| import { | ||||
|   Container, | ||||
|   Graphics, | ||||
| @@ -13,7 +13,7 @@ import { Stack } from "@mui/material"; | ||||
| import { MapDataProvider, useMapData } from "./MapDataContext"; | ||||
| import { TransformProvider, useTransform } from "./TransformContext"; | ||||
| import { InfiniteCanvas } from "./InfiniteCanvas"; | ||||
| import { Sight } from "./Sight"; | ||||
|  | ||||
| import { UP_SCALE } from "./Constants"; | ||||
| import { Station } from "./Station"; | ||||
| import { TravelPath } from "./TravelPath"; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | ||||
| import { languageStore, snapshotStore } from "@shared"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { DatabaseBackup, Eye, Trash2 } from "lucide-react"; | ||||
| import { DatabaseBackup, Trash2 } from "lucide-react"; | ||||
|  | ||||
| import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets"; | ||||
|  | ||||
|   | ||||
| @@ -6,17 +6,15 @@ import { | ||||
|   MenuItem, | ||||
|   FormControl, | ||||
|   InputLabel, | ||||
|   Box, | ||||
| } from "@mui/material"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { ArrowLeft, Save, ImagePlus } from "lucide-react"; | ||||
| import { ArrowLeft, Save } from "lucide-react"; | ||||
| import { Loader2 } from "lucide-react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { toast } from "react-toastify"; | ||||
| import { stationsStore, languageStore, cityStore } from "@shared"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { LanguageSwitcher, MediaViewer } from "@widgets"; | ||||
| import { SelectMediaDialog } from "@shared"; | ||||
| import { LanguageSwitcher } from "@widgets"; | ||||
|  | ||||
| export const StationEditPage = observer(() => { | ||||
|   const navigate = useNavigate(); | ||||
|   | ||||
| @@ -17,7 +17,7 @@ export const UserEditPage = observer(() => { | ||||
|   const navigate = useNavigate(); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const { id } = useParams(); | ||||
|   const { editUserData, editUser, getUser, setEditUserData, user } = userStore; | ||||
|   const { editUserData, editUser, getUser, setEditUserData } = userStore; | ||||
|  | ||||
|   const handleEdit = async () => { | ||||
|     try { | ||||
|   | ||||
| @@ -31,7 +31,7 @@ export const VehicleCreatePage = observer(() => { | ||||
|       setIsLoading(true); | ||||
|       await vehicleStore.createVehicle( | ||||
|         Number(tailNumber), | ||||
|         type, | ||||
|         Number(type), | ||||
|         carrierStore.carriers.data.find((c) => c.id === carrierId)?.full_name!, | ||||
|         carrierId! | ||||
|       ); | ||||
|   | ||||
| @@ -108,7 +108,7 @@ export const VehicleEditPage = observer(() => { | ||||
|               }) | ||||
|             } | ||||
|           > | ||||
|             {carrierStore.carriers.map((carrier) => ( | ||||
|             {carrierStore.carriers.data.map((carrier) => ( | ||||
|               <MenuItem key={carrier.id} value={carrier.id}> | ||||
|                 {carrier.full_name} | ||||
|               </MenuItem> | ||||
|   | ||||
| @@ -8,11 +8,8 @@ import { | ||||
|   Earth, | ||||
|   Landmark, | ||||
|   BusFront, | ||||
|   Bus, | ||||
|   GitBranch, | ||||
|   Car, | ||||
|   Train, | ||||
|   Ship, | ||||
|   Table, | ||||
|   Split, | ||||
|   Newspaper, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { authInstance, languageStore } from "@shared"; | ||||
| import { authInstance } from "@shared"; | ||||
| import { makeAutoObservable, runInAction } from "mobx"; | ||||
|  | ||||
| export type Carrier = { | ||||
|   | ||||
| @@ -1,73 +0,0 @@ | ||||
| import { authInstance, languageInstance } from "@shared"; | ||||
| import { makeAutoObservable, runInAction } from "mobx"; | ||||
|  | ||||
| type City = { | ||||
|   id: number; | ||||
|   name: string; | ||||
|   country_code: string; | ||||
|   country: string; | ||||
|   arms?: string; | ||||
| }; | ||||
|  | ||||
| class CityStore { | ||||
|   cities: City[] = []; | ||||
|   ruCities: City[] = []; | ||||
|   city: City | null = null; | ||||
|  | ||||
|   constructor() { | ||||
|     makeAutoObservable(this); | ||||
|   } | ||||
|  | ||||
|   getCities = async () => { | ||||
|     const response = await authInstance.get("/city"); | ||||
|  | ||||
|     runInAction(() => { | ||||
|       this.cities = response.data; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   getRuCities = async () => { | ||||
|     const response = await languageInstance("ru").get("/city"); | ||||
|  | ||||
|     runInAction(() => { | ||||
|       this.ruCities = response.data; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   deleteCity = async (id: number) => { | ||||
|     await authInstance.delete(`/city/${id}`); | ||||
|  | ||||
|     runInAction(() => { | ||||
|       this.cities = this.cities.filter((city) => city.id !== id); | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   getCity = async (id: string) => { | ||||
|     const response = await authInstance.get(`/city/${id}`); | ||||
|  | ||||
|     runInAction(() => { | ||||
|       this.city = response.data; | ||||
|     }); | ||||
|  | ||||
|     return response.data; | ||||
|   }; | ||||
|  | ||||
|   createCity = async ( | ||||
|     name: string, | ||||
|     country: string, | ||||
|     countryCode: string, | ||||
|     mediaId: string | ||||
|   ) => { | ||||
|     const response = await authInstance.post("/city", { | ||||
|       name: name, | ||||
|       country: country, | ||||
|       country_code: countryCode, | ||||
|       arms: mediaId, | ||||
|     }); | ||||
|     runInAction(() => { | ||||
|       this.cities.push(response.data); | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // export const cityStore = new CityStore(); | ||||
| @@ -256,14 +256,17 @@ class StationsStore { | ||||
|         // Update the cached preview data and station lists after successful patch | ||||
|         if (this.stationPreview[id]) { | ||||
|           this.stationPreview[id][language] = { | ||||
|             ...this.stationPreview[id][language], // Preserve common fields that might not be in the language-specific patch response | ||||
|             id: response.data.id, | ||||
|             name: response.data.name, | ||||
|             system_name: response.data.system_name, | ||||
|             description: response.data.description, | ||||
|             address: response.data.address, | ||||
|             ...commonDataPayload, | ||||
|           } as Station; // Cast to Station to satisfy type | ||||
|             loaded: true, | ||||
|             data: { | ||||
|               ...this.stationPreview[id][language].data, | ||||
|               id: response.data.id, | ||||
|               name: response.data.name, | ||||
|               system_name: response.data.system_name, | ||||
|               description: response.data.description, | ||||
|               address: response.data.address, | ||||
|               ...commonDataPayload, | ||||
|             } as Station, | ||||
|           }; | ||||
|         } | ||||
|         if (this.stationLists[language].data) { | ||||
|           this.stationLists[language].data = this.stationLists[ | ||||
| @@ -327,8 +330,8 @@ class StationsStore { | ||||
|         }; | ||||
|       } | ||||
|       this.stationPreview[id][language] = { | ||||
|         data: response.data, | ||||
|         loaded: true, | ||||
|         data: response.data as Station, | ||||
|       }; | ||||
|     }); | ||||
|   }; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { authInstance, languageStore, languageInstance } from "@shared"; | ||||
| import { languageInstance } from "@shared"; | ||||
| import { makeAutoObservable, runInAction } from "mobx"; | ||||
|  | ||||
| export type Vehicle = { | ||||
|   | ||||
| @@ -121,7 +121,7 @@ export const DevicesTable = observer(() => { | ||||
|   // Transform the raw devices data into rows suitable for the table | ||||
|   // This will also filter out devices without a UUID, as those cannot be acted upon. | ||||
|   const currentTableRows = transformDevicesToRows( | ||||
|     vehicles as Vehicle[] | ||||
|     vehicles.data as Vehicle[] | ||||
|     //  devices as ConnectedDevice[] | ||||
|   ); | ||||
|  | ||||
|   | ||||
| @@ -35,7 +35,7 @@ export const SightsTable = observer(() => { | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|       await getSights(); | ||||
|       await getCities(); | ||||
|       await getCities(language); | ||||
|     }; | ||||
|     fetchData(); | ||||
|   }, [language, getSights, getCities]); | ||||
| @@ -67,7 +67,7 @@ export const SightsTable = observer(() => { | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
|           <TableBody> | ||||
|             {rows(sights, cities)?.map((row) => ( | ||||
|             {rows(sights, cities[language])?.map((row) => ( | ||||
|               <TableRow | ||||
|                 key={row?.id} | ||||
|                 sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user