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 DataGridProps,
type GridColumnVisibilityModel, type GridColumnVisibilityModel,
} from "@mui/x-data-grid"; } 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 { ExportButton } from "@refinedev/mui";
import { useExport } from "@refinedev/core"; import { useExport } from "@refinedev/core";
import React, { useState, useEffect, useMemo } from "react"; import React, { useState, useEffect, useMemo } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { localeText } from "../locales/ru/localeText"; import { localeText } from "../locales/ru/localeText";
import { languageStore } from "../store/LanguageStore";
import { LanguageSwitch } from "./LanguageSwitch";
interface CustomDataGridProps extends DataGridProps { interface CustomDataGridProps extends DataGridProps {
hasCoordinates?: boolean; hasCoordinates?: boolean;
resource?: string; // Add this prop resource?: string; // Add this prop
languageEnabled?: boolean;
} }
const DEV_FIELDS = [ const DEV_FIELDS = [
@ -46,6 +49,7 @@ const DEV_FIELDS = [
] as const; ] as const;
export const CustomDataGrid = ({ export const CustomDataGrid = ({
languageEnabled = false,
hasCoordinates = false, hasCoordinates = false,
columns = [], columns = [],
resource, resource,
@ -130,6 +134,9 @@ export const CustomDataGrid = ({
return ( return (
<Stack spacing={2}> <Stack spacing={2}>
<Box sx={{ visibility: languageEnabled ? "visible" : "hidden" }}>
<LanguageSwitch />
</Box>
<DataGrid <DataGrid
{...props} {...props}
columns={columns} columns={columns}
@ -149,7 +156,6 @@ export const CustomDataGrid = ({
}} }}
pageSizeOptions={[10, 25, 50, 100]} pageSizeOptions={[10, 25, 50, 100]}
/> />
<Stack direction="row" spacing={2} justifyContent="space-between" mb={2}> <Stack direction="row" spacing={2} justifyContent="space-between" mb={2}>
<Stack direction="row" spacing={2} sx={{ mb: 2 }}> <Stack direction="row" spacing={2} sx={{ mb: 2 }}>
{hasCoordinates && ( {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 { useState, useEffect } from "react";
import { Close } from "@mui/icons-material";
import { import {
Stack, Stack,
Typography, Typography,
@ -20,14 +21,19 @@ import {
Paper, Paper,
TableBody, TableBody,
IconButton, IconButton,
Collapse,
Modal,
} from "@mui/material"; } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import { axiosInstance } from "../providers/data"; import { axiosInstance } from "../providers/data";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; 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[] { function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
const index = pos - 1; const index = pos - 1;
if (index >= arr.length) { if (index >= arr.length) {
@ -82,41 +88,8 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
type, type,
onSave, onSave,
}: LinkedItemsProps<T>) => { }: LinkedItemsProps<T>) => {
const [articleLanguages, setArticleLanguages] = useState< const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
Record<number, string> const { setStationModalOpenAction, setStationIdAction } = stationStore;
>({});
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 [position, setPosition] = useState<number>(1); const [position, setPosition] = useState<number>(1);
const [items, setItems] = useState<T[]>([]); const [items, setItems] = useState<T[]>([]);
const [linkedItems, setLinkedItems] = useState<T[]>([]); const [linkedItems, setLinkedItems] = useState<T[]>([]);
@ -143,22 +116,6 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
} }
}, [linkedItems, setItemsParent]); }, [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) => { const onDragEnd = (result: any) => {
if (!result.destination) return; if (!result.destination) return;
@ -294,6 +251,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
}; };
return ( return (
<>
<Accordion> <Accordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon />}
@ -347,7 +305,24 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
isDragDisabled={type !== "edit" || !dragAllowed} isDragDisabled={type !== "edit" || !dragAllowed}
> >
{(provided) => ( {(provided) => (
<>
<TableRow <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} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
@ -386,9 +361,11 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
</TableCell> </TableCell>
)} )}
</TableRow> </TableRow>
</>
)} )}
</Draggable> </Draggable>
))} ))}
{provided.placeholder} {provided.placeholder}
</TableBody> </TableBody>
)} )}
@ -409,8 +386,9 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
<Autocomplete <Autocomplete
fullWidth fullWidth
value={ value={
availableItems?.find((item) => item.id === selectedItemId) || availableItems?.find(
null (item) => item.id === selectedItemId
) || null
} }
onChange={(_, newValue) => onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null) setSelectedItemId(newValue?.id || null)
@ -512,5 +490,8 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
</Stack> </Stack>
</AccordionDetails> </AccordionDetails>
</Accordion> </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(() => { useEffect(() => {
if (articleData[language as keyof typeof articleData]?.heading) {
setValue( setValue(
"heading", "heading",
articleData[language as keyof typeof articleData]?.heading
);
setHeadingPreview(
articleData[language as keyof typeof articleData]?.heading || "" articleData[language as keyof typeof articleData]?.heading || ""
); );
}
if (articleData[language as keyof typeof articleData]?.body) {
setValue( setValue(
"body", "body",
articleData[language as keyof typeof articleData]?.body || "" articleData[language as keyof typeof articleData]?.body || ""
); );
setPreview(articleData[language as keyof typeof articleData]?.body || ""); setPreview(articleData[language as keyof typeof articleData]?.body || "");
setHeadingPreview( }
articleData[language as keyof typeof articleData]?.heading || ""
);
}, [language, articleData, setValue]); }, [language, articleData, setValue]);
const handleLanguageChange = (lang: string) => { const handleLanguageChange = (lang: string) => {

View File

@ -1,34 +1,49 @@
import {type GridColDef} from '@mui/x-data-grid' import { type GridColDef } from "@mui/x-data-grid";
import {CustomDataGrid} from '../../components/CustomDataGrid' import { CustomDataGrid } from "../../components/CustomDataGrid";
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui' import {
import React from 'react' 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 = observer(() => {
const { language } = languageStore;
export const ArticleList = () => {
const { dataGridProps } = useDataGrid({ const { dataGridProps } = useDataGrid({
resource: 'article/', resource: "article/",
}) meta: {
headers: {
"Accept-Language": language,
},
},
});
const columns = React.useMemo<GridColDef[]>( const columns = React.useMemo<GridColDef[]>(
() => [ () => [
{ {
field: 'id', field: "id",
headerName: 'ID', headerName: "ID",
type: 'number', type: "number",
minWidth: 70, minWidth: 70,
display: 'flex', display: "flex",
align: 'left', align: "left",
headerAlign: 'left', headerAlign: "left",
}, },
{ {
field: 'heading', field: "heading",
headerName: 'Заголовок', headerName: "Заголовок",
type: 'string', type: "string",
minWidth: 300, minWidth: 300,
display: 'flex', display: "flex",
align: 'left', align: "left",
headerAlign: 'left', headerAlign: "left",
flex: 1, flex: 1,
}, },
// { // {
@ -41,12 +56,12 @@ export const ArticleList = () => {
// flex: 1, // flex: 1,
// }, // },
{ {
field: 'actions', field: "actions",
headerName: 'Действия', headerName: "Действия",
align: 'right', align: "right",
headerAlign: 'center', headerAlign: "center",
minWidth: 120, minWidth: 120,
display: 'flex', display: "flex",
sortable: false, sortable: false,
filterable: false, filterable: false,
disableColumnMenu: true, disableColumnMenu: true,
@ -57,16 +72,22 @@ export const ArticleList = () => {
<ShowButton hideText recordItemId={row.id} /> <ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} /> <DeleteButton hideText recordItemId={row.id} />
</> </>
) );
}, },
}, },
], ],
[], []
) );
return ( return (
<List> <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> </List>
) );
} });

