station edit in the route edit page

This commit is contained in:
Илья Куприец 2025-04-29 21:16:53 +03:00
parent a1a2264758
commit 03829aacc6
21 changed files with 1642 additions and 720 deletions

View File

@ -3,17 +3,20 @@ import {
type DataGridProps,
type GridColumnVisibilityModel,
} from "@mui/x-data-grid";
import { Stack, Button, Typography } from "@mui/material";
import { Stack, Button, Typography, Box } from "@mui/material";
import { ExportButton } from "@refinedev/mui";
import { useExport } from "@refinedev/core";
import React, { useState, useEffect, useMemo } from "react";
import Cookies from "js-cookie";
import { localeText } from "../locales/ru/localeText";
import { languageStore } from "../store/LanguageStore";
import { LanguageSwitch } from "./LanguageSwitch";
interface CustomDataGridProps extends DataGridProps {
hasCoordinates?: boolean;
resource?: string; // Add this prop
languageEnabled?: boolean;
}
const DEV_FIELDS = [
@ -46,6 +49,7 @@ const DEV_FIELDS = [
] as const;
export const CustomDataGrid = ({
languageEnabled = false,
hasCoordinates = false,
columns = [],
resource,
@ -130,6 +134,9 @@ export const CustomDataGrid = ({
return (
<Stack spacing={2}>
<Box sx={{ visibility: languageEnabled ? "visible" : "hidden" }}>
<LanguageSwitch />
</Box>
<DataGrid
{...props}
columns={columns}
@ -149,7 +156,6 @@ export const CustomDataGrid = ({
}}
pageSizeOptions={[10, 25, 50, 100]}
/>
<Stack direction="row" spacing={2} justifyContent="space-between" mb={2}>
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
{hasCoordinates && (

View File

@ -0,0 +1,70 @@
import { Box } from "@mui/material";
import { languageStore } from "../../store/LanguageStore";
import { observer } from "mobx-react-lite";
export const LanguageSwitch = observer(({ action }: any) => {
const { language, setLanguageAction } = languageStore;
const handleLanguageChange = (lang: string) => {
if (action) {
action();
}
setLanguageAction(lang);
};
return (
<Box
sx={{
flex: 1,
display: "flex",
gap: 2,
}}
>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "ru" ? "primary.main" : "transparent",
color: language === "ru" ? "white" : "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => handleLanguageChange("ru")}
>
RU
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "en" ? "primary.main" : "transparent",
color: language === "en" ? "white" : "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => handleLanguageChange("en")}
>
EN
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "zh" ? "primary.main" : "transparent",
color: language === "zh" ? "white" : "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => handleLanguageChange("zh")}
>
ZH
</Box>
</Box>
);
});

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { Close } from "@mui/icons-material";
import {
Stack,
Typography,
@ -20,14 +21,19 @@ import {
Paper,
TableBody,
IconButton,
Collapse,
Modal,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import { axiosInstance } from "../providers/data";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
import axios from "axios";
import { articleStore } from "../store/ArticleStore";
import { ArticleEditModal } from "./modals/ArticleEditModal";
import { StationEditModal } from "./modals/StationEditModal";
import { stationStore } from "../store/StationStore";
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
const index = pos - 1;
if (index >= arr.length) {
@ -82,41 +88,8 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
type,
onSave,
}: LinkedItemsProps<T>) => {
const [articleLanguages, setArticleLanguages] = useState<
Record<number, string>
>({});
const handleArticleLanguageChange = (
articleId: number,
languageCode: string
) => {
setArticleLanguages((prev) => ({ ...prev, [articleId]: languageCode }));
console.log(articleId, languageCode);
// Отправка запроса на сервер для сохранения языка
axios
.get(
`${import.meta.env.VITE_KRBL_API}/article/${articleId}/`, // Пример эндпоинта
{
headers: {
Authorization: `Bearer ${localStorage.getItem("refine-auth")}`,
"X-language": languageCode.toLowerCase(),
},
}
)
.then((response) => {
setLinkedItems(
linkedItems.map((item) => {
if (item.id == articleId) {
console.log(response.data);
return { ...response.data, language: languageCode };
} else {
return item;
}
})
);
});
};
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
const { setStationModalOpenAction, setStationIdAction } = stationStore;
const [position, setPosition] = useState<number>(1);
const [items, setItems] = useState<T[]>([]);
const [linkedItems, setLinkedItems] = useState<T[]>([]);
@ -143,22 +116,6 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
}
}, [linkedItems, setItemsParent]);
useEffect(() => {
// При загрузке linkedItems можно запросить текущие языки для статей
if (childResource === "article" && linkedItems.length > 0) {
const initialLanguages: Record<number, string> = {};
linkedItems.forEach((article) => {
// Предполагается, что у объекта article есть свойство language
if (article.language) {
initialLanguages[article.id] = article.language;
} else {
initialLanguages[article.id] = "RU"; // Или другой язык по умолчанию
}
});
setArticleLanguages(initialLanguages);
}
}, [linkedItems, childResource]);
const onDragEnd = (result: any) => {
if (!result.destination) return;
@ -294,223 +251,247 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
};
return (
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="subtitle1" fontWeight="bold">
Привязанные {title}
</Typography>
</AccordionSummary>
<>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="subtitle1" fontWeight="bold">
Привязанные {title}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Stack gap={2}>
<DragDropContext onDragEnd={onDragEnd}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{type === "edit" && dragAllowed && (
<TableCell width="40px"></TableCell>
)}
<TableCell key="id"></TableCell>
{fields.map((field) => (
<TableCell key={String(field.data)}>
{field.label}
</TableCell>
))}
{type === "edit" && (
<TableCell width="120px">Действие</TableCell>
)}
</TableRow>
</TableHead>
<Droppable
droppableId="droppable"
isDropDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableBody
ref={provided.innerRef}
{...provided.droppableProps}
>
{linkedItems.map((item, index) => (
<Draggable
key={item.id}
draggableId={"q" + String(item.id)}
index={index}
isDragDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableRow
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
hover
>
{type === "edit" && dragAllowed && (
<TableCell {...provided.dragHandleProps}>
<IconButton size="small">
<DragIndicatorIcon />
</IconButton>
</TableCell>
)}
<TableCell key={String(item.id)}>
{index + 1}
</TableCell>
{fields.map((field, index) => (
<TableCell
key={String(field.data) + String(index)}
>
{field.render
? field.render(item[field.data])
: item[field.data]}
</TableCell>
))}
{type === "edit" && (
<TableCell>
<Button
variant="outlined"
color="error"
size="small"
onClick={() => deleteItem(item.id)}
>
Отвязать
</Button>
</TableCell>
)}
</TableRow>
)}
</Draggable>
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Stack gap={2}>
<DragDropContext onDragEnd={onDragEnd}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{type === "edit" && dragAllowed && (
<TableCell width="40px"></TableCell>
)}
<TableCell key="id"></TableCell>
{fields.map((field) => (
<TableCell key={String(field.data)}>
{field.label}
</TableCell>
))}
{provided.placeholder}
</TableBody>
{type === "edit" && (
<TableCell width="120px">Действие</TableCell>
)}
</TableRow>
</TableHead>
<Droppable
droppableId="droppable"
isDropDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<TableBody
ref={provided.innerRef}
{...provided.droppableProps}
>
{linkedItems.map((item, index) => (
<Draggable
key={item.id}
draggableId={"q" + String(item.id)}
index={index}
isDragDisabled={type !== "edit" || !dragAllowed}
>
{(provided) => (
<>
<TableRow
sx={{
cursor:
childResource === "article"
? "pointer"
: "default",
}}
onClick={() => {
if (childResource === "article") {
setArticleModalOpenAction(true);
setArticleIdAction(item.id);
}
if (childResource === "station") {
setStationModalOpenAction(true);
setStationIdAction(item.id);
}
}}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
hover
>
{type === "edit" && dragAllowed && (
<TableCell {...provided.dragHandleProps}>
<IconButton size="small">
<DragIndicatorIcon />
</IconButton>
</TableCell>
)}
<TableCell key={String(item.id)}>
{index + 1}
</TableCell>
{fields.map((field, index) => (
<TableCell
key={String(field.data) + String(index)}
>
{field.render
? field.render(item[field.data])
: item[field.data]}
</TableCell>
))}
{type === "edit" && (
<TableCell>
<Button
variant="outlined"
color="error"
size="small"
onClick={() => deleteItem(item.id)}
>
Отвязать
</Button>
</TableCell>
)}
</TableRow>
</>
)}
</Draggable>
))}
{provided.placeholder}
</TableBody>
)}
</Droppable>
</Table>
</TableContainer>
</DragDropContext>
{linkedItems.length === 0 && !isLoading && (
<Typography color="textSecondary" textAlign="center" py={2}>
{title} не найдены
</Typography>
)}
{type === "edit" && (
<Stack gap={2} mt={2}>
<Typography variant="subtitle1">Добавить {title}</Typography>
<Autocomplete
fullWidth
value={
availableItems?.find(
(item) => item.id === selectedItemId
) || null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
}
options={availableItems}
getOptionLabel={(item) => String(item[fields[0].data])}
renderInput={(params) => (
<TextField
{...params}
label={`Выберите ${title}`}
fullWidth
/>
)}
</Droppable>
</Table>
</TableContainer>
</DragDropContext>
{linkedItems.length === 0 && !isLoading && (
<Typography color="textSecondary" textAlign="center" py={2}>
{title} не найдены
</Typography>
)}
{type === "edit" && (
<Stack gap={2} mt={2}>
<Typography variant="subtitle1">Добавить {title}</Typography>
<Autocomplete
fullWidth
value={
availableItems?.find((item) => item.id === selectedItemId) ||
null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
}
options={availableItems}
getOptionLabel={(item) => String(item[fields[0].data])}
renderInput={(params) => (
<TextField
{...params}
label={`Выберите ${title}`}
fullWidth
/>
)}
isOptionEqualToValue={(option, value) =>
option.id === value?.id
}
filterOptions={(options, { inputValue }) => {
const searchWords = inputValue
.toLowerCase()
.split(" ")
.filter((word) => word.length > 0);
return options.filter((option) => {
const optionWords = String(option[fields[0].data])
isOptionEqualToValue={(option, value) =>
option.id === value?.id
}
filterOptions={(options, { inputValue }) => {
const searchWords = inputValue
.toLowerCase()
.split(" ");
return searchWords.every((searchWord) =>
optionWords.some((word) => word.startsWith(searchWord))
);
});
}}
renderOption={(props, option) => (
<li {...props} key={option.id}>
{String(option[fields[0].data])}
</li>
)}
/>
{childResource === "article" && (
<FormControl fullWidth>
<TextField
type="number"
label="Номер страницы"
name="page_num"
value={pageNum}
onChange={(e) => {
const newValue = Number(e.target.value);
const minValue = linkedItems.length + 1;
setPageNum(newValue < minValue ? minValue : newValue);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)}
{childResource === "media" && (
<FormControl fullWidth>
<TextField
type="number"
label="Порядок отображения медиа"
value={mediaOrder}
onChange={(e) => {
const newValue = Number(e.target.value);
const maxValue = linkedItems.length + 1;
const value = Math.max(1, Math.min(newValue, maxValue));
setMediaOrder(value);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)}
<Button
variant="contained"
onClick={linkItem}
disabled={!selectedItemId}
sx={{ alignSelf: "flex-start" }}
>
Добавить
</Button>
{childResource == "station" && (
<TextField
type="text"
label="Позиция добавляемой остановки к маршруту"
value={position}
onChange={(e) => {
const newValue = Number(e.target.value);
setPosition(
newValue > linkedItems.length + 1
? linkedItems.length + 1
: newValue
);
.split(" ")
.filter((word) => word.length > 0);
return options.filter((option) => {
const optionWords = String(option[fields[0].data])
.toLowerCase()
.split(" ");
return searchWords.every((searchWord) =>
optionWords.some((word) => word.startsWith(searchWord))
);
});
}}
></TextField>
)}
</Stack>
)}
</Stack>
</AccordionDetails>
</Accordion>
renderOption={(props, option) => (
<li {...props} key={option.id}>
{String(option[fields[0].data])}
</li>
)}
/>
{childResource === "article" && (
<FormControl fullWidth>
<TextField
type="number"
label="Номер страницы"
name="page_num"
value={pageNum}
onChange={(e) => {
const newValue = Number(e.target.value);
const minValue = linkedItems.length + 1;
setPageNum(newValue < minValue ? minValue : newValue);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)}
{childResource === "media" && (
<FormControl fullWidth>
<TextField
type="number"
label="Порядок отображения медиа"
value={mediaOrder}
onChange={(e) => {
const newValue = Number(e.target.value);
const maxValue = linkedItems.length + 1;
const value = Math.max(1, Math.min(newValue, maxValue));
setMediaOrder(value);
}}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)}
<Button
variant="contained"
onClick={linkItem}
disabled={!selectedItemId}
sx={{ alignSelf: "flex-start" }}
>
Добавить
</Button>
{childResource == "station" && (
<TextField
type="text"
label="Позиция добавляемой остановки к маршруту"
value={position}
onChange={(e) => {
const newValue = Number(e.target.value);
setPosition(
newValue > linkedItems.length + 1
? linkedItems.length + 1
: newValue
);
}}
></TextField>
)}
</Stack>
)}
</Stack>
</AccordionDetails>
</Accordion>
<ArticleEditModal />
<StationEditModal />
</>
);
};

View File

@ -1 +0,0 @@
export {Header} from './header'

View File

@ -0,0 +1,171 @@
import { Modal, Box, Button, TextField, Typography } from "@mui/material";
import { articleStore } from "../../../store/ArticleStore";
import { observer } from "mobx-react-lite";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import "easymde/dist/easymde.min.css";
import { memo, useMemo, useEffect } from "react";
import { MarkdownEditor } from "../../MarkdownEditor";
import { Edit } from "@refinedev/mui";
import { languageStore } from "../../../store/LanguageStore";
import { LanguageSwitch } from "../../LanguageSwitch/index";
import { useNavigate } from "react-router";
import { useState } from "react";
const MemoizedSimpleMDE = memo(MarkdownEditor);
const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "60%",
bgcolor: "background.paper",
border: "2px solid #000",
boxShadow: 24,
p: 4,
};
export const ArticleEditModal = observer(() => {
const [articleData, setArticleData] = useState({
ru: {
heading: "",
body: "",
},
en: {
heading: "",
body: "",
},
zh: {
heading: "",
body: "",
},
});
const { articleModalOpen, setArticleModalOpenAction, selectedArticleId } =
articleStore;
const { language } = languageStore;
useEffect(() => {
return () => {
setArticleModalOpenAction(false);
};
}, []);
const {
register,
control,
formState: { errors },
saveButtonProps,
reset,
setValue,
watch,
} = useForm({
refineCoreProps: {
resource: "article",
id: selectedArticleId ?? undefined,
action: "edit",
redirect: false,
onMutationSuccess: () => {
setArticleModalOpenAction(false);
reset();
window.location.reload();
},
meta: {
headers: {
"Accept-Language": language,
},
},
},
});
useEffect(() => {
if (articleData[language as keyof typeof articleData]?.heading) {
setValue(
"heading",
articleData[language as keyof typeof articleData]?.heading || ""
);
}
if (articleData[language as keyof typeof articleData]?.body) {
setValue(
"body",
articleData[language as keyof typeof articleData]?.body || ""
);
}
}, [language, articleData, setValue]);
const handleLanguageChange = () => {
setArticleData((prevData) => ({
...prevData,
[language]: {
heading: watch("heading") || "",
body: watch("body") || "",
},
}));
};
const simpleMDEOptions = useMemo(
() => ({
placeholder: "Введите контент в формате Markdown...",
spellChecker: false,
}),
[]
);
return (
<Modal
open={articleModalOpen}
onClose={() => setArticleModalOpenAction(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Edit
title={<Typography variant="h5">Редактирование статьи</Typography>}
headerProps={{
sx: {
fontSize: "50px",
},
}}
saveButtonProps={saveButtonProps}
>
<LanguageSwitch action={handleLanguageChange} />
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("heading", {
required: "Это поле является обязательным",
})}
error={!!errors.heading}
helperText={errors.heading?.message as string}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
name="heading"
label="Заголовок *"
/>
<Controller
control={control}
name="body"
rules={{ required: "Это поле является обязательным" }}
defaultValue=""
render={({ field: { onChange, value } }) => (
<MemoizedSimpleMDE
value={value}
onChange={onChange}
options={simpleMDEOptions}
className="my-markdown-editor"
/>
)}
/>
</Box>
</Edit>
</Box>
</Modal>
);
});

View File

@ -0,0 +1,164 @@
import {
Modal,
Box,
Button,
TextField,
Typography,
Grid,
Paper,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import "easymde/dist/easymde.min.css";
import { memo, useMemo, useEffect } from "react";
import { MarkdownEditor } from "../../MarkdownEditor";
import { Edit } from "@refinedev/mui";
import { languageStore } from "../../../store/LanguageStore";
import { LanguageSwitch } from "../../LanguageSwitch/index";
import { useState } from "react";
import { stationStore } from "../../../store/StationStore";
const MemoizedSimpleMDE = memo(MarkdownEditor);
const TRANSFER_FIELDS = [
{ name: "bus", label: "Автобус" },
{ name: "metro_blue", label: "Метро (синяя)" },
{ name: "metro_green", label: "Метро (зеленая)" },
{ name: "metro_orange", label: "Метро (оранжевая)" },
{ name: "metro_purple", label: "Метро (фиолетовая)" },
{ name: "metro_red", label: "Метро (красная)" },
{ name: "train", label: "Электричка" },
{ name: "tram", label: "Трамвай" },
{ name: "trolleybus", label: "Троллейбус" },
];
const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "60%",
bgcolor: "background.paper",
border: "2px solid #000",
boxShadow: 24,
p: 4,
};
export const StationEditModal = observer(() => {
const { stationModalOpen, setStationModalOpenAction, selectedStationId } =
stationStore;
const { language } = languageStore;
useEffect(() => {
return () => {
setStationModalOpenAction(false);
};
}, []);
const {
register,
control,
formState: { errors },
saveButtonProps,
reset,
setValue,
watch,
} = useForm({
refineCoreProps: {
resource: "station",
id: selectedStationId ?? undefined,
action: "edit",
redirect: false,
onMutationSuccess: () => {
setStationModalOpenAction(false);
reset();
window.location.reload();
},
meta: {
headers: {
"Accept-Language": language,
},
},
},
});
return (
<Modal
open={stationModalOpen}
onClose={() => setStationModalOpenAction(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Edit
title={<Typography variant="h5">Редактирование станции</Typography>}
saveButtonProps={saveButtonProps}
>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("offset_x", {
setValueAs: (value) => parseFloat(value),
})}
error={!!(errors as any)?.offset_x}
helperText={(errors as any)?.offset_x?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Смещение (X)"}
name="offset_x"
/>
<TextField
{...register("offset_y", {
required: "Это поле является обязательным",
setValueAs: (value) => parseFloat(value),
})}
error={!!(errors as any)?.offset_y}
helperText={(errors as any)?.offset_y?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Смещение (Y)"}
name="offset_y"
/>
{/* Группа полей пересадок */}
<Paper sx={{ p: 2, mt: 2 }}>
<Typography variant="h6" gutterBottom>
Пересадки
</Typography>
<Grid container spacing={2}>
{TRANSFER_FIELDS.map((field) => (
<Grid item xs={12} sm={6} md={4} key={field.name}>
<TextField
{...register(`transfers.${field.name}`)}
error={!!(errors as any)?.transfers?.[field.name]}
helperText={
(errors as any)?.transfers?.[field.name]?.message
}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={field.label}
name={`transfers.${field.name}`}
/>
</Grid>
))}
</Grid>
</Paper>
</Box>
</Edit>
</Box>
</Modal>
);
});

View File

@ -58,18 +58,22 @@ export const ArticleEdit = observer(() => {
});
useEffect(() => {
setValue(
"heading",
articleData[language as keyof typeof articleData]?.heading || ""
);
setValue(
"body",
articleData[language as keyof typeof articleData]?.body || ""
);
setPreview(articleData[language as keyof typeof articleData]?.body || "");
setHeadingPreview(
articleData[language as keyof typeof articleData]?.heading || ""
);
if (articleData[language as keyof typeof articleData]?.heading) {
setValue(
"heading",
articleData[language as keyof typeof articleData]?.heading
);
setHeadingPreview(
articleData[language as keyof typeof articleData]?.heading || ""
);
}
if (articleData[language as keyof typeof articleData]?.body) {
setValue(
"body",
articleData[language as keyof typeof articleData]?.body || ""
);
setPreview(articleData[language as keyof typeof articleData]?.body || "");
}
}, [language, articleData, setValue]);
const handleLanguageChange = (lang: string) => {

View File

@ -1,34 +1,49 @@
import {type GridColDef} from '@mui/x-data-grid'
import {CustomDataGrid} from '../../components/CustomDataGrid'
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
import React from 'react'
import { type GridColDef } from "@mui/x-data-grid";
import { CustomDataGrid } from "../../components/CustomDataGrid";
import {
DeleteButton,
EditButton,
List,
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import React, { useEffect } from "react";
import {localeText} from '../../locales/ru/localeText'
import { localeText } from "../../locales/ru/localeText";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const ArticleList = () => {
const {dataGridProps} = useDataGrid({
resource: 'article/',
})
export const ArticleList = observer(() => {
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "article/",
meta: {
headers: {
"Accept-Language": language,
},
},
});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'id',
headerName: 'ID',
type: 'number',
field: "id",
headerName: "ID",
type: "number",
minWidth: 70,
display: 'flex',
align: 'left',
headerAlign: 'left',
display: "flex",
align: "left",
headerAlign: "left",
},
{
field: 'heading',
headerName: 'Заголовок',
type: 'string',
field: "heading",
headerName: "Заголовок",
type: "string",
minWidth: 300,
display: 'flex',
align: 'left',
headerAlign: 'left',
display: "flex",
align: "left",
headerAlign: "left",
flex: 1,
},
// {
@ -41,32 +56,38 @@ export const ArticleList = () => {
// flex: 1,
// },
{
field: 'actions',
headerName: 'Действия',
align: 'right',
headerAlign: 'center',
field: "actions",
headerName: "Действия",
align: "right",
headerAlign: "center",
minWidth: 120,
display: 'flex',
display: "flex",
sortable: false,
filterable: false,
disableColumnMenu: true,
renderCell: function render({row}) {
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
)
);
},
},
],
[],
)
[]
);
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} />
<CustomDataGrid
{...dataGridProps}
languageEnabled
columns={columns}
localeText={localeText}
getRowId={(row: any) => row.id}
/>
</List>
)
}
);
});

View File

@ -1,172 +1,202 @@
import {Autocomplete, Box, TextField} from '@mui/material'
import {Create, useAutocomplete} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form'
import {Controller} from 'react-hook-form'
export const CarrierCreate = () => {
import { Autocomplete, Box, TextField } from "@mui/material";
import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const CarrierCreate = observer(() => {
const { language } = languageStore;
const {
saveButtonProps,
refineCore: {formLoading},
refineCore: { formLoading },
register,
control,
formState: {errors},
} = useForm({})
formState: { errors },
} = useForm({
refineCoreProps: {
meta: {
headers: {
"Accept-Language": language,
},
},
},
});
const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({
resource: 'city',
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city",
onSearch: (value) => [
{
field: 'name',
operator: 'contains',
field: "name",
operator: "contains",
value,
},
],
})
});
const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({
resource: 'media',
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: "media",
onSearch: (value) => [
{
field: 'media_name',
operator: 'contains',
field: "media_name",
operator: "contains",
value,
},
],
})
});
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<Controller
control={control}
name="city_id"
rules={{required: 'Это поле является обязательным'}}
rules={{ required: "Это поле является обязательным" }}
defaultValue={null}
render={({field}) => (
render={({ field }) => (
<Autocomplete
{...cityAutocompleteProps}
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null}
value={
cityAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || '')
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.name : ''
return item ? item.name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id
return option.id === value?.id;
}}
filterOptions={(options, {inputValue}) => {
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()))
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.name.toLowerCase().includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => <TextField {...params} label="Выберите город" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
renderInput={(params) => (
<TextField
{...params}
label="Выберите город"
margin="normal"
variant="outlined"
error={!!errors.city_id}
helperText={(errors as any)?.city_id?.message}
required
/>
)}
/>
)}
/>
<TextField
{...register('full_name', {
required: 'Это поле является обязательным',
{...register("full_name", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.full_name}
helperText={(errors as any)?.full_name?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="text"
label={'Полное имя *'}
label={"Полное имя *"}
name="full_name"
/>
<TextField
{...register('short_name', {
required: 'Это поле является обязательным',
{...register("short_name", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.short_name}
helperText={(errors as any)?.short_name?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="text"
label={'Короткое имя *'}
label={"Короткое имя *"}
name="short_name"
/>
<TextField
{...register('main_color', {
{...register("main_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.main_color}
helperText={(errors as any)?.main_color?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="color"
label={'Основной цвет'}
label={"Основной цвет"}
name="main_color"
sx={{
'& input': {
height: '50px',
paddingBlock: '14px',
paddingInline: '14px',
cursor: 'pointer',
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register('left_color', {
{...register("left_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.left_color}
helperText={(errors as any)?.left_color?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="color"
label={'Цвет левого виджета'}
label={"Цвет левого виджета"}
name="left_color"
sx={{
'& input': {
height: '50px',
paddingBlock: '14px',
paddingInline: '14px',
cursor: 'pointer',
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register('right_color', {
{...register("right_color", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.right_color}
helperText={(errors as any)?.right_color?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="color"
label={'Цвет правого виджета'}
label={"Цвет правого виджета"}
name="right_color"
sx={{
'& input': {
height: '50px',
paddingBlock: '14px',
paddingInline: '14px',
cursor: 'pointer',
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
{...register('slogan', {
{...register("slogan", {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.slogan}
helperText={(errors as any)?.slogan?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="text"
label={'Слоган'}
label={"Слоган"}
name="slogan"
/>
@ -175,27 +205,44 @@ export const CarrierCreate = () => {
name="logo"
// rules={{required: 'Это поле является обязательным'}}
defaultValue={null}
render={({field}) => (
render={({ field }) => (
<Autocomplete
{...mediaAutocompleteProps}
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || '')
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : ''
return item ? item.media_name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id
return option.id === value?.id;
}}
filterOptions={(options, {inputValue}) => {
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.media_name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => <TextField {...params} label="Выберите логотип" margin="normal" variant="outlined" error={!!errors.logo} helperText={(errors as any)?.logo?.message} />}
renderInput={(params) => (
<TextField
{...params}
label="Выберите логотип"
margin="normal"
variant="outlined"
error={!!errors.logo}
helperText={(errors as any)?.logo?.message}
/>
)}
/>
)}
/>
</Box>
</Create>
)
}
);
});

View File

@ -10,11 +10,19 @@ import {
import React from "react";
import { observer } from "mobx-react-lite";
import { cityStore } from "../../store/CityStore";
import { languageStore } from "../../store/LanguageStore";
export const CarrierList = observer(() => {
const { city_id } = cityStore;
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "carrier",
meta: {
headers: {
"Accept-Language": language,
},
},
filters: {
permanent: [
{
@ -167,7 +175,7 @@ export const CarrierList = observer(() => {
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} />
<CustomDataGrid {...dataGridProps} languageEnabled columns={columns} />
</List>
);
});

View File

@ -1,78 +1,100 @@
import {type GridColDef} from '@mui/x-data-grid'
import {CustomDataGrid} from '../../components/CustomDataGrid'
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
import React from 'react'
import { type GridColDef } from "@mui/x-data-grid";
import { CustomDataGrid } from "../../components/CustomDataGrid";
import {
DeleteButton,
EditButton,
List,
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import React, { useEffect } from "react";
export const CityList = () => {
const {dataGridProps} = useDataGrid({})
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const CityList = observer(() => {
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "city",
meta: {
headers: {
"Accept-Language": language,
},
},
});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'id',
headerName: 'ID',
type: 'number',
field: "id",
headerName: "ID",
type: "number",
minWidth: 50,
align: 'left',
headerAlign: 'left',
align: "left",
headerAlign: "left",
},
{
field: 'country_code',
headerName: 'Код страны',
type: 'string',
field: "country_code",
headerName: "Код страны",
type: "string",
minWidth: 150,
align: 'left',
headerAlign: 'left',
align: "left",
headerAlign: "left",
},
{
field: 'country',
headerName: 'Cтрана',
type: 'string',
field: "country",
headerName: "Cтрана",
type: "string",
minWidth: 150,
align: 'left',
headerAlign: 'left',
align: "left",
headerAlign: "left",
},
{
field: 'name',
headerName: 'Название',
type: 'string',
field: "name",
headerName: "Название",
type: "string",
minWidth: 150,
flex: 1,
},
{
field: 'arms',
headerName: 'Герб',
type: 'string',
field: "arms",
headerName: "Герб",
type: "string",
flex: 1,
},
{
field: 'actions',
headerName: 'Действия',
cellClassName: 'city-actions',
field: "actions",
headerName: "Действия",
cellClassName: "city-actions",
minWidth: 120,
display: 'flex',
align: 'right',
headerAlign: 'center',
display: "flex",
align: "right",
headerAlign: "center",
sortable: false,
filterable: false,
disableColumnMenu: true,
renderCell: function render({row}) {
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
<DeleteButton
hideText
confirmTitle="Вы уверены?"
recordItemId={row.id}
/>
</>
)
);
},
},
],
[],
)
[]
);
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} />
<CustomDataGrid {...dataGridProps} columns={columns} languageEnabled />
</List>
)
}
);
});

View File

@ -1,56 +1,82 @@
import {type GridColDef} from '@mui/x-data-grid'
import {CustomDataGrid} from '../../components/CustomDataGrid'
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
import React from 'react'
import { type GridColDef } from "@mui/x-data-grid";
import { CustomDataGrid } from "../../components/CustomDataGrid";
import {
DeleteButton,
EditButton,
List,
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import React from "react";
import { languageStore } from "../../store/LanguageStore";
import { observer } from "mobx-react-lite";
export const CountryList = () => {
const {dataGridProps} = useDataGrid({})
export const CountryList = observer(() => {
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "country",
meta: {
headers: {
"Accept-Language": language,
},
},
});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'code',
headerName: 'Код',
type: 'string',
field: "code",
headerName: "Код",
type: "string",
minWidth: 150,
align: 'left',
headerAlign: 'left',
align: "left",
headerAlign: "left",
},
{
field: 'name',
headerName: 'Название',
type: 'string',
field: "name",
headerName: "Название",
type: "string",
minWidth: 150,
flex: 1,
},
{
field: 'actions',
headerName: 'Действия',
cellClassName: 'country-actions',
field: "actions",
headerName: "Действия",
cellClassName: "country-actions",
minWidth: 120,
display: 'flex',
align: 'right',
headerAlign: 'center',
display: "flex",
align: "right",
headerAlign: "center",
sortable: false,
filterable: false,
disableColumnMenu: true,
renderCell: function render({row}) {
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.code} />
<ShowButton hideText recordItemId={row.code} />
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.code} />
<DeleteButton
hideText
confirmTitle="Вы уверены?"
recordItemId={row.code}
/>
</>
)
);
},
},
],
[],
)
[]
);
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} getRowId={(row: any) => row.code} />
<CustomDataGrid
{...dataGridProps}
languageEnabled
columns={columns}
getRowId={(row: any) => row.code}
/>
</List>
)
}
);
});

View File

@ -8,24 +8,36 @@ import { TOKEN_KEY } from "../../authProvider";
import { observer } from "mobx-react-lite";
import Cookies from "js-cookie";
import { useLocation } from "react-router";
import { languageStore } from "../../store/LanguageStore";
export const SightCreate = observer(() => {
const [language, setLanguage] = useState(Cookies.get("lang") || "ru");
const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({
ru: {
name: "",
address: "",
},
en: {
name: "",
address: "",
},
zh: {
name: "",
address: "",
},
});
// Состояния для предпросмотра
const handleLanguageChange = (lang: string) => {
setLanguage(lang);
Cookies.set("lang", lang);
setSightData((prevData) => ({
...prevData,
[language]: {
name: watch("name") ?? "",
address: watch("address") ?? "",
},
}));
setLanguageAction(lang);
};
useEffect(() => {
const lang = Cookies.get("lang")!;
Cookies.set("lang", language);
return () => {
Cookies.set("lang", lang);
};
}, [language]);
const {
saveButtonProps,
refineCore: { formLoading },
@ -41,6 +53,17 @@ export const SightCreate = observer(() => {
});
const { city_id } = cityStore;
useEffect(() => {
if (sightData[language as keyof typeof sightData]?.name) {
setValue("name", sightData[language as keyof typeof sightData]?.name);
}
if (sightData[language as keyof typeof sightData]?.address) {
setValue(
"address",
sightData[language as keyof typeof sightData]?.address
);
}
}, [sightData, language, setValue]);
const [namePreview, setNamePreview] = useState("");
const [coordinatesPreview, setCoordinatesPreview] = useState({
latitude: "",

View File

@ -19,6 +19,8 @@ import { TOKEN_KEY } from "../../authProvider";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
import axios from "axios";
import { LanguageSwitch } from "../../components/LanguageSwitch/index";
function a11yProps(index: number) {
return {
@ -53,6 +55,21 @@ export const SightEdit = observer(() => {
const { id: sightId } = useParams<{ id: string }>();
const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({
ru: {
name: "",
address: "",
},
en: {
name: "",
address: "",
},
zh: {
name: "",
address: "",
},
});
const {
saveButtonProps,
register,
@ -71,6 +88,10 @@ export const SightEdit = observer(() => {
},
});
useEffect(() => {
setLanguageAction("ru");
}, []);
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city",
onSearch: (value) => [
@ -80,7 +101,20 @@ export const SightEdit = observer(() => {
value,
},
],
meta: {
headers: {
"Accept-Language": "ru",
},
},
});
const [mediaFile, setMediaFile] = useState<{
src: string;
filename: string;
}>({
src: "",
filename: "",
});
const [tabValue, setTabValue] = useState(0);
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: "media",
@ -112,6 +146,18 @@ export const SightEdit = observer(() => {
],
});
useEffect(() => {
if (sightData[language as keyof typeof sightData]?.name) {
setValue("name", sightData[language as keyof typeof sightData]?.name);
}
if (sightData[language as keyof typeof sightData]?.address) {
setValue(
"address",
sightData[language as keyof typeof sightData]?.address || ""
);
}
}, [language, sightData, setValue]);
useEffect(() => {
const latitude = getValues("latitude");
const longitude = getValues("longitude");
@ -183,6 +229,46 @@ export const SightEdit = observer(() => {
});
}, [latitudeContent, longitudeContent]);
useEffect(() => {
const getMedia = async () => {
if (!linkedArticles[selectedArticleIndex]?.id) return;
try {
const response = await axios.get(
`${import.meta.env.VITE_KRBL_API}/article/${
linkedArticles[selectedArticleIndex].id
}/media`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
},
}
);
const media = response.data[0];
if (media) {
setMediaFile({
src: `${import.meta.env.VITE_KRBL_MEDIA}${
media.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`,
filename: media.filename,
});
console.log(media);
} else {
setMediaFile({
src: "",
filename: "",
}); // или другой дефолт
}
} catch (error) {
setMediaFile({
src: "",
filename: "",
}); // или обработка ошибки
}
};
getMedia();
}, [selectedArticleIndex, linkedArticles]);
useEffect(() => {
const selectedCity = cityAutocompleteProps.options.find(
(option) => option.id === cityContent
@ -243,6 +329,22 @@ export const SightEdit = observer(() => {
setPreviewArticlePreview(selectedPreviewArticle?.heading || "");
}, [previewArticleContent, articleAutocompleteProps.options]);
const handleLanguageChange = (lang: string) => {
setSightData((prevData) => ({
...prevData,
[language]: {
name: watch("name") ?? "",
address: watch("address") ?? "",
},
}));
setLanguageAction(lang);
};
useEffect(() => {
return () => {
setLanguageAction("ru");
};
}, [setLanguageAction]);
return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
@ -251,13 +353,167 @@ export const SightEdit = observer(() => {
onChange={(_, newValue) => setTabValue(newValue)}
aria-label="basic tabs example"
>
<Tab label="Основная информация" {...a11yProps(1)} />
<Tab label="Левый виджет" {...a11yProps(2)} />
<Tab label="Правый информация" {...a11yProps(3)} />
<Tab label="Левый виджет" {...a11yProps(1)} />
<Tab label="Правый виджет" {...a11yProps(2)} />
<Tab label="Основная информация" {...a11yProps(3)} />
</Tabs>
</Box>
<CustomTabPanel value={tabValue} index={0}>
<Edit
saveButtonProps={saveButtonProps}
footerButtonProps={{
sx: {
bottom: 0,
left: 0,
},
}}
>
<Box
sx={{
maxWidth: "50%",
display: "flex",
flexDirection: "column",
gap: 2,
position: "relative",
}}
>
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("name", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Название *"}
name="name"
/>
</Box>
<LanguageSwitch />
<Box sx={{ mt: 3 }}>
<LinkedItems<ArticleItem>
type="edit"
parentId={sightId!}
dragAllowed={true}
setItemsParent={setLinkedArticles}
parentResource="sight"
fields={articleFields}
childResource="article"
title="статьи"
/>
<CreateSightArticle
parentId={sightId!}
parentResource="sight"
childResource="article"
title="статью"
/>
</Box>
</Box>
</Edit>
<Paper
sx={{
position: "fixed",
p: 2,
width: "30%",
top: "178px",
right: 50,
zIndex: 1000,
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "background.paper" : "#fff",
}}
>
<Typography variant="h6" gutterBottom color="primary">
Предпросмотр
</Typography>
<Box sx={{ mb: 2 }}>
{mediaFile.src && (
<>
{mediaFile.filename.endsWith(".mp4") ? (
<video
style={{ width: "100%", height: "100%" }}
src={mediaFile.src}
controls
/>
) : (
<img
style={{ width: "100%", height: "100%" }}
src={mediaFile.src}
alt="Предпросмотр"
/>
)}
</>
)}
<p style={{ fontSize: "12px", color: "white" }}>
{mediaFile.filename}
</p>
</Box>
{/* Водяные знаки */}
<Box sx={{ mb: 2 }}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{selectedArticle && (
<Typography
variant="h4"
gutterBottom
sx={{ color: "text.primary" }}
>
{selectedArticle.heading}
</Typography>
)}
{selectedArticle && (
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.primary" }}
>
{selectedArticle.body}
</Typography>
)}
</Box>
{/* Координаты */}
<Box sx={{ display: "flex", gap: 1, mt: 2 }}>
{linkedArticles.map((article, index) => (
<Box
key={article.id}
onClick={() => setSelectedArticleIndex(index)}
sx={{
cursor: "pointer",
bgcolor:
selectedArticleIndex === index
? "primary.main"
: "transparent",
color: selectedArticleIndex === index ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
>
<Typography variant="body1" gutterBottom>
{article.heading}
</Typography>
</Box>
))}
</Box>
</Box>
</Paper>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}>
<Edit
saveButtonProps={saveButtonProps}
footerButtonProps={{
@ -297,7 +553,7 @@ export const SightEdit = observer(() => {
p: 1,
borderRadius: 1,
}}
onClick={() => setLanguageAction("ru")}
onClick={() => handleLanguageChange("ru")}
>
RU
</Box>
@ -312,7 +568,7 @@ export const SightEdit = observer(() => {
p: 1,
borderRadius: 1,
}}
onClick={() => setLanguageAction("en")}
onClick={() => handleLanguageChange("en")}
>
EN
</Box>
@ -327,7 +583,7 @@ export const SightEdit = observer(() => {
p: 1,
borderRadius: 1,
}}
onClick={() => setLanguageAction("zh")}
onClick={() => handleLanguageChange("zh")}
>
ZH
</Box>
@ -776,135 +1032,6 @@ export const SightEdit = observer(() => {
</Box>
</Edit>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}>
<Edit
saveButtonProps={saveButtonProps}
footerButtonProps={{
sx: {
bottom: 0,
left: 0,
},
}}
>
<Box
sx={{
maxWidth: "50%",
display: "flex",
flexDirection: "column",
gap: 2,
position: "relative",
}}
>
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("name", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Название *"}
name="name"
/>
</Box>
<Box sx={{ mt: 3 }}>
<LinkedItems<ArticleItem>
type="edit"
parentId={sightId!}
setItemsParent={setLinkedArticles}
parentResource="sight"
fields={articleFields}
childResource="article"
title="статьи"
/>
<CreateSightArticle
parentId={sightId!}
parentResource="sight"
childResource="article"
title="статью"
/>
</Box>
</Box>
</Edit>
<Paper
sx={{
position: "fixed",
p: 2,
width: "30%",
top: "178px",
right: 50,
zIndex: 1000,
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "background.paper" : "#fff",
}}
>
<Typography variant="h6" gutterBottom color="primary">
Предпросмотр
</Typography>
{/* Водяные знаки */}
<Box sx={{ mb: 2 }}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{selectedArticle && (
<Typography
variant="h4"
gutterBottom
sx={{ color: "text.primary" }}
>
{selectedArticle.heading}
</Typography>
)}
{selectedArticle && (
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.primary" }}
>
{selectedArticle.body}
</Typography>
)}
</Box>
{/* Координаты */}
<Box sx={{ display: "flex", gap: 1, mt: 2 }}>
{linkedArticles.map((article, index) => (
<Box
key={article.id}
onClick={() => setSelectedArticleIndex(index)}
sx={{
cursor: "pointer",
bgcolor:
selectedArticleIndex === index
? "primary.main"
: "transparent",
color: selectedArticleIndex === index ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
>
<Typography variant="body1" gutterBottom>
{article.heading}
</Typography>
</Box>
))}
</Box>
</Box>
</Paper>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={2}>
<Edit
saveButtonProps={saveButtonProps}
@ -928,6 +1055,7 @@ export const SightEdit = observer(() => {
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSwitch />
<Controller
control={control}
name="watermark_lu"

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { type GridColDef } from "@mui/x-data-grid";
import {
DeleteButton,
@ -12,11 +12,21 @@ import { CustomDataGrid } from "../../components/CustomDataGrid";
import { localeText } from "../../locales/ru/localeText";
import { cityStore } from "../../store/CityStore";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const SightList = observer(() => {
const { city_id } = cityStore;
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "sight/",
resource: "sight",
meta: {
headers: {
"Accept-Language": language,
},
},
filters: {
permanent: [
{
@ -147,6 +157,7 @@ export const SightList = observer(() => {
<Stack gap={2.5}>
<CustomDataGrid
{...dataGridProps}
languageEnabled
columns={columns}
localeText={localeText}
getRowId={(row: any) => row.id}

View File

@ -15,6 +15,10 @@ import { Controller } from "react-hook-form";
import { useParams } from "react-router";
import { LinkedItems } from "../../components/LinkedItems";
import { type SightItem, sightFields } from "./types";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
import { useEffect, useState } from "react";
import { LanguageSwitch } from "../../components/LanguageSwitch/index";
const TRANSFER_FIELDS = [
{ name: "bus", label: "Автобус" },
@ -28,16 +32,126 @@ const TRANSFER_FIELDS = [
{ name: "trolleybus", label: "Троллейбус" },
];
export const StationEdit = () => {
export const StationEdit = observer(() => {
const { language, setLanguageAction } = languageStore;
const [stationData, setStationData] = useState({
ru: {
name: "",
system_name: "",
description: "",
address: "",
latitude: "",
longitude: "",
},
en: {
name: "",
system_name: "",
description: "",
address: "",
latitude: "",
longitude: "",
},
zh: {
name: "",
system_name: "",
description: "",
address: "",
latitude: "",
longitude: "",
},
});
const handleLanguageChange = () => {
setStationData((prevData) => ({
...prevData,
[language]: {
name: watch("name") ?? "",
system_name: watch("system_name") ?? "",
description: watch("description") ?? "",
address: watch("address") ?? "",
latitude: watch("latitude") ?? "",
longitude: watch("longitude") ?? "",
},
}));
};
const [coordinatesPreview, setCoordinatesPreview] = useState({
latitude: "",
longitude: "",
});
const {
saveButtonProps,
register,
control,
getValues,
setValue,
watch,
formState: { errors },
} = useForm({});
} = useForm({
refineCoreProps: {
meta: {
headers: { "Accept-Language": language },
},
},
});
useEffect(() => {
if (stationData[language as keyof typeof stationData]?.name) {
setValue("name", stationData[language as keyof typeof stationData]?.name);
}
if (stationData[language as keyof typeof stationData]?.address) {
setValue(
"system_name",
stationData[language as keyof typeof stationData]?.system_name || ""
);
}
if (stationData[language as keyof typeof stationData]?.description) {
setValue(
"description",
stationData[language as keyof typeof stationData]?.description || ""
);
}
if (stationData[language as keyof typeof stationData]?.latitude) {
setValue(
"latitude",
stationData[language as keyof typeof stationData]?.latitude || ""
);
}
if (stationData[language as keyof typeof stationData]?.longitude) {
setValue(
"longitude",
stationData[language as keyof typeof stationData]?.longitude || ""
);
}
}, [language, stationData, setValue]);
useEffect(() => {
setLanguageAction("ru");
}, []);
const { id: stationId } = useParams<{ id: string }>();
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const [lat, lon] = e.target.value.split(",").map((s) => s.trim());
setCoordinatesPreview({
latitude: lat,
longitude: lon,
});
setValue("latitude", lat);
setValue("longitude", lon);
};
const latitudeContent = watch("latitude");
const longitudeContent = watch("longitude");
useEffect(() => {
setCoordinatesPreview({
latitude: latitudeContent || "",
longitude: longitudeContent || "",
});
}, [latitudeContent, longitudeContent]);
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city",
onSearch: (value) => [
@ -47,8 +161,28 @@ export const StationEdit = () => {
value,
},
],
meta: {
headers: {
"Accept-Language": "ru",
},
},
queryOptions: {
queryKey: ["city"],
},
});
useEffect(() => {
const latitude = getValues("latitude");
const longitude = getValues("longitude");
if (latitude && longitude) {
setCoordinatesPreview({
latitude: latitude,
longitude: longitude,
});
}
}, [getValues]);
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
@ -56,6 +190,7 @@ export const StationEdit = () => {
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSwitch action={handleLanguageChange} />
<TextField
{...register("name", {
required: "Это поле является обязательным",
@ -125,33 +260,27 @@ export const StationEdit = () => {
label={"Адрес"}
name="address"
/>
<TextField
{...register("latitude", {
required: "Это поле является обязательным",
valueAsNumber: true,
})}
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Широта *"}
name="latitude"
type="text"
label={"Координаты *"}
/>
<TextField
{...register("longitude", {
required: "Это поле является обязательным",
valueAsNumber: true,
<input
type="hidden"
{...register("latitude", {
value: coordinatesPreview.latitude,
})}
error={!!(errors as any)?.longitude}
helperText={(errors as any)?.longitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Долгота *"}
name="longitude"
/>
<input
type="hidden"
{...register("longitude", { value: coordinatesPreview.longitude })}
/>
<Controller
@ -210,4 +339,4 @@ export const StationEdit = () => {
)}
</Edit>
);
};
});

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useEffect, useMemo } from "react";
import { type GridColDef } from "@mui/x-data-grid";
import {
DeleteButton,
@ -12,12 +12,19 @@ import { CustomDataGrid } from "../../components/CustomDataGrid";
import { localeText } from "../../locales/ru/localeText";
import { cityStore } from "../../store/CityStore";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const StationList = observer(() => {
const { city_id } = cityStore;
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "station",
meta: {
headers: {
"Accept-Language": language,
},
},
filters: {
permanent: [
{
@ -160,6 +167,7 @@ export const StationList = observer(() => {
<CustomDataGrid
{...dataGridProps}
columns={columns}
languageEnabled
localeText={localeText}
getRowId={(row: any) => row.id}
hasCoordinates

View File

@ -1,48 +1,52 @@
import {Autocomplete, Box, TextField} from '@mui/material'
import {Edit, useAutocomplete} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form'
import {Controller} from 'react-hook-form'
import { Autocomplete, Box, TextField } from "@mui/material";
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import {VEHICLE_TYPES} from '../../lib/constants'
import { VEHICLE_TYPES } from "../../lib/constants";
type VehicleFormValues = {
tail_number: number
type: number
city_id: number
}
tail_number: number;
type: number;
city_id: number;
};
export const VehicleEdit = () => {
const {
saveButtonProps,
register,
control,
formState: {errors},
} = useForm<VehicleFormValues>({})
formState: { errors },
} = useForm<VehicleFormValues>({});
const {autocompleteProps: carrierAutocompleteProps} = useAutocomplete({
resource: 'carrier',
const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({
resource: "carrier",
onSearch: (value) => [
{
field: 'short_name',
operator: 'contains',
field: "short_name",
operator: "contains",
value,
},
],
})
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register('tail_number', {
required: 'Это поле является обязательным',
{...register("tail_number", {
required: "Это поле является обязательным",
valueAsNumber: true,
})}
error={!!(errors as any)?.tail_number}
helperText={(errors as any)?.tail_number?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
InputLabelProps={{ shrink: true }}
type="number"
label="Бортовой номер *"
name="tail_number"
@ -52,23 +56,36 @@ export const VehicleEdit = () => {
control={control}
name="type"
rules={{
required: 'Это поле является обязательным',
required: "Это поле является обязательным",
}}
defaultValue={null}
render={({field}) => (
render={({ field }) => (
<Autocomplete
options={VEHICLE_TYPES}
value={VEHICLE_TYPES.find((option) => option.value === field.value) || null}
value={
VEHICLE_TYPES.find((option) => option.value === field.value) ||
null
}
onChange={(_, value) => {
field.onChange(value?.value || null)
field.onChange(value?.value || null);
}}
getOptionLabel={(item) => {
return item ? item.label : ''
return item ? item.label : "";
}}
isOptionEqualToValue={(option, value) => {
return option.value === value?.value
return option.value === value?.value;
}}
renderInput={(params) => <TextField {...params} label="Выберите тип" margin="normal" variant="outlined" error={!!errors.type} helperText={(errors as any)?.type?.message} required />}
renderInput={(params) => (
<TextField
{...params}
label="Выберите тип"
margin="normal"
variant="outlined"
error={!!errors.type}
helperText={(errors as any)?.type?.message}
required
/>
)}
/>
)}
/>
@ -76,29 +93,47 @@ export const VehicleEdit = () => {
<Controller
control={control}
name="carrier_id"
rules={{required: 'Это поле является обязательным'}}
rules={{ required: "Это поле является обязательным" }}
defaultValue={null}
render={({field}) => (
render={({ field }) => (
<Autocomplete
{...carrierAutocompleteProps}
value={carrierAutocompleteProps.options.find((option) => option.id === field.value) || null}
value={
carrierAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || '')
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.short_name : ''
return item ? item.short_name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id
return option.id === value?.id;
}}
filterOptions={(options, {inputValue}) => {
return options.filter((option) => option.short_name.toLowerCase().includes(inputValue.toLowerCase()))
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.short_name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => <TextField {...params} label="Выберите перевозчика" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
renderInput={(params) => (
<TextField
{...params}
label="Выберите перевозчика"
margin="normal"
variant="outlined"
error={!!errors.city_id}
helperText={(errors as any)?.city_id?.message}
required
/>
)}
/>
)}
/>
</Box>
</Edit>
)
}
);
};

View File

@ -1,91 +1,120 @@
import {type GridColDef} from '@mui/x-data-grid'
import {CustomDataGrid} from '../../components/CustomDataGrid'
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
import React from 'react'
import {VEHICLE_TYPES} from '../../lib/constants'
import { type GridColDef } from "@mui/x-data-grid";
import { CustomDataGrid } from "../../components/CustomDataGrid";
import {
DeleteButton,
EditButton,
List,
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import React, { useEffect } from "react";
import { VEHICLE_TYPES } from "../../lib/constants";
import {localeText} from '../../locales/ru/localeText'
import { localeText } from "../../locales/ru/localeText";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const VehicleList = () => {
const {dataGridProps} = useDataGrid({})
export const VehicleList = observer(() => {
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "vehicle",
meta: {
headers: {
"Accept-Language": language,
},
},
});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'id',
headerName: 'ID',
type: 'number',
field: "id",
headerName: "ID",
type: "number",
minWidth: 70,
display: 'flex',
align: 'left',
headerAlign: 'left',
display: "flex",
align: "left",
headerAlign: "left",
},
{
field: 'carrier_id',
headerName: 'ID перевозчика',
type: 'string',
field: "carrier_id",
headerName: "ID перевозчика",
type: "string",
minWidth: 150,
display: 'flex',
align: 'left',
headerAlign: 'left',
display: "flex",
align: "left",
headerAlign: "left",
},
{
field: 'tail_number',
headerName: 'Бортовой номер',
type: 'number',
field: "tail_number",
headerName: "Бортовой номер",
type: "number",
minWidth: 150,
display: 'flex',
align: 'left',
headerAlign: 'left',
display: "flex",
align: "left",
headerAlign: "left",
},
{
field: 'type',
headerName: 'Тип',
type: 'string',
field: "type",
headerName: "Тип",
type: "string",
minWidth: 200,
display: 'flex',
align: 'left',
headerAlign: 'left',
display: "flex",
align: "left",
headerAlign: "left",
renderCell: (params) => {
const value = params.row.type
return VEHICLE_TYPES.find((type) => type.value === value)?.label || value
const value = params.row.type;
return (
VEHICLE_TYPES.find((type) => type.value === value)?.label || value
);
},
},
{
field: 'city',
headerName: 'Город',
type: 'string',
align: 'left',
headerAlign: 'left',
field: "city",
headerName: "Город",
type: "string",
align: "left",
headerAlign: "left",
flex: 1,
},
{
field: 'actions',
headerName: 'Действия',
field: "actions",
headerName: "Действия",
minWidth: 120,
display: 'flex',
align: 'right',
headerAlign: 'center',
display: "flex",
align: "right",
headerAlign: "center",
sortable: false,
filterable: false,
disableColumnMenu: true,
renderCell: function render({row}) {
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
<DeleteButton
hideText
confirmTitle="Вы уверены?"
recordItemId={row.id}
/>
</>
)
);
},
},
],
[],
)
[]
);
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} />
<CustomDataGrid
{...dataGridProps}
languageEnabled
columns={columns}
localeText={localeText}
getRowId={(row: any) => row.id}
/>
</List>
)
}
);
});

20
src/store/ArticleStore.ts Normal file
View File

@ -0,0 +1,20 @@
import { makeAutoObservable } from "mobx";
class ArticleStore {
articleModalOpen: boolean = false;
selectedArticleId: number | null = null;
constructor() {
makeAutoObservable(this);
}
setArticleIdAction = (id: number) => {
this.selectedArticleId = id;
};
setArticleModalOpenAction = (open: boolean) => {
this.articleModalOpen = open;
};
}
export const articleStore = new ArticleStore();

20
src/store/StationStore.ts Normal file
View File

@ -0,0 +1,20 @@
import { makeAutoObservable } from "mobx";
class StationStore {
stationModalOpen: boolean = false;
selectedStationId: number | null = null;
constructor() {
makeAutoObservable(this);
}
setStationIdAction = (id: number) => {
this.selectedStationId = id;
};
setStationModalOpenAction = (open: boolean) => {
this.stationModalOpen = open;
};
}
export const stationStore = new StationStore();