import { useState, useEffect } from "react"; import { Stack, Typography, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField, Autocomplete, TableCell, TableContainer, Table, TableHead, TableRow, Paper, TableBody, Checkbox, FormControlLabel, Tabs, Tab, Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { AnimatedCircleButton, authInstance, languageStore, selectedCityStore, } from "@shared"; type Field = { label: string; data: keyof T; render?: (value: any) => React.ReactNode; }; type LinkedStationsProps = { parentId: string | number; fields: Field[]; setItemsParent?: (items: T[]) => void; type: "show" | "edit"; onUpdate?: () => void; disableCreation?: boolean; updatedLinkedItems?: T[]; refresh?: number; }; export const LinkedStations = < T extends { id: number; name: string; [key: string]: any } >( props: LinkedStationsProps ) => { const theme = useTheme(); return ( <> } sx={{ background: theme.palette.background.paper, borderBottom: `1px solid ${theme.palette.divider}`, width: "100%", }} > Привязанные остановки ); }; const LinkedStationsContentsInner = < T extends { id: number; name: string; [key: string]: any } >({ parentId, setItemsParent, fields, type, onUpdate, disableCreation = false, updatedLinkedItems, refresh, }: LinkedStationsProps) => { const { language } = languageStore; const [allItems, setAllItems] = useState([]); const [linkedItems, setLinkedItems] = useState([]); const [selectedItemId, setSelectedItemId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState(0); const [selectedItems, setSelectedItems] = useState>(new Set()); const [searchQuery, setSearchQuery] = useState(""); const [selectedToDetach, setSelectedToDetach] = useState>( new Set() ); const [isLinkingSingle, setIsLinkingSingle] = useState(false); const [isLinkingBulk, setIsLinkingBulk] = useState(false); const [isBulkDetaching, setIsBulkDetaching] = useState(false); const [detachingIds, setDetachingIds] = useState>(new Set()); useEffect(() => {}, [error]); const parentResource = "sight"; const childResource = "station"; const buildPayload = (ids: number[]) => ({ [`${childResource}_ids`]: ids, }); 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; return String(item.name).toLowerCase().includes(searchQuery.toLowerCase()); }); useEffect(() => { if (updatedLinkedItems) { setLinkedItems(updatedLinkedItems); } }, [updatedLinkedItems]); useEffect(() => { setItemsParent?.(linkedItems); }, [linkedItems, setItemsParent]); useEffect(() => { setSelectedToDetach((prev) => { const updated = new Set(); linkedItems.forEach((item) => { if (prev.has(item.id)) { updated.add(item.id); } }); return updated; }); }, [linkedItems]); const linkItem = () => { if (selectedItemId !== null) { setError(null); const requestData = buildPayload([selectedItemId]); setIsLinkingSingle(true); 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 station:", 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: buildPayload([itemId]), }) .then(() => { setLinkedItems(linkedItems.filter((item) => item.id !== itemId)); onUpdate?.(); }) .catch((error) => { console.error("Error deleting station:", error); setError("Failed to delete station"); }) .finally(() => { setDetachingIds((prev) => { const next = new Set(prev); next.delete(itemId); return next; }); }); }; const handleCheckboxChange = (itemId: number) => { const updated = new Set(selectedItems); if (updated.has(itemId)) { updated.delete(itemId); } else { updated.add(itemId); } setSelectedItems(updated); }; const handleBulkLink = async () => { if (selectedItems.size === 0) return; setError(null); setIsLinkingBulk(true); const idsToLink = Array.from(selectedItems); try { await authInstance.post( `/${parentResource}/${parentId}/${childResource}`, buildPayload(idsToLink) ); const newItems = allItems.filter((item) => idsToLink.includes(item.id)); setLinkedItems((prev) => { const existingIds = new Set(prev.map((item) => item.id)); const additions = newItems.filter((item) => !existingIds.has(item.id)); return [...prev, ...additions]; }); setSelectedItems((prev) => { const remaining = new Set(prev); idsToLink.forEach((id) => remaining.delete(id)); return remaining; }); onUpdate?.(); } catch (error) { console.error("Error linking stations:", error); setError("Failed to link stations"); } setIsLinkingBulk(false); }; const toggleDetachSelection = (itemId: number) => { const updated = new Set(selectedToDetach); if (updated.has(itemId)) { updated.delete(itemId); } else { updated.add(itemId); } setSelectedToDetach(updated); }; const handleToggleAllDetach = (checked: boolean) => { if (!checked) { setSelectedToDetach(new Set()); return; } setSelectedToDetach(new Set(linkedItems.map((item) => item.id))); }; const handleBulkDetach = async () => { const idsToDetach = Array.from(selectedToDetach); if (idsToDetach.length === 0) return; setError(null); setIsBulkDetaching(true); setDetachingIds((prev) => { const next = new Set(prev); idsToDetach.forEach((id) => next.add(id)); return next; }); try { await authInstance.delete( `/${parentResource}/${parentId}/${childResource}`, { data: buildPayload(idsToDetach), } ); setLinkedItems((prev) => prev.filter((item) => !idsToDetach.includes(item.id)) ); setSelectedToDetach((prev) => { const remaining = new Set(prev); idsToDetach.forEach((id) => remaining.delete(id)); return remaining; }); onUpdate?.(); } catch (error) { console.error("Error deleting stations:", error); setError("Failed to delete stations"); } setDetachingIds((prev) => { const next = new Set(prev); idsToDetach.forEach((id) => next.delete(id)); return next; }); setIsBulkDetaching(false); }; const allSelectedForDetach = linkedItems.length > 0 && linkedItems.every((item) => selectedToDetach.has(item.id)); const isIndeterminateDetach = selectedToDetach.size > 0 && !allSelectedForDetach; useEffect(() => { if (parentId) { setIsLoading(true); setError(null); authInstance .get(`/${parentResource}/${parentId}/${childResource}`) .then((response) => { setLinkedItems(response?.data || []); }) .catch((error) => { console.error("Error fetching linked stations:", 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 stations:", error); setError("Failed to load available stations"); setAllItems([]); }); } }, [type]); return ( <> {linkedItems?.length > 0 && ( {type === "edit" && ( handleToggleAllDetach(e.target.checked)} /> )} {fields.map((field) => ( {field.label} ))} {type === "edit" && ( Действие )} {linkedItems.map((item, index) => ( {type === "edit" && ( toggleDetachSelection(item.id)} /> )} {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)} > Отвязать )} ))}
)} {type === "edit" && linkedItems.length > 0 && ( Отвязать выбранные ({selectedToDetach.size}) )} {linkedItems.length === 0 && !isLoading && ( Остановки не найдены )} {type === "edit" && !disableCreation && ( Добавить остановки setActiveTab(value)} variant="fullWidth" > {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 }) => { 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) => (
  • {String(option.name)}

    {String(option.description)}

  • )} /> Добавить
    )} {activeTab === 1 && ( setSearchQuery(e.target.value)} placeholder="Введите название остановки..." size="small" /> {filteredAvailableItems.map((item) => ( handleCheckboxChange(item.id)} size="small" /> } label={String(item.name)} sx={{ margin: 0, "& .MuiFormControlLabel-label": { fontSize: "0.9rem", }, }} /> ))} {filteredAvailableItems.length === 0 && ( {searchQuery.trim() ? "Остановки не найдены" : "Нет доступных остановок"} )} Добавить выбранные ({selectedItems.size}) )}
    )} {isLoading && ( Загрузка... )} {error && ( {error} )} ); }; export const LinkedStationsContents = observer( LinkedStationsContentsInner ) as typeof LinkedStationsContentsInner;