Compare commits
3 Commits
27cb644242
...
2117a6836e
Author | SHA1 | Date | |
---|---|---|---|
2117a6836e | |||
f49caf3ec8 | |||
300ff262ce |
@ -16,22 +16,22 @@ import {
|
|||||||
SnapshotListPage,
|
SnapshotListPage,
|
||||||
CarrierListPage,
|
CarrierListPage,
|
||||||
StationListPage,
|
StationListPage,
|
||||||
VehicleListPage,
|
// VehicleListPage,
|
||||||
ArticleListPage,
|
ArticleListPage,
|
||||||
CityPreviewPage,
|
CityPreviewPage,
|
||||||
CountryPreviewPage,
|
// CountryPreviewPage,
|
||||||
VehiclePreviewPage,
|
// VehiclePreviewPage,
|
||||||
CarrierPreviewPage,
|
// CarrierPreviewPage,
|
||||||
SnapshotCreatePage,
|
SnapshotCreatePage,
|
||||||
CountryCreatePage,
|
CountryCreatePage,
|
||||||
CityCreatePage,
|
CityCreatePage,
|
||||||
CarrierCreatePage,
|
CarrierCreatePage,
|
||||||
VehicleCreatePage,
|
// VehicleCreatePage,
|
||||||
CountryEditPage,
|
CountryEditPage,
|
||||||
CityEditPage,
|
CityEditPage,
|
||||||
UserCreatePage,
|
UserCreatePage,
|
||||||
UserEditPage,
|
UserEditPage,
|
||||||
VehicleEditPage,
|
// VehicleEditPage,
|
||||||
CarrierEditPage,
|
CarrierEditPage,
|
||||||
StationCreatePage,
|
StationCreatePage,
|
||||||
StationPreviewPage,
|
StationPreviewPage,
|
||||||
@ -39,6 +39,7 @@ import {
|
|||||||
RouteCreatePage,
|
RouteCreatePage,
|
||||||
RoutePreview,
|
RoutePreview,
|
||||||
RouteEditPage,
|
RouteEditPage,
|
||||||
|
ArticlePreviewPage,
|
||||||
} from "@pages";
|
} from "@pages";
|
||||||
import { authStore, createSightStore, editSightStore } from "@shared";
|
import { authStore, createSightStore, editSightStore } from "@shared";
|
||||||
import { Layout } from "@widgets";
|
import { Layout } from "@widgets";
|
||||||
@ -133,7 +134,7 @@ const router = createBrowserRouter([
|
|||||||
// Country
|
// Country
|
||||||
{ path: "country", element: <CountryListPage /> },
|
{ path: "country", element: <CountryListPage /> },
|
||||||
{ path: "country/create", element: <CountryCreatePage /> },
|
{ path: "country/create", element: <CountryCreatePage /> },
|
||||||
{ path: "country/:id", element: <CountryPreviewPage /> },
|
// { path: "country/:id", element: <CountryPreviewPage /> },
|
||||||
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
||||||
// City
|
// City
|
||||||
{ path: "city", element: <CityListPage /> },
|
{ path: "city", element: <CityListPage /> },
|
||||||
@ -156,7 +157,7 @@ const router = createBrowserRouter([
|
|||||||
// Carrier
|
// Carrier
|
||||||
{ path: "carrier", element: <CarrierListPage /> },
|
{ path: "carrier", element: <CarrierListPage /> },
|
||||||
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
||||||
{ path: "carrier/:id", element: <CarrierPreviewPage /> },
|
// { path: "carrier/:id", element: <CarrierPreviewPage /> },
|
||||||
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
||||||
// Station
|
// Station
|
||||||
{ path: "station", element: <StationListPage /> },
|
{ path: "station", element: <StationListPage /> },
|
||||||
@ -164,13 +165,13 @@ const router = createBrowserRouter([
|
|||||||
{ path: "station/:id", element: <StationPreviewPage /> },
|
{ path: "station/:id", element: <StationPreviewPage /> },
|
||||||
{ path: "station/:id/edit", element: <StationEditPage /> },
|
{ 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 /> },
|
||||||
{ path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
// { path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
||||||
{ 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: "article/:id", element: <ArticlePreviewPage /> },
|
||||||
// { path: "media/create", element: <CreateMediaPage /> },
|
// { path: "media/create", element: <CreateMediaPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,7 @@ 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, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
interface NavigationItemProps {
|
interface NavigationItemProps {
|
||||||
item: NavigationItem;
|
item: NavigationItem;
|
||||||
@ -58,7 +59,7 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
isNested && {
|
isNested && {
|
||||||
pl: 4,
|
pl: open ? 4 : 2.5,
|
||||||
},
|
},
|
||||||
isActive && {
|
isActive && {
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.08)",
|
backgroundColor: "rgba(0, 0, 0, 0.08)",
|
||||||
@ -84,7 +85,7 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Icon />
|
{Icon ? <Icon /> : <Plus />}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={item.label}
|
primary={item.label}
|
||||||
@ -108,7 +109,7 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
|||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{item.nestedItems && (
|
{item.nestedItems && (
|
||||||
<Collapse in={isExpanded && open} timeout="auto" unmountOnExit>
|
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
{item.nestedItems.map((nestedItem) => (
|
{item.nestedItems.map((nestedItem) => (
|
||||||
<NavigationItemComponent
|
<NavigationItemComponent
|
||||||
|
28
src/pages/Article/ArticleCreatePage/index.tsx
Normal file
28
src/pages/Article/ArticleCreatePage/index.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
|
const ArticleCreatePage: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<h1 className="text-3xl break-words">Создание статьи</h1>
|
||||||
|
</div>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArticleCreatePage;
|
44
src/pages/Article/ArticleEditPage/index.tsx
Normal file
44
src/pages/Article/ArticleEditPage/index.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
import { articlesStore } from "@shared";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
const ArticleEditPage: React.FC = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const { articleData, getArticle } = articlesStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
// Fetch data for all languages
|
||||||
|
getArticle(parseInt(id), "ru");
|
||||||
|
getArticle(parseInt(id), "en");
|
||||||
|
getArticle(parseInt(id), "zh");
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<h1 className="text-3xl break-words">
|
||||||
|
{articleData?.ru?.heading || "Редактирование статьи"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ArticleEditPage;
|
@ -2,16 +2,18 @@ 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, Eye } from "lucide-react";
|
import { Trash2, Eye, Minus } 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";
|
||||||
|
|
||||||
export const ArticleListPage = observer(() => {
|
export const ArticleListPage = observer(() => {
|
||||||
const { articleList, getArticleList } = articlesStore;
|
const { articleList, getArticleList, deleteArticles } = articlesStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<string | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getArticleList();
|
getArticleList();
|
||||||
@ -22,6 +24,15 @@ export const ArticleListPage = observer(() => {
|
|||||||
field: "heading",
|
field: "heading",
|
||||||
headerName: "Название",
|
headerName: "Название",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full gap-7 items-center">
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -59,18 +70,42 @@ export const ArticleListPage = observer(() => {
|
|||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DataGrid
|
<div className="flex justify-between items-center mb-10">
|
||||||
rows={rows}
|
<h1 className="text-2xl">Статьи</h1>
|
||||||
columns={columns}
|
</div>
|
||||||
hideFooterPagination
|
|
||||||
hideFooter
|
<div
|
||||||
/>
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
|
hideFooter
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
|
await deleteArticles([parseInt(rowId)]);
|
||||||
getArticleList();
|
getArticleList();
|
||||||
}
|
}
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
@ -81,6 +116,19 @@ export const ArticleListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await deleteArticles(ids);
|
||||||
|
getArticleList();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
85
src/pages/Article/ArticlePreviewPage/PreviewLeftWidget.tsx
Normal file
85
src/pages/Article/ArticlePreviewPage/PreviewLeftWidget.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Paper, Box, Typography } from "@mui/material";
|
||||||
|
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
|
||||||
|
import { articlesStore, languageStore } from "@shared";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
export const PreviewLeftWidget = observer(() => {
|
||||||
|
const { articleMedia, articleData } = articlesStore;
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
minWidth: 320,
|
||||||
|
background:
|
||||||
|
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||||
|
overflowY: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
borderRadius: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 100,
|
||||||
|
padding: "3px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
"& img": {
|
||||||
|
borderTopLeftRadius: "10px",
|
||||||
|
borderTopRightRadius: "10px",
|
||||||
|
width: "100%",
|
||||||
|
height: "auto",
|
||||||
|
objectFit: "contain",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{articleMedia && <MediaViewer media={articleMedia} fullWidth />}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
background:
|
||||||
|
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||||
|
color: "white",
|
||||||
|
margin: "5px 0px 5px 0px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
padding: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
component="h2"
|
||||||
|
sx={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: "120%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{articleData?.[language]?.heading || "Название информации"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{articleData?.[language]?.body && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: 1,
|
||||||
|
maxHeight: "300px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
background:
|
||||||
|
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReactMarkdownComponent value={articleData?.[language]?.body} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
139
src/pages/Article/ArticlePreviewPage/PreviewRightWidget.tsx
Normal file
139
src/pages/Article/ArticlePreviewPage/PreviewRightWidget.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { Paper, Box, Typography } from "@mui/material";
|
||||||
|
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
|
||||||
|
import { articlesStore, languageStore } from "@shared";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ImagePlus } from "lucide-react";
|
||||||
|
|
||||||
|
export const PreviewRightWidget = observer(() => {
|
||||||
|
const { articleData, articleMedia } = articlesStore;
|
||||||
|
const { language } = languageStore;
|
||||||
|
const article = articleData?.[language];
|
||||||
|
if (!article) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
className="flex-1 flex flex-col max-w-[500px]"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "10px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
elevation={2}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className="overflow-hidden"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
background: "#877361",
|
||||||
|
borderColor: "grey.300",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{articleMedia ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
width: "100%",
|
||||||
|
padding: "2px 2px 0px 2px",
|
||||||
|
"& img": {
|
||||||
|
borderTopLeftRadius: "10px",
|
||||||
|
borderTopRightRadius: "10px",
|
||||||
|
width: "100%",
|
||||||
|
height: "auto",
|
||||||
|
objectFit: "contain",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MediaViewer media={articleMedia} fullWidth />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: 200,
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.1)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImagePlus size={48} color="white" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
wordBreak: "break-word",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: "120%",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
borderBottom: "1px solid #A89F90",
|
||||||
|
boxShadow: "inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
|
||||||
|
background:
|
||||||
|
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" color="white">
|
||||||
|
{article.heading || "Выберите статью"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: 1,
|
||||||
|
minHeight: "200px",
|
||||||
|
maxHeight: "300px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
background:
|
||||||
|
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{article.body ? (
|
||||||
|
<ReactMarkdownComponent value={article.body} />
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
color="rgba(255,255,255,0.7)"
|
||||||
|
sx={{ textAlign: "center", mt: 4 }}
|
||||||
|
>
|
||||||
|
Предпросмотр статьи появится здесь
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
{articleData?.right && articleData?.right.length > 1 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: "120%",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 1,
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
boxShadow: "inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
|
||||||
|
background:
|
||||||
|
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
{articleData.right.map((a, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
className="inline-block text-left text-xs text-white"
|
||||||
|
>
|
||||||
|
{a.heading}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
57
src/pages/Article/ArticlePreviewPage/index.tsx
Normal file
57
src/pages/Article/ArticlePreviewPage/index.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { PreviewLeftWidget } from "./PreviewLeftWidget";
|
||||||
|
import { PreviewRightWidget } from "./PreviewRightWidget";
|
||||||
|
import { articlesStore, languageStore } from "@shared";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
export const ArticlePreviewPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { getArticle, getArticleMedia, getArticlePreview } = articlesStore;
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (id) {
|
||||||
|
await getArticle(Number(id), language);
|
||||||
|
await getArticleMedia(Number(id));
|
||||||
|
await getArticlePreview(Number(id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, [id, language]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-4 mb-10">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 2,
|
||||||
|
p: 2,
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: "320px" }}>
|
||||||
|
<PreviewLeftWidget />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ width: "500px" }}>
|
||||||
|
<PreviewRightWidget />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1 +1,2 @@
|
|||||||
export * from "./ArticleListPage";
|
export * from "./ArticleListPage";
|
||||||
|
export * from "./ArticlePreviewPage";
|
||||||
|
@ -14,19 +14,29 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { carrierStore, cityStore, mediaStore } from "@shared";
|
import { carrierStore, cityStore, mediaStore } from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { MediaViewer } from "@widgets";
|
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
||||||
|
import {
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
export const CarrierCreatePage = observer(() => {
|
export const CarrierCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [fullName, setFullName] = useState("");
|
const [fullName, setFullName] = useState("");
|
||||||
const [shortName, setShortName] = useState("");
|
const [shortName, setShortName] = useState("");
|
||||||
const [cityId, setCityId] = useState<number | null>(null);
|
const [cityId, setCityId] = useState<number | null>(null);
|
||||||
const [main_color, setMainColor] = useState("#000000");
|
|
||||||
const [left_color, setLeftColor] = useState("#ffffff");
|
|
||||||
const [right_color, setRightColor] = useState("#ff0000");
|
|
||||||
const [slogan, setSlogan] = useState("");
|
const [slogan, setSlogan] = useState("");
|
||||||
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cityStore.getCities("ru");
|
cityStore.getCities("ru");
|
||||||
@ -39,11 +49,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
await carrierStore.createCarrier(
|
await carrierStore.createCarrier(
|
||||||
fullName,
|
fullName,
|
||||||
shortName,
|
shortName,
|
||||||
cityStore.cities.ru.find((c) => c.id === cityId)?.name!,
|
|
||||||
cityId!,
|
cityId!,
|
||||||
main_color,
|
|
||||||
left_color,
|
|
||||||
right_color,
|
|
||||||
slogan,
|
slogan,
|
||||||
selectedMediaId!
|
selectedMediaId!
|
||||||
);
|
);
|
||||||
@ -56,8 +62,22 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setSelectedMediaId(media.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMedia = selectedMediaId
|
||||||
|
? mediaStore.media.find((m) => m.id === selectedMediaId)
|
||||||
|
: null;
|
||||||
|
|
||||||
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">
|
||||||
|
<LanguageSwitcher />
|
||||||
<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"
|
||||||
@ -69,6 +89,10 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<h1 className="text-3xl break-words">Создание перевозчика</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>Город</InputLabel>
|
<InputLabel>Город</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@ -77,7 +101,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
required
|
required
|
||||||
onChange={(e) => setCityId(e.target.value as number)}
|
onChange={(e) => setCityId(e.target.value as number)}
|
||||||
>
|
>
|
||||||
{cityStore.cities.ru.map((city) => (
|
{cityStore.cities.ru.data.map((city) => (
|
||||||
<MenuItem key={city.id} value={city.id}>
|
<MenuItem key={city.id} value={city.id}>
|
||||||
{city.name}
|
{city.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -101,57 +125,6 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
onChange={(e) => setShortName(e.target.value)}
|
onChange={(e) => setShortName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-4 w-full ">
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Основной цвет"
|
|
||||||
value={main_color}
|
|
||||||
className="flex-1 w-full"
|
|
||||||
onChange={(e) => setMainColor(e.target.value)}
|
|
||||||
type="color"
|
|
||||||
sx={{
|
|
||||||
"& input": {
|
|
||||||
height: "50px",
|
|
||||||
paddingBlock: "14px",
|
|
||||||
paddingInline: "14px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Цвет левого виджета"
|
|
||||||
value={left_color}
|
|
||||||
className="flex-1 w-full"
|
|
||||||
onChange={(e) => setLeftColor(e.target.value)}
|
|
||||||
type="color"
|
|
||||||
sx={{
|
|
||||||
"& input": {
|
|
||||||
height: "50px",
|
|
||||||
paddingBlock: "14px",
|
|
||||||
paddingInline: "14px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Цвет правого виджета"
|
|
||||||
value={right_color}
|
|
||||||
className="flex-1 w-full"
|
|
||||||
onChange={(e) => setRightColor(e.target.value)}
|
|
||||||
type="color"
|
|
||||||
sx={{
|
|
||||||
"& input": {
|
|
||||||
height: "50px",
|
|
||||||
paddingBlock: "14px",
|
|
||||||
paddingInline: "14px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Слоган"
|
label="Слоган"
|
||||||
@ -159,29 +132,28 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
onChange={(e) => setSlogan(e.target.value)}
|
onChange={(e) => setSlogan(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
<FormControl fullWidth>
|
<ImageUploadCard
|
||||||
<InputLabel>Логотип</InputLabel>
|
title="Логотип перевозчика"
|
||||||
<Select
|
imageKey="thumbnail"
|
||||||
value={selectedMediaId || ""}
|
imageUrl={selectedMedia?.id}
|
||||||
label="Логотип"
|
onImageClick={() => {
|
||||||
required
|
setIsPreviewMediaOpen(true);
|
||||||
onChange={(e) => setSelectedMediaId(e.target.value as string)}
|
setMediaId(selectedMedia?.id ?? "");
|
||||||
>
|
}}
|
||||||
{mediaStore.media
|
onDeleteImageClick={() => {
|
||||||
.filter((media) => media.media_type === 3)
|
setSelectedMediaId(null);
|
||||||
.map((media) => (
|
setActiveMenuType(null);
|
||||||
<MenuItem key={media.id} value={media.id}>
|
}}
|
||||||
{media.media_name || media.filename}
|
onSelectFileClick={() => {
|
||||||
</MenuItem>
|
setActiveMenuType("thumbnail");
|
||||||
))}
|
setIsSelectMediaOpen(true);
|
||||||
</Select>
|
}}
|
||||||
</FormControl>
|
setUploadMediaOpen={() => {
|
||||||
{selectedMediaId && (
|
setIsUploadMediaOpen(true);
|
||||||
<div className="w-32 h-32">
|
setActiveMenuType("thumbnail");
|
||||||
<MediaViewer media={{ id: selectedMediaId, media_type: 1 }} />
|
}}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -200,6 +172,26 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -12,33 +12,64 @@ import { ArrowLeft, Save } from "lucide-react";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { carrierStore, cityStore, mediaStore } from "@shared";
|
import { carrierStore, cityStore, mediaStore, languageStore } from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { MediaViewer } from "@widgets";
|
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
||||||
|
import {
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
export const CarrierEditPage = observer(() => {
|
export const CarrierEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { carrier, getCarrier, setEditCarrierData, editCarrierData } =
|
const { getCarrier, setEditCarrierData, editCarrierData } = carrierStore;
|
||||||
carrierStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
await getCarrier(Number(id));
|
await cityStore.getCities("ru");
|
||||||
setEditCarrierData(
|
await cityStore.getCities("en");
|
||||||
carrier?.[Number(id)]?.full_name as string,
|
await cityStore.getCities("zh");
|
||||||
carrier?.[Number(id)]?.short_name as string,
|
const carrierData = await getCarrier(Number(id));
|
||||||
carrier?.[Number(id)]?.city as string,
|
|
||||||
carrier?.[Number(id)]?.city_id as number,
|
if (carrierData) {
|
||||||
carrier?.[Number(id)]?.main_color as string,
|
setEditCarrierData(
|
||||||
carrier?.[Number(id)]?.left_color as string,
|
carrierData.ru?.full_name || "",
|
||||||
carrier?.[Number(id)]?.right_color as string,
|
carrierData.ru?.short_name || "",
|
||||||
carrier?.[Number(id)]?.slogan as string,
|
carrierData.ru?.city_id || 0,
|
||||||
carrier?.[Number(id)]?.logo as string
|
carrierData.ru?.slogan || "",
|
||||||
);
|
carrierData.ru?.logo || "",
|
||||||
cityStore.getCities("ru");
|
"ru"
|
||||||
|
);
|
||||||
|
setEditCarrierData(
|
||||||
|
carrierData.en?.full_name || "",
|
||||||
|
carrierData.en?.short_name || "",
|
||||||
|
carrierData.en?.city_id || 0,
|
||||||
|
carrierData.en?.slogan || "",
|
||||||
|
carrierData.en?.logo || "",
|
||||||
|
"en"
|
||||||
|
);
|
||||||
|
setEditCarrierData(
|
||||||
|
carrierData.zh?.full_name || "",
|
||||||
|
carrierData.zh?.short_name || "",
|
||||||
|
carrierData.zh?.city_id || 0,
|
||||||
|
carrierData.zh?.slogan || "",
|
||||||
|
carrierData.zh?.logo || "",
|
||||||
|
"zh"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
mediaStore.getMedia();
|
mediaStore.getMedia();
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@ -56,8 +87,29 @@ export const CarrierEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData[language].full_name,
|
||||||
|
editCarrierData[language].short_name,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData[language].slogan,
|
||||||
|
media.id,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMedia = editCarrierData.logo
|
||||||
|
? mediaStore.media.find((m) => m.id === editCarrierData.logo)
|
||||||
|
: null;
|
||||||
|
|
||||||
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">
|
||||||
|
<LanguageSwitcher />
|
||||||
<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"
|
||||||
@ -68,6 +120,9 @@ export const CarrierEditPage = observer(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
|
||||||
|
<h1 className="text-3xl break-words">{editCarrierData.ru.full_name}</h1>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>Город</InputLabel>
|
<InputLabel>Город</InputLabel>
|
||||||
@ -77,19 +132,16 @@ export const CarrierEditPage = observer(() => {
|
|||||||
required
|
required
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
editCarrierData.full_name,
|
editCarrierData[language].full_name,
|
||||||
editCarrierData.short_name,
|
editCarrierData[language].short_name,
|
||||||
editCarrierData.city,
|
|
||||||
Number(e.target.value),
|
Number(e.target.value),
|
||||||
editCarrierData.main_color,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.left_color,
|
editCarrierData.logo,
|
||||||
editCarrierData.right_color,
|
language
|
||||||
editCarrierData.slogan,
|
|
||||||
editCarrierData.logo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{cityStore.cities.ru.map((city) => (
|
{cityStore.cities[language].data?.map((city) => (
|
||||||
<MenuItem key={city.id} value={city.id}>
|
<MenuItem key={city.id} value={city.id}>
|
||||||
{city.name}
|
{city.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -100,19 +152,16 @@ export const CarrierEditPage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Полное название"
|
label="Полное название"
|
||||||
value={editCarrierData.full_name}
|
value={editCarrierData[language].full_name}
|
||||||
required
|
required
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCarrierData.short_name,
|
editCarrierData[language].short_name,
|
||||||
editCarrierData.city,
|
|
||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData.main_color,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.left_color,
|
editCarrierData.logo,
|
||||||
editCarrierData.right_color,
|
language
|
||||||
editCarrierData.slogan,
|
|
||||||
editCarrierData.logo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -120,166 +169,65 @@ export const CarrierEditPage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Короткое название"
|
label="Короткое название"
|
||||||
value={editCarrierData.short_name}
|
value={editCarrierData[language].short_name}
|
||||||
required
|
required
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
editCarrierData.full_name,
|
editCarrierData[language].full_name,
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCarrierData.city,
|
|
||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData.main_color,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.left_color,
|
editCarrierData.logo,
|
||||||
editCarrierData.right_color,
|
language
|
||||||
editCarrierData.slogan,
|
|
||||||
editCarrierData.logo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-4 w-full">
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Основной цвет"
|
|
||||||
value={editCarrierData.main_color}
|
|
||||||
className="flex-1 w-full"
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditCarrierData(
|
|
||||||
editCarrierData.full_name,
|
|
||||||
editCarrierData.short_name,
|
|
||||||
editCarrierData.city,
|
|
||||||
editCarrierData.city_id,
|
|
||||||
e.target.value,
|
|
||||||
editCarrierData.left_color,
|
|
||||||
editCarrierData.right_color,
|
|
||||||
editCarrierData.slogan,
|
|
||||||
editCarrierData.logo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
type="color"
|
|
||||||
sx={{
|
|
||||||
"& input": {
|
|
||||||
height: "50px",
|
|
||||||
paddingBlock: "14px",
|
|
||||||
paddingInline: "14px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Цвет левого виджета"
|
|
||||||
value={editCarrierData.left_color}
|
|
||||||
className="flex-1 w-full"
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditCarrierData(
|
|
||||||
editCarrierData.full_name,
|
|
||||||
editCarrierData.short_name,
|
|
||||||
editCarrierData.city,
|
|
||||||
editCarrierData.city_id,
|
|
||||||
editCarrierData.main_color,
|
|
||||||
e.target.value,
|
|
||||||
editCarrierData.right_color,
|
|
||||||
editCarrierData.slogan,
|
|
||||||
editCarrierData.logo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
type="color"
|
|
||||||
sx={{
|
|
||||||
"& input": {
|
|
||||||
height: "50px",
|
|
||||||
paddingBlock: "14px",
|
|
||||||
paddingInline: "14px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Цвет правого виджета"
|
|
||||||
value={editCarrierData.right_color}
|
|
||||||
className="flex-1 w-full"
|
|
||||||
onChange={(e) =>
|
|
||||||
setEditCarrierData(
|
|
||||||
editCarrierData.full_name,
|
|
||||||
editCarrierData.short_name,
|
|
||||||
editCarrierData.city,
|
|
||||||
editCarrierData.city_id,
|
|
||||||
editCarrierData.main_color,
|
|
||||||
editCarrierData.left_color,
|
|
||||||
e.target.value,
|
|
||||||
editCarrierData.slogan,
|
|
||||||
editCarrierData.logo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
type="color"
|
|
||||||
sx={{
|
|
||||||
"& input": {
|
|
||||||
height: "50px",
|
|
||||||
paddingBlock: "14px",
|
|
||||||
paddingInline: "14px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Слоган"
|
label="Слоган"
|
||||||
value={editCarrierData.slogan}
|
value={editCarrierData[language].slogan}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
editCarrierData.full_name,
|
editCarrierData[language].full_name,
|
||||||
editCarrierData.short_name,
|
editCarrierData[language].short_name,
|
||||||
editCarrierData.city,
|
|
||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData.main_color,
|
|
||||||
editCarrierData.left_color,
|
|
||||||
editCarrierData.right_color,
|
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCarrierData.logo
|
editCarrierData.logo,
|
||||||
|
language
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
<FormControl fullWidth>
|
<ImageUploadCard
|
||||||
<InputLabel>Логотип</InputLabel>
|
title="Логотип перевозчика"
|
||||||
<Select
|
imageKey="thumbnail"
|
||||||
value={editCarrierData.logo || ""}
|
imageUrl={selectedMedia?.id}
|
||||||
label="Логотип"
|
onImageClick={() => {
|
||||||
required
|
setIsPreviewMediaOpen(true);
|
||||||
onChange={(e) =>
|
setMediaId(selectedMedia?.id ?? "");
|
||||||
setEditCarrierData(
|
}}
|
||||||
editCarrierData.full_name,
|
onDeleteImageClick={() => {
|
||||||
editCarrierData.short_name,
|
setEditCarrierData(
|
||||||
editCarrierData.city,
|
editCarrierData[language].full_name,
|
||||||
editCarrierData.city_id,
|
editCarrierData[language].short_name,
|
||||||
editCarrierData.main_color,
|
editCarrierData.city_id,
|
||||||
editCarrierData.left_color,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.right_color,
|
"",
|
||||||
editCarrierData.slogan,
|
language
|
||||||
e.target.value as string
|
);
|
||||||
)
|
setActiveMenuType(null);
|
||||||
}
|
}}
|
||||||
>
|
onSelectFileClick={() => {
|
||||||
{mediaStore.media
|
setActiveMenuType("thumbnail");
|
||||||
.filter((media) => media.media_type === 3)
|
setIsSelectMediaOpen(true);
|
||||||
.map((media) => (
|
}}
|
||||||
<MenuItem key={media.id} value={media.id}>
|
setUploadMediaOpen={() => {
|
||||||
{media.media_name || media.filename}
|
setIsUploadMediaOpen(true);
|
||||||
</MenuItem>
|
setActiveMenuType("thumbnail");
|
||||||
))}
|
}}
|
||||||
</Select>
|
/>
|
||||||
</FormControl>
|
|
||||||
{editCarrierData.logo && (
|
|
||||||
<div className="w-32 h-32">
|
|
||||||
<MediaViewer
|
|
||||||
media={{ id: editCarrierData.logo, media_type: 1 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -289,8 +237,8 @@ export const CarrierEditPage = observer(() => {
|
|||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
disabled={
|
disabled={
|
||||||
isLoading ||
|
isLoading ||
|
||||||
!editCarrierData.full_name ||
|
!editCarrierData[language].full_name ||
|
||||||
!editCarrierData.short_name ||
|
!editCarrierData[language].short_name ||
|
||||||
!editCarrierData.city_id ||
|
!editCarrierData.city_id ||
|
||||||
!editCarrierData.logo
|
!editCarrierData.logo
|
||||||
}
|
}
|
||||||
@ -302,6 +250,26 @@ export const CarrierEditPage = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,37 +1,74 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { carrierStore } from "@shared";
|
import { carrierStore, 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 { Eye, Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal } from "@widgets";
|
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const CarrierListPage = observer(() => {
|
export const CarrierListPage = observer(() => {
|
||||||
const { carriers, getCarriers, deleteCarrier } = carrierStore;
|
const { carriers, getCarriers, deleteCarrier } = carrierStore;
|
||||||
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 [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<number | null>(null);
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCarriers();
|
(async () => {
|
||||||
}, []);
|
await getCarriers(language);
|
||||||
|
})();
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: "full_name",
|
field: "full_name",
|
||||||
headerName: "Полное имя",
|
headerName: "Полное имя",
|
||||||
|
|
||||||
width: 300,
|
width: 300,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "short_name",
|
field: "short_name",
|
||||||
headerName: "Короткое имя",
|
headerName: "Короткое имя",
|
||||||
width: 200,
|
width: 200,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "city",
|
field: "city",
|
||||||
headerName: "Город",
|
headerName: "Город",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
@ -45,9 +82,9 @@ export const CarrierListPage = observer(() => {
|
|||||||
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => navigate(`/carrier/${params.row.id}`)}>
|
{/* <button onClick={() => navigate(`/carrier/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button> */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -62,7 +99,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = carriers.data?.map((carrier) => ({
|
const rows = carriers[language].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,
|
||||||
@ -71,15 +108,34 @@ export const CarrierListPage = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<LanguageSwitcher />
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<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>
|
||||||
<CreateButton label="Создать перевозчика" path="/carrier/create" />
|
<CreateButton label="Создать перевозчика" path="/carrier/create" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -98,6 +154,19 @@ export const CarrierListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteCarrier(id)));
|
||||||
|
await getCarriers(language);
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
import { Paper } from "@mui/material";
|
|
||||||
import { carrierStore, mediaStore } from "@shared";
|
|
||||||
import { MediaViewer } 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 CarrierPreviewPage = observer(() => {
|
|
||||||
const { id } = useParams();
|
|
||||||
const { getCarrier, carrier, setEditCarrierData } = carrierStore;
|
|
||||||
const { oneMedia, getOneMedia } = mediaStore;
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const carrierResponse = await getCarrier(Number(id));
|
|
||||||
setEditCarrierData(
|
|
||||||
carrierResponse?.full_name as string,
|
|
||||||
carrierResponse?.short_name as string,
|
|
||||||
carrierResponse?.city as string,
|
|
||||||
carrierResponse?.city_id as number,
|
|
||||||
carrierResponse?.main_color as string,
|
|
||||||
carrierResponse?.left_color as string,
|
|
||||||
carrierResponse?.right_color as string,
|
|
||||||
carrierResponse?.slogan as string,
|
|
||||||
carrierResponse?.logo as string
|
|
||||||
);
|
|
||||||
console.log(carrierResponse);
|
|
||||||
await getOneMedia(carrierResponse?.logo as string);
|
|
||||||
})();
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
|
||||||
{carrier && (
|
|
||||||
<>
|
|
||||||
<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>{carrier[Number(id)]?.full_name}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h1 className="text-lg font-bold">Полное имя</h1>
|
|
||||||
<p>{carrier[Number(id)]?.full_name}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h1 className="text-lg font-bold">Город</h1>
|
|
||||||
<p>{carrier[Number(id)]?.city}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 ">
|
|
||||||
<h1 className="text-lg font-bold">Основной цвет</h1>
|
|
||||||
<div
|
|
||||||
className="w-min"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${carrier[Number(id)]?.main_color}90`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{carrier[Number(id)]?.main_color}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h1 className="text-lg font-bold">Цвет левого виджета</h1>
|
|
||||||
<div
|
|
||||||
className="w-min"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${carrier[Number(id)]?.left_color}90`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{carrier[Number(id)]?.left_color}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h1 className="text-lg font-bold">Цвет правого виджета</h1>
|
|
||||||
<div
|
|
||||||
className="w-min"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${carrier[Number(id)]?.right_color}90`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{carrier[Number(id)]?.right_color}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h1 className="text-lg font-bold">Краткое имя</h1>
|
|
||||||
<p>{carrier[Number(id)]?.short_name}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h1 className="text-lg font-bold">Логотип</h1>
|
|
||||||
|
|
||||||
<MediaViewer
|
|
||||||
media={{
|
|
||||||
id: oneMedia?.id as string,
|
|
||||||
media_type: oneMedia?.media_type as number,
|
|
||||||
filename: oneMedia?.filename,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./CarrierListPage";
|
export * from "./CarrierListPage";
|
||||||
export * from "./CarrierPreviewPage";
|
|
||||||
export * from "./CarrierCreatePage";
|
export * from "./CarrierCreatePage";
|
||||||
export * from "./CarrierEditPage";
|
export * from "./CarrierEditPage";
|
||||||
|
@ -6,17 +6,20 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Box,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save, ImagePlus } from "lucide-react";
|
import { ArrowLeft, Save, Minus } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { cityStore, countryStore, languageStore, mediaStore } from "@shared";
|
import { cityStore, countryStore, languageStore, mediaStore } from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||||
import { SelectMediaDialog } from "@shared";
|
import {
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
export const CityCreatePage = observer(() => {
|
export const CityCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -24,12 +27,20 @@ export const CityCreatePage = observer(() => {
|
|||||||
const { createCityData, setCreateCityData } = cityStore;
|
const { createCityData, setCreateCityData } = cityStore;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||||||
|
>(null);
|
||||||
const { getCountries } = countryStore;
|
const { getCountries } = countryStore;
|
||||||
const { getMedia } = mediaStore;
|
const { getMedia } = mediaStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
await getCountries(language);
|
await getCountries("ru");
|
||||||
|
await getCountries("en");
|
||||||
|
await getCountries("zh");
|
||||||
await getMedia();
|
await getMedia();
|
||||||
})();
|
})();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
@ -55,7 +66,6 @@ export const CityCreatePage = observer(() => {
|
|||||||
}) => {
|
}) => {
|
||||||
setCreateCityData(
|
setCreateCityData(
|
||||||
createCityData[language].name,
|
createCityData[language].name,
|
||||||
createCityData.country,
|
|
||||||
createCityData.country_code,
|
createCityData.country_code,
|
||||||
media.id,
|
media.id,
|
||||||
language
|
language
|
||||||
@ -80,6 +90,9 @@ export const CityCreatePage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
|
||||||
|
<h1 className="text-3xl break-words">{createCityData.ru.name}</h1>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Название города"
|
label="Название города"
|
||||||
@ -88,7 +101,6 @@ export const CityCreatePage = observer(() => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCreateCityData(
|
setCreateCityData(
|
||||||
e.target.value,
|
e.target.value,
|
||||||
createCityData.country,
|
|
||||||
createCityData.country_code,
|
createCityData.country_code,
|
||||||
createCityData.arms,
|
createCityData.arms,
|
||||||
language
|
language
|
||||||
@ -103,19 +115,15 @@ export const CityCreatePage = observer(() => {
|
|||||||
label="Страна"
|
label="Страна"
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const selectedCountry = countryStore.countries[language]?.find(
|
|
||||||
(country) => country.code === e.target.value
|
|
||||||
);
|
|
||||||
setCreateCityData(
|
setCreateCityData(
|
||||||
createCityData[language].name,
|
createCityData[language].name,
|
||||||
selectedCountry?.name || "",
|
|
||||||
e.target.value,
|
e.target.value,
|
||||||
createCityData.arms,
|
createCityData.arms,
|
||||||
language
|
language
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{countryStore.countries[language].map((country) => (
|
{countryStore.countries["ru"]?.data?.map((country) => (
|
||||||
<MenuItem key={country.code} value={country.code}>
|
<MenuItem key={country.code} value={country.code}>
|
||||||
{country.name}
|
{country.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -123,44 +131,39 @@ export const CityCreatePage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
<label className="text-sm text-gray-600">Герб города</label>
|
{!selectedMedia && (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2 text-red-500">
|
||||||
<Button
|
<Minus size={20} />
|
||||||
variant="outlined"
|
<span className="text-sm">Герб города не выбран</span>
|
||||||
onClick={() => setIsSelectMediaOpen(true)}
|
</div>
|
||||||
startIcon={<ImagePlus size={20} />}
|
|
||||||
>
|
|
||||||
Выбрать герб
|
|
||||||
</Button>
|
|
||||||
{selectedMedia && (
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{selectedMedia.media_name || selectedMedia.filename}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{selectedMedia && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "200px",
|
|
||||||
height: "200px",
|
|
||||||
border: "1px solid #e0e0e0",
|
|
||||||
borderRadius: "8px",
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MediaViewer
|
|
||||||
media={{
|
|
||||||
id: selectedMedia.id,
|
|
||||||
media_type: selectedMedia.media_type,
|
|
||||||
filename: selectedMedia.filename,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Герб города"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={selectedMedia?.id}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(selectedMedia?.id ?? "");
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => {
|
||||||
|
setCreateCityData(
|
||||||
|
createCityData[language].name,
|
||||||
|
createCityData.country_code,
|
||||||
|
"",
|
||||||
|
language
|
||||||
|
);
|
||||||
|
setActiveMenuType(null);
|
||||||
|
}}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveMenuType("thumbnail");
|
||||||
|
setIsSelectMediaOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadMediaOpen(true);
|
||||||
|
setActiveMenuType("thumbnail");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -184,6 +187,19 @@ export const CityCreatePage = observer(() => {
|
|||||||
onSelectMedia={handleMediaSelect}
|
onSelectMedia={handleMediaSelect}
|
||||||
mediaType={3} // Тип медиа для иконок
|
mediaType={3} // Тип медиа для иконок
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -6,10 +6,9 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Box,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save, ImagePlus } from "lucide-react";
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@ -21,13 +20,23 @@ import {
|
|||||||
CashedCities,
|
CashedCities,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LanguageSwitcher, MediaViewer } from "@widgets";
|
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||||
import { SelectMediaDialog } from "@shared";
|
import {
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
export const CityEditPage = observer(() => {
|
export const CityEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||||||
|
>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { editCityData, editCity, getCity, setEditCityData } = cityStore;
|
const { editCityData, editCity, getCity, setEditCityData } = cityStore;
|
||||||
@ -49,20 +58,22 @@ export const CityEditPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const data = await getCity(id as string, language);
|
// Fetch data for all languages
|
||||||
setEditCityData(
|
const ruData = await getCity(id as string, "ru");
|
||||||
data.name,
|
const enData = await getCity(id as string, "en");
|
||||||
data.country,
|
const zhData = await getCity(id as string, "zh");
|
||||||
data.country_code,
|
|
||||||
data.arms,
|
// Set data for each language
|
||||||
language
|
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||||
);
|
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||||
await getOneMedia(data.arms as string);
|
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||||
await getCountries(language);
|
|
||||||
|
await getOneMedia(ruData.arms as string);
|
||||||
|
await getCountries("ru");
|
||||||
await getMedia();
|
await getMedia();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id, language]);
|
}, [id]);
|
||||||
|
|
||||||
const handleMediaSelect = (media: {
|
const handleMediaSelect = (media: {
|
||||||
id: string;
|
id: string;
|
||||||
@ -72,7 +83,6 @@ export const CityEditPage = observer(() => {
|
|||||||
}) => {
|
}) => {
|
||||||
setEditCityData(
|
setEditCityData(
|
||||||
editCityData[language].name,
|
editCityData[language].name,
|
||||||
editCityData.country,
|
|
||||||
editCityData.country_code,
|
editCityData.country_code,
|
||||||
media.id,
|
media.id,
|
||||||
language
|
language
|
||||||
@ -97,6 +107,9 @@ export const CityEditPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start ">
|
||||||
|
<h1 className="text-3xl break-words">{editCityData.ru.name}</h1>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Название"
|
label="Название"
|
||||||
@ -105,7 +118,6 @@ export const CityEditPage = observer(() => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditCityData(
|
setEditCityData(
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCityData.country,
|
|
||||||
editCityData.country_code,
|
editCityData.country_code,
|
||||||
editCityData.arms,
|
editCityData.arms,
|
||||||
language
|
language
|
||||||
@ -120,19 +132,15 @@ export const CityEditPage = observer(() => {
|
|||||||
label="Страна"
|
label="Страна"
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const selectedCountry = countryStore.countries[language]?.find(
|
|
||||||
(country) => country.code === e.target.value
|
|
||||||
);
|
|
||||||
setEditCityData(
|
setEditCityData(
|
||||||
editCityData[language as keyof CashedCities]?.name || "",
|
editCityData[language].name,
|
||||||
selectedCountry?.name || "",
|
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCityData.arms,
|
editCityData.arms,
|
||||||
language
|
language
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{countryStore.countries[language].map((country) => (
|
{countryStore.countries.ru.data.map((country) => (
|
||||||
<MenuItem key={country.code} value={country.code}>
|
<MenuItem key={country.code} value={country.code}>
|
||||||
{country.name}
|
{country.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -140,44 +148,33 @@ export const CityEditPage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
<label className="text-sm text-gray-600">Герб города</label>
|
<ImageUploadCard
|
||||||
<div className="flex items-center gap-4">
|
title="Герб города"
|
||||||
<Button
|
imageKey="thumbnail"
|
||||||
variant="outlined"
|
imageUrl={selectedMedia?.id}
|
||||||
onClick={() => setIsSelectMediaOpen(true)}
|
onImageClick={() => {
|
||||||
startIcon={<ImagePlus size={20} />}
|
setIsPreviewMediaOpen(true);
|
||||||
>
|
setMediaId(selectedMedia?.id ?? "");
|
||||||
Выбрать герб
|
}}
|
||||||
</Button>
|
onDeleteImageClick={() => {
|
||||||
{selectedMedia && (
|
setEditCityData(
|
||||||
<span className="text-sm text-gray-600">
|
editCityData[language].name,
|
||||||
{selectedMedia.media_name || selectedMedia.filename}
|
editCityData.country_code,
|
||||||
</span>
|
"",
|
||||||
)}
|
language
|
||||||
</div>
|
);
|
||||||
{selectedMedia && (
|
setActiveMenuType(null);
|
||||||
<Box
|
}}
|
||||||
sx={{
|
onSelectFileClick={() => {
|
||||||
width: "200px",
|
setActiveMenuType("thumbnail");
|
||||||
height: "200px",
|
setIsSelectMediaOpen(true);
|
||||||
border: "1px solid #e0e0e0",
|
}}
|
||||||
borderRadius: "8px",
|
setUploadMediaOpen={() => {
|
||||||
overflow: "hidden",
|
setIsUploadMediaOpen(true);
|
||||||
display: "flex",
|
setActiveMenuType("thumbnail");
|
||||||
alignItems: "center",
|
}}
|
||||||
justifyContent: "center",
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MediaViewer
|
|
||||||
media={{
|
|
||||||
id: selectedMedia.id,
|
|
||||||
media_type: selectedMedia.media_type,
|
|
||||||
filename: selectedMedia.filename,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -201,6 +198,20 @@ export const CityEditPage = observer(() => {
|
|||||||
open={isSelectMediaOpen}
|
open={isSelectMediaOpen}
|
||||||
onClose={() => setIsSelectMediaOpen(false)}
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
onSelectMedia={handleMediaSelect}
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={3} // Тип медиа для иконок
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { languageStore, cityStore, CashedCities } from "@shared";
|
import { languageStore, cityStore } 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, Pencil, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2, Minus } 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";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const CityListPage = observer(() => {
|
export const CityListPage = observer(() => {
|
||||||
const { cities, getCities, deleteCity } = cityStore;
|
const { cities, getCities, deleteCity } = cityStore;
|
||||||
@ -22,11 +23,33 @@ export const CityListPage = observer(() => {
|
|||||||
field: "country",
|
field: "country",
|
||||||
headerName: "Страна",
|
headerName: "Страна",
|
||||||
width: 150,
|
width: 150,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "name",
|
field: "name",
|
||||||
headerName: "Название",
|
headerName: "Название",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
@ -58,7 +81,7 @@ export const CityListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = cities[language].map((city) => ({
|
const rows = cities[language]?.data?.map((city) => ({
|
||||||
id: city.id,
|
id: city.id,
|
||||||
name: city.name,
|
name: city.name,
|
||||||
country: city.country,
|
country: city.country,
|
||||||
@ -85,7 +108,8 @@ export const CityListPage = observer(() => {
|
|||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
deleteCity(rowId.toString(), language as keyof CashedCities);
|
await deleteCity(rowId.toString());
|
||||||
|
toast.success("Город успешно удален");
|
||||||
}
|
}
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
@ -16,18 +16,18 @@ export const CityPreviewPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const cityResponse = await getCity(id as string, language);
|
const ruData = await getCity(id as string, "ru");
|
||||||
setEditCityData(
|
const enData = await getCity(id as string, "en");
|
||||||
cityResponse.name,
|
const zhData = await getCity(id as string, "zh");
|
||||||
cityResponse.country,
|
|
||||||
cityResponse.country_code,
|
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||||
cityResponse.arms,
|
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||||
language
|
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||||
);
|
|
||||||
await getOneMedia(cityResponse.arms as string);
|
await getOneMedia(ruData.arms as string);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id, language]);
|
}, [id]);
|
||||||
|
|
||||||
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">
|
||||||
|
@ -41,6 +41,9 @@ export const CountryCreatePage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
|
||||||
|
<h1 className="text-3xl break-words">{createCountryData.ru.name}</h1>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Код страны"
|
label="Код страны"
|
||||||
|
@ -31,11 +31,18 @@ export const CountryEditPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const data = await getCountry(id as string, language);
|
// Fetch data for all languages
|
||||||
setEditCountryData(data.name, language);
|
const ruData = await getCountry(id as string, "ru");
|
||||||
|
const enData = await getCountry(id as string, "en");
|
||||||
|
const zhData = await getCountry(id as string, "zh");
|
||||||
|
|
||||||
|
// Set data for each language
|
||||||
|
setEditCountryData(ruData.name, "ru");
|
||||||
|
setEditCountryData(enData.name, "en");
|
||||||
|
setEditCountryData(zhData.name, "zh");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id, language]);
|
}, [id]);
|
||||||
|
|
||||||
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">
|
||||||
@ -51,6 +58,9 @@ export const CountryEditPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
|
||||||
|
<h1 className="text-3xl break-words t">{editCountryData.ru.name}</h1>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Код страны"
|
label="Код страны"
|
||||||
|
@ -2,15 +2,15 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { countryStore, languageStore } from "@shared";
|
import { countryStore, 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 { Eye, Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2, Minus } 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 CountryListPage = observer(() => {
|
export const CountryListPage = observer(() => {
|
||||||
const { countries, getCountries } = countryStore;
|
const { countries, getCountries, deleteCountry } = countryStore;
|
||||||
const navigate = useNavigate();
|
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);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,6 +22,17 @@ export const CountryListPage = observer(() => {
|
|||||||
field: "name",
|
field: "name",
|
||||||
headerName: "Название",
|
headerName: "Название",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center ">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
@ -37,9 +48,9 @@ export const CountryListPage = observer(() => {
|
|||||||
>
|
>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => navigate(`/country/${params.row.code}`)}>
|
{/* <button onClick={() => navigate(`/country/${params.row.code}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button> */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -54,7 +65,7 @@ export const CountryListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = countries[language]?.map((country) => ({
|
const rows = countries[language]?.data.map((country) => ({
|
||||||
id: country.code,
|
id: country.code,
|
||||||
code: country.code,
|
code: country.code,
|
||||||
name: country.name,
|
name: country.name,
|
||||||
@ -75,17 +86,14 @@ export const CountryListPage = observer(() => {
|
|||||||
<DeleteModal
|
<DeleteModal
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (rowId) {
|
if (!rowId) return;
|
||||||
await countryStore.deleteCountry(rowId, language);
|
await deleteCountry(rowId, language);
|
||||||
getCountries(language); // Refresh the list after deletion
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
}
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -15,11 +15,16 @@ export const CountryPreviewPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const data = await getCountry(id as string, language);
|
const ruData = await getCountry(id as string, "ru");
|
||||||
setEditCountryData(data.name, language);
|
const enData = await getCountry(id as string, "en");
|
||||||
|
const zhData = await getCountry(id as string, "zh");
|
||||||
|
|
||||||
|
setEditCountryData(ruData.name, "ru");
|
||||||
|
setEditCountryData(enData.name, "en");
|
||||||
|
setEditCountryData(zhData.name, "zh");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id, language]);
|
}, [id]);
|
||||||
|
|
||||||
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">
|
||||||
@ -55,7 +60,7 @@ export const CountryPreviewPage = observer(() => {
|
|||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex flex-col gap-10 w-full">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-lg font-bold">Название</h1>
|
<h1 className="text-lg font-bold">Название</h1>
|
||||||
<p>{country[id!]?.[language]?.name}</p>
|
<p>{country[id!]?.ru?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -3,12 +3,7 @@ import { InformationTab, LeaveAgree, RightWidgetTab } from "@widgets";
|
|||||||
import { LeftWidgetTab } from "@widgets";
|
import { LeftWidgetTab } from "@widgets";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import {
|
import { articlesStore, cityStore, editSightStore } from "@shared";
|
||||||
articlesStore,
|
|
||||||
cityStore,
|
|
||||||
editSightStore,
|
|
||||||
languageStore,
|
|
||||||
} from "@shared";
|
|
||||||
import { useBlocker, useParams } from "react-router-dom";
|
import { useBlocker, useParams } from "react-router-dom";
|
||||||
|
|
||||||
function a11yProps(index: number) {
|
function a11yProps(index: number) {
|
||||||
@ -22,7 +17,7 @@ export const EditSightPage = observer(() => {
|
|||||||
const [value, setValue] = useState(0);
|
const [value, setValue] = useState(0);
|
||||||
const { sight, getSightInfo, needLeaveAgree } = editSightStore;
|
const { sight, getSightInfo, needLeaveAgree } = editSightStore;
|
||||||
const { getArticles } = articlesStore;
|
const { getArticles } = articlesStore;
|
||||||
const { language } = languageStore;
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { getRuCities } = cityStore;
|
const { getRuCities } = cityStore;
|
||||||
|
|
||||||
@ -38,13 +33,17 @@ export const EditSightPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
await getSightInfo(+id, language);
|
await getSightInfo(+id, "ru");
|
||||||
await getArticles(language);
|
await getSightInfo(+id, "en");
|
||||||
|
await getSightInfo(+id, "zh");
|
||||||
|
await getArticles("ru");
|
||||||
|
await getArticles("en");
|
||||||
|
await getArticles("zh");
|
||||||
await getRuCities();
|
await getRuCities();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id, language]);
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -54,20 +54,22 @@ class MapStore {
|
|||||||
sights: ApiSight[] = [];
|
sights: ApiSight[] = [];
|
||||||
|
|
||||||
getRoutes = async () => {
|
getRoutes = async () => {
|
||||||
const routes = await languageInstance("ru").get("/route");
|
const response = await languageInstance("ru").get("/route");
|
||||||
const routedIds = routes.data.map((route: any) => route.id);
|
console.log(response.data);
|
||||||
const mappedRoutes: ApiRoute[] = [];
|
const routesIds = response.data.map((route: any) => route.id);
|
||||||
for (const routeId of routedIds) {
|
for (const id of routesIds) {
|
||||||
const responseSoloRoute = await languageInstance("ru").get(
|
const route = await languageInstance("ru").get(`/route/${id}`);
|
||||||
`/route/${routeId}`
|
this.routes.push({
|
||||||
);
|
id: route.data.id,
|
||||||
const route = responseSoloRoute.data;
|
route_number: route.data.route_number,
|
||||||
mappedRoutes.push({
|
path: route.data.path,
|
||||||
id: route.id,
|
|
||||||
route_number: route.route_number,
|
|
||||||
path: route.path,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const mappedRoutes: ApiRoute[] = response.data.map((route: any) => ({
|
||||||
|
id: route.id,
|
||||||
|
route_number: route.route_number,
|
||||||
|
path: route.path,
|
||||||
|
}));
|
||||||
this.routes = mappedRoutes.sort((a, b) =>
|
this.routes = mappedRoutes.sort((a, b) =>
|
||||||
a.route_number.localeCompare(b.route_number)
|
a.route_number.localeCompare(b.route_number)
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
import { languageStore, MEDIA_TYPE_LABELS, mediaStore } 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, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
|
|
||||||
@ -10,7 +10,9 @@ export const MediaListPage = observer(() => {
|
|||||||
const { media, getMedia, deleteMedia } = mediaStore;
|
const { media, getMedia, deleteMedia } = mediaStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<string | null>(null);
|
||||||
|
const [ids, setIds] = useState<string[]>([]);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,6 +24,17 @@ export const MediaListPage = observer(() => {
|
|||||||
field: "media_name",
|
field: "media_name",
|
||||||
headerName: "Название",
|
headerName: "Название",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "media_type",
|
field: "media_type",
|
||||||
@ -30,13 +43,15 @@ export const MediaListPage = observer(() => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<p>
|
<div className="w-full h-full flex items-center">
|
||||||
{
|
{params.value ? (
|
||||||
MEDIA_TYPE_LABELS[
|
MEDIA_TYPE_LABELS[
|
||||||
params.row.media_type as keyof typeof MEDIA_TYPE_LABELS
|
params.row.media_type as keyof typeof MEDIA_TYPE_LABELS
|
||||||
]
|
]
|
||||||
}
|
) : (
|
||||||
</p>
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -80,10 +95,28 @@ export const MediaListPage = observer(() => {
|
|||||||
<h1 className="text-2xl">Медиа</h1>
|
<h1 className="text-2xl">Медиа</h1>
|
||||||
<CreateButton label="Создать медиа" path="/media/create" />
|
<CreateButton label="Создать медиа" path="/media/create" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as string[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -103,6 +136,19 @@ export const MediaListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteMedia(id)));
|
||||||
|
getMedia();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -6,10 +6,10 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Typography,
|
// Typography,
|
||||||
Box,
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
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 { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -18,6 +18,7 @@ import { toast } from "react-toastify";
|
|||||||
import { carrierStore } from "../../../shared/store/CarrierStore";
|
import { carrierStore } from "../../../shared/store/CarrierStore";
|
||||||
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||||
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
||||||
|
import { languageStore } from "@shared";
|
||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -33,11 +34,11 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const [centerLat, setCenterLat] = useState("");
|
const [centerLat, setCenterLat] = useState("");
|
||||||
const [centerLng, setCenterLng] = useState("");
|
const [centerLng, setCenterLng] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
carrierStore.getCarriers();
|
carrierStore.getCarriers(language);
|
||||||
articlesStore.getArticleList();
|
articlesStore.getArticleList();
|
||||||
}, []);
|
}, [language]);
|
||||||
|
|
||||||
const handleCreateRoute = async () => {
|
const handleCreateRoute = async () => {
|
||||||
try {
|
try {
|
||||||
@ -65,8 +66,9 @@ export const RouteCreatePage = observer(() => {
|
|||||||
// Собираем объект маршрута
|
// Собираем объект маршрута
|
||||||
const newRoute: Partial<Route> = {
|
const newRoute: Partial<Route> = {
|
||||||
carrier:
|
carrier:
|
||||||
carrierStore.carriers.data.find((c: any) => c.id === carrier_id)
|
carrierStore.carriers[
|
||||||
?.full_name || "",
|
language as keyof typeof carrierStore.carriers
|
||||||
|
].data?.find((c: any) => c.id === carrier_id)?.full_name || "",
|
||||||
carrier_id,
|
carrier_id,
|
||||||
route_number: routeNumber,
|
route_number: routeNumber,
|
||||||
route_sys_number: govRouteNumber,
|
route_sys_number: govRouteNumber,
|
||||||
@ -93,134 +95,139 @@ export const RouteCreatePage = observer(() => {
|
|||||||
|
|
||||||
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">
|
<LanguageSwitcher />
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Маршруты / Создать
|
Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
<Box className="flex flex-col gap-6 w-full">
|
<FormControl fullWidth required>
|
||||||
<FormControl fullWidth required>
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
<InputLabel>Выберите перевозчика</InputLabel>
|
<Select
|
||||||
<Select
|
value={carrier}
|
||||||
value={carrier}
|
label="Выберите перевозчика"
|
||||||
label="Выберите перевозчика"
|
onChange={(e) => setCarrier(e.target.value as string)}
|
||||||
onChange={(e) => setCarrier(e.target.value as string)}
|
disabled={
|
||||||
disabled={carrierStore.carriers.data.length === 0}
|
carrierStore.carriers[
|
||||||
>
|
language as keyof typeof carrierStore.carriers
|
||||||
<MenuItem value="">Не выбрано</MenuItem>
|
].data?.length === 0
|
||||||
{carrierStore.carriers.data.map(
|
}
|
||||||
(c: (typeof carrierStore.carriers.data)[number]) => (
|
>
|
||||||
<MenuItem key={c.id} value={c.id}>
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
{c.full_name}
|
{carrierStore.carriers[
|
||||||
|
language as keyof typeof carrierStore.carriers
|
||||||
|
].data?.map((carrier) => (
|
||||||
|
<MenuItem key={carrier.id} value={carrier.id}>
|
||||||
|
{carrier.full_name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
))}
|
||||||
)}
|
</Select>
|
||||||
</Select>
|
</FormControl>
|
||||||
</FormControl>
|
<TextField
|
||||||
<TextField
|
className="w-full"
|
||||||
className="w-full"
|
label="Номер маршрута"
|
||||||
label="Номер маршрута"
|
required
|
||||||
required
|
value={routeNumber}
|
||||||
value={routeNumber}
|
onChange={(e) => setRouteNumber(e.target.value)}
|
||||||
onChange={(e) => setRouteNumber(e.target.value)}
|
/>
|
||||||
/>
|
<TextField
|
||||||
<TextField
|
className="w-full"
|
||||||
className="w-full"
|
label="Координаты маршрута"
|
||||||
label="Координаты маршрута"
|
multiline
|
||||||
multiline
|
minRows={3}
|
||||||
minRows={3}
|
value={routeCoords}
|
||||||
value={routeCoords}
|
onChange={(e) => setRouteCoords(e.target.value)}
|
||||||
onChange={(e) => setRouteCoords(e.target.value)}
|
/>
|
||||||
/>
|
<TextField
|
||||||
<TextField
|
className="w-full"
|
||||||
className="w-full"
|
label="Номер маршрута в Говорящем Городе"
|
||||||
label="Номер маршрута в Говорящем Городе"
|
required
|
||||||
required
|
value={govRouteNumber}
|
||||||
value={govRouteNumber}
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
onChange={(e) => setGovRouteNumber(e.target.value)}
|
/>
|
||||||
/>
|
<FormControl fullWidth required>
|
||||||
<FormControl fullWidth required>
|
<InputLabel>Обращение губернатора</InputLabel>
|
||||||
<InputLabel>Обращение губернатора</InputLabel>
|
<Select
|
||||||
<Select
|
value={governorAppeal}
|
||||||
value={governorAppeal}
|
label="Обращение губернатора"
|
||||||
label="Обращение губернатора"
|
onChange={(e) => setGovernorAppeal(e.target.value as string)}
|
||||||
onChange={(e) => setGovernorAppeal(e.target.value as string)}
|
disabled={articlesStore.articleList.ru.data.length === 0}
|
||||||
disabled={articlesStore.articleList.ru.data.length === 0}
|
>
|
||||||
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{articlesStore.articleList.ru.data.map(
|
||||||
|
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
||||||
|
<MenuItem key={a.id} value={a.id}>
|
||||||
|
{a.heading}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</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
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleCreateRoute}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<MenuItem value="">Не выбрано</MenuItem>
|
{isLoading ? (
|
||||||
{articlesStore.articleList.ru.data.map(
|
<Loader2 size={20} className="animate-spin" />
|
||||||
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
) : (
|
||||||
<MenuItem key={a.id} value={a.id}>
|
"Сохранить"
|
||||||
{a.heading}
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Button>
|
||||||
</FormControl>
|
</div>
|
||||||
<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
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
className="w-min flex gap-2 items-center"
|
|
||||||
startIcon={<Save size={20} />}
|
|
||||||
onClick={handleCreateRoute}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Сохранить"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -6,10 +6,10 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Typography,
|
// Typography,
|
||||||
Box,
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -19,22 +19,23 @@ import { carrierStore } from "../../../shared/store/CarrierStore";
|
|||||||
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||||
import { routeStore } from "../../../shared/store/RouteStore";
|
import { routeStore } from "../../../shared/store/RouteStore";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import { languageStore } from "@shared";
|
||||||
|
|
||||||
export const RouteEditPage = observer(() => {
|
export const RouteEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { editRouteData } = routeStore;
|
const { editRouteData } = routeStore;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const response = await routeStore.getRoute(Number(id));
|
const response = await routeStore.getRoute(Number(id));
|
||||||
routeStore.setEditRouteData(response);
|
routeStore.setEditRouteData(response);
|
||||||
carrierStore.getCarriers();
|
carrierStore.getCarriers(language);
|
||||||
articlesStore.getArticleList();
|
articlesStore.getArticleList();
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id]);
|
}, [id, language]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -45,180 +46,186 @@ export const RouteEditPage = observer(() => {
|
|||||||
|
|
||||||
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">
|
<LanguageSwitcher />
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Маршруты / Редактировать
|
Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
<Box className="flex flex-col gap-6 w-full">
|
<FormControl fullWidth required>
|
||||||
<FormControl fullWidth required>
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
<InputLabel>Выберите перевозчика</InputLabel>
|
<Select
|
||||||
<Select
|
value={editRouteData.carrier_id}
|
||||||
value={editRouteData.carrier_id}
|
label="Выберите перевозчика"
|
||||||
label="Выберите перевозчика"
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
routeStore.setEditRouteData({
|
||||||
routeStore.setEditRouteData({
|
carrier_id: Number(e.target.value),
|
||||||
carrier_id: Number(e.target.value),
|
carrier:
|
||||||
carrier:
|
carrierStore.carriers[
|
||||||
carrierStore.carriers.data.find(
|
language as keyof typeof carrierStore.carriers
|
||||||
(c) => c.id === Number(e.target.value)
|
].data?.find((c) => c.id === Number(e.target.value))
|
||||||
)?.full_name || "",
|
?.full_name || "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={carrierStore.carriers.data.length === 0}
|
disabled={
|
||||||
>
|
carrierStore.carriers[
|
||||||
<MenuItem value="">Не выбрано</MenuItem>
|
language as keyof typeof carrierStore.carriers
|
||||||
{carrierStore.carriers.data.map(
|
].data?.length === 0
|
||||||
(c: (typeof carrierStore.carriers.data)[number]) => (
|
}
|
||||||
<MenuItem key={c.id} value={c.id}>
|
>
|
||||||
{c.full_name}
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{carrierStore.carriers[
|
||||||
|
language as keyof typeof carrierStore.carriers
|
||||||
|
].data?.map((carrier) => (
|
||||||
|
<MenuItem key={carrier.id} value={carrier.id}>
|
||||||
|
{carrier.full_name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
))}
|
||||||
)}
|
</Select>
|
||||||
</Select>
|
</FormControl>
|
||||||
</FormControl>
|
<TextField
|
||||||
<TextField
|
className="w-full"
|
||||||
className="w-full"
|
label="Номер маршрута"
|
||||||
label="Номер маршрута"
|
required
|
||||||
required
|
value={editRouteData.route_number || ""}
|
||||||
value={editRouteData.route_number || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
route_number: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Координаты маршрута"
|
|
||||||
multiline
|
|
||||||
minRows={3}
|
|
||||||
value={editRouteData.path.map((p) => p.join(" ")).join("\n") || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
path: e.target.value
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.split(" ").map(Number)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Номер маршрута в Говорящем Городе"
|
|
||||||
required
|
|
||||||
value={editRouteData.route_sys_number || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
route_sys_number: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FormControl fullWidth required>
|
|
||||||
<InputLabel>Обращение губернатора</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={editRouteData.governor_appeal || ""}
|
|
||||||
label="Обращение губернатора"
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
governor_appeal: Number(e.target.value),
|
route_number: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={articlesStore.articleList.ru.data.length === 0}
|
/>
|
||||||
>
|
<TextField
|
||||||
<MenuItem value="">Не выбрано</MenuItem>
|
className="w-full"
|
||||||
{articlesStore.articleList.ru.data.map(
|
label="Координаты маршрута"
|
||||||
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
multiline
|
||||||
<MenuItem key={a.id} value={a.id}>
|
minRows={3}
|
||||||
{a.heading}
|
value={editRouteData.path.map((p) => p.join(" ")).join("\n") || ""}
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl fullWidth required>
|
|
||||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={editRouteData.route_direction ? "forward" : "backward"}
|
|
||||||
label="Прямой/обратный маршрут"
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
route_direction: e.target.value === "forward",
|
path: e.target.value
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.split(" ").map(Number)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Номер маршрута в Говорящем Городе"
|
||||||
|
required
|
||||||
|
value={editRouteData.route_sys_number || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
route_sys_number: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Обращение губернатора</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editRouteData.governor_appeal || ""}
|
||||||
|
label="Обращение губернатора"
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
governor_appeal: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={articlesStore.articleList.ru.data.length === 0}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Не выбрано</MenuItem>
|
||||||
|
{articlesStore.articleList.ru.data.map(
|
||||||
|
(a: (typeof articlesStore.articleList.ru.data)[number]) => (
|
||||||
|
<MenuItem key={a.id} value={a.id}>
|
||||||
|
{a.heading}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editRouteData.route_direction ? "forward" : "backward"}
|
||||||
|
label="Прямой/обратный маршрут"
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
route_direction: e.target.value === "forward",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="forward">Прямой</MenuItem>
|
||||||
|
<MenuItem value="backward">Обратный</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (мин)"
|
||||||
|
value={editRouteData.scale_min || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
scale_min: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Масштаб (макс)"
|
||||||
|
value={editRouteData.scale_max || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
scale_max: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Поворот"
|
||||||
|
value={editRouteData.rotate || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
rotate: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. широта"
|
||||||
|
value={editRouteData.center_latitude || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
center_latitude: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Центр. долгота"
|
||||||
|
value={editRouteData.center_longitude || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
center_longitude: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<MenuItem value="forward">Прямой</MenuItem>
|
Сохранить
|
||||||
<MenuItem value="backward">Обратный</MenuItem>
|
</Button>
|
||||||
</Select>
|
</div>
|
||||||
</FormControl>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Масштаб (мин)"
|
|
||||||
value={editRouteData.scale_min || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
scale_min: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Масштаб (макс)"
|
|
||||||
value={editRouteData.scale_max || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
scale_max: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Поворот"
|
|
||||||
value={editRouteData.rotate || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
rotate: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Центр. широта"
|
|
||||||
value={editRouteData.center_latitude || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
center_latitude: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Центр. долгота"
|
|
||||||
value={editRouteData.center_longitude || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
routeStore.setEditRouteData({
|
|
||||||
center_longitude: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<div className="flex w-full justify-end">
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
className="w-min flex gap-2 items-center"
|
|
||||||
startIcon={<Save size={20} />}
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Сохранить
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -2,15 +2,18 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, routeStore } from "@shared";
|
import { languageStore, routeStore } 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 { Map, Pencil, Trash2 } from "lucide-react";
|
import { Map, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const RouteListPage = observer(() => {
|
export const RouteListPage = observer(() => {
|
||||||
const { routes, getRoutes, deleteRoute } = routeStore;
|
const { routes, getRoutes, deleteRoute } = routeStore;
|
||||||
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 [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<number | null>(null);
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,11 +25,33 @@ export const RouteListPage = observer(() => {
|
|||||||
field: "carrier",
|
field: "carrier",
|
||||||
headerName: "Перевозчик",
|
headerName: "Перевозчик",
|
||||||
width: 250,
|
width: 250,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "route_number",
|
field: "route_number",
|
||||||
headerName: "Номер маршрута",
|
headerName: "Номер маршрута",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "route_direction",
|
field: "route_direction",
|
||||||
@ -87,15 +112,35 @@ 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>
|
||||||
<CreateButton label="Создать маршрут" path="/route/create" />
|
<CreateButton label="Создать маршрут" path="/route/create" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -114,6 +159,19 @@ export const RouteListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteRoute(id)));
|
||||||
|
getRoutes();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { languageStore, sightsStore } from "@shared";
|
import { languageStore, sightsStore } 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 { Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2, Minus } 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";
|
||||||
|
|
||||||
@ -10,7 +10,9 @@ export const SightListPage = observer(() => {
|
|||||||
const { sights, getSights, deleteListSight } = sightsStore;
|
const { sights, getSights, deleteListSight } = sightsStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<string | null>(null);
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,13 +24,34 @@ export const SightListPage = observer(() => {
|
|||||||
field: "name",
|
field: "name",
|
||||||
headerName: "Имя",
|
headerName: "Имя",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "city",
|
field: "city",
|
||||||
headerName: "Город",
|
headerName: "Город",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
@ -76,10 +99,28 @@ export const SightListPage = observer(() => {
|
|||||||
path="/sight/create"
|
path="/sight/create"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -98,6 +139,19 @@ export const SightListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteListSight(id)));
|
||||||
|
getSights();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { stationsStore } from "@shared";
|
import { stationsStore } from "@shared";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const StationCreatePage = observer(() => {
|
export const StationCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -36,7 +37,8 @@ export const StationCreatePage = observer(() => {
|
|||||||
|
|
||||||
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">
|
<LanguageSwitcher />
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
@ -45,8 +47,10 @@ export const StationCreatePage = observer(() => {
|
|||||||
Назад
|
Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold">Создание станции</h1>
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
|
||||||
|
<h1 className="text-3xl break-words">{name}</h1>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Название"
|
label="Название"
|
||||||
|
@ -49,11 +49,13 @@ export const StationEditPage = observer(() => {
|
|||||||
|
|
||||||
const stationId = Number(id);
|
const stationId = Number(id);
|
||||||
await getEditStation(stationId);
|
await getEditStation(stationId);
|
||||||
await getCities(language);
|
await getCities("ru");
|
||||||
|
await getCities("en");
|
||||||
|
await getCities("zh");
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAndSetStationData();
|
fetchAndSetStationData();
|
||||||
}, [id, language]);
|
}, [id]);
|
||||||
|
|
||||||
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">
|
||||||
@ -69,6 +71,9 @@ export const StationEditPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||||
|
<h1 className="text-3xl break-words">{editStationData.ru.name}</h1>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Название"
|
label="Название"
|
||||||
@ -141,7 +146,7 @@ export const StationEditPage = observer(() => {
|
|||||||
value={editStationData.common.city_id || ""}
|
value={editStationData.common.city_id || ""}
|
||||||
label="Город"
|
label="Город"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const selectedCity = cities[language].find(
|
const selectedCity = cities[language].data.find(
|
||||||
(city) => city.id === e.target.value
|
(city) => city.id === e.target.value
|
||||||
);
|
);
|
||||||
setEditCommonData({
|
setEditCommonData({
|
||||||
@ -150,7 +155,7 @@ export const StationEditPage = observer(() => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cities[language].map((city) => (
|
{cities[language].data.map((city) => (
|
||||||
<MenuItem key={city.id} value={city.id}>
|
<MenuItem key={city.id} value={city.id}>
|
||||||
{city.name}
|
{city.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -2,7 +2,7 @@ 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, Pencil, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2, Minus } 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";
|
||||||
|
|
||||||
@ -10,7 +10,9 @@ export const StationListPage = observer(() => {
|
|||||||
const { stationLists, getStationList, 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 [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<number | null>(null);
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,11 +24,33 @@ export const StationListPage = observer(() => {
|
|||||||
field: "name",
|
field: "name",
|
||||||
headerName: "Название",
|
headerName: "Название",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "system_name",
|
field: "system_name",
|
||||||
headerName: "Системное название",
|
headerName: "Системное название",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "direction",
|
field: "direction",
|
||||||
@ -88,15 +112,33 @@ export const StationListPage = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
||||||
<div style={{ width: "100%" }}>
|
<div className="w-full">
|
||||||
<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>
|
||||||
<CreateButton label="Создать станцию" path="/station/create" />
|
<CreateButton label="Создать станцию" path="/station/create" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -115,6 +157,19 @@ export const StationListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteStation(id)));
|
||||||
|
getStationList();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { userStore } from "@shared";
|
import { userStore } 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 { Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { CreateButton, DeleteModal } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
@ -11,7 +11,9 @@ export const UserListPage = observer(() => {
|
|||||||
const { users, getUsers, deleteUser } = userStore;
|
const { users, getUsers, deleteUser } = userStore;
|
||||||
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 [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
|
const [rowId, setRowId] = useState<number | null>(null);
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUsers();
|
getUsers();
|
||||||
@ -22,11 +24,33 @@ export const UserListPage = observer(() => {
|
|||||||
field: "name",
|
field: "name",
|
||||||
headerName: "Имя",
|
headerName: "Имя",
|
||||||
width: 400,
|
width: 400,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "email",
|
field: "email",
|
||||||
headerName: "Email",
|
headerName: "Email",
|
||||||
width: 400,
|
width: 400,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "is_admin",
|
field: "is_admin",
|
||||||
@ -93,10 +117,28 @@ export const UserListPage = observer(() => {
|
|||||||
<h1 className="text-2xl">Пользователи</h1>
|
<h1 className="text-2xl">Пользователи</h1>
|
||||||
<CreateButton label="Создать пользователя" path="/user/create" />
|
<CreateButton label="Создать пользователя" path="/user/create" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +149,6 @@ export const UserListPage = observer(() => {
|
|||||||
if (rowId) {
|
if (rowId) {
|
||||||
await deleteUser(rowId);
|
await deleteUser(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
@ -116,6 +157,19 @@ export const UserListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteUser(id)));
|
||||||
|
getUsers();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,12 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { vehicleStore, VEHICLE_TYPES, carrierStore } from "@shared";
|
import {
|
||||||
|
vehicleStore,
|
||||||
|
VEHICLE_TYPES,
|
||||||
|
carrierStore,
|
||||||
|
languageStore,
|
||||||
|
} from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
@ -21,10 +26,11 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
const [type, setType] = useState("");
|
const [type, setType] = useState("");
|
||||||
const [carrierId, setCarrierId] = useState<number | null>(null);
|
const [carrierId, setCarrierId] = useState<number | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
carrierStore.getCarriers();
|
carrierStore.getCarriers(language);
|
||||||
}, []);
|
}, [language]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
@ -32,7 +38,8 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
await vehicleStore.createVehicle(
|
await vehicleStore.createVehicle(
|
||||||
Number(tailNumber),
|
Number(tailNumber),
|
||||||
Number(type),
|
Number(type),
|
||||||
carrierStore.carriers.data.find((c) => c.id === carrierId)?.full_name!,
|
carrierStore.carriers[language].data?.find((c) => c.id === carrierId)
|
||||||
|
?.full_name as string,
|
||||||
carrierId!
|
carrierId!
|
||||||
);
|
);
|
||||||
toast.success("Транспорт успешно создан");
|
toast.success("Транспорт успешно создан");
|
||||||
@ -88,7 +95,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.data.map((carrier) => (
|
{carrierStore.carriers[language].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>
|
||||||
|
@ -11,7 +11,12 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { carrierStore, VEHICLE_TYPES, vehicleStore } from "@shared";
|
import {
|
||||||
|
carrierStore,
|
||||||
|
languageStore,
|
||||||
|
VEHICLE_TYPES,
|
||||||
|
vehicleStore,
|
||||||
|
} from "@shared";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const VehicleEditPage = observer(() => {
|
export const VehicleEditPage = observer(() => {
|
||||||
@ -25,11 +30,12 @@ export const VehicleEditPage = observer(() => {
|
|||||||
editVehicle,
|
editVehicle,
|
||||||
} = vehicleStore;
|
} = vehicleStore;
|
||||||
const { getCarriers } = carrierStore;
|
const { getCarriers } = carrierStore;
|
||||||
|
const { language } = languageStore;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
await getVehicle(Number(id));
|
await getVehicle(Number(id));
|
||||||
await getCarriers();
|
await getCarriers(language);
|
||||||
|
|
||||||
setEditVehicleData({
|
setEditVehicleData({
|
||||||
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
|
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
|
||||||
type: vehicle[Number(id)]?.vehicle.type,
|
type: vehicle[Number(id)]?.vehicle.type,
|
||||||
@ -37,7 +43,7 @@ export const VehicleEditPage = observer(() => {
|
|||||||
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
|
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id, language]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const handleEdit = async () => {
|
const handleEdit = async () => {
|
||||||
try {
|
try {
|
||||||
@ -108,7 +114,7 @@ export const VehicleEditPage = observer(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{carrierStore.carriers.data.map((carrier) => (
|
{carrierStore.carriers[language].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>
|
||||||
|
@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|||||||
import { carrierStore, languageStore, vehicleStore } from "@shared";
|
import { carrierStore, languageStore, vehicleStore } 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, Pencil, Trash2 } from "lucide-react";
|
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CreateButton, DeleteModal } from "@widgets";
|
import { CreateButton, DeleteModal } from "@widgets";
|
||||||
import { VEHICLE_TYPES } from "@shared";
|
import { VEHICLE_TYPES } from "@shared";
|
||||||
@ -12,12 +12,14 @@ export const VehicleListPage = observer(() => {
|
|||||||
const { carriers, getCarriers } = carrierStore;
|
const { carriers, getCarriers } = carrierStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
const [rowId, setRowId] = useState<number | null>(null);
|
const [rowId, setRowId] = useState<number | null>(null);
|
||||||
|
const [ids, setIds] = useState<number[]>([]);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVehicles();
|
getVehicles();
|
||||||
getCarriers();
|
getCarriers(language);
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
@ -25,17 +27,31 @@ export const VehicleListPage = observer(() => {
|
|||||||
field: "tail_number",
|
field: "tail_number",
|
||||||
headerName: "Бортовой номер",
|
headerName: "Бортовой номер",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: "type",
|
||||||
headerName: "Тип",
|
headerName: "Тип",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 items-center">
|
<div className="w-full h-full flex items-center">
|
||||||
{VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
{params.value ? (
|
||||||
?.label || params.row.type}
|
VEHICLE_TYPES.find((type) => type.value === params.row.type)
|
||||||
|
?.label || params.row.type
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -44,13 +60,34 @@ export const VehicleListPage = observer(() => {
|
|||||||
field: "carrier",
|
field: "carrier",
|
||||||
headerName: "Перевозчик",
|
headerName: "Перевозчик",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "city",
|
field: "city",
|
||||||
headerName: "Город",
|
headerName: "Город",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center">
|
||||||
|
{params.value ? (
|
||||||
|
params.value
|
||||||
|
) : (
|
||||||
|
<Minus size={20} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
@ -86,7 +123,7 @@ export const VehicleListPage = observer(() => {
|
|||||||
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.data?.find(
|
city: carriers[language].data?.find(
|
||||||
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
||||||
)?.city,
|
)?.city,
|
||||||
}));
|
}));
|
||||||
@ -101,10 +138,28 @@ export const VehicleListPage = observer(() => {
|
|||||||
path="/vehicle/create"
|
path="/vehicle/create"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex justify-end mb-5 duration-300"
|
||||||
|
style={{ opacity: ids.length > 0 ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||||
|
onClick={() => setIsBulkDeleteModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 size={20} className="text-white" /> Удалить выбранные (
|
||||||
|
{ids.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hideFooterPagination
|
hideFooterPagination
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newSelection) => {
|
||||||
|
setIds(Array.from(newSelection.ids) as number[]);
|
||||||
|
}}
|
||||||
hideFooter
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -123,6 +178,19 @@ export const VehicleListPage = observer(() => {
|
|||||||
setRowId(null);
|
setRowId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isBulkDeleteModalOpen}
|
||||||
|
onDelete={async () => {
|
||||||
|
await Promise.all(ids.map((id) => deleteVehicle(id)));
|
||||||
|
getVehicles();
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
setIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsBulkDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
62
src/shared/config/CarrierSvg.tsx
Normal file
62
src/shared/config/CarrierSvg.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
export const CarrierSvg = () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
height="26px"
|
||||||
|
width="26px"
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 489.785 489.785"
|
||||||
|
>
|
||||||
|
<g id="XMLID_196_">
|
||||||
|
<path
|
||||||
|
id="XMLID_203_"
|
||||||
|
d="M409.772,379.327l-81.359-124.975c-5.884-9.054-15.925-13.119-25.987-13.119
|
||||||
|
c-2.082,0-6.392,0.05-11.051,0.115c-0.363-0.61-0.742-1.215-1.355-1.627l-20.492-13.609c-2.364-1.569-5.434-1.486-7.701,0.182
|
||||||
|
l-16.948,12.508l-16.959-12.508c-2.285-1.668-5.337-1.751-7.72-0.182l-20.455,13.609c-0.578,0.377-0.945,0.907-1.282,1.461
|
||||||
|
c-4.828,0.031-9.327,0.057-11.222,0.057c-10.016,0-20.011,4.119-25.859,13.113L80.022,379.327
|
||||||
|
c-8.65,13.267-5.149,31.008,7.896,39.992l18.06,12.449c10.887-25.926,28.868-48.094,51.45-64.279l4.657-7.162v3.861
|
||||||
|
c16.364-10.811,34.941-18.477,54.885-22.234c-5.926-13.152-10.899-28.819-14.546-43.586c-4.249-17.232-6.741-33.201-6.741-42.245
|
||||||
|
c0-3.351,0.433-6.579,1.09-9.727l14.8,48.975c0.766,2.565,2.984,4.417,5.641,4.73c0.268,0.03,0.529,0.046,0.784,0.046
|
||||||
|
c2.365,0,4.602-1.25,5.818-3.34l11.538-19.873l3.246,3.235c-7.768,10.276-10.82,39.199-12.005,60.314
|
||||||
|
c5.994-0.734,12.066-1.222,18.254-1.222c6.201,0,12.292,0.497,18.304,1.23c-1.186-21.114-4.237-50.037-12.024-60.322l3.246-3.255
|
||||||
|
l11.574,19.892c1.216,2.09,3.422,3.34,5.805,3.34c0.255,0,0.522-0.016,0.779-0.046c2.655-0.314,4.874-2.166,5.659-4.73
|
||||||
|
l14.791-48.872c0.634,3.116,1.051,6.313,1.051,9.624c0,16.806-8.425,57.342-21.276,85.831
|
||||||
|
c19.981,3.768,38.588,11.453,54.953,22.291v-3.899l4.735,7.256c22.504,16.193,40.436,38.324,51.293,64.206l18.139-12.488
|
||||||
|
C414.919,410.335,418.403,392.594,409.772,379.327z M219.962,276.685l-8.613-28.53l12.388-8.24l12.322,9.088L219.962,276.685z
|
||||||
|
M269.783,276.685l-16.079-27.683l12.31-9.088l12.401,8.24L269.783,276.685z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="XMLID_202_"
|
||||||
|
d="M202.716,424.721l14.705,19.349c8.151-4.914,17.598-7.607,27.427-7.607c9.848,0,19.313,2.692,27.464,7.615
|
||||||
|
l14.705-19.363c-11.465-10.799-26.346-16.721-42.15-16.721C229.055,407.994,214.156,413.925,202.716,424.721z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="XMLID_201_"
|
||||||
|
d="M176.693,160.576c0.499,25.456,14.96,47.266,36.03,58.591c9.622,5.18,20.473,8.384,32.174,8.384
|
||||||
|
c11.683,0,22.503-3.198,32.114-8.368c21.063-11.311,35.579-33.117,36.06-58.582c-17.379,12.075-41.896,19.923-68.174,19.923
|
||||||
|
S194.096,172.676,176.693,160.576z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="XMLID_200_"
|
||||||
|
d="M174.741,100.132l-0.225,20.205c0.037,15.991,31.524,36.82,70.38,36.82
|
||||||
|
c38.855,0,70.314-20.829,70.331-36.82l-0.207-20.195c10.224-2.662,18.158-6.617,23.239-12.301
|
||||||
|
c3.981-4.434,6.267-9.902,6.267-16.783C344.528,39.883,299.879,0,244.897,0c-55.031,0-99.631,39.883-99.631,71.058
|
||||||
|
c0,6.881,2.273,12.34,6.236,16.783C156.585,93.524,164.529,97.479,174.741,100.132z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="XMLID_197_"
|
||||||
|
d="M244.848,356.925c-73.255,0-132.858,59.605-132.858,132.86h33.47c0-0.048,0-0.114,0-0.161v-0.031
|
||||||
|
c1.088-6.557,6.711-11.334,13.313-11.334c0.115,0,0.243,0.01,0.37,0.01l51.707,1.341c-0.973,3.247-1.648,6.619-1.648,10.176h71.322
|
||||||
|
c0-3.557-0.669-6.929-1.66-10.176l51.724-1.341c0.109,0,0.219-0.01,0.353-0.01c6.595,0,12.243,4.777,13.324,11.334v0.031
|
||||||
|
c0,0.047,0,0.113,0,0.161h33.44C377.706,416.53,318.122,356.925,244.848,356.925z M302.201,433.91l-27.562,36.317
|
||||||
|
c-6.389-9.687-17.325-16.104-29.792-16.104c-12.437,0-23.385,6.411-29.762,16.098l-27.555-36.3
|
||||||
|
c-4.699-6.194-4.11-14.923,1.392-20.424c15.452-15.443,35.689-23.166,55.943-23.166c20.249,0,40.484,7.723,55.961,23.179
|
||||||
|
C306.322,419.007,306.901,427.719,302.201,433.91z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -7,22 +7,24 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
Earth,
|
Earth,
|
||||||
Landmark,
|
Landmark,
|
||||||
BusFront,
|
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Car,
|
// Car,
|
||||||
Table,
|
Table,
|
||||||
|
Notebook,
|
||||||
Split,
|
Split,
|
||||||
Newspaper,
|
Newspaper,
|
||||||
PersonStanding,
|
PersonStanding,
|
||||||
Cpu,
|
Cpu,
|
||||||
BookImage,
|
BookImage,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { CarrierSvg } from "./CarrierSvg";
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 300;
|
export const DRAWER_WIDTH = 300;
|
||||||
|
|
||||||
interface NavigationItem {
|
interface NavigationItem {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon?: LucideIcon | React.ReactNode;
|
||||||
path?: string;
|
path?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
nestedItems?: NavigationItem[];
|
nestedItems?: NavigationItem[];
|
||||||
@ -34,43 +36,6 @@ export const NAVIGATION_ITEMS: {
|
|||||||
secondary: NavigationItem[];
|
secondary: NavigationItem[];
|
||||||
} = {
|
} = {
|
||||||
primary: [
|
primary: [
|
||||||
{
|
|
||||||
id: "countries",
|
|
||||||
label: "Страны",
|
|
||||||
icon: Earth,
|
|
||||||
path: "/country",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cities",
|
|
||||||
label: "Города",
|
|
||||||
icon: Building2,
|
|
||||||
path: "/city",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "carriers",
|
|
||||||
label: "Перевозчики",
|
|
||||||
icon: BusFront,
|
|
||||||
path: "/carrier",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: "snapshots",
|
|
||||||
label: "Снапшоты",
|
|
||||||
icon: GitBranch,
|
|
||||||
path: "/snapshot",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "map",
|
|
||||||
label: "Карта",
|
|
||||||
icon: Map,
|
|
||||||
path: "/map",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "devices",
|
|
||||||
label: "Устройства",
|
|
||||||
icon: Cpu,
|
|
||||||
path: "/devices",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "all",
|
id: "all",
|
||||||
label: "Все сущности",
|
label: "Все сущности",
|
||||||
@ -106,15 +71,58 @@ export const NAVIGATION_ITEMS: {
|
|||||||
icon: Split,
|
icon: Split,
|
||||||
path: "/route",
|
path: "/route",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "reference",
|
||||||
|
label: "Справочник",
|
||||||
|
icon: Notebook,
|
||||||
|
nestedItems: [
|
||||||
|
{
|
||||||
|
id: "countries",
|
||||||
|
label: "Страны",
|
||||||
|
icon: Earth,
|
||||||
|
path: "/country",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cities",
|
||||||
|
label: "Города",
|
||||||
|
icon: Building2,
|
||||||
|
path: "/city",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "carriers",
|
||||||
|
label: "Перевозчики",
|
||||||
|
// @ts-ignore
|
||||||
|
icon: CarrierSvg,
|
||||||
|
path: "/carrier",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "vehicles",
|
id: "snapshots",
|
||||||
label: "Транспорт",
|
label: "Снапшоты",
|
||||||
icon: Car,
|
icon: GitBranch,
|
||||||
path: "/vehicle",
|
path: "/snapshot",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "map",
|
||||||
|
label: "Карта",
|
||||||
|
icon: Map,
|
||||||
|
path: "/map",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "devices",
|
||||||
|
label: "Устройства",
|
||||||
|
icon: Cpu,
|
||||||
|
path: "/devices",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: "vehicles",
|
||||||
|
// label: "Транспорт",
|
||||||
|
// icon: Car,
|
||||||
|
// path: "/vehicle",
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
id: "users",
|
id: "users",
|
||||||
label: "Пользователи",
|
label: "Пользователи",
|
||||||
|
@ -120,7 +120,6 @@ export const PreviewMediaDialog = observer(
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Тип медиа"
|
label="Тип медиа"
|
||||||
@ -133,7 +132,7 @@ export const PreviewMediaDialog = observer(
|
|||||||
sx={{ width: "50%" }}
|
sx={{ width: "50%" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box className="flex gap-4 h-full">
|
<Box className="flex gap-4">
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
sx={{
|
sx={{
|
||||||
@ -142,8 +141,8 @@ export const PreviewMediaDialog = observer(
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
minHeight: 400,
|
|
||||||
}}
|
}}
|
||||||
|
className="max-h-[40vh]"
|
||||||
>
|
>
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
@ -151,6 +150,7 @@ export const PreviewMediaDialog = observer(
|
|||||||
media_type: media.media_type,
|
media_type: media.media_type,
|
||||||
filename: media.filename,
|
filename: media.filename,
|
||||||
}}
|
}}
|
||||||
|
fullHeight
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ export const UploadMediaDialog = observer(
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Box className="flex gap-4 h-full">
|
<Box className="flex gap-4 h-[40vh]">
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
sx={{
|
sx={{
|
||||||
@ -197,7 +197,7 @@ export const UploadMediaDialog = observer(
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
minHeight: 400,
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <MediaViewer
|
{/* <MediaViewer
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { authInstance, editSightStore, Language, languageStore } from "@shared";
|
import {
|
||||||
|
authInstance,
|
||||||
|
editSightStore,
|
||||||
|
Language,
|
||||||
|
languageStore,
|
||||||
|
languageInstance,
|
||||||
|
} from "@shared";
|
||||||
import { computed, makeAutoObservable, runInAction } from "mobx";
|
import { computed, makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
export type Article = {
|
export type Article = {
|
||||||
@ -6,6 +12,18 @@ export type Article = {
|
|||||||
heading: string;
|
heading: string;
|
||||||
body: string;
|
body: string;
|
||||||
service_name: string;
|
service_name: string;
|
||||||
|
ru?: {
|
||||||
|
heading: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
en?: {
|
||||||
|
heading: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
zh?: {
|
||||||
|
heading: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type Media = {
|
type Media = {
|
||||||
@ -99,13 +117,25 @@ class ArticlesStore {
|
|||||||
this.articleLoading = false;
|
this.articleLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
getArticle = async (id: number) => {
|
getArticle = async (id: number, language?: Language) => {
|
||||||
this.articleLoading = true;
|
this.articleLoading = true;
|
||||||
const response = await authInstance.get(`/article/${id}`);
|
if (language) {
|
||||||
|
const response = await languageInstance(language).get(`/article/${id}`);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.articleData = response.data;
|
if (!this.articleData) {
|
||||||
});
|
this.articleData = { id, heading: "", body: "", service_name: "" };
|
||||||
|
}
|
||||||
|
this.articleData[language] = {
|
||||||
|
heading: response.data.heading,
|
||||||
|
body: response.data.body,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const response = await authInstance.get(`/article/${id}`);
|
||||||
|
runInAction(() => {
|
||||||
|
this.articleData = response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
this.articleLoading = false;
|
this.articleLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,6 +167,20 @@ class ArticlesStore {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
deleteArticles = async (ids: number[]) => {
|
||||||
|
for (const id of ids) {
|
||||||
|
await authInstance.delete(`/article/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of ["ru", "en", "zh"] as Language[]) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.articleList[id].data = this.articleList[id].data.filter(
|
||||||
|
(article) => !ids.includes(article.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const articlesStore = new ArticlesStore();
|
export const articlesStore = new ArticlesStore();
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { authInstance } from "@shared";
|
import {
|
||||||
|
authInstance,
|
||||||
|
cityStore,
|
||||||
|
languageStore,
|
||||||
|
languageInstance,
|
||||||
|
Language,
|
||||||
|
} from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
export type Carrier = {
|
export type Carrier = {
|
||||||
@ -9,22 +15,45 @@ export type Carrier = {
|
|||||||
city: string;
|
city: string;
|
||||||
city_id: number;
|
city_id: number;
|
||||||
logo: string;
|
logo: string;
|
||||||
main_color: string;
|
// main_color: string;
|
||||||
left_color: string;
|
// left_color: string;
|
||||||
right_color: string;
|
// right_color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Carriers = {
|
type CarrierData = {
|
||||||
data: Carrier[];
|
data: Carrier[];
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CashedCarrier = Record<number, Carrier>;
|
type Carriers = {
|
||||||
|
ru: CarrierData;
|
||||||
|
en: CarrierData;
|
||||||
|
zh: CarrierData;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CashedCarrier = Record<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
ru: Carrier | null;
|
||||||
|
en: Carrier | null;
|
||||||
|
zh: Carrier | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
class CarrierStore {
|
class CarrierStore {
|
||||||
carriers: Carriers = {
|
carriers: Carriers = {
|
||||||
data: [],
|
ru: {
|
||||||
loaded: false,
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
carrier: CashedCarrier = {};
|
carrier: CashedCarrier = {};
|
||||||
|
|
||||||
@ -32,14 +61,14 @@ class CarrierStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCarriers = async () => {
|
getCarriers = async (language: Language) => {
|
||||||
if (this.carriers.loaded) return;
|
if (this.carriers[language as keyof Carriers].loaded) return;
|
||||||
|
|
||||||
const response = await authInstance.get("/carrier");
|
const response = await authInstance.get("/carrier");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers.data = response.data;
|
this.carriers[language as keyof Carriers].data = response.data;
|
||||||
this.carriers.loaded = true;
|
this.carriers[language as keyof Carriers].loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -47,113 +76,163 @@ class CarrierStore {
|
|||||||
await authInstance.delete(`/carrier/${id}`);
|
await authInstance.delete(`/carrier/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers.data = this.carriers.data.filter(
|
for (const language of ["ru", "en", "zh"] as const) {
|
||||||
(carrier) => carrier.id !== id
|
this.carriers[language].data = this.carriers[language].data.filter(
|
||||||
);
|
(carrier: 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;
|
if (this.carrier[id]?.ru && this.carrier[id]?.en && this.carrier[id]?.zh)
|
||||||
const response = await authInstance.get(`/carrier/${id}`);
|
return;
|
||||||
|
|
||||||
|
const ruResponse = await languageInstance("ru").get(`/carrier/${id}`);
|
||||||
|
const enResponse = await languageInstance("en").get(`/carrier/${id}`);
|
||||||
|
const zhResponse = await languageInstance("zh").get(`/carrier/${id}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (!this.carrier[id]) {
|
if (!this.carrier[id]) {
|
||||||
this.carrier[id] = {
|
this.carrier[id] = {
|
||||||
id: 0,
|
ru: null,
|
||||||
short_name: "",
|
en: null,
|
||||||
full_name: "",
|
zh: null,
|
||||||
slogan: "",
|
|
||||||
city: "",
|
|
||||||
city_id: 0,
|
|
||||||
logo: "",
|
|
||||||
main_color: "",
|
|
||||||
left_color: "",
|
|
||||||
right_color: "",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.carrier[id] = response.data;
|
this.carrier[id].ru = ruResponse.data;
|
||||||
|
this.carrier[id].en = enResponse.data;
|
||||||
|
this.carrier[id].zh = zhResponse.data;
|
||||||
});
|
});
|
||||||
return response.data;
|
return this.carrier[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
createCarrier = async (
|
createCarrier = async (
|
||||||
fullName: string,
|
fullName: string,
|
||||||
shortName: string,
|
shortName: string,
|
||||||
city: string,
|
|
||||||
cityId: number,
|
cityId: number,
|
||||||
main_color: string,
|
|
||||||
left_color: string,
|
|
||||||
right_color: string,
|
|
||||||
slogan: string,
|
slogan: string,
|
||||||
logoId: string
|
logoId: string
|
||||||
) => {
|
) => {
|
||||||
const response = await authInstance.post("/carrier", {
|
const { language } = languageStore;
|
||||||
|
const cityName =
|
||||||
|
cityStore.cities[language].data.find((city) => city.id === cityId)
|
||||||
|
?.name || "";
|
||||||
|
|
||||||
|
const response = await languageInstance(language).post("/carrier", {
|
||||||
full_name: fullName,
|
full_name: fullName,
|
||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
city,
|
city: cityName,
|
||||||
city_id: cityId,
|
city_id: cityId,
|
||||||
main_color,
|
|
||||||
left_color,
|
|
||||||
right_color,
|
|
||||||
slogan,
|
slogan,
|
||||||
logo: logoId,
|
logo: logoId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const carrierId = response.data.id;
|
||||||
|
|
||||||
|
// Create translations for other languages
|
||||||
|
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
|
||||||
|
await languageInstance(lang as Language).patch(`/carrier/${carrierId}`, {
|
||||||
|
full_name: fullName,
|
||||||
|
short_name: shortName,
|
||||||
|
city: cityName,
|
||||||
|
city_id: cityId,
|
||||||
|
slogan,
|
||||||
|
logo: logoId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.carriers.data.push(response.data);
|
for (const language of ["ru", "en", "zh"] as const) {
|
||||||
|
this.carriers[language].data.push(response.data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
editCarrierData = {
|
editCarrierData = {
|
||||||
full_name: "",
|
ru: {
|
||||||
short_name: "",
|
full_name: "",
|
||||||
city: "",
|
short_name: "",
|
||||||
|
|
||||||
|
// main_color: "",
|
||||||
|
// left_color: "",
|
||||||
|
// right_color: "",
|
||||||
|
slogan: "",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
full_name: "",
|
||||||
|
short_name: "",
|
||||||
|
|
||||||
|
// main_color: "",
|
||||||
|
// left_color: "",
|
||||||
|
// right_color: "",
|
||||||
|
slogan: "",
|
||||||
|
},
|
||||||
city_id: 0,
|
city_id: 0,
|
||||||
main_color: "",
|
|
||||||
left_color: "",
|
|
||||||
right_color: "",
|
|
||||||
slogan: "",
|
|
||||||
logo: "",
|
logo: "",
|
||||||
|
zh: {
|
||||||
|
full_name: "",
|
||||||
|
short_name: "",
|
||||||
|
|
||||||
|
// main_color: "",
|
||||||
|
// left_color: "",
|
||||||
|
// right_color: "",
|
||||||
|
slogan: "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditCarrierData = (
|
setEditCarrierData = (
|
||||||
fullName: string,
|
fullName: string,
|
||||||
shortName: string,
|
shortName: string,
|
||||||
city: string,
|
|
||||||
cityId: number,
|
cityId: number,
|
||||||
main_color: string,
|
// main_color: string,
|
||||||
left_color: string,
|
// left_color: string,
|
||||||
right_color: string,
|
// right_color: string,
|
||||||
slogan: string,
|
slogan: string,
|
||||||
logoId: string
|
logoId: string,
|
||||||
|
language: Language
|
||||||
) => {
|
) => {
|
||||||
this.editCarrierData = {
|
this.editCarrierData.city_id = cityId;
|
||||||
|
this.editCarrierData.logo = logoId;
|
||||||
|
this.editCarrierData[language] = {
|
||||||
full_name: fullName,
|
full_name: fullName,
|
||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
city,
|
// main_color: main_color,
|
||||||
city_id: cityId,
|
// left_color: left_color,
|
||||||
main_color: main_color,
|
// right_color: right_color,
|
||||||
left_color: left_color,
|
|
||||||
right_color: right_color,
|
|
||||||
slogan: slogan,
|
slogan: slogan,
|
||||||
logo: logoId,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
editCarrier = async (id: number) => {
|
editCarrier = async (id: number) => {
|
||||||
const response = await authInstance.patch(
|
const cityName =
|
||||||
`/carrier/${id}`,
|
cityStore.cities[languageStore.language].data.find(
|
||||||
this.editCarrierData
|
(city) => city.id === this.editCarrierData.city_id
|
||||||
);
|
)?.name || "";
|
||||||
|
|
||||||
runInAction(() => {
|
for (const language of ["ru", "en", "zh"] as const) {
|
||||||
this.carriers.data = this.carriers.data.map((carrier) =>
|
const response = await languageInstance(language).patch(
|
||||||
carrier.id === id ? { ...carrier, ...response.data } : carrier
|
`/carrier/${id}`,
|
||||||
|
{
|
||||||
|
...this.editCarrierData[language],
|
||||||
|
city: cityName,
|
||||||
|
logo: this.editCarrierData.logo,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.carrier[id] = response.data;
|
runInAction(() => {
|
||||||
});
|
if (this.carrier[id]) {
|
||||||
|
this.carrier[id][language] = response.data;
|
||||||
|
}
|
||||||
|
for (const language of ["ru", "en", "zh"] as const) {
|
||||||
|
this.carriers[language].data = this.carriers[language].data.map(
|
||||||
|
(carrier: Carrier) =>
|
||||||
|
carrier.id === id ? { ...carrier, ...response.data } : carrier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
Language,
|
Language,
|
||||||
languageStore,
|
languageStore,
|
||||||
countryStore,
|
countryStore,
|
||||||
|
CashedCountries,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
@ -16,9 +17,18 @@ export type City = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CashedCities = {
|
export type CashedCities = {
|
||||||
ru: City[];
|
ru: {
|
||||||
en: City[];
|
data: City[];
|
||||||
zh: City[];
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
en: {
|
||||||
|
data: City[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
zh: {
|
||||||
|
data: City[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CashedCity = {
|
export type CashedCity = {
|
||||||
@ -29,9 +39,18 @@ export type CashedCity = {
|
|||||||
|
|
||||||
class CityStore {
|
class CityStore {
|
||||||
cities: CashedCities = {
|
cities: CashedCities = {
|
||||||
ru: [],
|
ru: {
|
||||||
en: [],
|
data: [],
|
||||||
zh: [],
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
city: Record<string, CashedCity> = {};
|
city: Record<string, CashedCity> = {};
|
||||||
@ -40,25 +59,37 @@ class CityStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
ruCities: City[] = [];
|
ruCities: {
|
||||||
|
data: City[];
|
||||||
|
loaded: boolean;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
getRuCities = async () => {
|
getRuCities = async () => {
|
||||||
|
if (this.ruCities.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await languageInstance("ru").get(`/city`);
|
const response = await languageInstance("ru").get(`/city`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.ruCities = response.data;
|
this.ruCities.data = response.data;
|
||||||
|
this.ruCities.loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getCities = async (language: keyof CashedCities) => {
|
getCities = async (language: keyof CashedCities) => {
|
||||||
if (this.cities[language] && this.cities[language].length > 0) {
|
if (this.cities[language].loaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authInstance.get(`/city`);
|
const response = await authInstance.get(`/city`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.cities[language] = response.data;
|
this.cities[language].data = response.data;
|
||||||
|
this.cities[language].loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,19 +114,22 @@ class CityStore {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteCity = async (code: string, language: keyof CashedCities) => {
|
deleteCity = async (code: string) => {
|
||||||
await authInstance.delete(`/city/${code}`);
|
await authInstance.delete(`/city/${code}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.cities[language] = this.cities[language].filter(
|
for (const secondaryLanguage of ["ru", "en", "zh"] as Language[]) {
|
||||||
(city) => city.country_code !== code
|
this.cities[secondaryLanguage].data = this.cities[
|
||||||
);
|
secondaryLanguage
|
||||||
this.city[code][language] = null;
|
].data.filter((city) => city.id !== Number(code));
|
||||||
|
if (this.city[code]) {
|
||||||
|
this.city[code][secondaryLanguage] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createCityData = {
|
createCityData = {
|
||||||
country: "",
|
|
||||||
country_code: "",
|
country_code: "",
|
||||||
arms: "",
|
arms: "",
|
||||||
ru: {
|
ru: {
|
||||||
@ -111,14 +145,12 @@ class CityStore {
|
|||||||
|
|
||||||
setCreateCityData = (
|
setCreateCityData = (
|
||||||
name: string,
|
name: string,
|
||||||
country: string,
|
|
||||||
country_code: string,
|
country_code: string,
|
||||||
arms: string,
|
arms: string,
|
||||||
language: keyof CashedCities
|
language: keyof CashedCities
|
||||||
) => {
|
) => {
|
||||||
this.createCityData = {
|
this.createCityData = {
|
||||||
...this.createCityData,
|
...this.createCityData,
|
||||||
country: country,
|
|
||||||
country_code: country_code,
|
country_code: country_code,
|
||||||
arms: arms,
|
arms: arms,
|
||||||
[language]: {
|
[language]: {
|
||||||
@ -127,73 +159,84 @@ class CityStore {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
createCity = async () => {
|
async createCity() {
|
||||||
const { language } = languageStore;
|
const language = languageStore.language as Language;
|
||||||
const { country, country_code, arms } = this.createCityData;
|
const { country_code, arms } = this.createCityData;
|
||||||
const { name } = this.createCityData[language as keyof CashedCities];
|
const { name } = this.createCityData[language];
|
||||||
|
|
||||||
if (name && country && country_code && arms) {
|
if (!name || !country_code) {
|
||||||
const cityResponse = await languageInstance(language as Language).post(
|
return;
|
||||||
"/city",
|
}
|
||||||
{
|
|
||||||
name: name,
|
|
||||||
country: country,
|
|
||||||
country_code: country_code,
|
|
||||||
arms: arms,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
runInAction(() => {
|
try {
|
||||||
this.cities[language as keyof CashedCities] = [
|
// Create city in primary language
|
||||||
...this.cities[language as keyof CashedCities],
|
const cityResponse = await languageInstance(language).post("/city", {
|
||||||
cityResponse.data,
|
name,
|
||||||
];
|
country:
|
||||||
|
countryStore.countries[language as keyof CashedCountries]?.data.find(
|
||||||
|
(c) => c.code === country_code
|
||||||
|
)?.name || "",
|
||||||
|
country_code,
|
||||||
|
arms: arms || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const secondaryLanguage of ["ru", "en", "zh"].filter(
|
const cityId = cityResponse.data.id;
|
||||||
|
|
||||||
|
// Create/update other language versions
|
||||||
|
for (const secondaryLanguage of (["ru", "en", "zh"] as Language[]).filter(
|
||||||
(l) => l !== language
|
(l) => l !== language
|
||||||
)) {
|
)) {
|
||||||
const { name } =
|
const { name: secondaryName } = this.createCityData[secondaryLanguage];
|
||||||
this.createCityData[secondaryLanguage as keyof CashedCities];
|
|
||||||
|
|
||||||
const patchResponse = await languageInstance(
|
// Get country name in secondary language
|
||||||
secondaryLanguage as Language
|
const countryName =
|
||||||
).patch(`/city/${cityResponse.data.id}`, {
|
countryStore.countries[secondaryLanguage]?.data.find(
|
||||||
name: name,
|
(c) => c.code === country_code
|
||||||
country: country,
|
)?.name || "";
|
||||||
country_code: country_code,
|
|
||||||
arms: arms,
|
const patchResponse = await languageInstance(secondaryLanguage).patch(
|
||||||
});
|
`/city/${cityId}`,
|
||||||
|
{
|
||||||
|
name: secondaryName || "",
|
||||||
|
country: countryName,
|
||||||
|
country_code: country_code || "",
|
||||||
|
arms: arms || "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.cities[secondaryLanguage as keyof CashedCities] = [
|
this.cities[secondaryLanguage].data = [
|
||||||
...this.cities[secondaryLanguage as keyof CashedCities],
|
...this.cities[secondaryLanguage].data,
|
||||||
patchResponse.data,
|
patchResponse.data,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
runInAction(() => {
|
// Update primary language data
|
||||||
this.createCityData = {
|
runInAction(() => {
|
||||||
country: "",
|
this.cities[language].data = [
|
||||||
country_code: "",
|
...this.cities[language].data,
|
||||||
arms: "",
|
cityResponse.data,
|
||||||
ru: {
|
];
|
||||||
name: "",
|
});
|
||||||
},
|
|
||||||
en: {
|
// Reset form data
|
||||||
name: "",
|
runInAction(() => {
|
||||||
},
|
this.createCityData = {
|
||||||
zh: {
|
country_code: "",
|
||||||
name: "",
|
arms: "",
|
||||||
},
|
ru: { name: "" },
|
||||||
};
|
en: { name: "" },
|
||||||
});
|
zh: { name: "" },
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating city:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
editCityData = {
|
editCityData = {
|
||||||
country: "",
|
|
||||||
country_code: "",
|
country_code: "",
|
||||||
arms: "",
|
arms: "",
|
||||||
ru: {
|
ru: {
|
||||||
@ -209,14 +252,12 @@ class CityStore {
|
|||||||
|
|
||||||
setEditCityData = (
|
setEditCityData = (
|
||||||
name: string,
|
name: string,
|
||||||
country: string,
|
|
||||||
country_code: string,
|
country_code: string,
|
||||||
arms: string,
|
arms: string,
|
||||||
language: keyof CashedCities
|
language: keyof CashedCities
|
||||||
) => {
|
) => {
|
||||||
this.editCityData = {
|
this.editCityData = {
|
||||||
...this.editCityData,
|
...this.editCityData,
|
||||||
country: country,
|
|
||||||
country_code: country_code,
|
country_code: country_code,
|
||||||
arms: arms,
|
arms: arms,
|
||||||
|
|
||||||
@ -232,7 +273,7 @@ class CityStore {
|
|||||||
const { name } = this.editCityData[language as keyof CashedCities];
|
const { name } = this.editCityData[language as keyof CashedCities];
|
||||||
const { countries } = countryStore;
|
const { countries } = countryStore;
|
||||||
|
|
||||||
const country = countries[language as keyof CashedCities].find(
|
const country = countries[language as keyof CashedCities].data.find(
|
||||||
(country) => country.code === country_code
|
(country) => country.code === country_code
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -255,9 +296,9 @@ class CityStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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].data = this.cities[
|
||||||
language as keyof CashedCities
|
language as keyof CashedCities
|
||||||
].map((city) =>
|
].data.map((city) =>
|
||||||
city.id === Number(code)
|
city.id === Number(code)
|
||||||
? {
|
? {
|
||||||
id: city.id,
|
id: city.id,
|
||||||
|
@ -12,9 +12,18 @@ export type Country = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CashedCountries = {
|
export type CashedCountries = {
|
||||||
ru: Country[];
|
ru: {
|
||||||
en: Country[];
|
data: Country[];
|
||||||
zh: Country[];
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
en: {
|
||||||
|
data: Country[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
zh: {
|
||||||
|
data: Country[];
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CashedCountry = {
|
export type CashedCountry = {
|
||||||
@ -25,9 +34,18 @@ export type CashedCountry = {
|
|||||||
|
|
||||||
class CountryStore {
|
class CountryStore {
|
||||||
countries: CashedCountries = {
|
countries: CashedCountries = {
|
||||||
ru: [],
|
ru: {
|
||||||
en: [],
|
data: [],
|
||||||
zh: [],
|
loaded: false,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
data: [],
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
country: Record<string, CashedCountry> = {};
|
country: Record<string, CashedCountry> = {};
|
||||||
@ -37,14 +55,15 @@ class CountryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCountries = async (language: keyof CashedCountries) => {
|
getCountries = async (language: keyof CashedCountries) => {
|
||||||
if (this.countries[language] && this.countries[language].length > 0) {
|
if (this.countries[language].loaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authInstance.get(`/country`);
|
const response = await languageInstance(language).get(`/country`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.countries[language] = response.data;
|
this.countries[language].data = response.data;
|
||||||
|
this.countries[language].loaded = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,10 +95,15 @@ class CountryStore {
|
|||||||
await authInstance.delete(`/country/${code}`);
|
await authInstance.delete(`/country/${code}`);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.countries[language] = this.countries[language].filter(
|
this.countries[language].data = this.countries[language].data.filter(
|
||||||
(country) => country.code !== code
|
(country) => country.code !== code
|
||||||
);
|
);
|
||||||
this.country[code][language] = null;
|
this.countries[language].loaded = true;
|
||||||
|
this.country[code] = {
|
||||||
|
ru: null,
|
||||||
|
en: null,
|
||||||
|
zh: null,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,8 +145,8 @@ class CountryStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.countries[language as keyof CashedCountries] = [
|
this.countries[language as keyof CashedCountries].data = [
|
||||||
...this.countries[language as keyof CashedCountries],
|
...this.countries[language as keyof CashedCountries].data,
|
||||||
{ code: code, name: name },
|
{ code: code, name: name },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@ -142,8 +166,8 @@ class CountryStore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.countries[secondaryLanguage as keyof CashedCountries] = [
|
this.countries[secondaryLanguage as keyof CashedCountries].data = [
|
||||||
...this.countries[secondaryLanguage as keyof CashedCountries],
|
...this.countries[secondaryLanguage as keyof CashedCountries].data,
|
||||||
{ code: code, name: name },
|
{ code: code, name: name },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@ -204,11 +228,10 @@ class CountryStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (this.countries[language as keyof CashedCountries]) {
|
if (this.countries[language as keyof CashedCountries]) {
|
||||||
this.countries[language as keyof CashedCountries] = this.countries[
|
this.countries[language as keyof CashedCountries].data =
|
||||||
language as keyof CashedCountries
|
this.countries[language as keyof CashedCountries].data.map(
|
||||||
].map((country) =>
|
(country) => (country.code === code ? { code, name } : country)
|
||||||
country.code === code ? { code, name } : country
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,20 @@ import { Canvas } from "@react-three/fiber";
|
|||||||
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
||||||
|
|
||||||
type ModelViewerProps = {
|
type ModelViewerProps = {
|
||||||
|
width?: string;
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ThreeView = ({ fileUrl, height = "100%" }: ModelViewerProps) => {
|
export const ThreeView = ({
|
||||||
|
fileUrl,
|
||||||
|
height = "100%",
|
||||||
|
width = "100%",
|
||||||
|
}: ModelViewerProps) => {
|
||||||
const { scene } = useGLTF(fileUrl);
|
const { scene } = useGLTF(fileUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas style={{ width: "100%", height: height }}>
|
<Canvas style={{ height: height, width: width }}>
|
||||||
<ambientLight />
|
<ambientLight />
|
||||||
<directionalLight />
|
<directionalLight />
|
||||||
<Stage environment="city" intensity={0.6}>
|
<Stage environment="city" intensity={0.6}>
|
||||||
|
@ -13,16 +13,30 @@ export function MediaViewer({
|
|||||||
media,
|
media,
|
||||||
className,
|
className,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
}: Readonly<{ media?: MediaData; className?: string; fullWidth?: boolean }>) {
|
fullHeight,
|
||||||
|
}: Readonly<{
|
||||||
|
media?: MediaData;
|
||||||
|
className?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
fullHeight?: boolean;
|
||||||
|
}>) {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
return (
|
return (
|
||||||
<Box className={className} width={fullWidth ? "100%" : "auto"}>
|
<Box
|
||||||
|
className={className}
|
||||||
|
width={fullWidth ? "100%" : "auto"}
|
||||||
|
height={fullHeight ? "100%" : "auto"}
|
||||||
|
>
|
||||||
{media?.media_type === 1 && (
|
{media?.media_type === 1 && (
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||||
media?.id
|
media?.id
|
||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
alt={media?.filename}
|
alt={media?.filename}
|
||||||
|
style={{
|
||||||
|
height: fullHeight ? "100%" : "auto",
|
||||||
|
width: fullWidth ? "100%" : "auto",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -48,6 +62,10 @@ export function MediaViewer({
|
|||||||
media?.id
|
media?.id
|
||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
alt={media?.filename}
|
alt={media?.filename}
|
||||||
|
style={{
|
||||||
|
height: fullHeight ? "100%" : "auto",
|
||||||
|
width: fullWidth ? "100%" : "auto",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{media?.media_type === 4 && (
|
{media?.media_type === 4 && (
|
||||||
@ -78,6 +96,7 @@ export function MediaViewer({
|
|||||||
media?.id
|
media?.id
|
||||||
}/download?token=${token}`}
|
}/download?token=${token}`}
|
||||||
height="100%"
|
height="100%"
|
||||||
|
width="1000px"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -175,9 +175,10 @@ export const CreateInformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={ruCities ?? []}
|
options={ruCities.data ?? []}
|
||||||
value={
|
value={
|
||||||
ruCities.find((city) => city.id === sight.city_id) ?? null
|
ruCities.data.find((city) => city.id === sight.city_id) ??
|
||||||
|
null
|
||||||
}
|
}
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) => option.name}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
|
@ -19,14 +19,7 @@ import {
|
|||||||
MediaViewer,
|
MediaViewer,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import {
|
import { Trash2, ImagePlus, Unlink, Plus, Save, Search } from "lucide-react";
|
||||||
Trash2,
|
|
||||||
ImagePlus,
|
|
||||||
Unlink,
|
|
||||||
MousePointer,
|
|
||||||
Plus,
|
|
||||||
Save,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@ -160,7 +153,7 @@ export const CreateLeftTab = observer(
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<MousePointer color="white" size={18} />}
|
startIcon={<Search color="white" size={18} />}
|
||||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Выбрать статью
|
Выбрать статью
|
||||||
|
@ -551,6 +551,7 @@ export const CreateRightTab = observer(
|
|||||||
media={
|
media={
|
||||||
sight[language].right[activeArticleIndex].media[0]
|
sight[language].right[activeArticleIndex].media[0]
|
||||||
}
|
}
|
||||||
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
@ -163,17 +163,22 @@ export const InformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={ruCities ?? []}
|
options={ruCities?.data ?? []}
|
||||||
value={
|
value={
|
||||||
ruCities.find((city) => city.id === sight.common.city_id) ??
|
ruCities?.data?.find(
|
||||||
null
|
(city) => city.id === sight.common.city_id
|
||||||
|
) ?? null
|
||||||
}
|
}
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) => option.name}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
setCity(value?.id ?? 0);
|
setCity(value?.id ?? 0);
|
||||||
handleChange(language as Language, {
|
handleChange(
|
||||||
city_id: value?.id ?? 0,
|
language as Language,
|
||||||
});
|
{
|
||||||
|
city_id: value?.id ?? 0,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label="Город" />
|
<TextField {...params} label="Город" />
|
||||||
|
@ -18,14 +18,7 @@ import {
|
|||||||
MediaViewer,
|
MediaViewer,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import {
|
import { Trash2, ImagePlus, Unlink, Plus, Save, Search } from "lucide-react";
|
||||||
Trash2,
|
|
||||||
ImagePlus,
|
|
||||||
Unlink,
|
|
||||||
Plus,
|
|
||||||
MousePointer,
|
|
||||||
Save,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@ -175,7 +168,7 @@ export const LeftWidgetTab = observer(
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<MousePointer color="white" size={18} />}
|
startIcon={<Search color="white" size={18} />}
|
||||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Выбрать статью
|
Выбрать статью
|
||||||
|
@ -493,6 +493,7 @@ export const RightWidgetTab = observer(
|
|||||||
media={
|
media={
|
||||||
sight[language].right[activeArticleIndex].media[0]
|
sight[language].right[activeArticleIndex].media[0]
|
||||||
}
|
}
|
||||||
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
@ -67,7 +67,7 @@ export const SightsTable = observer(() => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows(sights, cities[language])?.map((row) => (
|
{rows(sights, cities[language]?.data ?? [])?.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row?.id}
|
key={row?.id}
|
||||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user