View File

@ -1,71 +1,101 @@
import {Autocomplete, Box, TextField} from '@mui/material' import { Autocomplete, Box, TextField } from "@mui/material";
import {Create, useAutocomplete} from '@refinedev/mui' import { Create, useAutocomplete } from "@refinedev/mui";
import {useForm} from '@refinedev/react-hook-form' import { useForm } from "@refinedev/react-hook-form";
import {Controller} from 'react-hook-form' import { Controller } from "react-hook-form";
import { observer } from "mobx-react-lite";
export const CarrierCreate = () => { import { languageStore } from "../../store/LanguageStore";
export const CarrierCreate = observer(() => {
const { language } = languageStore;
const { const {
saveButtonProps, saveButtonProps,
refineCore: { formLoading }, refineCore: { formLoading },
register, register,
control, control,
formState: { errors }, formState: { errors },
} = useForm({}) } = useForm({
refineCoreProps: {
meta: {
headers: {
"Accept-Language": language,
},
},
},
});
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: 'city', resource: "city",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: 'name', field: "name",
operator: 'contains', operator: "contains",
value, value,
}, },
], ],
}) });
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: 'media', resource: "media",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: 'media_name', field: "media_name",
operator: 'contains', operator: "contains",
value, value,
}, },
], ],
}) });
return ( return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}> <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 <Controller
control={control} control={control}
name="city_id" name="city_id"
rules={{required: 'Это поле является обязательным'}} rules={{ required: "Это поле является обязательным" }}
defaultValue={null} defaultValue={null}
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...cityAutocompleteProps} {...cityAutocompleteProps}
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null} value={
cityAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id || '') field.onChange(value?.id || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.name : '' return item ? item.name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase())) 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 <TextField
{...register('full_name', { {...register("full_name", {
required: 'Это поле является обязательным', required: "Это поле является обязательным",
})} })}
error={!!(errors as any)?.full_name} error={!!(errors as any)?.full_name}
helperText={(errors as any)?.full_name?.message} helperText={(errors as any)?.full_name?.message}
@ -73,13 +103,13 @@ export const CarrierCreate = () => {
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={'Полное имя *'} label={"Полное имя *"}
name="full_name" name="full_name"
/> />
<TextField <TextField
{...register('short_name', { {...register("short_name", {
required: 'Это поле является обязательным', required: "Это поле является обязательным",
})} })}
error={!!(errors as any)?.short_name} error={!!(errors as any)?.short_name}
helperText={(errors as any)?.short_name?.message} helperText={(errors as any)?.short_name?.message}
@ -87,12 +117,12 @@ export const CarrierCreate = () => {
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={'Короткое имя *'} label={"Короткое имя *"}
name="short_name" name="short_name"
/> />
<TextField <TextField
{...register('main_color', { {...register("main_color", {
// required: 'Это поле является обязательным', // required: 'Это поле является обязательным',
})} })}
error={!!(errors as any)?.main_color} error={!!(errors as any)?.main_color}
@ -101,20 +131,20 @@ export const CarrierCreate = () => {
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="color" type="color"
label={'Основной цвет'} label={"Основной цвет"}
name="main_color" name="main_color"
sx={{ sx={{
'& input': { "& input": {
height: '50px', height: "50px",
paddingBlock: '14px', paddingBlock: "14px",
paddingInline: '14px', paddingInline: "14px",
cursor: 'pointer', cursor: "pointer",
}, },
}} }}
/> />
<TextField <TextField
{...register('left_color', { {...register("left_color", {
// required: 'Это поле является обязательным', // required: 'Это поле является обязательным',
})} })}
error={!!(errors as any)?.left_color} error={!!(errors as any)?.left_color}
@ -123,19 +153,19 @@ export const CarrierCreate = () => {
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="color" type="color"
label={'Цвет левого виджета'} label={"Цвет левого виджета"}
name="left_color" name="left_color"
sx={{ sx={{
'& input': { "& input": {
height: '50px', height: "50px",
paddingBlock: '14px', paddingBlock: "14px",
paddingInline: '14px', paddingInline: "14px",
cursor: 'pointer', cursor: "pointer",
}, },
}} }}
/> />
<TextField <TextField
{...register('right_color', { {...register("right_color", {
// required: 'Это поле является обязательным', // required: 'Это поле является обязательным',
})} })}
error={!!(errors as any)?.right_color} error={!!(errors as any)?.right_color}
@ -144,20 +174,20 @@ export const CarrierCreate = () => {
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="color" type="color"
label={'Цвет правого виджета'} label={"Цвет правого виджета"}
name="right_color" name="right_color"
sx={{ sx={{
'& input': { "& input": {
height: '50px', height: "50px",
paddingBlock: '14px', paddingBlock: "14px",
paddingInline: '14px', paddingInline: "14px",
cursor: 'pointer', cursor: "pointer",
}, },
}} }}
/> />
<TextField <TextField
{...register('slogan', { {...register("slogan", {
// required: 'Это поле является обязательным', // required: 'Это поле является обязательным',
})} })}
error={!!(errors as any)?.slogan} error={!!(errors as any)?.slogan}
@ -166,7 +196,7 @@ export const CarrierCreate = () => {
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={'Слоган'} label={"Слоган"}
name="slogan" name="slogan"
/> />
@ -178,24 +208,41 @@ export const CarrierCreate = () => {
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...mediaAutocompleteProps} {...mediaAutocompleteProps}
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null} value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id || '') field.onChange(value?.id || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.media_name : '' return item ? item.media_name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) 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> </Box>
</Create> </Create>
) );
} });

