fix: Add route-station link area

This commit is contained in:
2025-06-16 12:26:19 +03:00
parent 32a7cb44d1
commit d415441af8
8 changed files with 570 additions and 119 deletions

View File

@ -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)}

View 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>
)}
</>
);
};

View File

@ -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) => {

View File

@ -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"

View File

@ -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);

View File

@ -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;
}; };

View File

@ -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) => {

View File

@ -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;