feat: pagination for tables with deleted station directions

This commit is contained in:
2025-12-24 15:43:25 +03:00
parent 39e11ad5ca
commit a3d574a79c
20 changed files with 448 additions and 302 deletions

View File

@@ -22,6 +22,7 @@ import {
CityCreatePage,
CarrierCreatePage,
VehicleCreatePage,
VehicleEditPage,
CountryEditPage,
CityEditPage,
UserCreatePage,
@@ -153,6 +154,7 @@ const router = createBrowserRouter([
{ path: "station/:id", element: <StationPreviewPage /> },
{ path: "station/:id/edit", element: <StationEditPage /> },
{ path: "vehicle/create", element: <VehicleCreatePage /> },
{ path: "vehicle/:id/edit", element: <VehicleEditPage /> },
{ path: "article", element: <ArticleListPage /> },
{ path: "article/:id", element: <ArticlePreviewPage /> },
],

View File

@@ -17,6 +17,10 @@ export const ArticleListPage = observer(() => {
const { language } = languageStore;
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
useEffect(() => {
const fetchArticles = async () => {
@@ -83,31 +87,41 @@ export const ArticleListPage = observer(() => {
<h1 className="text-2xl">Статьи</h1>
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<div className="w-full">
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
hideFooter
slots={{
noRowsOverlay: () => (
<Box

View File

@@ -17,6 +17,10 @@ export const CarrierListPage = observer(() => {
const [rowId, setRowId] = 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;
useEffect(() => {
@@ -129,28 +133,39 @@ export const CarrierListPage = observer(() => {
<CreateButton label="Создать перевозчика" path="/carrier/create" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooter
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids as unknown as number[]));
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
slots={{
noRowsOverlay: () => (

View File

@@ -18,6 +18,10 @@ export const CityListPage = observer(() => {
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [rows, setRows] = useState<any[]>([]);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
const { language } = languageStore;
useEffect(() => {
@@ -128,28 +132,39 @@ export const CityListPage = observer(() => {
<CreateButton label="Создать город" path="/city/create" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooter
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids as unknown as number[]));
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
slots={{
noRowsOverlay: () => (

View File

@@ -16,6 +16,10 @@ export const CountryListPage = observer(() => {
const [rowId, setRowId] = useState<string | null>(null);
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
const { language } = languageStore;
useEffect(() => {
@@ -93,28 +97,39 @@ export const CountryListPage = observer(() => {
<CreateButton label="Добавить страну" path="/country/add" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows || []}
columns={columns}
hideFooter
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids as unknown as number[]));
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
slots={{
noRowsOverlay: () => (

View File

@@ -16,6 +16,10 @@ export const MediaListPage = observer(() => {
const [rowId, setRowId] = useState<string | null>(null);
const [ids, setIds] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
const { language } = languageStore;
useEffect(() => {
@@ -100,29 +104,39 @@ export const MediaListPage = observer(() => {
return (
<>
<div className="w-full">
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as string[]);
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => String(id));
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) => String(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
hideFooter
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{
noRowsOverlay: () => (

View File

@@ -78,7 +78,6 @@ type LinkedItemsProps<T> = {
disableCreation?: boolean;
updatedLinkedItems?: T[];
refresh?: number;
routeDirection?: boolean;
};
export const LinkedItems = <
@@ -128,7 +127,6 @@ const LinkedItemsContentsInner = <
disableCreation = false,
updatedLinkedItems,
refresh,
routeDirection,
}: LinkedItemsProps<T>) => {
const { language } = languageStore;
@@ -153,11 +151,6 @@ const LinkedItemsContentsInner = <
const availableItems = allItems
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
.filter((item) => {
if (routeDirection === undefined) return true;
return item.direction === routeDirection;
})
.filter((item) => {
const selectedCityId = selectedCityStore.selectedCityId;
if (selectedCityId && "city_id" in item) {
@@ -513,12 +506,6 @@ const LinkedItemsContentsInner = <
{type === "edit" && !disableCreation && (
<Stack gap={2} mt={2}>
<Typography variant="subtitle1">Добавить остановки</Typography>
{routeDirection !== undefined && (
<Typography variant="body2" color="textSecondary">
Показываются только остановки для{" "}
{routeDirection ? "прямого" : "обратного"} направления
</Typography>
)}
<Tabs
value={activeTab}
@@ -547,6 +534,7 @@ const LinkedItemsContentsInner = <
<TextField
{...params}
label="Выберите остановку"
placeholder="Введите название или описание остановки..."
fullWidth
/>
)}
@@ -554,16 +542,15 @@ const LinkedItemsContentsInner = <
option.id === value?.id
}
filterOptions={(options, { inputValue }) => {
const searchWords = inputValue
.toLowerCase()
.split(" ")
.filter(Boolean);
if (!inputValue.trim()) return options;
const query = inputValue.toLowerCase();
return options.filter((option) => {
const optionWords = String(option.name)
.toLowerCase()
.split(" ");
return searchWords.every((searchWord) =>
optionWords.some((word) => word.startsWith(searchWord))
const name = String(option.name || "").toLowerCase();
const description = String(
option.description || ""
).toLowerCase();
return (
name.includes(query) || description.includes(query)
);
});
}}

View File

@@ -575,7 +575,6 @@ export const RouteEditPage = observer(() => {
onUpdate={() => {
routeStore.getRoute(Number(id));
}}
routeDirection={editRouteData.route_direction}
/>
<div className="flex w-full justify-between">

View File

@@ -17,6 +17,10 @@ export const RouteListPage = observer(() => {
const [rowId, setRowId] = 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;
useEffect(() => {
@@ -149,28 +153,39 @@ export const RouteListPage = observer(() => {
<CreateButton label="Создать маршрут" path="/route/create" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooter
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids as unknown as number[]));
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
slots={{
noRowsOverlay: () => (

View File

@@ -22,6 +22,10 @@ export const SightListPage = observer(() => {
const [rowId, setRowId] = useState<string | null>(null);
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
const { language } = languageStore;
useEffect(() => {
@@ -98,7 +102,6 @@ export const SightListPage = observer(() => {
},
];
// Фильтрация достопримечательностей по выбранному городу
const filteredSights = useMemo(() => {
const { selectedCityId } = selectedCityStore;
if (!selectedCityId) {
@@ -126,28 +129,39 @@ export const SightListPage = observer(() => {
/>
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooter
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids as unknown as number[]));
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
slots={{
noRowsOverlay: () => (

View File

@@ -12,10 +12,14 @@ export const SnapshotListPage = observer(() => {
snapshotStore;
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
const [rowId, setRowId] = useState<string | null>(null);
const { language } = languageStore;
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
useEffect(() => {
const fetchSnapshots = async () => {
@@ -87,9 +91,10 @@ export const SnapshotListPage = observer(() => {
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
hideFooter
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{
noRowsOverlay: () => (

View File

@@ -136,23 +136,6 @@ export const StationCreatePage = observer(() => {
}
/>
<FormControl fullWidth>
<InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel>
<Select
labelId="direction-label"
value={createStationData.common.direction ? "Прямой" : "Обратный"}
label="Прямой/обратный маршрут"
onChange={(e) =>
setCreateCommonData({
direction: e.target.value === "Прямой",
})
}
>
<MenuItem value="Прямой">Прямой</MenuItem>
<MenuItem value="Обратный">Обратный</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
label="Описание"

View File

@@ -162,23 +162,6 @@ export const StationEditPage = observer(() => {
}
/>
<FormControl fullWidth>
<InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel>
<Select
labelId="direction-label"
value={editStationData.common.direction ? "Прямой" : "Обратный"}
label="Прямой/обратный маршрут"
onChange={(e) =>
setEditCommonData({
direction: e.target.value === "Прямой",
})
}
>
<MenuItem value="Прямой">Прямой</MenuItem>
<MenuItem value="Обратный">Обратный</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
label="Описание"

View File

@@ -30,6 +30,10 @@ export const StationListPage = observer(() => {
);
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
const { language } = languageStore;
useEffect(() => {
@@ -75,25 +79,6 @@ export const StationListPage = observer(() => {
);
},
},
{
field: "direction",
headerName: "Направление",
width: 200,
align: "center",
headerAlign: "center",
renderCell: (params: GridRenderCellParams) => {
return (
<p
className={
params.row.direction === true ? "text-green-500" : "text-red-500"
}
>
{params.row.direction ? "Прямой" : "Обратный"}
</p>
);
},
},
{
field: "actions",
headerName: "Действия",
@@ -134,7 +119,6 @@ export const StationListPage = observer(() => {
},
];
// Фильтрация станций по выбранному городу
const filteredStations = () => {
const { selectedCityId } = selectedCityStore;
if (!selectedCityId) {
@@ -149,7 +133,6 @@ export const StationListPage = observer(() => {
id: station.id,
name: station.name,
description: station.description,
direction: station.direction,
}));
return (
@@ -162,29 +145,75 @@ export const StationListPage = observer(() => {
<CreateButton label="Создать остановки" path="/station/create" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 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)}
className={`px-4 py-2 rounded flex gap-2 items-center transition-all ${
ids.length > 0
? "bg-red-500 text-white cursor-pointer opacity-100"
: "bg-gray-300 text-gray-500 cursor-not-allowed opacity-0 pointer-events-none"
}`}
onClick={() => {
if (ids.length > 0) {
setIsBulkDeleteModalOpen(true);
}
}}
disabled={ids.length === 0}
>
<Trash2 size={20} className="text-white" /> Удалить выбранные (
{ids.length})
<Trash2
size={20}
className={ids.length > 0 ? "text-white" : "text-gray-500"}
/>
Удалить выбранные ({ids.length})
</button>
</div>
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
onRowSelectionModelChange={(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([]);
}
}}
hideFooter
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{
noRowsOverlay: () => (

View File

@@ -67,21 +67,6 @@ export const StationPreviewPage = observer(() => {
<p>{stationPreview[id!]?.[language]?.data.system_name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Направление</h1>
<p
className={`${
stationPreview[id!]?.[language]?.data.direction
? "text-green-500"
: "text-red-500"
}`}
>
{stationPreview[id!]?.[language]?.data.direction
? "Прямой"
: "Обратный"}
</p>
</div>
{stationPreview[id!]?.[language]?.data.address && (
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Адрес</h1>

View File

@@ -16,6 +16,10 @@ export const UserListPage = observer(() => {
const [rowId, setRowId] = useState<number | null>(null);
const [ids, setIds] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 50,
});
useEffect(() => {
const fetchUsers = async () => {
@@ -126,29 +130,39 @@ export const UserListPage = observer(() => {
<CreateButton label="Создать пользователя" path="/user/create" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
hideFooter
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{
noRowsOverlay: () => (

View File

@@ -6,6 +6,7 @@ import {
FormControl,
InputLabel,
Button,
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
@@ -16,6 +17,7 @@ import {
languageStore,
VEHICLE_TYPES,
vehicleStore,
LoadingSpinner,
} from "@shared";
import { toast } from "react-toastify";
@@ -31,6 +33,8 @@ export const VehicleEditPage = observer(() => {
} = vehicleStore;
const { getCarriers } = carrierStore;
const { language } = languageStore;
const [isLoading, setIsLoading] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
useEffect(() => {
// Устанавливаем русский язык при загрузке страницы
@@ -38,31 +42,58 @@ export const VehicleEditPage = observer(() => {
}, []);
useEffect(() => {
(async () => {
await getVehicle(Number(id));
await getCarriers(language);
const fetchAndSetVehicleData = async () => {
if (!id) {
setIsLoadingData(false);
return;
}
setEditVehicleData({
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
type: vehicle[Number(id)]?.vehicle.type,
carrier: vehicle[Number(id)]?.vehicle.carrier,
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
});
})();
setIsLoadingData(true);
try {
await getVehicle(Number(id));
await getCarriers(language);
setEditVehicleData({
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
type: vehicle[Number(id)]?.vehicle.type,
carrier: vehicle[Number(id)]?.vehicle.carrier,
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
});
} finally {
setIsLoadingData(false);
}
};
fetchAndSetVehicleData();
}, [id, language]);
const [isLoading, setIsLoading] = useState(false);
const handleEdit = async () => {
try {
setIsLoading(true);
await editVehicle(Number(id), editVehicleData);
toast.success("Транспортное средство успешно обновлено");
navigate("/vehicle");
navigate("/devices");
} catch (error) {
toast.error("Ошибка при обновлении транспортного средства");
} finally {
setIsLoading(false);
}
};
if (isLoadingData) {
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "60vh",
}}
>
<LoadingSpinner message="Загрузка данных транспортного средства..." />
</Box>
);
}
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex items-center gap-4">

View File

@@ -18,6 +18,10 @@ export const VehicleListPage = observer(() => {
const [rowId, setRowId] = 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;
useEffect(() => {
@@ -148,29 +152,39 @@ export const VehicleListPage = observer(() => {
/>
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<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>
{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>
)}
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
checkboxSelection
disableRowSelectionExcludeModel
loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => {
if (Array.isArray(newSelection)) {
const selectedIds = newSelection.map((id: string | number) => Number(id));
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) => Number(id));
setIds(selectedIds);
} else {
setIds([]);
}
}}
hideFooter
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{
noRowsOverlay: () => (

View File

@@ -13,7 +13,6 @@ type StationLanguageData = {
type StationCommonData = {
city_id: number;
direction: boolean;
description: string;
icon: string;
latitude: number;
@@ -44,7 +43,6 @@ type Station = {
city: string;
city_id: number;
description: string;
direction: boolean;
icon: string;
latitude: number;
longitude: number;
@@ -123,7 +121,6 @@ class StationsStore {
common: {
city: "",
city_id: 0,
direction: false,
description: "",
icon: "",
latitude: 0,
@@ -169,7 +166,6 @@ class StationsStore {
common: {
city: "",
city_id: 0,
direction: false,
description: "",
icon: "",
latitude: 0,
@@ -252,7 +248,6 @@ class StationsStore {
common: {
city: ruResponse.data.city,
city_id: ruResponse.data.city_id,
direction: ruResponse.data.direction,
description: ruResponse.data.description,
icon: ruResponse.data.icon,
latitude: ruResponse.data.latitude,
@@ -277,7 +272,6 @@ class StationsStore {
editStation = async (id: number) => {
const commonDataPayload = {
city_id: this.editStationData.common.city_id,
direction: this.editStationData.common.direction,
icon: this.editStationData.common.icon,
latitude: this.editStationData.common.latitude,
longitude: this.editStationData.common.longitude,
@@ -405,7 +399,6 @@ class StationsStore {
const { language } = languageStore;
let commonDataPayload: Partial<StationCommonData> = {
city_id: this.createStationData.common.city_id,
direction: this.createStationData.common.direction,
icon: this.createStationData.common.icon,
latitude: this.createStationData.common.latitude,
longitude: this.createStationData.common.longitude,
@@ -479,7 +472,6 @@ class StationsStore {
common: {
city: "",
city_id: 0,
direction: false,
icon: "",
latitude: 0,
description: "",
@@ -526,7 +518,6 @@ class StationsStore {
common: {
city: "",
city_id: 0,
direction: false,
description: "",
icon: "",
latitude: 0,
@@ -575,7 +566,6 @@ class StationsStore {
// Формируем commonDataPayload как в editStation, с обновленными transfers
const commonDataPayload = {
city_id: stationData.city_id,
direction: stationData.direction,
latitude: stationData.latitude,
longitude: stationData.longitude,
offset_x: stationData.offset_x,

View File

@@ -5,18 +5,18 @@ import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { Check, Copy, RotateCcw, Trash2, X } from "lucide-react";
import { Check, Copy, Pencil, RotateCcw, Trash2, X } from "lucide-react";
import {
authInstance,
devicesStore,
Modal,
snapshotStore,
vehicleStore,
Vehicle,
} from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Button, Checkbox, Typography } from "@mui/material";
import { Vehicle } from "@shared";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
import { DeleteModal } from "@widgets";
@@ -47,6 +47,7 @@ const formatDate = (dateString: string | null) => {
};
type TableRowData = {
vehicle_id: number;
tail_number: number;
online: boolean;
lastUpdate: string | null;
@@ -56,6 +57,7 @@ type TableRowData = {
device_uuid: string | null;
};
function createData(
vehicle_id: number,
tail_number: number,
online: boolean,
lastUpdate: string | null,
@@ -65,6 +67,7 @@ function createData(
device_uuid: string | null
): TableRowData {
return {
vehicle_id,
tail_number,
online,
lastUpdate,
@@ -80,6 +83,7 @@ const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => {
const uuid = vehicle.vehicle.uuid;
if (!uuid)
return {
vehicle_id: vehicle.vehicle.id,
tail_number: vehicle.vehicle.tail_number,
online: false,
lastUpdate: null,
@@ -89,6 +93,7 @@ const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => {
device_uuid: null,
};
return createData(
vehicle.vehicle.id,
vehicle.vehicle.tail_number,
vehicle.device_status?.online ?? false,
vehicle.device_status?.last_update ?? null,
@@ -404,37 +409,54 @@ export const DevicesTable = observer(() => {
</div>
</TableCell>
<TableCell align="center">
<Button
onClick={async (e) => {
e.stopPropagation();
try {
if (
row.device_uuid &&
devices.find((device) => device === row.device_uuid)
) {
await handleReloadStatus(row.device_uuid);
toast.success("Статус устройства обновлен");
} else {
toast.error("Нет связи с устройством");
<div className="flex items-center justify-center gap-1">
<Button
onClick={(e) => {
e.stopPropagation();
navigate(`/vehicle/${row.vehicle_id}/edit`);
}}
title="Редактировать транспорт"
size="small"
variant="text"
>
<Pencil size={16} />
</Button>
<Button
onClick={async (e) => {
e.stopPropagation();
try {
if (
row.device_uuid &&
devices.find((device) => device === row.device_uuid)
) {
await handleReloadStatus(row.device_uuid);
toast.success("Статус устройства обновлен");
} else {
toast.error("Нет связи с устройством");
}
} catch (error) {
toast.error("Ошибка сервера");
}
} catch (error) {
toast.error("Ошибка сервера");
}
}}
title="Перезапросить статус"
size="small"
variant="text"
>
<RotateCcw size={16} />
</Button>
<Button
onClick={() => {
navigator.clipboard.writeText(row.device_uuid ?? "");
toast.success("UUID скопирован");
}}
>
<Copy size={16} />
</Button>
}}
title="Перезапросить статус"
size="small"
variant="text"
>
<RotateCcw size={16} />
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(row.device_uuid ?? "");
toast.success("UUID скопирован");
}}
title="Копировать UUID"
size="small"
variant="text"
>
<Copy size={16} />
</Button>
</div>
</TableCell>
</TableRow>
))}