fix: Update map with tables fixes
This commit is contained in:
		
							
								
								
									
										317
									
								
								src/pages/Station/LinkedSights.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								src/pages/Station/LinkedSights.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | ||||
| import { useState, useEffect } from "react"; | ||||
| import { | ||||
|   Stack, | ||||
|   Typography, | ||||
|   Button, | ||||
|   Accordion, | ||||
|   AccordionSummary, | ||||
|   AccordionDetails, | ||||
|   useTheme, | ||||
|   TextField, | ||||
|   Autocomplete, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   Table, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   Paper, | ||||
|   TableBody, | ||||
| } from "@mui/material"; | ||||
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | ||||
|  | ||||
| import { authInstance, languageStore } from "@shared"; | ||||
|  | ||||
| type Field<T> = { | ||||
|   label: string; | ||||
|   data: keyof T; | ||||
|   render?: (value: any) => React.ReactNode; | ||||
| }; | ||||
|  | ||||
| type LinkedSightsProps<T> = { | ||||
|   parentId: string | number; | ||||
|   fields: Field<T>[]; | ||||
|   setItemsParent?: (items: T[]) => void; | ||||
|   type: "show" | "edit"; | ||||
|   onUpdate?: () => void; | ||||
|   disableCreation?: boolean; | ||||
|   updatedLinkedItems?: T[]; | ||||
|   refresh?: number; | ||||
| }; | ||||
|  | ||||
| export const LinkedSights = < | ||||
|   T extends { id: number; name: string; [key: string]: any } | ||||
| >( | ||||
|   props: LinkedSightsProps<T> | ||||
| ) => { | ||||
|   const theme = useTheme(); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Accordion sx={{ width: "100%" }}> | ||||
|         <AccordionSummary | ||||
|           expandIcon={<ExpandMoreIcon />} | ||||
|           sx={{ | ||||
|             background: theme.palette.background.paper, | ||||
|             borderBottom: `1px solid ${theme.palette.divider}`, | ||||
|             width: "100%", | ||||
|           }} | ||||
|         > | ||||
|           <Typography variant="subtitle1" fontWeight="bold"> | ||||
|             Привязанные достопримечательности | ||||
|           </Typography> | ||||
|         </AccordionSummary> | ||||
|  | ||||
|         <AccordionDetails | ||||
|           sx={{ background: theme.palette.background.paper, width: "100%" }} | ||||
|         > | ||||
|           <Stack gap={2} width="100%"> | ||||
|             <LinkedSightsContents {...props} /> | ||||
|           </Stack> | ||||
|         </AccordionDetails> | ||||
|       </Accordion> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const LinkedSightsContents = < | ||||
|   T extends { id: number; name: string; [key: string]: any } | ||||
| >({ | ||||
|   parentId, | ||||
|   setItemsParent, | ||||
|   fields, | ||||
|   type, | ||||
|   onUpdate, | ||||
|   disableCreation = false, | ||||
|   updatedLinkedItems, | ||||
|   refresh, | ||||
| }: LinkedSightsProps<T>) => { | ||||
|   const { language } = languageStore; | ||||
|  | ||||
|   const [allItems, setAllItems] = useState<T[]>([]); | ||||
|   const [linkedItems, setLinkedItems] = useState<T[]>([]); | ||||
|   const [selectedItemId, setSelectedItemId] = useState<number | null>(null); | ||||
|   const [isLoading, setIsLoading] = useState<boolean>(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     console.log(error); | ||||
|   }, [error]); | ||||
|  | ||||
|   const parentResource = "station"; | ||||
|   const childResource = "sight"; | ||||
|  | ||||
|   const availableItems = allItems | ||||
|     .filter((item) => !linkedItems.some((linked) => linked.id === item.id)) | ||||
|     .sort((a, b) => a.name.localeCompare(b.name)); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (updatedLinkedItems) { | ||||
|       setLinkedItems(updatedLinkedItems); | ||||
|     } | ||||
|   }, [updatedLinkedItems]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setItemsParent?.(linkedItems); | ||||
|   }, [linkedItems, setItemsParent]); | ||||
|  | ||||
|   const linkItem = () => { | ||||
|     if (selectedItemId !== null) { | ||||
|       setError(null); | ||||
|       const requestData = { | ||||
|         sight_id: selectedItemId, | ||||
|       }; | ||||
|  | ||||
|       authInstance | ||||
|         .post(`/${parentResource}/${parentId}/${childResource}`, requestData) | ||||
|         .then(() => { | ||||
|           const newItem = allItems.find((item) => item.id === selectedItemId); | ||||
|           if (newItem) { | ||||
|             setLinkedItems([...linkedItems, newItem]); | ||||
|           } | ||||
|           setSelectedItemId(null); | ||||
|           onUpdate?.(); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error("Error linking sight:", error); | ||||
|           setError("Failed to link sight"); | ||||
|         }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const deleteItem = (itemId: number) => { | ||||
|     setError(null); | ||||
|     authInstance | ||||
|       .delete(`/${parentResource}/${parentId}/${childResource}`, { | ||||
|         data: { [`${childResource}_id`]: itemId }, | ||||
|       }) | ||||
|       .then(() => { | ||||
|         setLinkedItems(linkedItems.filter((item) => item.id !== itemId)); | ||||
|         onUpdate?.(); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error("Error deleting sight:", error); | ||||
|         setError("Failed to delete sight"); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (parentId) { | ||||
|       setIsLoading(true); | ||||
|       setError(null); | ||||
|       authInstance | ||||
|         .get(`/${parentResource}/${parentId}/${childResource}`) | ||||
|         .then((response) => { | ||||
|           setLinkedItems(response?.data || []); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error("Error fetching linked sights:", error); | ||||
|           setError("Failed to load linked sights"); | ||||
|           setLinkedItems([]); | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           setIsLoading(false); | ||||
|         }); | ||||
|     } | ||||
|   }, [parentId, language, refresh]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (type === "edit") { | ||||
|       setError(null); | ||||
|       authInstance | ||||
|         .get(`/${childResource}/`) | ||||
|         .then((response) => { | ||||
|           setAllItems(response?.data || []); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error("Error fetching all sights:", error); | ||||
|           setError("Failed to load available sights"); | ||||
|           setAllItems([]); | ||||
|         }); | ||||
|     } | ||||
|   }, [type]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {linkedItems?.length > 0 && ( | ||||
|         <TableContainer component={Paper} sx={{ width: "100%" }}> | ||||
|           <Table sx={{ width: "100%" }}> | ||||
|             <TableHead> | ||||
|               <TableRow> | ||||
|                 <TableCell key="id" width="60px"> | ||||
|                   № | ||||
|                 </TableCell> | ||||
|                 {fields.map((field) => ( | ||||
|                   <TableCell key={String(field.data)}>{field.label}</TableCell> | ||||
|                 ))} | ||||
|                 {type === "edit" && ( | ||||
|                   <TableCell width="120px">Действие</TableCell> | ||||
|                 )} | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|  | ||||
|             <TableBody> | ||||
|               {linkedItems.map((item, index) => ( | ||||
|                 <TableRow key={item.id} hover> | ||||
|                   <TableCell>{index + 1}</TableCell> | ||||
|                   {fields.map((field, idx) => ( | ||||
|                     <TableCell key={String(field.data) + String(idx)}> | ||||
|                       {field.render | ||||
|                         ? field.render(item[field.data]) | ||||
|                         : item[field.data]} | ||||
|                     </TableCell> | ||||
|                   ))} | ||||
|                   {type === "edit" && ( | ||||
|                     <TableCell> | ||||
|                       <Button | ||||
|                         variant="outlined" | ||||
|                         color="error" | ||||
|                         size="small" | ||||
|                         onClick={(e) => { | ||||
|                           e.stopPropagation(); | ||||
|                           deleteItem(item.id); | ||||
|                         }} | ||||
|                       > | ||||
|                         Отвязать | ||||
|                       </Button> | ||||
|                     </TableCell> | ||||
|                   )} | ||||
|                 </TableRow> | ||||
|               ))} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|         </TableContainer> | ||||
|       )} | ||||
|  | ||||
|       {linkedItems.length === 0 && !isLoading && ( | ||||
|         <Typography color="textSecondary" textAlign="center" py={2}> | ||||
|           Достопримечательности не найдены | ||||
|         </Typography> | ||||
|       )} | ||||
|  | ||||
|       {type === "edit" && !disableCreation && ( | ||||
|         <Stack gap={2} mt={2}> | ||||
|           <Typography variant="subtitle1"> | ||||
|             Добавить достопримечательность | ||||
|           </Typography> | ||||
|           <Autocomplete | ||||
|             fullWidth | ||||
|             value={ | ||||
|               availableItems?.find((item) => item.id === selectedItemId) || null | ||||
|             } | ||||
|             onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)} | ||||
|             options={availableItems} | ||||
|             getOptionLabel={(item) => String(item.name)} | ||||
|             renderInput={(params) => ( | ||||
|               <TextField | ||||
|                 {...params} | ||||
|                 label="Выберите достопримечательность" | ||||
|                 fullWidth | ||||
|               /> | ||||
|             )} | ||||
|             isOptionEqualToValue={(option, value) => option.id === value?.id} | ||||
|             filterOptions={(options, { inputValue }) => { | ||||
|               const searchWords = inputValue | ||||
|                 .toLowerCase() | ||||
|                 .split(" ") | ||||
|                 .filter(Boolean); | ||||
|               return options.filter((option) => { | ||||
|                 const optionWords = String(option.name) | ||||
|                   .toLowerCase() | ||||
|                   .split(" "); | ||||
|                 return searchWords.every((searchWord) => | ||||
|                   optionWords.some((word) => word.startsWith(searchWord)) | ||||
|                 ); | ||||
|               }); | ||||
|             }} | ||||
|             renderOption={(props, option) => ( | ||||
|               <li {...props} key={option.id}> | ||||
|                 {String(option.name)} | ||||
|               </li> | ||||
|             )} | ||||
|           /> | ||||
|  | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             onClick={linkItem} | ||||
|             disabled={!selectedItemId} | ||||
|             sx={{ alignSelf: "flex-start" }} | ||||
|           > | ||||
|             Добавить | ||||
|           </Button> | ||||
|         </Stack> | ||||
|       )} | ||||
|  | ||||
|       {isLoading && ( | ||||
|         <Typography color="textSecondary" textAlign="center" py={2}> | ||||
|           Загрузка... | ||||
|         </Typography> | ||||
|       )} | ||||
|  | ||||
|       {error && ( | ||||
|         <Typography color="error" textAlign="center" py={2}> | ||||
|           {error} | ||||
|         </Typography> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user