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} />
|
||||||
Назад
|
Назад
|
||||||
|
File diff suppressed because it is too large
Load Diff
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"
|
||||||
|
label="Координаты маршрута"
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={routeCoords}
|
||||||
|
onChange={(e) => setRouteCoords(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Номер маршрута в Говорящем Городе"
|
||||||
|
required
|
||||||
|
value={govRouteNumber}
|
||||||
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Обращение губернатора</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={direction}
|
value={governorAppeal}
|
||||||
label="Направление"
|
label="Обращение губернатора"
|
||||||
onChange={(e) => setDirection(e.target.value)}
|
onChange={(e) => setGovernorAppeal(e.target.value as string)}
|
||||||
required
|
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,39 +236,41 @@ class CityStore {
|
|||||||
(country) => country.code === country_code
|
(country) => country.code === country_code
|
||||||
);
|
);
|
||||||
|
|
||||||
await languageInstance(language as Language).patch(`/city/${code}`, {
|
if (name) {
|
||||||
name,
|
await languageInstance(language as Language).patch(`/city/${code}`, {
|
||||||
country: country?.name || "",
|
name,
|
||||||
country_code: country_code,
|
country: country?.name || "",
|
||||||
arms,
|
country_code: country_code,
|
||||||
});
|
arms,
|
||||||
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (this.city[code]) {
|
if (this.city[code]) {
|
||||||
this.city[code][language as keyof CashedCities] = {
|
this.city[code][language as keyof CashedCities] = {
|
||||||
name,
|
name,
|
||||||
country: country?.name || "",
|
country: country?.name || "",
|
||||||
country_code: country_code,
|
country_code: country_code,
|
||||||
arms,
|
arms,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.cities[language as keyof CashedCities]) {
|
if (this.cities[language as keyof CashedCities]) {
|
||||||
this.cities[language as keyof CashedCities] = this.cities[
|
this.cities[language as keyof CashedCities] = this.cities[
|
||||||
language as keyof CashedCities
|
language as keyof CashedCities
|
||||||
].map((city) =>
|
].map((city) =>
|
||||||
city.id === Number(code)
|
city.id === Number(code)
|
||||||
? {
|
? {
|
||||||
id: city.id,
|
id: city.id,
|
||||||
name,
|
name,
|
||||||
country: country?.name || "",
|
country: country?.name || "",
|
||||||
country_code: country_code,
|
country_code: country_code,
|
||||||
arms,
|
arms,
|
||||||
}
|
}
|
||||||
: city
|
: city
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,9 +58,12 @@ 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,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeStore = new RouteStore();
|
export const routeStore = new RouteStore();
|
||||||
|
@ -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