fix: Update map with tables fixes
This commit is contained in:
@@ -18,6 +18,11 @@ import {
|
||||
Paper,
|
||||
TableBody,
|
||||
IconButton,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
||||
@@ -28,7 +33,8 @@ import {
|
||||
DropResult,
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
import { authInstance, languageStore } from "@shared";
|
||||
import { authInstance, languageStore, routeStore } from "@shared";
|
||||
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
||||
|
||||
// Helper function to insert an item at a specific position (1-based index)
|
||||
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
||||
@@ -68,6 +74,7 @@ type LinkedItemsProps<T> = {
|
||||
updatedLinkedItems?: T[];
|
||||
refresh?: number;
|
||||
cityId?: number;
|
||||
routeDirection?: boolean;
|
||||
};
|
||||
|
||||
export const LinkedItems = <
|
||||
@@ -118,6 +125,7 @@ export const LinkedItemsContents = <
|
||||
updatedLinkedItems,
|
||||
refresh,
|
||||
cityId,
|
||||
routeDirection,
|
||||
}: LinkedItemsProps<T>) => {
|
||||
const { language } = languageStore;
|
||||
|
||||
@@ -127,6 +135,10 @@ export const LinkedItemsContents = <
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
console.log(error);
|
||||
@@ -137,8 +149,25 @@ export const LinkedItemsContents = <
|
||||
|
||||
const availableItems = allItems
|
||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||
.filter((item) => {
|
||||
// Если направление маршрута не указано, показываем все станции
|
||||
if (routeDirection === undefined) return true;
|
||||
// Фильтруем станции по направлению маршрута
|
||||
return item.direction === routeDirection;
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Фильтрация по поиску для массового режима
|
||||
const filteredAvailableItems = availableItems.filter((item) => {
|
||||
if (!cityId || item.city_id == cityId) {
|
||||
if (!searchQuery.trim()) return true;
|
||||
return String(item.name)
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedLinkedItems) {
|
||||
setLinkedItems(updatedLinkedItems);
|
||||
@@ -250,12 +279,57 @@ export const LinkedItemsContents = <
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
})
|
||||
.then(() => {
|
||||
setLinkedItems((prev) => prev.filter((item) => item.id !== itemId));
|
||||
setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error unlinking item:", error);
|
||||
setError("Failed to unlink station");
|
||||
console.error("Error deleting item:", error);
|
||||
setError("Failed to delete station");
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
const selectedStations = Array.from(selectedItems).map((id) => ({ id }));
|
||||
const requestData = {
|
||||
stations: [
|
||||
...linkedItems.map((item) => ({ id: item.id })),
|
||||
...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");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -306,6 +380,7 @@ export const LinkedItemsContents = <
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
hover
|
||||
onClick={() => handleStationClick(item)}
|
||||
>
|
||||
{type === "edit" && dragAllowed && (
|
||||
<TableCell {...provided.dragHandleProps}>
|
||||
@@ -358,72 +433,169 @@ export const LinkedItemsContents = <
|
||||
|
||||
{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.filter(
|
||||
(item) => !cityId || item.city_id == cityId
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
<Typography variant="subtitle1">Добавить остановки</Typography>
|
||||
{routeDirection !== undefined && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Показываются только остановки для{" "}
|
||||
{routeDirection ? "прямого" : "обратного"} направления
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Позиция добавляемой остановки"
|
||||
value={position}
|
||||
onChange={(e) => {
|
||||
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
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, newValue) => setActiveTab(newValue)}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
<Tab label="По одной" />
|
||||
<Tab label="Массово" />
|
||||
</Tabs>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{activeTab === 0 && (
|
||||
<Stack gap={2}>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find(
|
||||
(item) => item.id === selectedItemId
|
||||
) || null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setSelectedItemId(newValue?.id || null)
|
||||
}
|
||||
options={availableItems.filter(
|
||||
(item) => !cityId || item.city_id == cityId
|
||||
)}
|
||||
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}>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<p>{String(option.name)}</p>
|
||||
<p className="text-xs text-gray-500 max-w-[300px] mr-4 truncate">
|
||||
{String(option.description)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Позиция добавляемой остановки"
|
||||
value={position}
|
||||
onChange={(e) => {
|
||||
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
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<Stack gap={2}>
|
||||
{/* Поле поиска */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Поиск остановок"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Введите название остановки..."
|
||||
size="small"
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
|
||||
{/* Список доступных остановок с чекбоксами */}
|
||||
<Paper sx={{ maxHeight: 300, overflow: "auto", p: 2 }}>
|
||||
<Stack gap={1}>
|
||||
{filteredAvailableItems.map((item) => (
|
||||
<FormControlLabel
|
||||
key={item.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedItems.has(item.id)}
|
||||
onChange={() => handleCheckboxChange(item.id)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={String(item.name)}
|
||||
sx={{
|
||||
margin: 0,
|
||||
"& .MuiFormControlLabel-label": {
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{filteredAvailableItems.length === 0 && (
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
textAlign="center"
|
||||
py={1}
|
||||
>
|
||||
{searchQuery.trim()
|
||||
? "Остановки не найдены"
|
||||
: "Нет доступных остановок"}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleBulkLink}
|
||||
disabled={selectedItems.size === 0}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить выбранные ({selectedItems.size})
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
<EditStationModal open={isModalOpen} onClose={handleCloseModal} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user