feat: add search for list pages

This commit is contained in:
2026-04-05 15:31:42 +03:00
parent 5e0b56c7dc
commit a58f438dce
13 changed files with 255 additions and 105 deletions

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, articlesStore, languageStore } from "@shared"; import { authStore, articlesStore, languageStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Trash2, Eye, Minus } from "lucide-react"; import { Trash2, Eye, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -22,6 +22,7 @@ export const ArticleListPage = observer(() => {
pageSize: 50, pageSize: 50,
}); });
const canWriteArticles = authStore.canWrite("sights"); const canWriteArticles = authStore.canWrite("sights");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchArticles = async () => { const fetchArticles = async () => {
@@ -72,11 +73,16 @@ export const ArticleListPage = observer(() => {
}, },
]; ];
const rows = articleList[language].data.map((article) => ({ const rows = useMemo(() => {
const query = searchQuery.toLowerCase();
return articleList[language].data
.filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query))
.map((article) => ({
id: article.id, id: article.id,
heading: article.heading, heading: article.heading,
body: article.body, body: article.body,
})); }));
}, [articleList[language].data, searchQuery]);
return ( return (
<> <>
@@ -99,6 +105,8 @@ export const ArticleListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<div className="w-full"> <div className="w-full">
<DataGrid <DataGrid
rows={rows} rows={rows}

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, carrierStore, cityStore, languageStore } from "@shared"; import { authStore, carrierStore, cityStore, languageStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react"; import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -22,6 +22,7 @@ export const CarrierListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canReadCities = authStore.canRead("cities"); const canReadCities = authStore.canRead("cities");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -119,9 +120,15 @@ export const CarrierListPage = observer(() => {
const canWriteCarriers = authStore.canWrite("carriers"); const canWriteCarriers = authStore.canWrite("carriers");
const rows = carriers[language].data const rows = useMemo(() => {
?.filter((carrier) => const query = searchQuery.toLowerCase();
!allowedCityIds || allowedCityIds.includes(carrier.city_id), return (carriers[language].data ?? [])
.filter((carrier) => !allowedCityIds || allowedCityIds.includes(carrier.city_id))
.filter(
(carrier) =>
!query ||
(carrier.full_name ?? "").toLowerCase().includes(query) ||
(carrier.short_name ?? "").toLowerCase().includes(query)
) )
.map((carrier) => ({ .map((carrier) => ({
id: carrier.id, id: carrier.id,
@@ -129,6 +136,7 @@ export const CarrierListPage = observer(() => {
short_name: carrier.short_name, short_name: carrier.short_name,
city_id: carrier.city_id, city_id: carrier.city_id,
})); }));
}, [carriers[language].data, searchQuery, allowedCityIds]);
return ( return (
<> <>
@@ -153,6 +161,8 @@ export const CarrierListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, languageStore, cityStore, countryStore } from "@shared"; import { authStore, languageStore, cityStore, countryStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react"; import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -24,6 +24,7 @@ export const CityListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteCities = authStore.canWrite("cities"); const canWriteCities = authStore.canWrite("cities");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -57,6 +58,18 @@ export const CityListPage = observer(() => {
setRows(newRows2 || []); setRows(newRows2 || []);
}, [cities, countryStore.countries, language, isLoading]); }, [cities, countryStore.countries, language, isLoading]);
const filteredRows = useMemo(() => {
const query = searchQuery.toLowerCase();
if (!query) return rows;
return rows.filter((row) => {
const cityName = (row.name ?? "").toLowerCase();
const countryName = (
countryStore.countries[language]?.data?.find((c) => c.code === row.country)?.name ?? ""
).toLowerCase();
return cityName.includes(query) || countryName.includes(query);
});
}, [rows, searchQuery, countryStore.countries, language]);
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
field: "country", field: "country",
@@ -142,8 +155,10 @@ export const CityListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={filteredRows}
columns={columns} columns={columns}
checkboxSelection={canWriteCities} checkboxSelection={canWriteCities}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, countryStore, languageStore } from "@shared"; import { authStore, countryStore, languageStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Trash2, Minus } from "lucide-react"; import { Trash2, Minus } from "lucide-react";
@@ -22,6 +22,7 @@ export const CountryListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteCountries = authStore.canWrite("countries"); const canWriteCountries = authStore.canWrite("countries");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchCountries = async () => { const fetchCountries = async () => {
@@ -72,11 +73,16 @@ export const CountryListPage = observer(() => {
}] : []), }] : []),
]; ];
const rows = countries[language]?.data.map((country) => ({ const rows = useMemo(() => {
const query = searchQuery.toLowerCase();
return (countries[language]?.data ?? [])
.filter((country) => !query || (country.name ?? "").toLowerCase().includes(query))
.map((country) => ({
id: country.code, id: country.code,
code: country.code, code: country.code,
name: country.name, name: country.name,
})); }));
}, [countries[language]?.data, searchQuery]);
return ( return (
<> <>
@@ -102,8 +108,10 @@ export const CountryListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows || []} rows={rows}
columns={columns} columns={columns}
checkboxSelection={canWriteCountries} checkboxSelection={canWriteCountries}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared"; import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Eye, Trash2, Minus } from "lucide-react"; import { Eye, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -22,6 +22,7 @@ export const MediaListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteMedia = authStore.canWrite("sights"); const canWriteMedia = authStore.canWrite("sights");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchMedia = async () => { const fetchMedia = async () => {
@@ -95,15 +96,22 @@ export const MediaListPage = observer(() => {
}, },
]; ];
const rows = media.map((media) => ({ const rows = useMemo(() => {
id: media.id, const query = searchQuery.toLowerCase();
media_name: media.media_name, return media
media_type: media.media_type, .filter((item) => !query || (item.media_name ?? "").toLowerCase().includes(query))
.map((item) => ({
id: item.id,
media_name: item.media_name,
media_type: item.media_type,
})); }));
}, [media, searchQuery]);
return ( return (
<> <>
<div className="w-full"> <div className="w-full">
<SearchInput value={searchQuery} onChange={setSearchQuery} />
{canWriteMedia && ids.length > 0 && ( {canWriteMedia && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, carrierStore, languageStore, routeStore, selectedCityStore } from "@shared"; import { authStore, carrierStore, languageStore, routeStore, selectedCityStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Map, Pencil, Trash2, Minus } from "lucide-react"; import { Map, Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -28,6 +28,7 @@ export const RouteListPage = observer(() => {
authStore.canWrite("sights") && authStore.canWrite("sights") &&
authStore.canWrite("routes"); authStore.canWrite("routes");
const canShowActionsColumn = canWriteRoutes || canShowRoutePreview; const canShowActionsColumn = canWriteRoutes || canShowRoutePreview;
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -146,26 +147,33 @@ export const RouteListPage = observer(() => {
}] : []), }] : []),
]; ];
const filteredRoutes = () => { const rows = useMemo(() => {
const { selectedCityId } = selectedCityStore; const { selectedCityId } = selectedCityStore;
if (!selectedCityId) { const query = searchQuery.toLowerCase();
return routes.data; let filtered = routes.data;
} if (selectedCityId) {
const cityCarrierIds = new Set( const cityCarrierIds = new Set(
carriers["ru"].data carriers["ru"].data
.filter((c) => c.city_id === selectedCityId) .filter((c) => c.city_id === selectedCityId)
.map((c) => c.id) .map((c) => c.id)
); );
return routes.data.filter((route) => cityCarrierIds.has(route.carrier_id)); filtered = filtered.filter((route) => cityCarrierIds.has(route.carrier_id));
}; }
return filtered
const rows = filteredRoutes().map((route) => ({ .filter(
(route) =>
!query ||
(route.route_name ?? "").toLowerCase().includes(query) ||
String(route.route_number ?? "").toLowerCase().includes(query)
)
.map((route) => ({
id: route.id, id: route.id,
carrier_id: route.carrier_id, carrier_id: route.carrier_id,
route_number: route.route_number, route_number: route.route_number,
route_direction: route.route_direction ? "Прямой" : "Обратный", route_direction: route.route_direction ? "Прямой" : "Обратный",
route_name: route.route_name, route_name: route.route_name,
})); }));
}, [routes.data, carriers["ru"].data, selectedCityStore.selectedCityId, searchQuery]);
return ( return (
<> <>
@@ -191,6 +199,8 @@ export const RouteListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}

View File

@@ -6,6 +6,7 @@ import {
languageStore, languageStore,
sightsStore, sightsStore,
selectedCityStore, selectedCityStore,
SearchInput,
} from "@shared"; } from "@shared";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
@@ -28,6 +29,7 @@ export const SightListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canReadCities = authStore.canRead("cities"); const canReadCities = authStore.canRead("cities");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchSights = async () => { const fetchSights = async () => {
@@ -120,14 +122,17 @@ export const SightListPage = observer(() => {
}); });
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]); }, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
const canWriteSights = authStore.canWrite("sights"); const query = searchQuery.toLowerCase();
const rows = filteredSights
const rows = filteredSights.map((sight) => ({ .filter((sight: any) => !query || (sight.name ?? "").toLowerCase().includes(query))
.map((sight) => ({
id: sight.id, id: sight.id,
name: sight.name, name: sight.name,
city_id: sight.city_id, city_id: sight.city_id,
})); }));
const canWriteSights = authStore.canWrite("sights");
return ( return (
<> <>
<LanguageSwitcher /> <LanguageSwitcher />
@@ -155,6 +160,8 @@ export const SightListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, languageStore, snapshotStore } from "@shared"; import { authStore, languageStore, snapshotStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DatabaseBackup, Trash2 } from "lucide-react"; import { DatabaseBackup, Trash2 } from "lucide-react";
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets"; import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
@@ -19,6 +19,7 @@ export const SnapshotListPage = observer(() => {
const { language } = languageStore; const { language } = languageStore;
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [paginationModel, setPaginationModel] = useState({ const [paginationModel, setPaginationModel] = useState({
page: 0, page: 0,
pageSize: 50, pageSize: 50,
@@ -89,22 +90,35 @@ export const SnapshotListPage = observer(() => {
}] : []), }] : []),
]; ];
const rows = snapshots.map((snapshot) => ({ const rows = useMemo(() => {
const query = searchQuery.toLowerCase();
return snapshots
.filter(
(snapshot) =>
!query ||
(snapshot.Name ?? "").toLowerCase().includes(query) ||
(snapshots.find((s) => s.ID === snapshot.ParentID)?.Name ?? "").toLowerCase().includes(query)
)
.map((snapshot) => ({
id: snapshot.ID, id: snapshot.ID,
name: snapshot.Name, name: snapshot.Name,
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name, parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
created_at: formatCreationTime(snapshot.CreationTime), created_at: formatCreationTime(snapshot.CreationTime),
})); }));
}, [snapshots, searchQuery]);
return ( return (
<> <>
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<div className="flex justify-between items-center mb-10"> <div className="flex justify-between items-center mb-10">
<h1 className="text-2xl ">Экспорт Медиа</h1> <h1 className="text-2xl ">Экспорт Медиа</h1>
{canCreateSnapshot && ( {canCreateSnapshot && (
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" /> <CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
)} )}
</div> </div>
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}

View File

@@ -5,8 +5,9 @@ import {
languageStore, languageStore,
stationsStore, stationsStore,
selectedCityStore, selectedCityStore,
SearchInput,
} from "@shared"; } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus, Route } from "lucide-react"; import { Pencil, Trash2, Minus, Route } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -36,6 +37,7 @@ export const StationListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteStations = authStore.canWrite("stations"); const canWriteStations = authStore.canWrite("stations");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchStations = async () => { const fetchStations = async () => {
@@ -121,21 +123,23 @@ export const StationListPage = observer(() => {
}, },
]; ];
const filteredStations = () => { const rows = useMemo(() => {
const { selectedCityId } = selectedCityStore; const { selectedCityId } = selectedCityStore;
if (!selectedCityId) { const query = searchQuery.toLowerCase();
return stationLists[language].data; return stationLists[language].data
} .filter((station: any) => !selectedCityId || station.city_id === selectedCityId)
return stationLists[language].data.filter( .filter(
(station: any) => station.city_id === selectedCityId (station: any) =>
); !query ||
}; (station.name ?? "").toLowerCase().includes(query) ||
(station.description ?? "").toLowerCase().includes(query)
const rows = filteredStations().map((station: any) => ({ )
.map((station: any) => ({
id: station.id, id: station.id,
name: station.name, name: station.name,
description: station.description, description: station.description,
})); }));
}, [stationLists[language].data, selectedCityStore.selectedCityId, searchQuery]);
return ( return (
<> <>
@@ -161,6 +165,8 @@ export const StationListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, userStore } from "@shared"; import { authStore, userStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react"; import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -21,6 +21,7 @@ export const UserListPage = observer(() => {
pageSize: 50, pageSize: 50,
}); });
const canWriteUsers = authStore.canWrite("users"); const canWriteUsers = authStore.canWrite("users");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchUsers = async () => { const fetchUsers = async () => {
@@ -107,12 +108,21 @@ export const UserListPage = observer(() => {
}] : []), }] : []),
]; ];
const rows = users.data?.map((user) => ({ const rows = useMemo(() => {
const query = searchQuery.toLowerCase();
return (users.data ?? [])
.filter((user) =>
!query ||
(user.name ?? "").toLowerCase().includes(query) ||
(user.email ?? "").toLowerCase().includes(query)
)
.map((user) => ({
id: user.id, id: user.id,
email: user.email, email: user.email,
is_admin: user.is_admin || (user.roles ?? []).includes("admin"), is_admin: user.is_admin || (user.roles ?? []).includes("admin"),
name: user.name, name: user.name,
})); }));
}, [users.data, searchQuery]);
return ( return (
<> <>
@@ -136,6 +146,8 @@ export const UserListPage = observer(() => {
</div> </div>
)} )}
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}

