feat: Route list page
This commit is contained in:
@ -33,6 +33,10 @@ import {
|
|||||||
UserEditPage,
|
UserEditPage,
|
||||||
VehicleEditPage,
|
VehicleEditPage,
|
||||||
CarrierEditPage,
|
CarrierEditPage,
|
||||||
|
StationCreatePage,
|
||||||
|
StationPreviewPage,
|
||||||
|
StationEditPage,
|
||||||
|
RouteCreatePage,
|
||||||
} from "@pages";
|
} from "@pages";
|
||||||
import { authStore, createSightStore, editSightStore } from "@shared";
|
import { authStore, createSightStore, editSightStore } from "@shared";
|
||||||
import { Layout } from "@widgets";
|
import { Layout } from "@widgets";
|
||||||
@ -110,7 +114,7 @@ const router = createBrowserRouter([
|
|||||||
// Sight
|
// Sight
|
||||||
{ path: "sight", element: <SightListPage /> },
|
{ path: "sight", element: <SightListPage /> },
|
||||||
{ path: "sight/create", element: <CreateSightPage /> },
|
{ path: "sight/create", element: <CreateSightPage /> },
|
||||||
{ path: "sight/:id", element: <EditSightPage /> },
|
{ path: "sight/:id/edit", element: <EditSightPage /> },
|
||||||
|
|
||||||
// Device
|
// Device
|
||||||
{ path: "devices", element: <DevicesPage /> },
|
{ path: "devices", element: <DevicesPage /> },
|
||||||
@ -135,6 +139,7 @@ const router = createBrowserRouter([
|
|||||||
{ path: "city/:id/edit", element: <CityEditPage /> },
|
{ path: "city/:id/edit", element: <CityEditPage /> },
|
||||||
// Route
|
// Route
|
||||||
{ path: "route", element: <RouteListPage /> },
|
{ path: "route", element: <RouteListPage /> },
|
||||||
|
{ path: "route/create", element: <RouteCreatePage /> },
|
||||||
|
|
||||||
// User
|
// User
|
||||||
{ path: "user", element: <UserListPage /> },
|
{ path: "user", element: <UserListPage /> },
|
||||||
@ -151,7 +156,9 @@ const router = createBrowserRouter([
|
|||||||
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
||||||
// Station
|
// Station
|
||||||
{ path: "station", element: <StationListPage /> },
|
{ path: "station", element: <StationListPage /> },
|
||||||
|
{ path: "station/create", element: <StationCreatePage /> },
|
||||||
|
{ path: "station/:id", element: <StationPreviewPage /> },
|
||||||
|
{ path: "station/:id/edit", element: <StationEditPage /> },
|
||||||
// Vehicle
|
// Vehicle
|
||||||
{ path: "vehicle", element: <VehicleListPage /> },
|
{ path: "vehicle", element: <VehicleListPage /> },
|
||||||
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
||||||
@ -159,7 +166,7 @@ const router = createBrowserRouter([
|
|||||||
{ path: "vehicle/:id/edit", element: <VehicleEditPage /> },
|
{ path: "vehicle/:id/edit", element: <VehicleEditPage /> },
|
||||||
// Article
|
// Article
|
||||||
{ path: "article", element: <ArticleListPage /> },
|
{ path: "article", element: <ArticleListPage /> },
|
||||||
|
// { path: "article/:id", element: <ArticlePreviewPage /> },
|
||||||
// { path: "media/create", element: <CreateMediaPage /> },
|
// { path: "media/create", element: <CreateMediaPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import List from "@mui/material/List";
|
|||||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import type { NavigationItem } from "../model";
|
import type { NavigationItem } from "../model";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
interface NavigationItemProps {
|
interface NavigationItemProps {
|
||||||
item: NavigationItem;
|
item: NavigationItem;
|
||||||
@ -25,8 +25,11 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
|
|
||||||
|
const isActive = item.path ? location.pathname.startsWith(item.path) : false;
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (item.nestedItems) {
|
if (item.nestedItems) {
|
||||||
setIsExpanded(!isExpanded);
|
setIsExpanded(!isExpanded);
|
||||||
@ -57,6 +60,12 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
isNested && {
|
isNested && {
|
||||||
pl: 4,
|
pl: 4,
|
||||||
},
|
},
|
||||||
|
isActive && {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.08)",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.12)",
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
@ -64,6 +73,7 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
{
|
{
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
color: isActive ? "primary.main" : "inherit",
|
||||||
},
|
},
|
||||||
open
|
open
|
||||||
? {
|
? {
|
||||||
@ -86,6 +96,10 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
: {
|
: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
|
isActive && {
|
||||||
|
color: "primary.main",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{item.nestedItems &&
|
{item.nestedItems &&
|
||||||
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { articlesStore, languageStore } from "@shared";
|
import { articlesStore, languageStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, FileText } from "lucide-react";
|
import { Trash2, Eye } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
import { DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
@ -31,8 +31,8 @@ export const ArticleListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
<button onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||||
<FileText size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -48,7 +48,7 @@ export const ArticleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = articleList.map((article) => ({
|
const rows = articleList[language].data.map((article) => ({
|
||||||
id: article.id,
|
id: article.id,
|
||||||
heading: article.heading,
|
heading: article.heading,
|
||||||
body: article.body,
|
body: article.body,
|
||||||
|
@ -63,7 +63,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/carrier")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -61,7 +61,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/carrier")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -62,7 +62,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = carriers.map((carrier) => ({
|
const rows = carriers.data?.map((carrier) => ({
|
||||||
id: carrier.id,
|
id: carrier.id,
|
||||||
full_name: carrier.full_name,
|
full_name: carrier.full_name,
|
||||||
short_name: carrier.short_name,
|
short_name: carrier.short_name,
|
||||||
|
@ -72,7 +72,7 @@ export const CityCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/city")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -89,7 +89,7 @@ export const CityEditPage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/city")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -33,7 +33,7 @@ export const CountryCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/country")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -43,7 +43,7 @@ export const CountryEditPage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/country")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -5,6 +5,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Map, View, Overlay, MapBrowserEvent } from "ol";
|
import { Map, View, Overlay, MapBrowserEvent } from "ol";
|
||||||
import TileLayer from "ol/layer/Tile";
|
import TileLayer from "ol/layer/Tile";
|
||||||
import OSM from "ol/source/OSM";
|
import OSM from "ol/source/OSM";
|
||||||
@ -14,7 +15,13 @@ import { Draw, Modify, Select } from "ol/interaction";
|
|||||||
import { DrawEvent } from "ol/interaction/Draw";
|
import { DrawEvent } from "ol/interaction/Draw";
|
||||||
import { ModifyEvent } from "ol/interaction/Modify";
|
import { ModifyEvent } from "ol/interaction/Modify";
|
||||||
import { SelectEvent } from "ol/interaction/Select";
|
import { SelectEvent } from "ol/interaction/Select";
|
||||||
import { Style, Fill, Stroke, Circle as CircleStyle } from "ol/style";
|
import {
|
||||||
|
Style,
|
||||||
|
Fill,
|
||||||
|
Stroke,
|
||||||
|
Circle as CircleStyle,
|
||||||
|
RegularShape,
|
||||||
|
} from "ol/style";
|
||||||
import { Point, LineString, Geometry } from "ol/geom";
|
import { Point, LineString, Geometry } from "ol/geom";
|
||||||
import { transform } from "ol/proj";
|
import { transform } from "ol/proj";
|
||||||
import { GeoJSON } from "ol/format";
|
import { GeoJSON } from "ol/format";
|
||||||
@ -25,85 +32,21 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
ArrowRightLeft,
|
ArrowRightLeft,
|
||||||
Landmark,
|
Landmark,
|
||||||
|
Pencil,
|
||||||
|
Save,
|
||||||
|
Plus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import { singleClick, doubleClick } from "ol/events/condition";
|
import { singleClick, doubleClick } from "ol/events/condition";
|
||||||
import { Feature } from "ol";
|
import { Feature } from "ol";
|
||||||
import Layer from "ol/layer/Layer";
|
import Layer from "ol/layer/Layer";
|
||||||
import Source from "ol/source/Source";
|
import Source from "ol/source/Source";
|
||||||
import { FeatureLike } from "ol/Feature";
|
import { FeatureLike } from "ol/Feature";
|
||||||
import { authInstance } from "@shared";
|
|
||||||
|
|
||||||
|
import mapStore from "./mapStore";
|
||||||
// --- API INTERFACES ---
|
// --- API INTERFACES ---
|
||||||
interface ApiRoute {
|
|
||||||
id: number;
|
|
||||||
route_number: string;
|
|
||||||
path: [number, number][]; // [longitude, latitude][]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiStation {
|
// --- MOCK API ---
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiSight {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MOCK API (для имитации запросов к серверу) ---
|
|
||||||
const mockApi = {
|
|
||||||
getRoutes: async (): Promise<ApiRoute[]> => {
|
|
||||||
console.log("Fetching routes...");
|
|
||||||
await new Promise((res) => setTimeout(res, 500)); // Имитация задержки сети
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
route_number: "А-78",
|
|
||||||
path: [
|
|
||||||
[30.315, 59.934],
|
|
||||||
[30.32, 59.936],
|
|
||||||
[30.325, 59.938],
|
|
||||||
[30.33, 59.94],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
getStations: async (): Promise<ApiStation[]> => {
|
|
||||||
console.log("Fetching stations...");
|
|
||||||
// const stations = await authInstance.get("/station");
|
|
||||||
|
|
||||||
await new Promise((res) => setTimeout(res, 400));
|
|
||||||
return [
|
|
||||||
{ id: 101, name: "Гостиный двор", latitude: 59.934, longitude: 30.332 },
|
|
||||||
{ id: 102, name: "Пл. Восстания", latitude: 59.931, longitude: 30.362 },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
getSights: async (): Promise<ApiSight[]> => {
|
|
||||||
console.log("Fetching sights...");
|
|
||||||
await new Promise((res) => setTimeout(res, 600));
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 201,
|
|
||||||
name: "Спас на Крови",
|
|
||||||
description: "Храм Воскресения Христова",
|
|
||||||
latitude: 59.94,
|
|
||||||
longitude: 30.329,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 202,
|
|
||||||
name: "Казанский собор",
|
|
||||||
description: "Кафедральный собор",
|
|
||||||
latitude: 59.934,
|
|
||||||
longitude: 30.325,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- CONFIGURATION ---
|
// --- CONFIGURATION ---
|
||||||
export const mapConfig = {
|
export const mapConfig = {
|
||||||
@ -154,9 +97,11 @@ interface MapServiceConfig {
|
|||||||
|
|
||||||
interface HistoryState {
|
interface HistoryState {
|
||||||
action: string;
|
action: string;
|
||||||
state: string; // GeoJSON string
|
state: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FeatureType = "station" | "route" | "sight";
|
||||||
|
|
||||||
class MapService {
|
class MapService {
|
||||||
private map: Map | null;
|
private map: Map | null;
|
||||||
private vectorSource: VectorSource<Feature<Geometry>>;
|
private vectorSource: VectorSource<Feature<Geometry>>;
|
||||||
@ -164,14 +109,15 @@ class MapService {
|
|||||||
private tooltipElement: HTMLElement;
|
private tooltipElement: HTMLElement;
|
||||||
private tooltipOverlay: Overlay | null;
|
private tooltipOverlay: Overlay | null;
|
||||||
private mode: string | null;
|
private mode: string | null;
|
||||||
private currentDrawingType: string | null;
|
private currentDrawingType: "Point" | "LineString" | null;
|
||||||
|
private currentDrawingFeatureType: FeatureType | null;
|
||||||
private currentInteraction: Draw | null;
|
private currentInteraction: Draw | null;
|
||||||
private modifyInteraction: Modify;
|
private modifyInteraction: Modify;
|
||||||
private selectInteraction: Select;
|
private selectInteraction: Select;
|
||||||
private hoveredFeatureId: string | number | null;
|
private hoveredFeatureId: string | number | null;
|
||||||
private history: HistoryState[];
|
private history: HistoryState[];
|
||||||
private historyIndex: number;
|
private historyIndex: number;
|
||||||
private beforeModifyState: string | null; // GeoJSON string
|
private beforeModifyState: string | null;
|
||||||
private boundHandlePointerMove: (
|
private boundHandlePointerMove: (
|
||||||
event: MapBrowserEvent<PointerEvent>
|
event: MapBrowserEvent<PointerEvent>
|
||||||
) => void;
|
) => void;
|
||||||
@ -188,7 +134,9 @@ class MapService {
|
|||||||
private drawBusIconStyle: Style;
|
private drawBusIconStyle: Style;
|
||||||
private sightIconStyle: Style;
|
private sightIconStyle: Style;
|
||||||
private selectedSightIconStyle: Style;
|
private selectedSightIconStyle: Style;
|
||||||
|
private drawSightIconStyle: Style;
|
||||||
private universalHoverStylePoint: Style;
|
private universalHoverStylePoint: Style;
|
||||||
|
private hoverSightIconStyle: Style;
|
||||||
private universalHoverStyleLine: Style;
|
private universalHoverStyleLine: Style;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
@ -212,6 +160,7 @@ class MapService {
|
|||||||
this.tooltipOverlay = null;
|
this.tooltipOverlay = null;
|
||||||
this.mode = null;
|
this.mode = null;
|
||||||
this.currentDrawingType = null;
|
this.currentDrawingType = null;
|
||||||
|
this.currentDrawingFeatureType = null;
|
||||||
this.currentInteraction = null;
|
this.currentInteraction = null;
|
||||||
this.hoveredFeatureId = null;
|
this.hoveredFeatureId = null;
|
||||||
this.history = [];
|
this.history = [];
|
||||||
@ -226,11 +175,11 @@ class MapService {
|
|||||||
|
|
||||||
this.defaultStyle = new Style({
|
this.defaultStyle = new Style({
|
||||||
fill: new Fill({ color: "rgba(66, 153, 225, 0.2)" }),
|
fill: new Fill({ color: "rgba(66, 153, 225, 0.2)" }),
|
||||||
stroke: new Stroke({ color: "#3182ce", width: 2 }),
|
stroke: new Stroke({ color: "#3182ce", width: 3 }),
|
||||||
});
|
});
|
||||||
this.selectedStyle = new Style({
|
this.selectedStyle = new Style({
|
||||||
fill: new Fill({ color: "rgba(221, 107, 32, 0.3)" }),
|
fill: new Fill({ color: "rgba(221, 107, 32, 0.3)" }),
|
||||||
stroke: new Stroke({ color: "#dd6b20", width: 3 }),
|
stroke: new Stroke({ color: "#dd6b20", width: 4 }),
|
||||||
image: new CircleStyle({
|
image: new CircleStyle({
|
||||||
radius: 6,
|
radius: 6,
|
||||||
fill: new Fill({ color: "#dd6b20" }),
|
fill: new Fill({ color: "#dd6b20" }),
|
||||||
@ -249,6 +198,7 @@ class MapService {
|
|||||||
fill: new Fill({ color: "rgba(34, 197, 94, 0.7)" }),
|
fill: new Fill({ color: "rgba(34, 197, 94, 0.7)" }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.busIconStyle = new Style({
|
this.busIconStyle = new Style({
|
||||||
image: new CircleStyle({
|
image: new CircleStyle({
|
||||||
radius: 8,
|
radius: 8,
|
||||||
@ -270,20 +220,38 @@ class MapService {
|
|||||||
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
|
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sightIconStyle = new Style({
|
this.sightIconStyle = new Style({
|
||||||
image: new CircleStyle({
|
image: new RegularShape({
|
||||||
radius: 8,
|
fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }),
|
||||||
fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }), // Purple
|
stroke: new Stroke({ color: "#ffffff", width: 2 }),
|
||||||
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
|
points: 5,
|
||||||
|
radius: 12,
|
||||||
|
radius2: 6,
|
||||||
|
angle: 0,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
this.selectedSightIconStyle = new Style({
|
this.selectedSightIconStyle = new Style({
|
||||||
image: new CircleStyle({
|
image: new RegularShape({
|
||||||
radius: 10,
|
|
||||||
fill: new Fill({ color: "rgba(221, 107, 32, 0.9)" }),
|
fill: new Fill({ color: "rgba(221, 107, 32, 0.9)" }),
|
||||||
stroke: new Stroke({ color: "#ffffff", width: 2 }),
|
stroke: new Stroke({ color: "#ffffff", width: 2.5 }),
|
||||||
|
points: 5,
|
||||||
|
radius: 14,
|
||||||
|
radius2: 7,
|
||||||
|
angle: 0,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
this.drawSightIconStyle = new Style({
|
||||||
|
image: new RegularShape({
|
||||||
|
fill: new Fill({ color: "rgba(100, 180, 100, 0.8)" }),
|
||||||
|
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
|
||||||
|
points: 5,
|
||||||
|
radius: 12,
|
||||||
|
radius2: 6,
|
||||||
|
angle: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
this.universalHoverStylePoint = new Style({
|
this.universalHoverStylePoint = new Style({
|
||||||
image: new CircleStyle({
|
image: new CircleStyle({
|
||||||
radius: 11,
|
radius: 11,
|
||||||
@ -292,8 +260,19 @@ class MapService {
|
|||||||
}),
|
}),
|
||||||
zIndex: Infinity,
|
zIndex: Infinity,
|
||||||
});
|
});
|
||||||
|
this.hoverSightIconStyle = new Style({
|
||||||
|
image: new RegularShape({
|
||||||
|
fill: new Fill({ color: "rgba(255, 165, 0, 0.7)" }),
|
||||||
|
stroke: new Stroke({ color: "rgba(255,255,255,0.8)", width: 2 }),
|
||||||
|
points: 5,
|
||||||
|
radius: 15,
|
||||||
|
radius2: 7.5,
|
||||||
|
angle: 0,
|
||||||
|
}),
|
||||||
|
zIndex: Infinity,
|
||||||
|
});
|
||||||
this.universalHoverStyleLine = new Style({
|
this.universalHoverStyleLine = new Style({
|
||||||
stroke: new Stroke({ color: "rgba(255, 165, 0, 0.8)", width: 4.5 }),
|
stroke: new Stroke({ color: "rgba(255, 165, 0, 0.8)", width: 5 }),
|
||||||
zIndex: Infinity,
|
zIndex: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -326,7 +305,9 @@ class MapService {
|
|||||||
return selectedPointStyle;
|
return selectedPointStyle;
|
||||||
}
|
}
|
||||||
if (isHovered) {
|
if (isHovered) {
|
||||||
return this.universalHoverStylePoint;
|
return featureType === "sight"
|
||||||
|
? this.hoverSightIconStyle
|
||||||
|
: this.universalHoverStylePoint;
|
||||||
}
|
}
|
||||||
return defaultPointStyle;
|
return defaultPointStyle;
|
||||||
} else if (geometryType === "LineString") {
|
} else if (geometryType === "LineString") {
|
||||||
@ -339,7 +320,7 @@ class MapService {
|
|||||||
return this.defaultStyle;
|
return this.defaultStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.defaultStyle; // Fallback
|
return this.defaultStyle;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -382,7 +363,7 @@ class MapService {
|
|||||||
if (!renderCompleteHandled) {
|
if (!renderCompleteHandled) {
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
renderCompleteHandled = true;
|
renderCompleteHandled = true;
|
||||||
this.setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -400,10 +381,27 @@ class MapService {
|
|||||||
|
|
||||||
this.modifyInteraction = new Modify({
|
this.modifyInteraction = new Modify({
|
||||||
source: this.vectorSource,
|
source: this.vectorSource,
|
||||||
style: (f: FeatureLike) =>
|
style: (feature) => {
|
||||||
f.getGeometry()?.getType() === "Point"
|
const originalFeature = feature.get("features")[0];
|
||||||
? this.selectedBusIconStyle
|
if (
|
||||||
: this.selectedStyle,
|
originalFeature &&
|
||||||
|
originalFeature.getGeometry()?.getType() === "Point"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Style({
|
||||||
|
image: new CircleStyle({
|
||||||
|
radius: 5,
|
||||||
|
fill: new Fill({
|
||||||
|
color: "rgba(255, 255, 255, 0.7)",
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: "#0099ff",
|
||||||
|
width: 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
deleteCondition: (e: MapBrowserEvent<any>) => doubleClick(e),
|
deleteCondition: (e: MapBrowserEvent<any>) => doubleClick(e),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -411,10 +409,15 @@ class MapService {
|
|||||||
style: (featureLike: FeatureLike) => {
|
style: (featureLike: FeatureLike) => {
|
||||||
if (!featureLike || !featureLike.getGeometry) return this.defaultStyle;
|
if (!featureLike || !featureLike.getGeometry) return this.defaultStyle;
|
||||||
const feature = featureLike as Feature<Geometry>;
|
const feature = featureLike as Feature<Geometry>;
|
||||||
|
const featureType = feature.get("featureType");
|
||||||
const geometryType = feature.getGeometry()?.getType();
|
const geometryType = feature.getGeometry()?.getType();
|
||||||
return geometryType === "Point"
|
|
||||||
? this.selectedBusIconStyle
|
if (geometryType === "Point") {
|
||||||
: this.selectedStyle;
|
return featureType === "sight"
|
||||||
|
? this.selectedSightIconStyle
|
||||||
|
: this.selectedBusIconStyle;
|
||||||
|
}
|
||||||
|
return this.selectedStyle;
|
||||||
},
|
},
|
||||||
condition: (e: MapBrowserEvent<any>) => {
|
condition: (e: MapBrowserEvent<any>) => {
|
||||||
const isEdit = this.mode === "edit";
|
const isEdit = this.mode === "edit";
|
||||||
@ -507,10 +510,22 @@ class MapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public saveMapState(): void {
|
||||||
|
const geoJSON = this.getAllFeaturesAsGeoJSON();
|
||||||
|
if (geoJSON) {
|
||||||
|
console.log("Сохранение состояния карты (GeoJSON):", geoJSON);
|
||||||
|
alert(
|
||||||
|
"Данные карты выведены в консоль разработчика и готовы к отправке!"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert("Нет объектов для сохранения.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public loadFeaturesFromApi(
|
public loadFeaturesFromApi(
|
||||||
apiStations: ApiStation[],
|
apiStations: typeof mapStore.stations,
|
||||||
apiRoutes: ApiRoute[],
|
apiRoutes: typeof mapStore.routes,
|
||||||
apiSights: ApiSight[]
|
apiSights: typeof mapStore.sights
|
||||||
): void {
|
): void {
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
||||||
@ -518,6 +533,10 @@ class MapService {
|
|||||||
const featuresToAdd: Feature<Geometry>[] = [];
|
const featuresToAdd: Feature<Geometry>[] = [];
|
||||||
|
|
||||||
apiStations.forEach((station) => {
|
apiStations.forEach((station) => {
|
||||||
|
if (station.longitude == null || station.latitude == null) {
|
||||||
|
console.warn(`Station ${station.id} has null coordinates, skipping...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const point = new Point(
|
const point = new Point(
|
||||||
transform(
|
transform(
|
||||||
[station.longitude, station.latitude],
|
[station.longitude, station.latitude],
|
||||||
@ -532,9 +551,19 @@ class MapService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
apiRoutes.forEach((route) => {
|
apiRoutes.forEach((route) => {
|
||||||
const coordinates = route.path.map((coord) =>
|
if (!route.path || route.path.length === 0) {
|
||||||
transform(coord, "EPSG:4326", projection)
|
console.warn(`Route ${route.id} has no path coordinates, skipping...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const coordinates = route.path
|
||||||
|
.filter((coord) => coord[0] != null && coord[1] != null)
|
||||||
|
.map((coord) => transform(coord, "EPSG:4326", projection));
|
||||||
|
if (coordinates.length === 0) {
|
||||||
|
console.warn(
|
||||||
|
`Route ${route.id} has no valid coordinates after filtering, skipping...`
|
||||||
);
|
);
|
||||||
|
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 });
|
||||||
feature.setId(`route-${route.id}`);
|
feature.setId(`route-${route.id}`);
|
||||||
@ -543,6 +572,10 @@ class MapService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
apiSights.forEach((sight) => {
|
apiSights.forEach((sight) => {
|
||||||
|
if (sight.longitude == null || sight.latitude == null) {
|
||||||
|
console.warn(`Sight ${sight.id} has null coordinates, skipping...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const point = new Point(
|
const point = new Point(
|
||||||
transform([sight.longitude, sight.latitude], "EPSG:4326", projection)
|
transform([sight.longitude, sight.latitude], "EPSG:4326", projection)
|
||||||
);
|
);
|
||||||
@ -689,20 +722,35 @@ class MapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public startDrawing(type: "Point" | "LineString", options = {}): void {
|
public startDrawing(
|
||||||
|
type: "Point" | "LineString",
|
||||||
|
featureType: FeatureType
|
||||||
|
): void {
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
||||||
this.currentDrawingType = type;
|
this.currentDrawingType = type;
|
||||||
const drawingMode = `drawing-${type.toLowerCase()}`;
|
this.currentDrawingFeatureType = featureType;
|
||||||
|
|
||||||
|
const drawingMode = `drawing-${featureType}`;
|
||||||
this.setMode(drawingMode);
|
this.setMode(drawingMode);
|
||||||
|
|
||||||
if (this.currentInteraction instanceof Draw) {
|
if (this.currentInteraction instanceof Draw) {
|
||||||
this.map.removeInteraction(this.currentInteraction);
|
this.map.removeInteraction(this.currentInteraction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let styleForDrawing: Style;
|
||||||
|
if (featureType === "station") {
|
||||||
|
styleForDrawing = this.drawBusIconStyle;
|
||||||
|
} else if (featureType === "sight") {
|
||||||
|
styleForDrawing = this.drawSightIconStyle;
|
||||||
|
} else {
|
||||||
|
styleForDrawing = this.drawStyle;
|
||||||
|
}
|
||||||
|
|
||||||
this.currentInteraction = new Draw({
|
this.currentInteraction = new Draw({
|
||||||
source: this.vectorSource,
|
source: this.vectorSource,
|
||||||
type,
|
type,
|
||||||
style: type === "Point" ? this.drawBusIconStyle : this.drawStyle,
|
style: styleForDrawing,
|
||||||
...options,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let stateBeforeDraw: string | null = null;
|
let stateBeforeDraw: string | null = null;
|
||||||
@ -715,23 +763,23 @@ class MapService {
|
|||||||
this.addStateToHistory("draw-before", stateBeforeDraw);
|
this.addStateToHistory("draw-before", stateBeforeDraw);
|
||||||
}
|
}
|
||||||
const feature = event.feature as Feature<Geometry>;
|
const feature = event.feature as Feature<Geometry>;
|
||||||
const geometry = feature.getGeometry();
|
const fType = this.currentDrawingFeatureType;
|
||||||
if (!geometry) return;
|
if (!fType) return;
|
||||||
const geometryType = geometry.getType();
|
|
||||||
|
feature.set("featureType", fType);
|
||||||
|
|
||||||
let baseName = "";
|
let baseName = "";
|
||||||
let namePrefix = "";
|
let namePrefix = "";
|
||||||
|
|
||||||
if (geometryType === "Point") {
|
if (fType === "station") {
|
||||||
baseName = "Станция";
|
baseName = "Станция";
|
||||||
namePrefix = "Станция ";
|
namePrefix = "Станция ";
|
||||||
feature.set("featureType", "station");
|
} else if (fType === "sight") {
|
||||||
} else if (geometryType === "LineString") {
|
baseName = "Достопримечательность";
|
||||||
|
namePrefix = "Достопримечательность ";
|
||||||
|
} else if (fType === "route") {
|
||||||
baseName = "Маршрут";
|
baseName = "Маршрут";
|
||||||
namePrefix = "Маршрут ";
|
namePrefix = "Маршрут ";
|
||||||
feature.set("featureType", "route");
|
|
||||||
} else {
|
|
||||||
baseName = "Объект";
|
|
||||||
namePrefix = "Объект ";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingNamedFeatures = this.vectorSource
|
const existingNamedFeatures = this.vectorSource
|
||||||
@ -739,7 +787,7 @@ class MapService {
|
|||||||
.filter(
|
.filter(
|
||||||
(f) =>
|
(f) =>
|
||||||
f !== feature &&
|
f !== feature &&
|
||||||
f.getGeometry()?.getType() === geometryType &&
|
f.get("featureType") === fType &&
|
||||||
f.get("name") &&
|
f.get("name") &&
|
||||||
(f.get("name") as string).startsWith(namePrefix)
|
(f.get("name") as string).startsWith(namePrefix)
|
||||||
);
|
);
|
||||||
@ -757,22 +805,22 @@ class MapService {
|
|||||||
const newNumber = maxNumber + 1;
|
const newNumber = maxNumber + 1;
|
||||||
feature.set("name", `${baseName} ${newNumber}`);
|
feature.set("name", `${baseName} ${newNumber}`);
|
||||||
|
|
||||||
// DO NOT set style directly on the feature, so it uses the layer's style function
|
if (this.currentDrawingType === "LineString") {
|
||||||
// which handles hover effects.
|
this.finishDrawing();
|
||||||
// feature.setStyle(
|
}
|
||||||
// type === "Point" ? this.busIconStyle : this.defaultStyle
|
|
||||||
// );
|
|
||||||
|
|
||||||
if (type === "LineString") this.finishDrawing();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.addInteraction(this.currentInteraction);
|
this.map.addInteraction(this.currentInteraction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public startDrawingMarker(): void {
|
public startDrawingMarker(): void {
|
||||||
this.startDrawing("Point");
|
this.startDrawing("Point", "station");
|
||||||
}
|
}
|
||||||
public startDrawingLine(): void {
|
public startDrawingLine(): void {
|
||||||
this.startDrawing("LineString");
|
this.startDrawing("LineString", "route");
|
||||||
|
}
|
||||||
|
public startDrawingSight(): void {
|
||||||
|
this.startDrawing("Point", "sight");
|
||||||
}
|
}
|
||||||
|
|
||||||
public finishDrawing(): void {
|
public finishDrawing(): void {
|
||||||
@ -782,12 +830,13 @@ class MapService {
|
|||||||
try {
|
try {
|
||||||
this.currentInteraction.finishDrawing();
|
this.currentInteraction.finishDrawing();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Drawing could not be finished (e.g., LineString with 1 point)
|
// Drawing could not be finished
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.map.removeInteraction(this.currentInteraction);
|
this.map.removeInteraction(this.currentInteraction);
|
||||||
this.currentInteraction = null;
|
this.currentInteraction = null;
|
||||||
this.currentDrawingType = null;
|
this.currentDrawingType = null;
|
||||||
|
this.currentDrawingFeatureType = null;
|
||||||
this.activateEditMode();
|
this.activateEditMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -839,17 +888,17 @@ class MapService {
|
|||||||
const newHoveredFeatureId = featureAtPixel ? featureAtPixel.getId() : null;
|
const newHoveredFeatureId = featureAtPixel ? featureAtPixel.getId() : null;
|
||||||
|
|
||||||
if (this.tooltipOverlay && this.tooltipElement) {
|
if (this.tooltipOverlay && this.tooltipElement) {
|
||||||
const featureType = featureAtPixel?.get("featureType");
|
if (this.mode === "edit" && featureAtPixel) {
|
||||||
if (
|
const name = featureAtPixel.get("name");
|
||||||
featureAtPixel &&
|
if (name) {
|
||||||
(featureType === "station" || featureType === "sight")
|
this.tooltipElement.innerHTML = name as string;
|
||||||
) {
|
|
||||||
this.tooltipElement.innerHTML =
|
|
||||||
(featureAtPixel.get("name") as string) || "Объект";
|
|
||||||
this.tooltipOverlay.setPosition(event.coordinate);
|
this.tooltipOverlay.setPosition(event.coordinate);
|
||||||
} else {
|
} else {
|
||||||
this.tooltipOverlay.setPosition(undefined);
|
this.tooltipOverlay.setPosition(undefined);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.tooltipOverlay.setPosition(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hoveredFeatureId !== newHoveredFeatureId) {
|
if (this.hoveredFeatureId !== newHoveredFeatureId) {
|
||||||
@ -900,8 +949,16 @@ class MapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteFeature(featureId: string | number | undefined): void {
|
public deleteFeature(
|
||||||
|
featureId: string | number | undefined,
|
||||||
|
recourse: string
|
||||||
|
): void {
|
||||||
if (featureId === undefined) return;
|
if (featureId === undefined) return;
|
||||||
|
const id = featureId.toString().split("-")[1];
|
||||||
|
if (recourse) {
|
||||||
|
mapStore.deleteRecourse(recourse, Number(id));
|
||||||
|
toast.success("Объект успешно удален");
|
||||||
|
}
|
||||||
const feature = this.vectorSource.getFeatureById(featureId);
|
const feature = this.vectorSource.getFeatureById(featureId);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
const currentState = this.getCurrentStateAsGeoJSON();
|
const currentState = this.getCurrentStateAsGeoJSON();
|
||||||
@ -918,22 +975,20 @@ class MapService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- ИСПРАВЛЕННЫЙ МЕТОД ---
|
||||||
public getAllFeaturesAsGeoJSON(): string | null {
|
public getAllFeaturesAsGeoJSON(): string | null {
|
||||||
if (!this.vectorSource || !this.map) return null;
|
if (!this.vectorSource || !this.map) return null;
|
||||||
const feats = this.vectorSource.getFeatures();
|
const feats = this.vectorSource.getFeatures();
|
||||||
if (feats.length === 0) return null;
|
if (feats.length === 0) return null;
|
||||||
|
|
||||||
const geoJSONFmt = new GeoJSON();
|
const geoJSONFmt = new GeoJSON();
|
||||||
const featsExp = feats.map((f) => {
|
|
||||||
const cF = f.clone();
|
// Просто передаем опции трансформации в метод writeFeatures.
|
||||||
cF.setProperties(f.getProperties(), true);
|
// Он сам всё сделает правильно, не трогая оригинальные объекты.
|
||||||
cF.setId(f.getId());
|
return geoJSONFmt.writeFeatures(feats, {
|
||||||
const geom = cF.getGeometry();
|
dataProjection: "EPSG:4326", // В какую проекцию конвертировать (стандарт для GeoJSON)
|
||||||
if (geom)
|
featureProjection: this.map.getView().getProjection(), // В какой проекции находятся объекты на карте
|
||||||
geom.transform(this.map!.getView().getProjection(), "EPSG:4326");
|
|
||||||
return cF;
|
|
||||||
});
|
});
|
||||||
return geoJSONFmt.writeFeatures(featsExp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
@ -998,14 +1053,21 @@ const MapControls: React.FC<MapControlsProps> = ({
|
|||||||
action: () => mapService.activateEditMode(),
|
action: () => mapService.activateEditMode(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: "drawing-point",
|
mode: "drawing-station",
|
||||||
title: "Станция",
|
title: "Станция",
|
||||||
longTitle: "Добавить станцию",
|
longTitle: "Добавить станцию",
|
||||||
icon: <Bus size={16} className="mr-1 sm:mr-2" />,
|
icon: <Bus size={16} className="mr-1 sm:mr-2" />,
|
||||||
action: () => mapService.startDrawingMarker(),
|
action: () => mapService.startDrawingMarker(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: "drawing-linestring",
|
mode: "drawing-sight",
|
||||||
|
title: "Место",
|
||||||
|
longTitle: "Добавить достопримечательность",
|
||||||
|
icon: <Landmark size={16} className="mr-1 sm:mr-2" />,
|
||||||
|
action: () => mapService.startDrawingSight(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "drawing-route",
|
||||||
title: "Маршрут",
|
title: "Маршрут",
|
||||||
longTitle: "Добавить маршрут",
|
longTitle: "Добавить маршрут",
|
||||||
icon: <LineIconSvg />,
|
icon: <LineIconSvg />,
|
||||||
@ -1046,6 +1108,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
selectedFeature,
|
selectedFeature,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeSection, setActiveSection] = useState<string | null>("layers");
|
const [activeSection, setActiveSection] = useState<string | null>("layers");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const toggleSection = (id: string) =>
|
const toggleSection = (id: string) =>
|
||||||
setActiveSection(activeSection === id ? null : id);
|
setActiveSection(activeSection === id ? null : id);
|
||||||
|
|
||||||
@ -1057,16 +1121,33 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteFeature = useCallback(
|
const handleDeleteFeature = useCallback(
|
||||||
(id: string | number | undefined) => {
|
(id: string | number | undefined, recourse: string) => {
|
||||||
if (
|
if (
|
||||||
mapService &&
|
mapService &&
|
||||||
window.confirm("Вы действительно хотите удалить этот объект?")
|
window.confirm("Вы действительно хотите удалить этот объект?")
|
||||||
)
|
)
|
||||||
mapService.deleteFeature(id);
|
mapService.deleteFeature(id, recourse);
|
||||||
},
|
},
|
||||||
[mapService]
|
[mapService]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleEditFeature = useCallback(
|
||||||
|
(featureType: string | undefined, fullId: string | number | undefined) => {
|
||||||
|
if (!featureType || !fullId) return;
|
||||||
|
const numericId = String(fullId).split("-")[1];
|
||||||
|
if (!numericId) {
|
||||||
|
console.error("Could not parse numeric ID from", fullId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`/${featureType}/${numericId}/edit`);
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
mapStore.handleSave(mapService?.getAllFeaturesAsGeoJSON() || "");
|
||||||
|
}, [mapService]);
|
||||||
|
|
||||||
const stations = mapFeatures.filter(
|
const stations = mapFeatures.filter(
|
||||||
(f) =>
|
(f) =>
|
||||||
f.get("featureType") === "station" ||
|
f.get("featureType") === "station" ||
|
||||||
@ -1109,7 +1190,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => handleFeatureClick(sId)}
|
onClick={() => handleFeatureClick(sId)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col text-gray-800 text-sm flex-grow mr-2">
|
<div className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<MapPin
|
<MapPin
|
||||||
size={16}
|
size={16}
|
||||||
@ -1131,17 +1212,29 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-shrink-0 flex items-center space-x-1 opacity-60 group-hover:opacity-100">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteFeature(sId);
|
handleEditFeature(s.get("featureType"), sId);
|
||||||
}}
|
}}
|
||||||
className="p-1 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-100 transition-colors flex-shrink-0 opacity-60 group-hover:opacity-100"
|
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
|
||||||
|
title="Редактировать детали"
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFeature(sId, "station");
|
||||||
|
}}
|
||||||
|
className="p-1 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-100 transition-colors"
|
||||||
title="Удалить остановку"
|
title="Удалить остановку"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
@ -1178,7 +1271,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => handleFeatureClick(lId)}
|
onClick={() => handleFeatureClick(lId)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col text-gray-800 text-sm flex-grow mr-2">
|
<div className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0">
|
||||||
<div className="flex items-center mb-0.5">
|
<div className="flex items-center mb-0.5">
|
||||||
<ArrowRightLeft
|
<ArrowRightLeft
|
||||||
size={16}
|
size={16}
|
||||||
@ -1205,17 +1298,29 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-shrink-0 flex items-center space-x-1 opacity-60 group-hover:opacity-100">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteFeature(lId);
|
handleEditFeature(l.get("featureType"), lId);
|
||||||
}}
|
}}
|
||||||
className="p-1 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-100 transition-colors flex-shrink-0 opacity-60 group-hover:opacity-100"
|
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
|
||||||
|
title="Редактировать детали"
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFeature(lId, "route");
|
||||||
|
}}
|
||||||
|
className="p-1 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-100 transition-colors"
|
||||||
title="Удалить маршрут"
|
title="Удалить маршрут"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
@ -1245,7 +1350,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => handleFeatureClick(sId)}
|
onClick={() => handleFeatureClick(sId)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center text-gray-800 text-sm flex-grow mr-2">
|
<div className="flex items-center text-gray-800 text-sm flex-grow mr-2 min-w-0">
|
||||||
<Landmark
|
<Landmark
|
||||||
size={16}
|
size={16}
|
||||||
className={`mr-1.5 flex-shrink-0 ${
|
className={`mr-1.5 flex-shrink-0 ${
|
||||||
@ -1265,17 +1370,29 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
{sName}
|
{sName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-shrink-0 flex items-center space-x-1 opacity-60 group-hover:opacity-100">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteFeature(sId);
|
handleEditFeature(s.get("featureType"), sId);
|
||||||
}}
|
}}
|
||||||
className="p-1 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-100 transition-colors flex-shrink-0 opacity-60 group-hover:opacity-100"
|
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
|
||||||
|
title="Редактировать детали"
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFeature(sId, "sight");
|
||||||
|
}}
|
||||||
|
className="p-1 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-100 transition-colors"
|
||||||
title="Удалить достопримечательность"
|
title="Удалить достопримечательность"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
@ -1298,13 +1415,17 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72 md:w-80 bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-screen">
|
<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">
|
<div className="p-4 bg-gray-700 text-white">
|
||||||
<h2 className="text-lg font-semibold">Панель управления</h2>
|
<h2 className="text-lg font-semibold">Панель управления</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 flex flex-col">
|
||||||
|
<div className="flex-1 overflow-y-auto max-h-[70%]">
|
||||||
{sections.map((s) => (
|
{sections.map((s) => (
|
||||||
<div key={s.id} className="border-b border-gray-200 last:border-b-0">
|
<div
|
||||||
|
key={s.id}
|
||||||
|
className="border-b border-gray-200 last:border-b-0"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSection(s.id)}
|
onClick={() => toggleSection(s.id)}
|
||||||
className={`w-full p-3 flex items-center justify-between text-left hover:bg-gray-100 transition-colors duration-150 focus:outline-none focus:bg-gray-100 ${
|
className={`w-full p-3 flex items-center justify-between text-left hover:bg-gray-100 transition-colors duration-150 focus:outline-none focus:bg-gray-100 ${
|
||||||
@ -1346,6 +1467,15 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="m-3 w-[90%] flex items-center justify-center px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<Save size={16} className="mr-2" />
|
||||||
|
Сохранить изменения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1355,7 +1485,9 @@ export const MapPage: React.FC = () => {
|
|||||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [mapServiceInstance, setMapServiceInstance] =
|
const [mapServiceInstance, setMapServiceInstance] =
|
||||||
useState<MapService | null>(null);
|
useState<MapService | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
// --- ИЗМЕНЕНИЕ: Разделение состояния загрузки ---
|
||||||
|
const [isMapLoading, setIsMapLoading] = useState<boolean>(true); // Для рендеринга карты
|
||||||
|
const [isDataLoading, setIsDataLoading] = useState<boolean>(true); // Для загрузки данных с API
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [currentMapMode, setCurrentMapMode] = useState<string>("edit");
|
const [currentMapMode, setCurrentMapMode] = useState<string>("edit");
|
||||||
const [mapFeatures, setMapFeatures] = useState<Feature<Geometry>[]>([]);
|
const [mapFeatures, setMapFeatures] = useState<Feature<Geometry>[]>([]);
|
||||||
@ -1376,12 +1508,38 @@ export const MapPage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let service: MapService | null = null;
|
let service: MapService | null = null;
|
||||||
if (mapRef.current && tooltipRef.current && !mapServiceInstance) {
|
if (mapRef.current && tooltipRef.current && !mapServiceInstance) {
|
||||||
setIsLoading(true);
|
// Изначально оба процесса загрузки активны
|
||||||
|
setIsMapLoading(true);
|
||||||
|
setIsDataLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// --- ИЗМЕНЕНИЕ: Логика загрузки данных вынесена и управляет своим состоянием ---
|
||||||
|
const loadInitialData = async (mapService: MapService) => {
|
||||||
|
console.log("Starting data load...");
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
mapStore.getRoutes(),
|
||||||
|
mapStore.getStations(),
|
||||||
|
mapStore.getSights(),
|
||||||
|
]);
|
||||||
|
mapService.loadFeaturesFromApi(
|
||||||
|
mapStore.stations,
|
||||||
|
mapStore.routes,
|
||||||
|
mapStore.sights
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load initial map data:", e);
|
||||||
|
setError("Не удалось загрузить данные для карты.");
|
||||||
|
} finally {
|
||||||
|
// Завершаем состояние загрузки данных независимо от результата
|
||||||
|
setIsDataLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
service = new MapService(
|
service = new MapService(
|
||||||
{ ...mapConfig, target: mapRef.current },
|
{ ...mapConfig, target: mapRef.current },
|
||||||
setIsLoading,
|
setIsMapLoading, // MapService теперь управляет только состоянием загрузки карты
|
||||||
setError,
|
setError,
|
||||||
setCurrentMapMode,
|
setCurrentMapMode,
|
||||||
handleFeaturesChange,
|
handleFeaturesChange,
|
||||||
@ -1390,23 +1548,7 @@ export const MapPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
setMapServiceInstance(service);
|
setMapServiceInstance(service);
|
||||||
|
|
||||||
const loadInitialData = async (mapService: MapService) => {
|
// Запускаем загрузку данных
|
||||||
console.log("Starting data load...");
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
// Замените mockApi на реальные fetch запросы к вашему API
|
|
||||||
const [routes, stations, sights] = await Promise.all([
|
|
||||||
mockApi.getRoutes(),
|
|
||||||
mockApi.getStations(),
|
|
||||||
mockApi.getSights(),
|
|
||||||
]);
|
|
||||||
mapService.loadFeaturesFromApi(stations, routes, sights);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to load initial map data:", e);
|
|
||||||
setError("Не удалось загрузить данные для карты.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadInitialData(service);
|
loadInitialData(service);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("MapPage useEffect error:", e);
|
console.error("MapPage useEffect error:", e);
|
||||||
@ -1415,7 +1557,9 @@ export const MapPage: React.FC = () => {
|
|||||||
e.message || "Неизвестная ошибка"
|
e.message || "Неизвестная ошибка"
|
||||||
}. Пожалуйста, проверьте консоль.`
|
}. Пожалуйста, проверьте консоль.`
|
||||||
);
|
);
|
||||||
setIsLoading(false);
|
// В случае критической ошибки инициализации, завершаем все загрузки
|
||||||
|
setIsMapLoading(false);
|
||||||
|
setIsDataLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
@ -1427,6 +1571,9 @@ export const MapPage: React.FC = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const showLoader = isMapLoading || isDataLoading;
|
||||||
|
const showContent = mapServiceInstance && !showLoader && !error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row font-sans bg-gray-100 h-[90vh] overflow-hidden">
|
<div className="flex flex-col md:flex-row font-sans bg-gray-100 h-[90vh] overflow-hidden">
|
||||||
<div className="relative flex-grow flex">
|
<div className="relative flex-grow flex">
|
||||||
@ -1441,18 +1588,18 @@ export const MapPage: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
visibility: "hidden",
|
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
{isLoading && (
|
{/* --- ИЗМЕНЕНИЕ: Обновленный лоадер --- */}
|
||||||
|
{showLoader && (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-500 bg-opacity-50 z-[1001]">
|
<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>
|
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mb-3"></div>
|
||||||
<div className="text-md font-semibold text-white drop-shadow-md">
|
<div className="text-md font-semibold text-white drop-shadow-md">
|
||||||
Загрузка карты...
|
{isMapLoading ? "Загрузка карты..." : "Загрузка данных..."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && !isLoading && (
|
{error && !showLoader && (
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-red-100 border border-red-400 text-red-700 p-6 rounded-lg shadow-lg z-[1002] text-center max-w-md">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-red-100 border border-red-400 text-red-700 p-6 rounded-lg shadow-lg z-[1002] text-center max-w-md">
|
||||||
<h3 className="font-semibold text-lg mb-2">Произошла ошибка</h3>
|
<h3 className="font-semibold text-lg mb-2">Произошла ошибка</h3>
|
||||||
<p className="text-sm">{error}</p>
|
<p className="text-sm">{error}</p>
|
||||||
@ -1465,14 +1612,15 @@ export const MapPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{mapServiceInstance && !isLoading && !error && (
|
{/* --- ИЗМЕНЕНИЕ: Условие для отображения контента --- */}
|
||||||
|
{showContent && (
|
||||||
<MapControls
|
<MapControls
|
||||||
mapService={mapServiceInstance}
|
mapService={mapServiceInstance}
|
||||||
activeMode={currentMapMode}
|
activeMode={currentMapMode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{mapServiceInstance && !isLoading && !error && (
|
{showContent && (
|
||||||
<MapSightbar
|
<MapSightbar
|
||||||
mapService={mapServiceInstance}
|
mapService={mapServiceInstance}
|
||||||
mapFeatures={mapFeatures}
|
mapFeatures={mapFeatures}
|
||||||
|
129
src/pages/MapPage/mapStore.ts
Normal file
129
src/pages/MapPage/mapStore.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { languageInstance } from "@shared";
|
||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
|
interface ApiRoute {
|
||||||
|
id: number;
|
||||||
|
route_number: string;
|
||||||
|
path: [number, number][];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiStation {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiSight {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapStore {
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
routes: ApiRoute[] = [];
|
||||||
|
stations: ApiStation[] = [];
|
||||||
|
sights: ApiSight[] = [];
|
||||||
|
|
||||||
|
getRoutes = async () => {
|
||||||
|
const routes = await languageInstance("ru").get("/route");
|
||||||
|
|
||||||
|
const mappedRoutes = routes.data.map((route: any) => ({
|
||||||
|
id: route.id,
|
||||||
|
route_number: route.route_number,
|
||||||
|
path: route.path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.routes = mappedRoutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
getStations = async () => {
|
||||||
|
const stations = await languageInstance("ru").get("/station");
|
||||||
|
const mappedStations = stations.data.map((station: any) => ({
|
||||||
|
id: station.id,
|
||||||
|
name: station.name,
|
||||||
|
latitude: station.latitude,
|
||||||
|
longitude: station.longitude,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.stations = mappedStations;
|
||||||
|
};
|
||||||
|
|
||||||
|
getSights = async () => {
|
||||||
|
const sights = await languageInstance("ru").get("/sight");
|
||||||
|
const mappedSights = sights.data.map((sight: any) => ({
|
||||||
|
id: sight.id,
|
||||||
|
name: sight.name,
|
||||||
|
description: sight.description,
|
||||||
|
latitude: sight.latitude,
|
||||||
|
longitude: sight.longitude,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.sights = mappedSights;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRecourse = async (recourse: string, id: number) => {
|
||||||
|
await languageInstance("ru").delete(`/${recourse}/${id}`);
|
||||||
|
if (recourse === "route") {
|
||||||
|
this.routes = this.routes.filter((route) => route.id !== id);
|
||||||
|
} else if (recourse === "station") {
|
||||||
|
this.stations = this.stations.filter((station) => station.id !== id);
|
||||||
|
} else if (recourse === "sight") {
|
||||||
|
this.sights = this.sights.filter((sight) => sight.id !== id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSave = async (json: string) => {
|
||||||
|
const sights: any[] = [];
|
||||||
|
const routes: any[] = [];
|
||||||
|
const stations: any[] = [];
|
||||||
|
|
||||||
|
const parsedJSON = JSON.parse(json);
|
||||||
|
|
||||||
|
console.log(parsedJSON);
|
||||||
|
parsedJSON.features.forEach((feature: any) => {
|
||||||
|
const { geometry, properties, id } = feature;
|
||||||
|
const idCanBeSplited = id.split("-");
|
||||||
|
|
||||||
|
if (!(idCanBeSplited.length > 1)) {
|
||||||
|
if (properties.featureType === "station") {
|
||||||
|
stations.push({
|
||||||
|
name: properties.name || "",
|
||||||
|
latitude: geometry.coordinates[1],
|
||||||
|
longitude: geometry.coordinates[0],
|
||||||
|
});
|
||||||
|
} else if (properties.featureType === "route") {
|
||||||
|
routes.push({
|
||||||
|
route_number: properties.name || "",
|
||||||
|
path: geometry.coordinates,
|
||||||
|
});
|
||||||
|
} else if (properties.featureType === "sight") {
|
||||||
|
sights.push({
|
||||||
|
name: properties.name || "",
|
||||||
|
description: properties.description || "",
|
||||||
|
latitude: geometry.coordinates[1],
|
||||||
|
longitude: geometry.coordinates[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const station of stations) {
|
||||||
|
await languageInstance("ru").post("/station", station);
|
||||||
|
}
|
||||||
|
for (const route of routes) {
|
||||||
|
await languageInstance("ru").post("/route", route);
|
||||||
|
}
|
||||||
|
for (const sight of sights) {
|
||||||
|
await languageInstance("ru").post("/sight", sight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new MapStore();
|
@ -164,7 +164,7 @@ export const MediaEditPage = observer(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<ArrowLeft size={20} />}
|
startIcon={<ArrowLeft size={20} />}
|
||||||
onClick={() => navigate("/media")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
Назад
|
Назад
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -6,20 +6,94 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import { carrierStore } from "../../../shared/store/CarrierStore";
|
||||||
|
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||||
|
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [carrier, setCarrier] = useState<string>("");
|
||||||
const [routeNumber, setRouteNumber] = useState("");
|
const [routeNumber, setRouteNumber] = useState("");
|
||||||
const [direction, setDirection] = useState("");
|
const [routeCoords, setRouteCoords] = useState("");
|
||||||
|
const [govRouteNumber, setGovRouteNumber] = useState("");
|
||||||
|
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
||||||
|
const [direction, setDirection] = useState("backward");
|
||||||
|
const [scaleMin, setScaleMin] = useState("");
|
||||||
|
const [scaleMax, setScaleMax] = useState("");
|
||||||
|
const [turn, setTurn] = useState("");
|
||||||
|
const [centerLat, setCenterLat] = useState("");
|
||||||
|
const [centerLng, setCenterLng] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
carrierStore.getCarriers();
|
||||||
|
articlesStore.getArticleList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateRoute = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
// Преобразуем значения в нужные типы
|
||||||
|
const carrier_id = Number(carrier);
|
||||||
|
const governor_appeal = Number(governorAppeal);
|
||||||
|
const scale_min = scaleMin ? Number(scaleMin) : undefined;
|
||||||
|
const scale_max = scaleMax ? Number(scaleMax) : undefined;
|
||||||
|
const rotate = turn ? Number(turn) : undefined;
|
||||||
|
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||||
|
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||||
|
const route_direction = direction === "forward";
|
||||||
|
// Координаты маршрута как массив массивов чисел
|
||||||
|
const path = routeCoords
|
||||||
|
.split("\n")
|
||||||
|
.map((line) =>
|
||||||
|
line
|
||||||
|
.split(" ")
|
||||||
|
.map((coord) => Number(coord.trim()))
|
||||||
|
.filter((n) => !isNaN(n))
|
||||||
|
)
|
||||||
|
.filter((arr) => arr.length === 2);
|
||||||
|
|
||||||
|
// Собираем объект маршрута
|
||||||
|
const newRoute: Partial<Route> = {
|
||||||
|
carrier:
|
||||||
|
carrierStore.carriers.data.find((c: any) => c.id === carrier_id)
|
||||||
|
?.full_name || "",
|
||||||
|
carrier_id,
|
||||||
|
route_number: routeNumber,
|
||||||
|
route_sys_number: govRouteNumber,
|
||||||
|
governor_appeal,
|
||||||
|
route_direction,
|
||||||
|
scale_min,
|
||||||
|
scale_max,
|
||||||
|
rotate,
|
||||||
|
center_latitude,
|
||||||
|
center_longitude,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправка на сервер (пример, если есть routeStore.createRoute)
|
||||||
|
let createdRoute: Route | null = null;
|
||||||
|
|
||||||
|
await routeStore.createRoute(newRoute);
|
||||||
|
toast.success("Маршрут успешно создан");
|
||||||
|
navigate(-1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Произошла ошибка при создании маршрута");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -28,11 +102,31 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Маршруты / Создать
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold">Создание маршрута</h1>
|
<Typography variant="h5" fontWeight={700}>
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
Создать маршрут
|
||||||
|
</Typography>
|
||||||
|
<Box className="flex flex-col gap-6 w-full">
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={carrier}
|
||||||
|
label="Выберите перевозчика"
|
||||||
|
onChange={(e) => setCarrier(e.target.value as string)}
|
||||||
|
disabled={carrierStore.carriers.data.length === 0}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{carrierStore.carriers.data.map(
|
||||||
|
(c: (typeof carrierStore.carriers.data)[number]) => (
|
||||||
|
<MenuItem key={c.id} value={c.id}>
|
||||||
|
{c.full_name}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Номер маршрута"
|
label="Номер маршрута"
|
||||||
@ -40,38 +134,88 @@ export const RouteCreatePage = observer(() => {
|
|||||||
value={routeNumber}
|
value={routeNumber}
|
||||||
onChange={(e) => setRouteNumber(e.target.value)}
|
onChange={(e) => setRouteNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<FormControl fullWidth>
|
<TextField
|
||||||
<InputLabel>Направление</InputLabel>
|
className="w-full"
|
||||||
<Select
|
label="Координаты маршрута"
|
||||||
value={direction}
|
multiline
|
||||||
label="Направление"
|
minRows={3}
|
||||||
onChange={(e) => setDirection(e.target.value)}
|
value={routeCoords}
|
||||||
|
onChange={(e) => setRouteCoords(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Номер маршрута в Говорящем Городе"
|
||||||
required
|
required
|
||||||
|
value={govRouteNumber}
|
||||||
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Обращение губернатора</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={governorAppeal}
|
||||||
|
label="Обращение губернатора"
|
||||||
|
onChange={(e) => setGovernorAppeal(e.target.value as string)}
|
||||||
|
disabled={articlesStore.articleList.ru.data.length === 0}
|
||||||
>
|
>
|
||||||
<MenuItem value="forward">Прямое</MenuItem>
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
<MenuItem value="backward">Обратное</MenuItem>
|
{articlesStore.articleList.ru.data.map(
|
||||||
|
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
||||||
|
<MenuItem key={a.id} value={a.id}>
|
||||||
|
{a.heading}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={direction}
|
||||||
|
label="Прямой/обратный маршрут"
|
||||||
|
onChange={(e) => setDirection(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="forward">Прямой</MenuItem>
|
||||||
|
<MenuItem value="backward">Обратный</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (мин)"
|
||||||
|
value={scaleMin}
|
||||||
|
onChange={(e) => setScaleMin(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (макс)"
|
||||||
|
value={scaleMax}
|
||||||
|
onChange={(e) => setScaleMax(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Поворот"
|
||||||
|
value={turn}
|
||||||
|
onChange={(e) => setTurn(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. широта"
|
||||||
|
value={centerLat}
|
||||||
|
onChange={(e) => setCenterLat(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. долгота"
|
||||||
|
value={centerLng}
|
||||||
|
onChange={(e) => setCenterLng(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={async () => {
|
onClick={handleCreateRoute}
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
// await createRoute(routeNumber, direction === "forward");
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.success("Маршрут успешно создан");
|
|
||||||
navigate(-1);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("Произошла ошибка");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
@ -72,7 +72,7 @@ export const RouteListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = routes.map((route) => ({
|
const rows = routes.data.map((route) => ({
|
||||||
id: route.id,
|
id: route.id,
|
||||||
carrier: route.carrier,
|
carrier: route.carrier,
|
||||||
route_number: route.route_number,
|
route_number: route.route_number,
|
||||||
@ -81,8 +81,6 @@ export const RouteListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Маршруты</h1>
|
<h1 className="text-2xl">Маршруты</h1>
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from "./RouteListPage";
|
export * from "./RouteListPage";
|
||||||
|
export * from "./RouteCreatePage";
|
||||||
|
@ -38,7 +38,7 @@ export const SightListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
<button onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
{/* <button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
{/* <button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
||||||
|
@ -3,18 +3,13 @@ import { languageStore, snapshotStore } from "@shared";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DatabaseBackup, Eye, Trash2 } from "lucide-react";
|
import { DatabaseBackup, Eye, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import {
|
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
||||||
CreateButton,
|
|
||||||
DeleteModal,
|
|
||||||
LanguageSwitcher,
|
|
||||||
SnapshotRestore,
|
|
||||||
} from "@widgets";
|
|
||||||
|
|
||||||
export const SnapshotListPage = observer(() => {
|
export const SnapshotListPage = observer(() => {
|
||||||
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
|
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
|
||||||
snapshotStore;
|
snapshotStore;
|
||||||
const navigate = useNavigate();
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
@ -76,8 +71,6 @@ export const SnapshotListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl ">Снапшоты</h1>
|
<h1 className="text-2xl ">Снапшоты</h1>
|
||||||
|
179
src/pages/Station/StationEditPage/index.tsx
Normal file
179
src/pages/Station/StationEditPage/index.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Box,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft, Save, ImagePlus } 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";
|
||||||
|
|
||||||
|
export const StationEditPage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { id } = useParams();
|
||||||
|
const {
|
||||||
|
editStationData,
|
||||||
|
getEditStation,
|
||||||
|
setEditCommonData,
|
||||||
|
editStation,
|
||||||
|
setLanguageEditStationData,
|
||||||
|
} = stationsStore;
|
||||||
|
const { cities, getCities } = cityStore;
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await editStation(Number(id));
|
||||||
|
toast.success("Станция успешно обновлена");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating station:", error);
|
||||||
|
toast.error("Ошибка при обновлении станции");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAndSetStationData = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const stationId = Number(id);
|
||||||
|
await getEditStation(stationId);
|
||||||
|
await getCities(language);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAndSetStationData();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Название"
|
||||||
|
value={editStationData[language].name || ""}
|
||||||
|
required
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguageEditStationData(language, {
|
||||||
|
name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="direction-label"
|
||||||
|
value={editStationData.common.direction ? "Прямой" : "Обратный"}
|
||||||
|
label="Прямой/обратный маршрут"
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditCommonData({
|
||||||
|
direction: e.target.value === "Прямой",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="Прямой">Прямой</MenuItem>
|
||||||
|
<MenuItem value="Обратный">Обратный</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Описание"
|
||||||
|
value={editStationData[language].description || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguageEditStationData(language, {
|
||||||
|
description: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Адрес"
|
||||||
|
value={editStationData[language].address || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguageEditStationData(language, {
|
||||||
|
address: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Координаты"
|
||||||
|
value={`${editStationData.common.latitude} ${editStationData.common.longitude}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const [latitude, longitude] = e.target.value.split(" ").map(Number);
|
||||||
|
if (!isNaN(latitude) && !isNaN(longitude)) {
|
||||||
|
setEditCommonData({
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Город</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editStationData.common.city_id || ""}
|
||||||
|
label="Город"
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectedCity = cities[language].find(
|
||||||
|
(city) => city.id === e.target.value
|
||||||
|
);
|
||||||
|
setEditCommonData({
|
||||||
|
city_id: e.target.value as number,
|
||||||
|
city: selectedCity?.name || "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cities[language].map((city) => (
|
||||||
|
<MenuItem key={city.id} value={city.id}>
|
||||||
|
{city.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleEdit}
|
||||||
|
disabled={isLoading || !editStationData[language]?.name}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Обновить"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -2,19 +2,19 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, stationsStore } from "@shared";
|
import { languageStore, stationsStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const StationListPage = observer(() => {
|
export const StationListPage = observer(() => {
|
||||||
const { stations, getStations, deleteStation } = stationsStore;
|
const { stationLists, getStationList, deleteStation } = stationsStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getStations();
|
getStationList();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
@ -57,6 +57,9 @@ export const StationListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
|
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||||
|
<Pencil size={20} className="text-blue-500" />
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
@ -74,7 +77,7 @@ export const StationListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = stations.map((station) => ({
|
const rows = stationLists[language].data.map((station: any) => ({
|
||||||
id: station.id,
|
id: station.id,
|
||||||
name: station.name,
|
name: station.name,
|
||||||
system_name: station.system_name,
|
system_name: station.system_name,
|
||||||
|
77
src/pages/Station/StationPreviewPage/index.tsx
Normal file
77
src/pages/Station/StationPreviewPage/index.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Paper } from "@mui/material";
|
||||||
|
import { languageStore, stationsStore } from "@shared";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export const StationPreviewPage = observer(() => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { stationPreview, getStationPreview } = stationsStore;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (id) {
|
||||||
|
await getStationPreview(Number(id));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-10 w-full">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Название</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Системное название</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.system_name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Направление</h1>
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
stationPreview[id!]?.[language]?.data.direction
|
||||||
|
? "text-green-500"
|
||||||
|
: "text-red-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stationPreview[id!]?.[language]?.data.direction
|
||||||
|
? "Прямой"
|
||||||
|
: "Обратный"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stationPreview[id!]?.[language]?.data.address && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Адрес</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.address}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stationPreview[id!]?.[language]?.data.description && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-lg font-bold">Описание</h1>
|
||||||
|
<p>{stationPreview[id!]?.[language]?.data.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
@ -1 +1,4 @@
|
|||||||
export * from "./StationListPage";
|
export * from "./StationListPage";
|
||||||
|
export * from "./StationCreatePage";
|
||||||
|
export * from "./StationPreviewPage";
|
||||||
|
export * from "./StationEditPage";
|
||||||
|
@ -36,7 +36,7 @@ export const UserCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/user")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -52,7 +52,7 @@ export const UserEditPage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/user")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -79,7 +79,7 @@ export const UserListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = users.map((user) => ({
|
const rows = users.data?.map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
is_admin: user.is_admin,
|
is_admin: user.is_admin,
|
||||||
|
@ -32,7 +32,7 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
await vehicleStore.createVehicle(
|
await vehicleStore.createVehicle(
|
||||||
Number(tailNumber),
|
Number(tailNumber),
|
||||||
type,
|
type,
|
||||||
carrierStore.carriers.find((c) => c.id === carrierId)?.full_name!,
|
carrierStore.carriers.data.find((c) => c.id === carrierId)?.full_name!,
|
||||||
carrierId!
|
carrierId!
|
||||||
);
|
);
|
||||||
toast.success("Транспорт успешно создан");
|
toast.success("Транспорт успешно создан");
|
||||||
@ -48,7 +48,7 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/vehicle")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
@ -88,7 +88,7 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
required
|
required
|
||||||
onChange={(e) => setCarrierId(e.target.value as number)}
|
onChange={(e) => setCarrierId(e.target.value as number)}
|
||||||
>
|
>
|
||||||
{carrierStore.carriers.map((carrier) => (
|
{carrierStore.carriers.data.map((carrier) => (
|
||||||
<MenuItem key={carrier.id} value={carrier.id}>
|
<MenuItem key={carrier.id} value={carrier.id}>
|
||||||
{carrier.full_name}
|
{carrier.full_name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -56,7 +56,7 @@ export const VehicleEditPage = observer(() => {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate("/vehicle")}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
|
@ -25,18 +25,15 @@ export const VehicleListPage = observer(() => {
|
|||||||
field: "tail_number",
|
field: "tail_number",
|
||||||
headerName: "Бортовой номер",
|
headerName: "Бортовой номер",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: "type",
|
||||||
headerName: "Тип",
|
headerName: "Тип",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 items-center">
|
||||||
{VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
{VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
||||||
?.label || params.row.type}
|
?.label || params.row.type}
|
||||||
</div>
|
</div>
|
||||||
@ -47,21 +44,17 @@ export const VehicleListPage = observer(() => {
|
|||||||
field: "carrier",
|
field: "carrier",
|
||||||
headerName: "Перевозчик",
|
headerName: "Перевозчик",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "city",
|
field: "city",
|
||||||
headerName: "Город",
|
headerName: "Город",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
align: "center",
|
|
||||||
headerAlign: "center",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
flex: 1,
|
width: 200,
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
|
|
||||||
@ -88,13 +81,14 @@ export const VehicleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = vehicles.map((vehicle) => ({
|
const rows = vehicles.data?.map((vehicle) => ({
|
||||||
id: vehicle.vehicle.id,
|
id: vehicle.vehicle.id,
|
||||||
tail_number: vehicle.vehicle.tail_number,
|
tail_number: vehicle.vehicle.tail_number,
|
||||||
type: vehicle.vehicle.type,
|
type: vehicle.vehicle.type,
|
||||||
carrier: vehicle.vehicle.carrier,
|
carrier: vehicle.vehicle.carrier,
|
||||||
city: carriers.find((carrier) => carrier.id === vehicle.vehicle.carrier_id)
|
city: carriers.data?.find(
|
||||||
?.city,
|
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
||||||
|
)?.city,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
Newspaper,
|
Newspaper,
|
||||||
PersonStanding,
|
PersonStanding,
|
||||||
Cpu,
|
Cpu,
|
||||||
|
BookImage,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
export const DRAWER_WIDTH = 300;
|
export const DRAWER_WIDTH = 300;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ interface NavigationItem {
|
|||||||
path?: string;
|
path?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
nestedItems?: NavigationItem[];
|
nestedItems?: NavigationItem[];
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NAVIGATION_ITEMS: {
|
export const NAVIGATION_ITEMS: {
|
||||||
@ -77,18 +79,18 @@ export const NAVIGATION_ITEMS: {
|
|||||||
label: "Все сущности",
|
label: "Все сущности",
|
||||||
icon: Table,
|
icon: Table,
|
||||||
nestedItems: [
|
nestedItems: [
|
||||||
// {
|
{
|
||||||
// id: "media",
|
id: "media",
|
||||||
// label: "Медиа",
|
label: "Медиа",
|
||||||
// icon: BookImage,
|
icon: BookImage,
|
||||||
// path: "/media",
|
path: "/media",
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: "articles",
|
id: "articles",
|
||||||
// label: "Статьи",
|
label: "Статьи",
|
||||||
// icon: Newspaper,
|
icon: Newspaper,
|
||||||
// path: "/article",
|
path: "/article",
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
id: "attractions",
|
id: "attractions",
|
||||||
label: "Достопримечательности",
|
label: "Достопримечательности",
|
||||||
|
@ -15,6 +15,29 @@ type Media = {
|
|||||||
media_type: number;
|
media_type: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ArticleListCashed = {
|
||||||
|
ru: {
|
||||||
|
data: Article[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
en: {
|
||||||
|
data: Article[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
zh: {
|
||||||
|
data: Article[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreviewCashed = {
|
||||||
|
ru: Article;
|
||||||
|
en: Article;
|
||||||
|
zh: Article;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArticlePreviewCashed = Record<string, PreviewCashed>;
|
||||||
|
|
||||||
class ArticlesStore {
|
class ArticlesStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
@ -25,19 +48,47 @@ class ArticlesStore {
|
|||||||
en: [],
|
en: [],
|
||||||
zh: [],
|
zh: [],
|
||||||
};
|
};
|
||||||
articleList: Article[] = [];
|
articleList: ArticleListCashed = {
|
||||||
|
ru: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
articlePreview: ArticlePreviewCashed = {};
|
||||||
articleData: Article | null = null;
|
articleData: Article | null = null;
|
||||||
articleMedia: Media | null = null;
|
articleMedia: Media | null = null;
|
||||||
articleLoading: boolean = false;
|
articleLoading: boolean = false;
|
||||||
|
|
||||||
getArticleList = async () => {
|
getArticleList = async () => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
if (this.articleList[language].loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const response = await authInstance.get("/article");
|
const response = await authInstance.get("/article");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.articleList = response.data;
|
this.articleList[language].data = response.data;
|
||||||
|
this.articleList[language].loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getArticlePreview = async (id: number) => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
if (this.articlePreview[id][language]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await authInstance.get(`/article/${id}/preview`);
|
||||||
|
this.articlePreview[id][language] = response.data;
|
||||||
|
};
|
||||||
|
|
||||||
getArticles = async (language: Language) => {
|
getArticles = async (language: Language) => {
|
||||||
this.articleLoading = true;
|
this.articleLoading = true;
|
||||||
const response = await authInstance.get("/article");
|
const response = await authInstance.get("/article");
|
||||||
|
@ -14,23 +14,32 @@ export type Carrier = {
|
|||||||
right_color: string;
|
right_color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Carriers = Carrier[];
|
type Carriers = {
|
||||||
|
data: Carrier[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type CashedCarrier = Record<number, Carrier>;
|
type CashedCarrier = Record<number, Carrier>;
|
||||||
|
|
||||||
class CarrierStore {
|
class CarrierStore {
|
||||||
carriers: Carriers = [];
|
carriers: Carriers = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
carrier: CashedCarrier = {};
|
carrier: CashedCarrier = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCarriers = async () => {
|
getCarriers = async () => {
|
||||||
if (this.carriers.length > 0) return;
|
if (this.carriers.loaded) return;
|
||||||
|
|
||||||
const response = await authInstance.get("/carrier");
|
const response = await authInstance.get("/carrier");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers = response.data;
|
this.carriers.data = response.data;
|
||||||
|
this.carriers.loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,13 +47,15 @@ class CarrierStore {
|
|||||||
await authInstance.delete(`/carrier/${id}`);
|
await authInstance.delete(`/carrier/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers = this.carriers.filter((carrier) => carrier.id !== id);
|
this.carriers.data = this.carriers.data.filter(
|
||||||
|
(carrier) => carrier.id !== id
|
||||||
|
);
|
||||||
delete this.carrier[id];
|
delete this.carrier[id];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getCarrier = async (id: number) => {
|
getCarrier = async (id: number) => {
|
||||||
if (this.carrier[id]) return this.carrier[id];
|
if (this.carrier[id]) return;
|
||||||
const response = await authInstance.get(`/carrier/${id}`);
|
const response = await authInstance.get(`/carrier/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -90,7 +101,7 @@ class CarrierStore {
|
|||||||
logo: logoId,
|
logo: logoId,
|
||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers.push(response.data);
|
this.carriers.data.push(response.data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,7 +148,7 @@ class CarrierStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers = this.carriers.map((carrier) =>
|
this.carriers.data = this.carriers.data.map((carrier) =>
|
||||||
carrier.id === id ? { ...carrier, ...response.data } : carrier
|
carrier.id === id ? { ...carrier, ...response.data } : carrier
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -236,6 +236,7 @@ class CityStore {
|
|||||||
(country) => country.code === country_code
|
(country) => country.code === country_code
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (name) {
|
||||||
await languageInstance(language as Language).patch(`/city/${code}`, {
|
await languageInstance(language as Language).patch(`/city/${code}`, {
|
||||||
name,
|
name,
|
||||||
country: country?.name || "",
|
country: country?.name || "",
|
||||||
@ -270,6 +271,7 @@ class CityStore {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,17 +19,38 @@ export type Route = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class RouteStore {
|
class RouteStore {
|
||||||
routes: Route[] = [];
|
routes: {
|
||||||
|
data: Route[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
route: Record<string, Route> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoutes = async () => {
|
getRoutes = async () => {
|
||||||
|
if (this.routes.loaded) return;
|
||||||
const response = await authInstance.get("/route");
|
const response = await authInstance.get("/route");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.routes = response.data;
|
this.routes = {
|
||||||
|
data: response.data,
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createRoute = async (route: any) => {
|
||||||
|
const response = await authInstance.post("/route", route);
|
||||||
|
const id = response.data.id;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.route[id] = { ...route, id };
|
||||||
|
this.routes.data = [...this.routes.data, { ...route, id }];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,7 +58,10 @@ class RouteStore {
|
|||||||
await authInstance.delete(`/route/${id}`);
|
await authInstance.delete(`/route/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.routes = this.routes.filter((route) => route.id !== id);
|
this.routes = {
|
||||||
|
data: this.routes.data.filter((route) => route.id !== id),
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,42 @@
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance, languageInstance, languageStore } from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
|
type Language = "ru" | "en" | "zh";
|
||||||
|
|
||||||
|
type StationLanguageData = {
|
||||||
|
name: string;
|
||||||
|
system_name: string;
|
||||||
|
description: string;
|
||||||
|
address: string;
|
||||||
|
loaded: boolean; // Indicates if this language's data has been loaded/modified
|
||||||
|
};
|
||||||
|
|
||||||
|
type StationCommonData = {
|
||||||
|
city_id: number;
|
||||||
|
direction: boolean;
|
||||||
|
icon: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
offset_x: number;
|
||||||
|
offset_y: number;
|
||||||
|
transfers: {
|
||||||
|
bus: string;
|
||||||
|
metro_blue: string;
|
||||||
|
metro_green: string;
|
||||||
|
metro_orange: string;
|
||||||
|
metro_purple: string;
|
||||||
|
metro_red: string;
|
||||||
|
train: string;
|
||||||
|
tram: string;
|
||||||
|
trolleybus: string;
|
||||||
|
};
|
||||||
|
city: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditStationData = {
|
||||||
|
[key in Language]: StationLanguageData;
|
||||||
|
} & { common: StationCommonData };
|
||||||
|
|
||||||
type Station = {
|
type Station = {
|
||||||
id: number;
|
id: number;
|
||||||
address: string;
|
address: string;
|
||||||
@ -32,6 +68,77 @@ class StationsStore {
|
|||||||
stations: Station[] = [];
|
stations: Station[] = [];
|
||||||
station: Station | null = null;
|
station: Station | null = null;
|
||||||
|
|
||||||
|
stationLists: {
|
||||||
|
[key in Language]: {
|
||||||
|
data: Station[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
ru: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// This will store the full station data, keyed by ID and then by language
|
||||||
|
stationPreview: Record<
|
||||||
|
string,
|
||||||
|
Record<string, { loaded: boolean; data: Station }>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
editStationData: EditStationData = {
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
city: "",
|
||||||
|
city_id: 0,
|
||||||
|
direction: false,
|
||||||
|
icon: "",
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
offset_x: 0,
|
||||||
|
offset_y: 0,
|
||||||
|
transfers: {
|
||||||
|
bus: "",
|
||||||
|
metro_blue: "",
|
||||||
|
metro_green: "",
|
||||||
|
metro_orange: "",
|
||||||
|
metro_purple: "",
|
||||||
|
metro_red: "",
|
||||||
|
train: "",
|
||||||
|
tram: "",
|
||||||
|
trolleybus: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
@ -44,11 +151,157 @@ class StationsStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getStationList = async () => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
if (this.stationLists[language].loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await authInstance.get("/station");
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.stationLists[language].data = response.data;
|
||||||
|
this.stationLists[language].loaded = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditCommonData = (data: Partial<StationCommonData>) => {
|
||||||
|
this.editStationData.common = {
|
||||||
|
...this.editStationData.common,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getEditStation = async (id: number) => {
|
||||||
|
if (this.editStationData.ru.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ruResponse = await languageInstance("ru").get(`/station/${id}`);
|
||||||
|
const enResponse = await languageInstance("en").get(`/station/${id}`);
|
||||||
|
const zhResponse = await languageInstance("zh").get(`/station/${id}`);
|
||||||
|
|
||||||
|
this.editStationData = {
|
||||||
|
ru: {
|
||||||
|
name: ruResponse.data.name,
|
||||||
|
system_name: ruResponse.data.system_name,
|
||||||
|
description: ruResponse.data.description,
|
||||||
|
address: ruResponse.data.address,
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: enResponse.data.name,
|
||||||
|
system_name: enResponse.data.system_name,
|
||||||
|
description: enResponse.data.description,
|
||||||
|
address: enResponse.data.address,
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: zhResponse.data.name,
|
||||||
|
system_name: zhResponse.data.system_name,
|
||||||
|
description: zhResponse.data.description,
|
||||||
|
address: zhResponse.data.address,
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
city: ruResponse.data.city,
|
||||||
|
city_id: ruResponse.data.city_id,
|
||||||
|
direction: ruResponse.data.direction,
|
||||||
|
icon: ruResponse.data.icon,
|
||||||
|
latitude: ruResponse.data.latitude,
|
||||||
|
longitude: ruResponse.data.longitude,
|
||||||
|
offset_x: ruResponse.data.offset_x,
|
||||||
|
offset_y: ruResponse.data.offset_y,
|
||||||
|
transfers: ruResponse.data.transfers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sets language-specific station data
|
||||||
|
setLanguageEditStationData = (
|
||||||
|
language: Language,
|
||||||
|
data: Partial<StationLanguageData>
|
||||||
|
) => {
|
||||||
|
this.editStationData[language] = {
|
||||||
|
...this.editStationData[language],
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
editStation = async (id: number) => {
|
||||||
|
const commonDataPayload = {
|
||||||
|
city_id: this.editStationData.common.city_id,
|
||||||
|
direction: this.editStationData.common.direction,
|
||||||
|
icon: this.editStationData.common.icon,
|
||||||
|
latitude: this.editStationData.common.latitude,
|
||||||
|
longitude: this.editStationData.common.longitude,
|
||||||
|
offset_x: this.editStationData.common.offset_x,
|
||||||
|
offset_y: this.editStationData.common.offset_y,
|
||||||
|
transfers: this.editStationData.common.transfers,
|
||||||
|
city: this.editStationData.common.city,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const language of ["ru", "en", "zh"] as const) {
|
||||||
|
const { name, description, address } = this.editStationData[language];
|
||||||
|
const response = await languageInstance(language).patch(
|
||||||
|
`/station/${id}`,
|
||||||
|
{
|
||||||
|
name: name || "",
|
||||||
|
system_name: name || "", // system_name is often derived from name
|
||||||
|
description: description || "",
|
||||||
|
address: address || "",
|
||||||
|
...commonDataPayload,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
if (this.stationLists[language].data) {
|
||||||
|
this.stationLists[language].data = this.stationLists[
|
||||||
|
language
|
||||||
|
].data.map((station: Station) =>
|
||||||
|
station.id === id
|
||||||
|
? ({
|
||||||
|
...station,
|
||||||
|
name: response.data.name,
|
||||||
|
system_name: response.data.system_name,
|
||||||
|
description: response.data.description,
|
||||||
|
address: response.data.address,
|
||||||
|
...commonDataPayload,
|
||||||
|
} as Station)
|
||||||
|
: station
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
deleteStation = async (id: number) => {
|
deleteStation = async (id: number) => {
|
||||||
await authInstance.delete(`/station/${id}`);
|
await authInstance.delete(`/station/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.stations = this.stations.filter((station) => station.id !== id);
|
this.stations = this.stations.filter((station) => station.id !== id);
|
||||||
|
// Also clear from stationPreview cache
|
||||||
|
if (this.stationPreview[id]) {
|
||||||
|
delete this.stationPreview[id];
|
||||||
|
}
|
||||||
|
// Clear from stationLists as well for all languages
|
||||||
|
for (const lang of ["ru", "en", "zh"] as const) {
|
||||||
|
if (this.stationLists[lang].data) {
|
||||||
|
this.stationLists[lang].data = this.stationLists[lang].data.filter(
|
||||||
|
(station) => station.id !== id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,6 +310,29 @@ class StationsStore {
|
|||||||
this.station = response.data;
|
this.station = response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getStationPreview = async (id: number) => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
if (this.stationPreview[id]?.[language]?.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await languageInstance(language).get(`/station/${id}`);
|
||||||
|
runInAction(() => {
|
||||||
|
if (!this.stationPreview[id]) {
|
||||||
|
this.stationPreview[id] = {
|
||||||
|
ru: { loaded: false, data: {} as Station },
|
||||||
|
en: { loaded: false, data: {} as Station },
|
||||||
|
zh: { loaded: false, data: {} as Station },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.stationPreview[id][language] = {
|
||||||
|
data: response.data,
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
createStation = async (
|
createStation = async (
|
||||||
name: string,
|
name: string,
|
||||||
systemName: string,
|
systemName: string,
|
||||||
@ -69,8 +345,72 @@ class StationsStore {
|
|||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.stations.push(response.data);
|
this.stations.push(response.data);
|
||||||
|
const newStation = response.data as Station;
|
||||||
|
if (!this.stationPreview[newStation.id]) {
|
||||||
|
this.stationPreview[newStation.id] = {
|
||||||
|
ru: { loaded: false, data: newStation },
|
||||||
|
en: { loaded: false, data: newStation },
|
||||||
|
zh: { loaded: false, data: newStation },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.stationPreview[newStation.id]["ru"] = {
|
||||||
|
loaded: true,
|
||||||
|
data: newStation,
|
||||||
|
};
|
||||||
|
this.stationPreview[newStation.id]["en"] = {
|
||||||
|
loaded: true,
|
||||||
|
data: newStation,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset editStationData when navigating away or after saving
|
||||||
|
resetEditStationData = () => {
|
||||||
|
this.editStationData = {
|
||||||
|
ru: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
name: "",
|
||||||
|
system_name: "",
|
||||||
|
description: "",
|
||||||
|
address: "",
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
city: "",
|
||||||
|
city_id: 0,
|
||||||
|
direction: false,
|
||||||
|
icon: "",
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
offset_x: 0,
|
||||||
|
offset_y: 0,
|
||||||
|
transfers: {
|
||||||
|
bus: "",
|
||||||
|
metro_blue: "",
|
||||||
|
metro_green: "",
|
||||||
|
metro_orange: "",
|
||||||
|
metro_purple: "",
|
||||||
|
metro_red: "",
|
||||||
|
train: "",
|
||||||
|
tram: "",
|
||||||
|
trolleybus: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stationsStore = new StationsStore();
|
export const stationsStore = new StationsStore();
|
||||||
|
@ -10,7 +10,13 @@ export type User = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class UserStore {
|
class UserStore {
|
||||||
users: User[] = [];
|
users: {
|
||||||
|
data: User[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
user: Record<string, User> = {};
|
user: Record<string, User> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -18,12 +24,13 @@ class UserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUsers = async () => {
|
getUsers = async () => {
|
||||||
if (this.users.length > 0) return;
|
if (this.users.loaded) return;
|
||||||
|
|
||||||
const response = await authInstance.get("/user");
|
const response = await authInstance.get("/user");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users = response.data;
|
this.users.data = response.data;
|
||||||
|
this.users.loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,7 +49,7 @@ class UserStore {
|
|||||||
await authInstance.delete(`/user/${id}`);
|
await authInstance.delete(`/user/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users = this.users.filter((user) => user.id !== id);
|
this.users.data = this.users.data.filter((user) => user.id !== id);
|
||||||
delete this.user[id];
|
delete this.user[id];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -64,12 +71,15 @@ class UserStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createUser = async () => {
|
createUser = async () => {
|
||||||
const id = this.users[this.users.length - 1].id + 1;
|
let id = 1;
|
||||||
|
if (this.users.data.length > 0) {
|
||||||
|
id = this.users.data[this.users.data.length - 1].id + 1;
|
||||||
|
}
|
||||||
const response = await authInstance.post("/user", this.createUserData);
|
const response = await authInstance.post("/user", this.createUserData);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users.push({
|
this.users.data.push({
|
||||||
id,
|
id: id,
|
||||||
...response.data,
|
...response.data,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -95,7 +105,7 @@ class UserStore {
|
|||||||
const response = await authInstance.patch(`/user/${id}`, this.editUserData);
|
const response = await authInstance.patch(`/user/${id}`, this.editUserData);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users = this.users.map((user) =>
|
this.users.data = this.users.data.map((user) =>
|
||||||
user.id === id ? { ...user, ...response.data } : user
|
user.id === id ? { ...user, ...response.data } : user
|
||||||
);
|
);
|
||||||
this.user[id] = { ...this.user[id], ...response.data };
|
this.user[id] = { ...this.user[id], ...response.data };
|
||||||
|
@ -21,7 +21,13 @@ export type Vehicle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class VehicleStore {
|
class VehicleStore {
|
||||||
vehicles: Vehicle[] = [];
|
vehicles: {
|
||||||
|
data: Vehicle[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
vehicle: Record<string, Vehicle> = {};
|
vehicle: Record<string, Vehicle> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -29,10 +35,13 @@ class VehicleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVehicles = async () => {
|
getVehicles = async () => {
|
||||||
|
if (this.vehicles.loaded) return;
|
||||||
|
|
||||||
const response = await languageInstance("ru").get(`/vehicle`);
|
const response = await languageInstance("ru").get(`/vehicle`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.vehicles = response.data;
|
this.vehicles.data = response.data;
|
||||||
|
this.vehicles.loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,7 +49,7 @@ class VehicleStore {
|
|||||||
await languageInstance("ru").delete(`/vehicle/${id}`);
|
await languageInstance("ru").delete(`/vehicle/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.vehicles = this.vehicles.filter(
|
this.vehicles.data = this.vehicles.data.filter(
|
||||||
(vehicle) => vehicle.vehicle.id !== id
|
(vehicle) => vehicle.vehicle.id !== id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -68,7 +77,7 @@ class VehicleStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.vehicles.push({
|
this.vehicles.data.push({
|
||||||
vehicle: {
|
vehicle: {
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
tail_number: response.data.tail_number,
|
tail_number: response.data.tail_number,
|
||||||
@ -128,7 +137,7 @@ class VehicleStore {
|
|||||||
...response.data,
|
...response.data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.vehicles = this.vehicles.map((vehicle) =>
|
this.vehicles.data = this.vehicles.data.map((vehicle) =>
|
||||||
vehicle.vehicle.id === id
|
vehicle.vehicle.id === id
|
||||||
? {
|
? {
|
||||||
...vehicle,
|
...vehicle,
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user