station edit in the route edit page
This commit is contained in:
		| @@ -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 && ( | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								src/components/LanguageSwitch/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/components/LanguageSwitch/index.tsx
									
									
									
									
									
										Normal 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> | ||||||
|  |   ); | ||||||
|  | }); | ||||||
| @@ -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,223 +251,247 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({ | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Accordion> |     <> | ||||||
|       <AccordionSummary |       <Accordion> | ||||||
|         expandIcon={<ExpandMoreIcon />} |         <AccordionSummary | ||||||
|         sx={{ |           expandIcon={<ExpandMoreIcon />} | ||||||
|           background: theme.palette.background.paper, |           sx={{ | ||||||
|           borderBottom: `1px solid ${theme.palette.divider}`, |             background: theme.palette.background.paper, | ||||||
|         }} |             borderBottom: `1px solid ${theme.palette.divider}`, | ||||||
|       > |           }} | ||||||
|         <Typography variant="subtitle1" fontWeight="bold"> |         > | ||||||
|           Привязанные {title} |           <Typography variant="subtitle1" fontWeight="bold"> | ||||||
|         </Typography> |             Привязанные {title} | ||||||
|       </AccordionSummary> |           </Typography> | ||||||
|  |         </AccordionSummary> | ||||||
|  |  | ||||||
|       <AccordionDetails sx={{ background: theme.palette.background.paper }}> |         <AccordionDetails sx={{ background: theme.palette.background.paper }}> | ||||||
|         <Stack gap={2}> |           <Stack gap={2}> | ||||||
|           <DragDropContext onDragEnd={onDragEnd}> |             <DragDropContext onDragEnd={onDragEnd}> | ||||||
|             <TableContainer component={Paper}> |               <TableContainer component={Paper}> | ||||||
|               <Table> |                 <Table> | ||||||
|                 <TableHead> |                   <TableHead> | ||||||
|                   <TableRow> |                     <TableRow> | ||||||
|                     {type === "edit" && dragAllowed && ( |                       {type === "edit" && dragAllowed && ( | ||||||
|                       <TableCell width="40px"></TableCell> |                         <TableCell width="40px"></TableCell> | ||||||
|                     )} |                       )} | ||||||
|                     <TableCell key="id">№</TableCell> |                       <TableCell key="id">№</TableCell> | ||||||
|                     {fields.map((field) => ( |                       {fields.map((field) => ( | ||||||
|                       <TableCell key={String(field.data)}> |                         <TableCell key={String(field.data)}> | ||||||
|                         {field.label} |                           {field.label} | ||||||
|                       </TableCell> |                         </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> |  | ||||||
|                       ))} |                       ))} | ||||||
|                       {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> |                   isOptionEqualToValue={(option, value) => | ||||||
|               </Table> |                     option.id === value?.id | ||||||
|             </TableContainer> |                   } | ||||||
|           </DragDropContext> |                   filterOptions={(options, { inputValue }) => { | ||||||
|  |                     const searchWords = inputValue | ||||||
|           {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]) |  | ||||||
|                       .toLowerCase() |                       .toLowerCase() | ||||||
|                       .split(" "); |                       .split(" ") | ||||||
|                     return searchWords.every((searchWord) => |                       .filter((word) => word.length > 0); | ||||||
|                       optionWords.some((word) => word.startsWith(searchWord)) |                     return options.filter((option) => { | ||||||
|                     ); |                       const optionWords = String(option[fields[0].data]) | ||||||
|                   }); |                         .toLowerCase() | ||||||
|                 }} |                         .split(" "); | ||||||
|                 renderOption={(props, option) => ( |                       return searchWords.every((searchWord) => | ||||||
|                   <li {...props} key={option.id}> |                         optionWords.some((word) => word.startsWith(searchWord)) | ||||||
|                     {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> |                   renderOption={(props, option) => ( | ||||||
|               )} |                     <li {...props} key={option.id}> | ||||||
|             </Stack> |                       {String(option[fields[0].data])} | ||||||
|           )} |                     </li> | ||||||
|         </Stack> |                   )} | ||||||
|       </AccordionDetails> |                 /> | ||||||
|     </Accordion> |  | ||||||
|  |                 {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 /> | ||||||
|  |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1 +0,0 @@ | |||||||
| export {Header} from './header' |  | ||||||
							
								
								
									
										171
									
								
								src/components/modals/ArticleEditModal/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								src/components/modals/ArticleEditModal/index.tsx
									
									
									
									
									
										Normal 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> | ||||||
|  |   ); | ||||||
|  | }); | ||||||
							
								
								
									
										164
									
								
								src/components/modals/StationEditModal/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/components/modals/StationEditModal/index.tsx
									
									
									
									
									
										Normal 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> | ||||||
