Files
WhiteNightsAdminPanel/src/pages/Station/StationListPage/index.tsx

326 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import {
authStore,
languageStore,
stationsStore,
selectedCityStore,
SearchInput,
} from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus, Route, Check, X } from "lucide-react";
import { useNavigate } from "react-router-dom";
import {
CreateButton,
DeleteModal,
LanguageSwitcher,
EditStationTransfersModal,
} from "@widgets";
import { Box, CircularProgress, Tooltip } from "@mui/material";
export const StationListPage = observer(() => {
const { stationLists, getStationList, deleteStation, sightCounts, sightCountsLoading, loadSightCounts } = stationsStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [isTransfersModalOpen, setIsTransfersModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null);
const [selectedStationId, setSelectedStationId] = useState<number | null>(
null
);
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
const { language } = languageStore;
const canWriteStations = authStore.canWrite("stations");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
const fetchStations = async () => {
setIsLoading(true);
await getStationList();
setIsLoading(false);
const stationIds = stationLists[language].data.map((s: any) => s.id);
loadSightCounts(stationIds);
};
fetchStations();
}, [language]);
const columns: GridColDef[] = [
{
field: "name",
headerName: "Название",
flex: 1,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="w-full h-full flex items-center">
{params.value ? (
params.value
) : (
<Minus size={20} className="text-red-500" />
)}
</div>
);
},
},
{
field: "description",
headerName: "Описание",
flex: 1,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="w-full h-full flex items-center">
{params.value ? (
params.value
) : (
<Minus size={20} className="text-red-500" />
)}
</div>
);
},
},
{
field: "sightCount",
headerName: "Достопримечательности",
width: 180,
align: "center" as const,
headerAlign: "center" as const,
sortable: true,
renderHeader: (params) => (
<Tooltip title="Количество привязанных достопримечательностей">
<span>{params.colDef.headerName}</span>
</Tooltip>
),
renderCell: (params: GridRenderCellParams) => (
<div className="w-full h-full flex items-center justify-center">
{params.value === null ? (
<CircularProgress size={14} />
) : (
params.value
)}
</div>
),
},
{
field: "hasTransfers",
headerName: "Пересадки",
width: 120,
align: "center" as const,
headerAlign: "center" as const,
sortable: true,
renderHeader: (params) => (
<Tooltip title="Подтверждение добавленных пересадок">
<span>{params.colDef.headerName}</span>
</Tooltip>
),
renderCell: (params: GridRenderCellParams) => (
<Box
sx={{ display: "flex", justifyContent: "center", alignItems: "center", width: "100%", height: "100%" }}
>
{params.value ? (
<Check size={18} className="text-green-600" />
) : (
<X size={18} className="text-red-600" />
)}
</Box>
),
},
{
field: "actions",
headerName: "Действия",
width: 200,
align: "center" as const,
headerAlign: "center" as const,
sortable: false,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
{canWriteStations && (
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
)}
{canWriteStations && (
<button
onClick={() => {
setSelectedStationId(params.row.id);
setIsTransfersModalOpen(true);
}}
title="Редактировать пересадки"
>
<Route size={20} className="text-purple-500" />
</button>
)}
{canWriteStations && (
<button
onClick={() => {
setIsDeleteModalOpen(true);
setRowId(params.row.id);
}}
>
<Trash2 size={20} className="text-red-500" />
</button>
)}
</div>
);
},
},
];
const rows = useMemo(() => {
const { selectedCityId } = selectedCityStore;
const query = searchQuery.trim().toLowerCase();
return stationLists[language].data
.filter((station: any) => !selectedCityId || station.city_id === selectedCityId)
.filter(
(station: any) =>
!query ||
(station.name ?? "").toLowerCase().includes(query) ||
(station.description ?? "").toLowerCase().includes(query)
)
.map((station: any) => ({
id: station.id,
name: station.name,
description: station.description,
sightCount: sightCounts.has(station.id) ? sightCounts.get(station.id) : null,
hasTransfers: station.transfers
? Object.values(station.transfers).some((v: any) => v != null && v !== "")
: false,
}));
}, [stationLists[language].data, selectedCityStore.selectedCityId, searchQuery, sightCounts.size, sightCountsLoading]);
return (
<>
<LanguageSwitcher />
<div className="w-full">
<div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Остановки</h1>
{canWriteStations && (
<CreateButton label="Создать остановку" path="/station/create" />
)}
</div>
{canWriteStations && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300">
<button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
onClick={() => setIsBulkDeleteModalOpen(true)}
>
<Trash2 size={20} className="text-white" /> Удалить выбранные (
{ids.length})
</button>
</div>
)}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid
rows={rows}
columns={columns}
onRowDoubleClick={(params) => canWriteStations && navigate(`/station/${params.row.id}/edit`)}
checkboxSelection={canWriteStations}
disableRowSelectionExcludeModel
disableRowSelectionOnClick
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
onRowSelectionModelChange={
canWriteStations
? (newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection
.map((id: string | number) => {
const numId =
typeof id === "string"
? Number.parseInt(id, 10)
: Number(id);
return numId;
})
.filter(
(id: number) =>
!Number.isNaN(id) && id !== null && id !== undefined
);
setIds(selectedIds);
} else if (
newSelection &&
typeof newSelection === "object" &&
"ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet)
.map((id: string | number) => {
const numId =
typeof id === "string"
? Number.parseInt(id, 10)
: Number(id);
return numId;
})
.filter(
(id: number) =>
!Number.isNaN(id) && id !== null && id !== undefined
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{
noRowsOverlay: () => (
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
{isLoading ? <CircularProgress size={20} /> : "Нет остановок"}
</Box>
),
}}
/>
</div>
<DeleteModal
open={isDeleteModalOpen}
onDelete={async () => {
if (rowId) {
deleteStation(rowId);
}
setIsDeleteModalOpen(false);
setRowId(null);
}}
onCancel={() => {
setIsDeleteModalOpen(false);
setRowId(null);
}}
/>
<DeleteModal
open={isBulkDeleteModalOpen}
onDelete={async () => {
await Promise.all(ids.map((id) => deleteStation(id)));
getStationList();
setIsBulkDeleteModalOpen(false);
setIds([]);
}}
onCancel={() => {
setIsBulkDeleteModalOpen(false);
}}
/>
<EditStationTransfersModal
open={isTransfersModalOpen}
onClose={() => {
setIsTransfersModalOpen(false);
setSelectedStationId(null);
}}
stationId={selectedStationId}
/>
</>
);
});