fix: Add route-station
link area
This commit is contained in:
@ -172,7 +172,7 @@ class MapStore {
|
|||||||
} else if (featureType === "route") {
|
} else if (featureType === "route") {
|
||||||
data = {
|
data = {
|
||||||
route_number: properties.name,
|
route_number: properties.name,
|
||||||
path: geometry.coordinates,
|
path: geometry.coordinates.map((coord: any) => [coord[1], coord[0]]), // Swap coordinates
|
||||||
};
|
};
|
||||||
} else if (featureType === "sight") {
|
} else if (featureType === "sight") {
|
||||||
data = {
|
data = {
|
||||||
@ -210,8 +210,8 @@ class MapStore {
|
|||||||
{
|
{
|
||||||
...oldData,
|
...oldData,
|
||||||
path: data.path,
|
path: data.path,
|
||||||
center_latitude: data.path[0][1],
|
center_latitude: data.path[0][0], // First coordinate is latitude
|
||||||
center_longitude: data.path[0][0],
|
center_longitude: data.path[0][1], // Second coordinate is longitude
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -485,65 +485,38 @@ class MapService {
|
|||||||
const isHovered = this.hoveredFeatureId === fId;
|
const isHovered = this.hoveredFeatureId === fId;
|
||||||
const isLassoSelected = fId !== undefined && this.selectedIds.has(fId);
|
const isLassoSelected = fId !== undefined && this.selectedIds.has(fId);
|
||||||
|
|
||||||
if (geometryType === "Point") {
|
if (isHovered) {
|
||||||
const defaultPointStyle =
|
if (geometryType === "Point") {
|
||||||
featureType === "sight" ? this.sightIconStyle : this.busIconStyle;
|
return featureType === "sight"
|
||||||
const selectedPointStyle =
|
? this.hoverSightIconStyle
|
||||||
featureType === "sight"
|
: this.universalHoverStylePoint;
|
||||||
? this.selectedSightIconStyle
|
|
||||||
: this.selectedBusIconStyle;
|
|
||||||
|
|
||||||
if (isEditSelected) {
|
|
||||||
return selectedPointStyle;
|
|
||||||
}
|
}
|
||||||
if (isHovered) {
|
return this.universalHoverStyleLine;
|
||||||
// Only apply hover styles if not in edit mode
|
|
||||||
if (this.mode !== "edit") {
|
|
||||||
return featureType === "sight"
|
|
||||||
? this.hoverSightIconStyle
|
|
||||||
: this.universalHoverStylePoint;
|
|
||||||
}
|
|
||||||
return defaultPointStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLassoSelected) {
|
|
||||||
let imageStyle;
|
|
||||||
if (featureType === "sight") {
|
|
||||||
imageStyle = new RegularShape({
|
|
||||||
fill: new Fill({ color: "#14b8a6" }),
|
|
||||||
stroke: new Stroke({ color: "#fff", width: 2 }),
|
|
||||||
points: 5,
|
|
||||||
radius: 12,
|
|
||||||
radius2: 6,
|
|
||||||
angle: 0,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
imageStyle = new CircleStyle({
|
|
||||||
radius: 10,
|
|
||||||
fill: new Fill({ color: "#14b8a6" }),
|
|
||||||
stroke: new Stroke({ color: "#fff", width: 2 }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Style({ image: imageStyle, zIndex: Infinity });
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultPointStyle;
|
|
||||||
} else if (geometryType === "LineString") {
|
|
||||||
if (isEditSelected) {
|
|
||||||
return this.selectedStyle;
|
|
||||||
}
|
|
||||||
if (isHovered) {
|
|
||||||
return this.universalHoverStyleLine;
|
|
||||||
}
|
|
||||||
if (isLassoSelected) {
|
|
||||||
return new Style({
|
|
||||||
stroke: new Stroke({ color: "#14b8a6", width: 6 }),
|
|
||||||
zIndex: Infinity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.defaultStyle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLassoSelected) {
|
||||||
|
if (geometryType === "Point") {
|
||||||
|
return featureType === "sight"
|
||||||
|
? this.selectedSightIconStyle
|
||||||
|
: this.selectedBusIconStyle;
|
||||||
|
}
|
||||||
|
return this.selectedStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditSelected) {
|
||||||
|
if (geometryType === "Point") {
|
||||||
|
return featureType === "sight"
|
||||||
|
? this.selectedSightIconStyle
|
||||||
|
: this.selectedBusIconStyle;
|
||||||
|
}
|
||||||
|
return this.selectedStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geometryType === "Point") {
|
||||||
|
return featureType === "sight"
|
||||||
|
? this.sightIconStyle
|
||||||
|
: this.busIconStyle;
|
||||||
|
}
|
||||||
return this.defaultStyle;
|
return this.defaultStyle;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -763,7 +736,9 @@ class MapService {
|
|||||||
if (!route.path || route.path.length === 0) return;
|
if (!route.path || route.path.length === 0) return;
|
||||||
const coordinates = route.path
|
const coordinates = route.path
|
||||||
.filter((c) => c[0] != null && c[1] != null)
|
.filter((c) => c[0] != null && c[1] != null)
|
||||||
.map((c) => transform(c, "EPSG:4326", projection));
|
.map((c: [number, number]) =>
|
||||||
|
transform([c[1], c[0]], "EPSG:4326", projection)
|
||||||
|
); // Swap coordinates
|
||||||
if (coordinates.length === 0) return;
|
if (coordinates.length === 0) return;
|
||||||
const line = new LineString(coordinates);
|
const line = new LineString(coordinates);
|
||||||
const feature = new Feature({ geometry: line, name: route.route_number });
|
const feature = new Feature({ geometry: line, name: route.route_number });
|
||||||
@ -866,6 +841,11 @@ class MapService {
|
|||||||
this.redo();
|
this.redo();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "r") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.unselect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
this.unselect();
|
this.unselect();
|
||||||
}
|
}
|
||||||
@ -1090,29 +1070,29 @@ class MapService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!featureAtPixel) {
|
if (!featureAtPixel) {
|
||||||
if (ctrlKey) {
|
if (ctrlKey) this.unselect();
|
||||||
// При ctrl + клик вне сущности сбрасываем выбор
|
|
||||||
this.setSelectedIds(new Set());
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const featureId = featureAtPixel.getId();
|
const featureId = featureAtPixel.getId();
|
||||||
if (featureId === undefined) return;
|
if (featureId === undefined) return;
|
||||||
|
|
||||||
|
const newSet = new Set(this.selectedIds);
|
||||||
|
|
||||||
if (ctrlKey) {
|
if (ctrlKey) {
|
||||||
// При ctrl + клик на сущность добавляем/удаляем её из выбора
|
// Toggle selection for the clicked feature
|
||||||
const newSet = new Set(this.selectedIds);
|
|
||||||
if (newSet.has(featureId)) {
|
if (newSet.has(featureId)) {
|
||||||
newSet.delete(featureId);
|
newSet.delete(featureId);
|
||||||
} else {
|
} else {
|
||||||
newSet.add(featureId);
|
newSet.add(featureId);
|
||||||
}
|
}
|
||||||
this.setSelectedIds(newSet);
|
|
||||||
} else {
|
} else {
|
||||||
// При обычном клике на сущность выбираем только её
|
// Single selection
|
||||||
this.setSelectedIds(new Set([featureId]));
|
newSet.clear();
|
||||||
|
newSet.add(featureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setSelectedIds(newSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectFeature(featureId: string | number | undefined): void {
|
public selectFeature(featureId: string | number | undefined): void {
|
||||||
@ -1127,14 +1107,6 @@ class MapService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mode === "edit") {
|
|
||||||
this.selectInteraction.getFeatures().clear();
|
|
||||||
this.selectInteraction.getFeatures().push(feature);
|
|
||||||
// @ts-ignore
|
|
||||||
const selectEvent = new SelectEvent("select", [feature], []);
|
|
||||||
this.selectInteraction.dispatchEvent(selectEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setSelectedIds(new Set([featureId]));
|
this.setSelectedIds(new Set([featureId]));
|
||||||
|
|
||||||
const view = this.map.getView();
|
const view = this.map.getView();
|
||||||
@ -1286,7 +1258,30 @@ class MapService {
|
|||||||
public setSelectedIds(ids: Set<string | number>) {
|
public setSelectedIds(ids: Set<string | number>) {
|
||||||
this.selectedIds = new Set(ids);
|
this.selectedIds = new Set(ids);
|
||||||
if (this.onSelectionChange) this.onSelectionChange(this.selectedIds);
|
if (this.onSelectionChange) this.onSelectionChange(this.selectedIds);
|
||||||
this.vectorLayer.changed();
|
|
||||||
|
// Update selectInteraction to match selectedIds
|
||||||
|
if (this.selectInteraction) {
|
||||||
|
this.selectInteraction.getFeatures().clear();
|
||||||
|
ids.forEach((id) => {
|
||||||
|
const feature = this.vectorSource.getFeatureById(id);
|
||||||
|
if (feature) {
|
||||||
|
this.selectInteraction.getFeatures().push(feature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update modifyInteraction
|
||||||
|
this.modifyInteraction.setActive(ids.size > 0);
|
||||||
|
|
||||||
|
// Update feature selection in sidebar
|
||||||
|
if (ids.size === 1) {
|
||||||
|
const feature = this.vectorSource.getFeatureById(Array.from(ids)[0]);
|
||||||
|
if (feature) {
|
||||||
|
this.onFeatureSelect(feature);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.onFeatureSelect(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSelectedIds() {
|
public getSelectedIds() {
|
||||||
@ -1501,8 +1496,10 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
}, [mapFeatures, searchQuery]);
|
}, [mapFeatures, searchQuery]);
|
||||||
|
|
||||||
const handleFeatureClick = useCallback(
|
const handleFeatureClick = useCallback(
|
||||||
// @ts-ignore
|
(id: string | number | undefined) => {
|
||||||
(id) => mapService?.selectFeature(id),
|
if (!id || !mapService) return;
|
||||||
|
mapService.selectFeature(id);
|
||||||
|
},
|
||||||
[mapService]
|
[mapService]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1521,7 +1518,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
|
|
||||||
const handleCheckboxChange = useCallback(
|
const handleCheckboxChange = useCallback(
|
||||||
(id: string | number | undefined) => {
|
(id: string | number | undefined) => {
|
||||||
if (id === undefined) return;
|
if (!id || !mapService) return;
|
||||||
const newSet = new Set(selectedIds);
|
const newSet = new Set(selectedIds);
|
||||||
if (newSet.has(id)) {
|
if (newSet.has(id)) {
|
||||||
newSet.delete(id);
|
newSet.delete(id);
|
||||||
@ -1529,11 +1526,9 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
newSet.add(id);
|
newSet.add(id);
|
||||||
}
|
}
|
||||||
setSelectedIds(newSet);
|
setSelectedIds(newSet);
|
||||||
if (mapService) {
|
mapService.setSelectedIds(newSet);
|
||||||
mapService.setSelectedIds(newSet);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[selectedIds, setSelectedIds, mapService]
|
[mapService, selectedIds, setSelectedIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBulkDelete = useCallback(() => {
|
const handleBulkDelete = useCallback(() => {
|
||||||
@ -1630,7 +1625,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||||
checked={!!isChecked}
|
checked={isChecked}
|
||||||
onChange={() => handleCheckboxChange(sId)}
|
onChange={() => handleCheckboxChange(sId)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Выбрать ${sName}`}
|
aria-label={`Выбрать ${sName}`}
|
||||||
@ -1719,7 +1714,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||||
checked={!!isChecked}
|
checked={isChecked}
|
||||||
onChange={() => handleCheckboxChange(lId)}
|
onChange={() => handleCheckboxChange(lId)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Выбрать ${lName}`}
|
aria-label={`Выбрать ${lName}`}
|
||||||
@ -1808,7 +1803,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||||
checked={!!isChecked}
|
checked={isChecked}
|
||||||
onChange={() => handleCheckboxChange(sId)}
|
onChange={() => handleCheckboxChange(sId)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Выбрать ${sName}`}
|
aria-label={`Выбрать ${sName}`}
|
||||||
@ -2008,11 +2003,13 @@ export const MapPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleMapClick = useCallback(
|
const handleMapClick = useCallback(
|
||||||
(event: MapBrowserEvent<any>) => {
|
(event: any) => {
|
||||||
if (!mapServiceInstance) return;
|
if (!mapServiceInstance || isLassoActive) return;
|
||||||
mapServiceInstance.handleMapClick(event, event.originalEvent.ctrlKey);
|
const ctrlKey =
|
||||||
|
event.originalEvent.ctrlKey || event.originalEvent.metaKey;
|
||||||
|
mapServiceInstance.handleMapClick(event, ctrlKey);
|
||||||
},
|
},
|
||||||
[mapServiceInstance]
|
[mapServiceInstance, isLassoActive]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -2211,6 +2208,12 @@ export const MapPage: React.FC = () => {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
- Повторить действие
|
- Повторить действие
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-mono bg-gray-100 px-1 rounded">
|
||||||
|
Ctrl+R
|
||||||
|
</span>{" "}
|
||||||
|
- Отменить выделение
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowHelp(false)}
|
onClick={() => setShowHelp(false)}
|
||||||
|
425
src/pages/Route/LinekedStations.tsx
Normal file
425
src/pages/Route/LinekedStations.tsx
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
useTheme,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
TableBody,
|
||||||
|
IconButton,
|
||||||
|
} from "@mui/material";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
||||||
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Droppable,
|
||||||
|
Draggable,
|
||||||
|
DropResult,
|
||||||
|
} from "@hello-pangea/dnd";
|
||||||
|
|
||||||
|
import { authInstance, languageStore } from "@shared";
|
||||||
|
|
||||||
|
// Helper function to insert an item at a specific position (1-based index)
|
||||||
|
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
||||||
|
const index = pos - 1;
|
||||||
|
const result = [...arr];
|
||||||
|
if (index >= result.length) {
|
||||||
|
result.push(value);
|
||||||
|
} else {
|
||||||
|
result.splice(index, 0, value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to reorder items after drag and drop
|
||||||
|
const reorder = <T,>(list: T[], startIndex: number, endIndex: number): T[] => {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Field<T> = {
|
||||||
|
label: string;
|
||||||
|
data: keyof T;
|
||||||
|
render?: (value: any) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LinkedItemsProps<T> = {
|
||||||
|
parentId: string | number;
|
||||||
|
fields: Field<T>[];
|
||||||
|
setItemsParent?: (items: T[]) => void;
|
||||||
|
type: "show" | "edit";
|
||||||
|
dragAllowed?: boolean;
|
||||||
|
onUpdate?: () => void;
|
||||||
|
dontRecurse?: boolean;
|
||||||
|
disableCreation?: boolean;
|
||||||
|
updatedLinkedItems?: T[];
|
||||||
|
refresh?: number;
|
||||||
|
cityId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkedItems = <
|
||||||
|
T extends { id: number; name: string; [key: string]: any }
|
||||||
|
>(
|
||||||
|
props: LinkedItemsProps<T>
|
||||||
|
) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Accordion sx={{ width: "100%" }}>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
sx={{
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
|
Привязанные станции
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
|
||||||
|
<AccordionDetails
|
||||||
|
sx={{ background: theme.palette.background.paper, width: "100%" }}
|
||||||
|
>
|
||||||
|
<Stack gap={2} width="100%">
|
||||||
|
<LinkedItemsContents {...props} />
|
||||||
|
</Stack>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkedItemsContents = <
|
||||||
|
T extends { id: number; name: string; [key: string]: any }
|
||||||
|
>({
|
||||||
|
parentId,
|
||||||
|
setItemsParent,
|
||||||
|
fields,
|
||||||
|
dragAllowed = false,
|
||||||
|
type,
|
||||||
|
onUpdate,
|
||||||
|
disableCreation = false,
|
||||||
|
updatedLinkedItems,
|
||||||
|
refresh,
|
||||||
|
cityId,
|
||||||
|
}: LinkedItemsProps<T>) => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
const [position, setPosition] = useState<number>(1);
|
||||||
|
const [allItems, setAllItems] = useState<T[]>([]);
|
||||||
|
const [linkedItems, setLinkedItems] = useState<T[]>([]);
|
||||||
|
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const parentResource = "route";
|
||||||
|
const childResource = "station";
|
||||||
|
|
||||||
|
const availableItems = allItems
|
||||||
|
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updatedLinkedItems) {
|
||||||
|
setLinkedItems(updatedLinkedItems);
|
||||||
|
}
|
||||||
|
}, [updatedLinkedItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItemsParent?.(linkedItems);
|
||||||
|
}, [linkedItems, setItemsParent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPosition(linkedItems.length + 1);
|
||||||
|
}, [linkedItems.length]);
|
||||||
|
|
||||||
|
const onDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
|
||||||
|
const reorderedItems = reorder(
|
||||||
|
linkedItems,
|
||||||
|
result.source.index,
|
||||||
|
result.destination.index
|
||||||
|
);
|
||||||
|
|
||||||
|
setLinkedItems(reorderedItems);
|
||||||
|
|
||||||
|
authInstance
|
||||||
|
.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
|
stations: reorderedItems.map((item) => ({ id: item.id })),
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error updating station order:", error);
|
||||||
|
setError("Failed to update station order");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (parentId) {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
authInstance
|
||||||
|
.get(`/${parentResource}/${parentId}/${childResource}`)
|
||||||
|
.then((response) => {
|
||||||
|
setLinkedItems(response?.data || []);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching linked items:", error);
|
||||||
|
setError("Failed to load linked stations");
|
||||||
|
setLinkedItems([]);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [parentId, language, refresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type === "edit") {
|
||||||
|
setError(null);
|
||||||
|
authInstance
|
||||||
|
.get(`/${childResource}/`)
|
||||||
|
.then((response) => {
|
||||||
|
setAllItems(response?.data || []);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching all items:", error);
|
||||||
|
setError("Failed to load available stations");
|
||||||
|
setAllItems([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
const linkItem = () => {
|
||||||
|
if (selectedItemId !== null) {
|
||||||
|
setError(null);
|
||||||
|
const requestData = {
|
||||||
|
stations: insertAtPosition(
|
||||||
|
linkedItems.map((item) => ({ id: item.id })),
|
||||||
|
position,
|
||||||
|
{ id: selectedItemId }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
authInstance
|
||||||
|
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||||
|
.then((response) => {
|
||||||
|
const newItem = allItems.find((item) => item.id === selectedItemId);
|
||||||
|
if (newItem) {
|
||||||
|
const updatedList = insertAtPosition(
|
||||||
|
[...linkedItems],
|
||||||
|
position,
|
||||||
|
newItem
|
||||||
|
);
|
||||||
|
setLinkedItems(updatedList);
|
||||||
|
}
|
||||||
|
setSelectedItemId(null);
|
||||||
|
onUpdate?.();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error linking item:", error);
|
||||||
|
setError("Failed to link station");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItem = (itemId: number) => {
|
||||||
|
setError(null);
|
||||||
|
authInstance
|
||||||
|
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
|
data: { [`${childResource}_id`]: itemId },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setLinkedItems((prev) => prev.filter((item) => item.id !== itemId));
|
||||||
|
onUpdate?.();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error unlinking item:", error);
|
||||||
|
setError("Failed to unlink station");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{linkedItems?.length > 0 && (
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<TableContainer component={Paper} sx={{ width: "100%" }}>
|
||||||
|
<Table sx={{ width: "100%" }}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{type === "edit" && dragAllowed && (
|
||||||
|
<TableCell width="40px"></TableCell>
|
||||||
|
)}
|
||||||
|
<TableCell key="id" width="60px">
|
||||||
|
№
|
||||||
|
</TableCell>
|
||||||
|
{fields.map((field) => (
|
||||||
|
<TableCell key={String(field.data)}>
|
||||||
|
{field.label}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell width="120px">Действие</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<Droppable
|
||||||
|
droppableId="droppable-stations"
|
||||||
|
isDropDisabled={type !== "edit" || !dragAllowed}
|
||||||
|
>
|
||||||
|
{(provided) => (
|
||||||
|
<TableBody
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
|
>
|
||||||
|
{linkedItems.map((item, index) => (
|
||||||
|
<Draggable
|
||||||
|
key={item.id}
|
||||||
|
draggableId={"station-" + String(item.id)}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={type !== "edit" || !dragAllowed}
|
||||||
|
>
|
||||||
|
{(provided) => (
|
||||||
|
<TableRow
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
{type === "edit" && dragAllowed && (
|
||||||
|
<TableCell {...provided.dragHandleProps}>
|
||||||
|
<IconButton size="small">
|
||||||
|
<DragIndicatorIcon />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
<TableCell>{index + 1}</TableCell>
|
||||||
|
{fields.map((field, idx) => (
|
||||||
|
<TableCell key={String(field.data) + String(idx)}>
|
||||||
|
{field.render
|
||||||
|
? field.render(item[field.data])
|
||||||
|
: item[field.data]}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
{type === "edit" && (
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteItem(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Отвязать
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</TableBody>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</DragDropContext>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{linkedItems.length === 0 && !isLoading && (
|
||||||
|
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||||
|
Станции не найдены
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "edit" && !disableCreation && (
|
||||||
|
<Stack gap={2} mt={2}>
|
||||||
|
<Typography variant="subtitle1">Добавить станцию</Typography>
|
||||||
|
<Autocomplete
|
||||||
|
fullWidth
|
||||||
|
value={
|
||||||
|
availableItems?.find((item) => item.id === selectedItemId) || null
|
||||||
|
}
|
||||||
|
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
||||||
|
options={availableItems.filter(
|
||||||
|
(item) => !cityId || item.city_id == cityId
|
||||||
|
)}
|
||||||
|
getOptionLabel={(item) => String(item.name)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Выберите станцию" fullWidth />
|
||||||
|
)}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value?.id}
|
||||||
|
filterOptions={(options, { inputValue }) => {
|
||||||
|
const searchWords = inputValue
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ")
|
||||||
|
.filter(Boolean);
|
||||||
|
return options.filter((option) => {
|
||||||
|
const optionWords = String(option.name)
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ");
|
||||||
|
return searchWords.every((searchWord) =>
|
||||||
|
optionWords.some((word) => word.startsWith(searchWord))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<li {...props} key={option.id}>
|
||||||
|
{String(option.name)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Позиция добавляемой остановки"
|
||||||
|
value={position}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = Math.max(1, Number(e.target.value));
|
||||||
|
setPosition(
|
||||||
|
newValue > linkedItems.length + 1
|
||||||
|
? linkedItems.length + 1
|
||||||
|
: newValue
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
inputProps: { min: 1, max: linkedItems.length + 1 },
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={linkItem}
|
||||||
|
disabled={!selectedItemId}
|
||||||
|
sx={{ alignSelf: "flex-start" }}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -184,9 +184,8 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onChange={(e) => setRouteNumber(e.target.value)}
|
onChange={(e) => setRouteNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
|
||||||
label="Координаты маршрута"
|
|
||||||
multiline
|
multiline
|
||||||
|
className="w-full max-h-[300px] overflow-y-scroll"
|
||||||
minRows={4}
|
minRows={4}
|
||||||
value={routeCoords}
|
value={routeCoords}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
@ -19,7 +19,8 @@ import { carrierStore } from "../../../shared/store/CarrierStore";
|
|||||||
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||||
import { routeStore } from "../../../shared/store/RouteStore";
|
import { routeStore } from "../../../shared/store/RouteStore";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { languageStore } from "@shared";
|
import { languageStore, stationsStore } from "@shared";
|
||||||
|
import { LinkedItems } from "../LinekedStations";
|
||||||
|
|
||||||
export const RouteEditPage = observer(() => {
|
export const RouteEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -34,6 +35,7 @@ export const RouteEditPage = observer(() => {
|
|||||||
const response = await routeStore.getRoute(Number(id));
|
const response = await routeStore.getRoute(Number(id));
|
||||||
routeStore.setEditRouteData(response);
|
routeStore.setEditRouteData(response);
|
||||||
carrierStore.getCarriers(language);
|
carrierStore.getCarriers(language);
|
||||||
|
stationsStore.getStations();
|
||||||
articlesStore.getArticleList();
|
articlesStore.getArticleList();
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
@ -150,8 +152,7 @@ export const RouteEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full max-h-[300px] overflow-y-scroll -mt-5 h-full"
|
||||||
label="Координаты маршрута"
|
|
||||||
multiline
|
multiline
|
||||||
minRows={4}
|
minRows={4}
|
||||||
value={coordinates}
|
value={coordinates}
|
||||||
@ -245,54 +246,73 @@ export const RouteEditPage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (мин)"
|
label="Масштаб (мин)"
|
||||||
value={editRouteData.scale_min || ""}
|
value={editRouteData.scale_min ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_min: Number(e.target.value),
|
scale_min:
|
||||||
|
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (макс)"
|
label="Масштаб (макс)"
|
||||||
value={editRouteData.scale_max || ""}
|
value={editRouteData.scale_max ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_max: Number(e.target.value),
|
scale_max:
|
||||||
|
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Поворот"
|
label="Поворот"
|
||||||
value={editRouteData.rotate || ""}
|
value={editRouteData.rotate ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
rotate: Number(e.target.value),
|
rotate:
|
||||||
|
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Центр. широта"
|
label="Центр. широта"
|
||||||
value={editRouteData.center_latitude || ""}
|
value={editRouteData.center_latitude ?? ""}
|
||||||
|
type="text"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
center_latitude: Number(e.target.value),
|
center_latitude: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Центр. долгота"
|
label="Центр. долгота"
|
||||||
value={editRouteData.center_longitude || ""}
|
value={editRouteData.center_longitude ?? ""}
|
||||||
|
type="text"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
center_longitude: Number(e.target.value),
|
center_longitude: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<LinkedItems
|
||||||
|
parentId={id || ""}
|
||||||
|
type="edit"
|
||||||
|
dragAllowed={true}
|
||||||
|
fields={[
|
||||||
|
{ label: "Название", data: "name" },
|
||||||
|
{ label: "Описание", data: "description" },
|
||||||
|
]}
|
||||||
|
onUpdate={() => {
|
||||||
|
routeStore.getRoute(Number(id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -56,12 +56,12 @@ export function RightSidebar() {
|
|||||||
setMapRotation(rotationDegrees);
|
setMapRotation(rotationDegrees);
|
||||||
}, [rotationDegrees]);
|
}, [rotationDegrees]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const center = screenCenter ?? { x: 0, y: 0 };
|
// const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
const localCenter = screenToLocal(center.x, center.y);
|
// const localCenter = screenToLocal(center.x, center.y);
|
||||||
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
// const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
||||||
setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
// setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
||||||
}, [position]);
|
// }, [position]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMapCenter(localCenter.x, localCenter.y);
|
setMapCenter(localCenter.x, localCenter.y);
|
||||||
|
@ -515,6 +515,7 @@ class CreateSightStore {
|
|||||||
|
|
||||||
console.log("Sight created with ID:", newSightId);
|
console.log("Sight created with ID:", newSightId);
|
||||||
// Optionally: this.clearCreateSight(); // To reset form after successful creation
|
// Optionally: this.clearCreateSight(); // To reset form after successful creation
|
||||||
|
this.needLeaveAgree = false;
|
||||||
return newSightId;
|
return newSightId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -346,6 +346,8 @@ class EditSightStore {
|
|||||||
// body: this.sight.zh.left.body,
|
// body: this.sight.zh.left.body,
|
||||||
// }
|
// }
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
this.needLeaveAgree = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
getLeftArticle = async (id: number) => {
|
getLeftArticle = async (id: number) => {
|
||||||
|
@ -80,8 +80,8 @@ class RouteStore {
|
|||||||
editRouteData = {
|
editRouteData = {
|
||||||
carrier: "",
|
carrier: "",
|
||||||
carrier_id: 0,
|
carrier_id: 0,
|
||||||
center_latitude: 0,
|
center_latitude: "",
|
||||||
center_longitude: 0,
|
center_longitude: "",
|
||||||
governor_appeal: 0,
|
governor_appeal: 0,
|
||||||
id: 0,
|
id: 0,
|
||||||
path: [] as number[][],
|
path: [] as number[][],
|
||||||
@ -99,10 +99,11 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editRoute = async (id: number) => {
|
editRoute = async (id: number) => {
|
||||||
const response = await authInstance.patch(
|
const response = await authInstance.patch(`/route/${id}`, {
|
||||||
`/route/${id}`,
|
...this.editRouteData,
|
||||||
this.editRouteData
|
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||||
);
|
center_longitude: parseFloat(this.editRouteData.center_longitude),
|
||||||
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.route[id] = response.data;
|
this.route[id] = response.data;
|
||||||
|
Reference in New Issue
Block a user