|  |   ); | ||||||
|  | }); | ||||||
| @@ -58,18 +58,22 @@ export const ArticleEdit = observer(() => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     setValue( |     if (articleData[language as keyof typeof articleData]?.heading) { | ||||||
|       "heading", |       setValue( | ||||||
|       articleData[language as keyof typeof articleData]?.heading || "" |         "heading", | ||||||
|     ); |         articleData[language as keyof typeof articleData]?.heading | ||||||
|     setValue( |       ); | ||||||
|       "body", |       setHeadingPreview( | ||||||
|       articleData[language as keyof typeof articleData]?.body || "" |         articleData[language as keyof typeof articleData]?.heading || "" | ||||||
|     ); |       ); | ||||||
|     setPreview(articleData[language as keyof typeof articleData]?.body || ""); |     } | ||||||
|     setHeadingPreview( |     if (articleData[language as keyof typeof articleData]?.body) { | ||||||
|       articleData[language as keyof typeof articleData]?.heading || "" |       setValue( | ||||||
|     ); |         "body", | ||||||
|  |         articleData[language as keyof typeof articleData]?.body || "" | ||||||
|  |       ); | ||||||
|  |       setPreview(articleData[language as keyof typeof articleData]?.body || ""); | ||||||
|  |     } | ||||||
|   }, [language, articleData, setValue]); |   }, [language, articleData, setValue]); | ||||||
|  |  | ||||||
|   const handleLanguageChange = (lang: string) => { |   const handleLanguageChange = (lang: string) => { | ||||||
|   | |||||||
| @@ -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 = () => { | export const ArticleList = observer(() => { | ||||||
|   const {dataGridProps} = useDataGrid({ |   const { language } = languageStore; | ||||||
|     resource: 'article/', |  | ||||||
|   }) |   const { dataGridProps } = useDataGrid({ | ||||||
|  |     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,32 +56,38 @@ 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, | ||||||
|         renderCell: function render({row}) { |         renderCell: function render({ row }) { | ||||||
|           return ( |           return ( | ||||||
|             <> |             <> | ||||||
|               <EditButton hideText recordItemId={row.id} /> |               <EditButton hideText recordItemId={row.id} /> | ||||||
|               <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> | ||||||
|   ) |   ); | ||||||
| } | }); | ||||||
|   | |||||||
| @@ -1,172 +1,202 @@ | |||||||
| 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} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           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} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           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} | ||||||
|           helperText={(errors as any)?.main_color?.message} |           helperText={(errors as any)?.main_color?.message} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           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} | ||||||
|           helperText={(errors as any)?.left_color?.message} |           helperText={(errors as any)?.left_color?.message} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           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} | ||||||
|           helperText={(errors as any)?.right_color?.message} |           helperText={(errors as any)?.right_color?.message} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           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} | ||||||
|           helperText={(errors as any)?.slogan?.message} |           helperText={(errors as any)?.slogan?.message} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           fullWidth |           fullWidth | ||||||
|           InputLabelProps={{shrink: true}} |           InputLabelProps={{ shrink: true }} | ||||||
|           type="text" |           type="text" | ||||||
|           label={'Слоган'} |           label={"Слоган"} | ||||||
|           name="slogan" |           name="slogan" | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
| @@ -175,27 +205,44 @@ export const CarrierCreate = () => { | |||||||
|           name="logo" |           name="logo" | ||||||
|           // rules={{required: 'Это поле является обязательным'}} |           // rules={{required: 'Это поле является обязательным'}} | ||||||
|           defaultValue={null} |           defaultValue={null} | ||||||
|           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> | ||||||
|   ) |   ); | ||||||
| } | }); | ||||||
|   | |||||||
| @@ -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> | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,78 +1,100 @@ | |||||||
| 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, | ||||||
|         renderCell: function render({row}) { |         renderCell: function render({ row }) { | ||||||
|           return ( |           return ( | ||||||
|             <> |             <> | ||||||
|               <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> | ||||||
|   ) |   ); | ||||||
| } | }); | ||||||
|   | |||||||
| @@ -1,56 +1,82 @@ | |||||||
| 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, | ||||||
|         renderCell: function render({row}) { |         renderCell: function render({ row }) { | ||||||
|           return ( |           return ( | ||||||
|             <> |             <> | ||||||
|               <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> | ||||||
|   ) |   ); | ||||||
| } | }); | ||||||
|   | |||||||
| @@ -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: "", | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -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} | ||||||
|   | |||||||
| @@ -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> | ||||||
|   ); |   ); | ||||||
| }; | }); | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -1,48 +1,52 @@ | |||||||
| 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 { | ||||||
|     saveButtonProps, |     saveButtonProps, | ||||||
|     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} | ||||||
|           helperText={(errors as any)?.tail_number?.message} |           helperText={(errors as any)?.tail_number?.message} | ||||||
|           margin="normal" |           margin="normal" | ||||||
|           fullWidth |           fullWidth | ||||||
|           InputLabelProps={{shrink: true}} |           InputLabelProps={{ shrink: true }} | ||||||
|           type="number" |           type="number" | ||||||
|           label="Бортовой номер *" |           label="Бортовой номер *" | ||||||
|           name="tail_number" |           name="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> | ||||||
|   ) |   ); | ||||||
| } | }; | ||||||
|   | |||||||
| @@ -1,91 +1,120 @@ | |||||||
| 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, | ||||||
|         renderCell: function render({row}) { |         renderCell: function render({ row }) { | ||||||
|           return ( |           return ( | ||||||
|             <> |             <> | ||||||
|               <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
									
								
							
							
						
						
									
										20
									
								
								src/store/ArticleStore.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										20
									
								
								src/store/StationStore.ts
									
									
									
									
									
										Normal 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(); | ||||||
		Reference in New Issue
	
	Block a user