View File

@@ -1,7 +1,7 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, carrierStore, languageStore, vehicleStore } from "@shared"; import { authStore, carrierStore, languageStore, vehicleStore, SearchInput } from "@shared";
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { Eye, Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -24,6 +24,7 @@ export const VehicleListPage = observer(() => {
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteVehicles = authStore.canWrite("devices"); const canWriteVehicles = authStore.canWrite("devices");
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -136,7 +137,16 @@ export const VehicleListPage = observer(() => {
}, },
]; ];
const rows = vehicles.data?.map((vehicle) => ({ const rows = useMemo(() => {
const query = searchQuery.toLowerCase();
return (vehicles.data ?? [])
.filter(
(vehicle) =>
!query ||
(vehicle.vehicle.tail_number ?? "").toLowerCase().includes(query) ||
(vehicle.vehicle.carrier ?? "").toLowerCase().includes(query)
)
.map((vehicle) => ({
id: vehicle.vehicle.id, id: vehicle.vehicle.id,
tail_number: vehicle.vehicle.tail_number, tail_number: vehicle.vehicle.tail_number,
type: vehicle.vehicle.type, type: vehicle.vehicle.type,
@@ -145,18 +155,22 @@ export const VehicleListPage = observer(() => {
(carrier) => carrier.id === vehicle.vehicle.carrier_id (carrier) => carrier.id === vehicle.vehicle.carrier_id
)?.city, )?.city,
})); }));
}, [vehicles.data, carriers[language].data, searchQuery]);
return ( return (
<> <>
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<div className="flex justify-between items-center mb-10"> <div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Транспортные средства</h1> <h1 className="text-2xl">Транспортные средства</h1>
<CreateButton <CreateButton
label="Создать транспортное средство" label="Создать транспортное средство"
path="/vehicle/create" path="/vehicle/create"
/> />
</div> </div>
<SearchInput value={searchQuery} onChange={setSearchQuery} />
{canWriteVehicles && ids.length > 0 && ( {canWriteVehicles && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button

View File

@@ -0,0 +1,37 @@
import { Search, X } from "lucide-react";
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export const SearchInput = ({
value,
onChange,
placeholder = "Поиск...",
}: SearchInputProps) => {
return (
<div className="relative mb-4 w-full max-w-sm">
<Search
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none"
/>
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="w-full pl-9 pr-8 py-2 border border-gray-300 rounded-md text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
/>
{value && (
<button
onClick={() => onChange("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<X size={14} />
</button>
)}
</div>
);
};

View File

@@ -5,3 +5,4 @@ export * from "./CoordinatesInput";
export * from "./AnimatedCircleButton"; export * from "./AnimatedCircleButton";
export * from "./LoadingSpinner"; export * from "./LoadingSpinner";
export * from "./MultiSelect"; export * from "./MultiSelect";
export * from "./SearchInput";