import { useState, useEffect } from "react"; import { Stack, Typography, FormControl, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField, Autocomplete, TableCell, TableContainer, Table, TableHead, TableRow, Paper, TableBody, IconButton, Checkbox, FormControlLabel, Tabs, Tab, Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import { DragDropContext, Droppable, Draggable, DropResult, } from "@hello-pangea/dnd"; import { AnimatedCircleButton, authInstance, languageStore, routeStore, selectedCityStore, stationsStore, } from "@shared"; import { EditStationModal } from "../../widgets/modals/EditStationModal"; function insertAtPosition(arr: T[], pos: number, value: T): T[] { const index = pos - 1; const result = [...arr]; if (index >= result.length) { result.push(value); } else { result.splice(index, 0, value); } return result; } const reorder = (list: T[], startIndex: number, endIndex: number): T[] => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; }; type Field = { label: string; data: keyof T; render?: (value: any) => React.ReactNode; }; type LinkedItemsProps = { parentId: string | number; fields: Field[]; setItemsParent?: (items: T[]) => void; type: "show" | "edit"; dragAllowed?: boolean; onUpdate?: () => void; dontRecurse?: boolean; disableCreation?: boolean; updatedLinkedItems?: T[]; refresh?: number; }; export const LinkedItems = < T extends { id: number; name: string; [key: string]: any } >( props: LinkedItemsProps ) => { const theme = useTheme(); return ( <> } sx={{ background: theme.palette.background.paper, borderBottom: `1px solid ${theme.palette.divider}`, width: "100%", }} > Привязанные остановки ); }; const LinkedItemsContentsInner = < T extends { id: number; name: string; [key: string]: any } >({ parentId, setItemsParent, fields, dragAllowed = false, type, onUpdate, disableCreation = false, updatedLinkedItems, refresh, }: LinkedItemsProps) => { const { language } = languageStore; const [position, setPosition] = useState(1); const [allItems, setAllItems] = useState([]); const [linkedItems, setLinkedItems] = useState([]); const [selectedItemId, setSelectedItemId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedItems, setSelectedItems] = useState>(new Set()); const [activeTab, setActiveTab] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [isLinkingSingle, setIsLinkingSingle] = useState(false); const [isLinkingBulk, setIsLinkingBulk] = useState(false); const [detachingIds, setDetachingIds] = useState>(new Set()); useEffect(() => {}, [error]); const parentResource = "route"; const childResource = "station"; const availableItems = allItems .filter((item) => !linkedItems.some((linked) => linked.id === item.id)) .filter((item) => { const selectedCityId = selectedCityStore.selectedCityId; if (selectedCityId && "city_id" in item) { return item.city_id === selectedCityId; } return true; }) .sort((a, b) => a.name.localeCompare(b.name)); const filteredAvailableItems = availableItems.filter((item) => { if (!searchQuery.trim()) return true; const query = searchQuery.toLowerCase(); const name = String(item.name || "").toLowerCase(); const description = String(item.description || "").toLowerCase(); return name.includes(query) || description.includes(query); }); useEffect(() => { if (updatedLinkedItems) { setLinkedItems(updatedLinkedItems); } }, [updatedLinkedItems]); useEffect(() => { setItemsParent?.(linkedItems); }, [linkedItems, setItemsParent]); useEffect(() => { setPosition(linkedItems.length + 1); }, [linkedItems.length]); const getStationTransfers = (stationId: number, fallbackTransfers?: any) => { const { stationLists } = stationsStore; for (const lang of ["ru", "en", "zh"] as const) { const station = stationLists[lang].data.find( (s: any) => s.id === stationId ); if (station?.transfers) { return station.transfers; } } return fallbackTransfers; }; const onDragEnd = (result: DropResult) => { if (!result.destination) return; const reorderedItems = reorder( linkedItems, result.source.index, result.destination.index ); setLinkedItems(reorderedItems); authInstance .post(`/${parentResource}/${parentId}/${childResource}`, { stations: reorderedItems.map((item) => { const transfers = getStationTransfers(item.id, item.transfers); return { ...item, transfers: transfers || item.transfers, }; }), }) .catch((error) => { console.error("Error updating station order:", error); setError("Failed to update station order"); }); }; useEffect(() => { if (parentId) { setIsLoading(true); setError(null); authInstance .get(`/${parentResource}/${parentId}/${childResource}`) .then((response) => { setLinkedItems(response?.data || []); }) .catch((error) => { console.error("Error fetching linked items:", error); setError("Failed to load linked stations"); 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 items:", error); setError(null); setAllItems([]); }); } }, [type]); const linkItem = () => { if (selectedItemId !== null) { setError(null); const selectedItem = allItems.find((item) => item.id === selectedItemId); const requestData = { stations: insertAtPosition( linkedItems.map((item) => { const transfers = getStationTransfers(item.id, item.transfers); return { ...item, transfers: transfers || item.transfers, }; }), position, (() => { if (!selectedItem) return { id: selectedItemId }; const transfers = getStationTransfers( selectedItemId, selectedItem.transfers ); return { ...selectedItem, transfers: transfers || selectedItem.transfers, }; })() ), }; setIsLinkingSingle(true); authInstance .post(`/${parentResource}/${parentId}/${childResource}`, requestData) .then(() => { const newItem = allItems.find((item) => item.id === selectedItemId); if (newItem) { const updatedList = insertAtPosition( [...linkedItems], position, newItem ); setLinkedItems(updatedList); } setSelectedItemId(null); onUpdate?.(); }) .catch((error) => { console.error("Error linking item:", error); setError("Failed to link station"); }) .finally(() => { setIsLinkingSingle(false); }); } }; const deleteItem = (itemId: number) => { setError(null); setDetachingIds((prev) => { const next = new Set(prev); next.add(itemId); return next; }); authInstance .delete(`/${parentResource}/${parentId}/${childResource}`, { data: { [`${childResource}_id`]: itemId }, }) .then(() => { setLinkedItems(linkedItems.filter((item) => item.id !== itemId)); onUpdate?.(); }) .catch((error) => { console.error("Error deleting item:", error); setError("Failed to delete station"); }) .finally(() => { setDetachingIds((prev) => { const next = new Set(prev); next.delete(itemId); return next; }); }); }; const handleStationClick = (item: T) => { routeStore.setSelectedStationId(item.id); setIsModalOpen(true); }; const handleCloseModal = () => { setIsModalOpen(false); }; const handleCheckboxChange = (itemId: number) => { const newSelected = new Set(selectedItems); if (newSelected.has(itemId)) { newSelected.delete(itemId); } else { newSelected.add(itemId); } setSelectedItems(newSelected); }; const handleBulkLink = () => { if (selectedItems.size === 0) return; setError(null); setIsLinkingBulk(true); const selectedStations = Array.from(selectedItems).map((id) => { const item = allItems.find((item) => item.id === id); if (!item) return { id }; const transfers = getStationTransfers(id, item.transfers); return { ...item, transfers: transfers || item.transfers, }; }); const requestData = { stations: [ ...linkedItems.map((item) => { const transfers = getStationTransfers(item.id, item.transfers); return { ...item, transfers: transfers || item.transfers, }; }), ...selectedStations, ], }; authInstance .post(`/${parentResource}/${parentId}/${childResource}`, requestData) .then(() => { const newItems = allItems.filter((item) => selectedItems.has(item.id)); setLinkedItems([...linkedItems, ...newItems]); setSelectedItems(new Set()); onUpdate?.(); }) .catch((error) => { console.error("Error linking items:", error); setError("Failed to link stations"); }) .finally(() => { setIsLinkingBulk(false); }); }; return ( <> {linkedItems?.length > 0 && ( {type === "edit" && dragAllowed && ( )} № {fields.map((field) => ( {field.label} ))} {type === "edit" && ( Действие )} {(provided) => ( {linkedItems.map((item, index) => ( {(provided) => ( handleStationClick(item)} > {type === "edit" && dragAllowed && ( )} {index + 1} {fields.map((field, idx) => ( {field.render ? field.render(item[field.data]) : item[field.data]} ))} {type === "edit" && ( { e.stopPropagation(); deleteItem(item.id); }} disabled={detachingIds.has(item.id)} loading={detachingIds.has(item.id)} > Отвязать )} )} ))} {provided.placeholder} )}
)} {linkedItems.length === 0 && !isLoading && ( Остановки не найдены )} {type === "edit" && !disableCreation && ( Добавить остановки setActiveTab(newValue)} > {activeTab === 0 && ( item.id === selectedItemId ) || null } onChange={(_, newValue) => setSelectedItemId(newValue?.id || null) } options={availableItems} getOptionLabel={(item) => String(item.name)} renderInput={(params) => ( )} isOptionEqualToValue={(option, value) => option.id === value?.id } filterOptions={(options, { inputValue }) => { if (!inputValue.trim()) return options; const query = inputValue.toLowerCase(); return options.filter((option) => { const name = String(option.name || "").toLowerCase(); const description = String( option.description || "" ).toLowerCase(); return ( name.includes(query) || description.includes(query) ); }); }} renderOption={(props, option) => (
  • {String(option.name)}

    {String(option.description)}

  • )} /> { const newValue = Math.max(1, Number(e.target.value)); setPosition( newValue > linkedItems.length + 1 ? linkedItems.length + 1 : newValue ); }} InputProps={{ inputProps: { min: 1, max: linkedItems.length + 1 }, }} fullWidth /> Добавить
    )} {activeTab === 1 && ( {/* Поле поиска */} setSearchQuery(e.target.value)} placeholder="Введите название остановки..." size="small" sx={{ mb: 1 }} /> {/* Список доступных остановок с чекбоксами */} {filteredAvailableItems.map((item) => ( handleCheckboxChange(item.id)} size="small" /> } label={

    {String(item.name)}

    {String(item.description)}

    } sx={{ margin: 0, "& .MuiFormControlLabel-label": { fontSize: "0.875rem", }, }} /> ))} {filteredAvailableItems.length === 0 && ( {searchQuery.trim() ? "Остановки не найдены" : "Нет доступных остановок"} )}
    Добавить выбранные ({selectedItems.size})
    )}
    )} ); }; export const LinkedItemsContents = observer( LinkedItemsContentsInner ) as typeof LinkedItemsContentsInner;