View File

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

View File

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

View File

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

View File

@ -8,24 +8,36 @@ import { TOKEN_KEY } from "../../authProvider";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import { languageStore } from "../../store/LanguageStore";
export const SightCreate = observer(() => { 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) => { const handleLanguageChange = (lang: string) => {
setLanguage(lang); setSightData((prevData) => ({
Cookies.set("lang", lang); ...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 { const {
saveButtonProps, saveButtonProps,
refineCore: { formLoading }, refineCore: { formLoading },
@ -41,6 +53,17 @@ export const SightCreate = observer(() => {
}); });
const { city_id } = cityStore; 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 [namePreview, setNamePreview] = useState("");
const [coordinatesPreview, setCoordinatesPreview] = useState({ const [coordinatesPreview, setCoordinatesPreview] = useState({
latitude: "", latitude: "",

View File

@ -19,6 +19,8 @@ import { TOKEN_KEY } from "../../authProvider";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore"; import { languageStore } from "../../store/LanguageStore";
import axios from "axios";
import { LanguageSwitch } from "../../components/LanguageSwitch/index";
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
@ -53,6 +55,21 @@ export const SightEdit = observer(() => {
const { id: sightId } = useParams<{ id: string }>(); const { id: sightId } = useParams<{ id: string }>();
const { language, setLanguageAction } = languageStore; const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({
ru: {
name: "",
address: "",
},
en: {
name: "",
address: "",
},
zh: {
name: "",
address: "",
},
});
const { const {
saveButtonProps, saveButtonProps,
register, register,
@ -71,6 +88,10 @@ export const SightEdit = observer(() => {
}, },
}); });
useEffect(() => {
setLanguageAction("ru");
}, []);
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city", resource: "city",
onSearch: (value) => [ onSearch: (value) => [
@ -80,7 +101,20 @@ export const SightEdit = observer(() => {
value, value,
}, },
], ],
meta: {
headers: {
"Accept-Language": "ru",
},
},
}); });
const [mediaFile, setMediaFile] = useState<{
src: string;
filename: string;
}>({
src: "",
filename: "",
});
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: "media", 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(() => { useEffect(() => {
const latitude = getValues("latitude"); const latitude = getValues("latitude");
const longitude = getValues("longitude"); const longitude = getValues("longitude");
@ -183,6 +229,46 @@ export const SightEdit = observer(() => {
}); });
}, [latitudeContent, longitudeContent]); }, [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(() => { useEffect(() => {
const selectedCity = cityAutocompleteProps.options.find( const selectedCity = cityAutocompleteProps.options.find(
(option) => option.id === cityContent (option) => option.id === cityContent
@ -243,6 +329,22 @@ export const SightEdit = observer(() => {
setPreviewArticlePreview(selectedPreviewArticle?.heading || ""); setPreviewArticlePreview(selectedPreviewArticle?.heading || "");
}, [previewArticleContent, articleAutocompleteProps.options]); }, [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 ( return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
@ -251,13 +353,167 @@ export const SightEdit = observer(() => {
onChange={(_, newValue) => setTabValue(newValue)} onChange={(_, newValue) => setTabValue(newValue)}
aria-label="basic tabs example" aria-label="basic tabs example"
> >
<Tab label="Основная информация" {...a11yProps(1)} /> <Tab label="Левый виджет" {...a11yProps(1)} />
<Tab label="Левый виджет" {...a11yProps(2)} /> <Tab label="Правый виджет" {...a11yProps(2)} />
<Tab label="Правый информация" {...a11yProps(3)} /> <Tab label="Основная информация" {...a11yProps(3)} />
</Tabs> </Tabs>
</Box> </Box>
<CustomTabPanel value={tabValue} index={0}> <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 <Edit
saveButtonProps={saveButtonProps} saveButtonProps={saveButtonProps}
footerButtonProps={{ footerButtonProps={{
@ -297,7 +553,7 @@ export const SightEdit = observer(() => {
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
}} }}
onClick={() => setLanguageAction("ru")} onClick={() => handleLanguageChange("ru")}
> >
RU RU
</Box> </Box>
@ -312,7 +568,7 @@ export const SightEdit = observer(() => {
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
}} }}
onClick={() => setLanguageAction("en")} onClick={() => handleLanguageChange("en")}
> >
EN EN
</Box> </Box>
@ -327,7 +583,7 @@ export const SightEdit = observer(() => {
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
}} }}
onClick={() => setLanguageAction("zh")} onClick={() => handleLanguageChange("zh")}
> >
ZH ZH
</Box> </Box>
@ -776,135 +1032,6 @@ export const SightEdit = observer(() => {
</Box> </Box>
</Edit> </Edit>
</CustomTabPanel> </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}> <CustomTabPanel value={tabValue} index={2}>
<Edit <Edit
saveButtonProps={saveButtonProps} saveButtonProps={saveButtonProps}
@ -928,6 +1055,7 @@ export const SightEdit = observer(() => {
sx={{ flex: 1, display: "flex", flexDirection: "column" }} sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off" autoComplete="off"
> >
<LanguageSwitch />
<Controller <Controller
control={control} control={control}
name="watermark_lu" 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 { type GridColDef } from "@mui/x-data-grid";
import { import {
DeleteButton, DeleteButton,
@ -12,11 +12,21 @@ import { CustomDataGrid } from "../../components/CustomDataGrid";
import { localeText } from "../../locales/ru/localeText"; import { localeText } from "../../locales/ru/localeText";
import { cityStore } from "../../store/CityStore"; import { cityStore } from "../../store/CityStore";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const SightList = observer(() => { export const SightList = observer(() => {
const { city_id } = cityStore; const { city_id } = cityStore;
const { language } = languageStore;
const { dataGridProps } = useDataGrid({ const { dataGridProps } = useDataGrid({
resource: "sight/", resource: "sight",
meta: {
headers: {
"Accept-Language": language,
},
},
filters: { filters: {
permanent: [ permanent: [
{ {
@ -147,6 +157,7 @@ export const SightList = observer(() => {
<Stack gap={2.5}> <Stack gap={2.5}>
<CustomDataGrid <CustomDataGrid
{...dataGridProps} {...dataGridProps}
languageEnabled
columns={columns} columns={columns}
localeText={localeText} localeText={localeText}
getRowId={(row: any) => row.id} getRowId={(row: any) => row.id}

View File

@ -15,6 +15,10 @@ import { Controller } from "react-hook-form";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { LinkedItems } from "../../components/LinkedItems"; import { LinkedItems } from "../../components/LinkedItems";
import { type SightItem, sightFields } from "./types"; 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 = [ const TRANSFER_FIELDS = [
{ name: "bus", label: "Автобус" }, { name: "bus", label: "Автобус" },
@ -28,16 +32,126 @@ const TRANSFER_FIELDS = [
{ name: "trolleybus", label: "Троллейбус" }, { 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 { const {
saveButtonProps, saveButtonProps,
register, register,
control, control,
getValues,
setValue,
watch,
formState: { errors }, 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 { 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({ const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city", resource: "city",
onSearch: (value) => [ onSearch: (value) => [
@ -47,8 +161,28 @@ export const StationEdit = () => {
value, 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 ( return (
<Edit saveButtonProps={saveButtonProps}> <Edit saveButtonProps={saveButtonProps}>
<Box <Box
@ -56,6 +190,7 @@ export const StationEdit = () => {
sx={{ display: "flex", flexDirection: "column" }} sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off" autoComplete="off"
> >
<LanguageSwitch action={handleLanguageChange} />
<TextField <TextField
{...register("name", { {...register("name", {
required: "Это поле является обязательным", required: "Это поле является обязательным",
@ -125,33 +260,27 @@ export const StationEdit = () => {
label={"Адрес"} label={"Адрес"}
name="address" name="address"
/> />
<TextField <TextField
{...register("latitude", { value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
required: "Это поле является обязательным", onChange={handleCoordinatesChange}
valueAsNumber: true,
})}
error={!!(errors as any)?.latitude} error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message} helperText={(errors as any)?.latitude?.message}
margin="normal" margin="normal"
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="number" type="text"
label={"Широта *"} label={"Координаты *"}
name="latitude"
/> />
<TextField <input
{...register("longitude", { type="hidden"
required: "Это поле является обязательным", {...register("latitude", {
valueAsNumber: true, value: coordinatesPreview.latitude,
})} })}
error={!!(errors as any)?.longitude} />
helperText={(errors as any)?.longitude?.message} <input
margin="normal" type="hidden"
fullWidth {...register("longitude", { value: coordinatesPreview.longitude })}
InputLabelProps={{ shrink: true }}
type="number"
label={"Долгота *"}
name="longitude"
/> />
<Controller <Controller
@ -210,4 +339,4 @@ export const StationEdit = () => {
)} )}
</Edit> </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 { type GridColDef } from "@mui/x-data-grid";
import { import {
DeleteButton, DeleteButton,
@ -12,12 +12,19 @@ import { CustomDataGrid } from "../../components/CustomDataGrid";
import { localeText } from "../../locales/ru/localeText"; import { localeText } from "../../locales/ru/localeText";
import { cityStore } from "../../store/CityStore"; import { cityStore } from "../../store/CityStore";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
export const StationList = observer(() => { export const StationList = observer(() => {
const { city_id } = cityStore; const { city_id } = cityStore;
const { language } = languageStore;
const { dataGridProps } = useDataGrid({ const { dataGridProps } = useDataGrid({
resource: "station", resource: "station",
meta: {
headers: {
"Accept-Language": language,
},
},
filters: { filters: {
permanent: [ permanent: [
{ {
@ -160,6 +167,7 @@ export const StationList = observer(() => {
<CustomDataGrid <CustomDataGrid
{...dataGridProps} {...dataGridProps}
columns={columns} columns={columns}
languageEnabled
localeText={localeText} localeText={localeText}
getRowId={(row: any) => row.id} getRowId={(row: any) => row.id}
hasCoordinates hasCoordinates

View File

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

View File

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