Compare commits

2 Commits

Author SHA1 Message Date
193f53c029 feat: update map, admin to and cache 2026-05-08 13:33:41 +03:00
4bda233b63 feat: update route widget and leftsidebar in route-preview 2026-05-08 01:12:28 +03:00
24 changed files with 219 additions and 119 deletions

14
.env
View File

@@ -1,8 +1,8 @@
# VITE_API_URL='https://wn.st.unprism.ru'
# VITE_REACT_APP ='https://wn.st.unprism.ru/'
# VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/'
# VITE_NEED_AUTH='true'
VITE_API_URL='https://wn.krbl.ru'
VITE_REACT_APP ='https://wn.krbl.ru/'
VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
VITE_API_URL='https://wn.st.unprism.ru'
VITE_REACT_APP ='https://wn.st.unprism.ru/'
VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/'
VITE_NEED_AUTH='true'
# VITE_API_URL='https://wn.krbl.ru'
# VITE_REACT_APP ='https://wn.krbl.ru/'
# VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
# VITE_NEED_AUTH='true'

View File

@@ -47,7 +47,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
const textWrapperRef = useRef(null);
// Автозакрытие fullscreen 3D при бездействии (45 сек)
// Автозакрытие fullscreen 3D при бездействии (60 сек)
useEffect(() => {
if (!isFullscreen3D) {
if (idleTimerRef.current) {
@@ -61,7 +61,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
const checkIdle = () => {
idleSeconds += 1;
if (idleSeconds >= 45) {
if (idleSeconds >= 60) {
setIsFullscreen3D(false);
}
};

View File

@@ -264,15 +264,20 @@ const SideMenu = observer(({ onMenuToggle }) => {
}
};
const isMenuOpenRef = useRef(isMenuOpen);
const handleMenuToggleRef = useRef(handleMenuToggle);
useEffect(() => { isMenuOpenRef.current = isMenuOpen; }, [isMenuOpen]);
useEffect(() => { handleMenuToggleRef.current = handleMenuToggle; });
useEffect(() => {
// Автоматическое закрытие сайд-меню после 45 секунд бездействия
// Автоматическое закрытие сайд-меню после 60 секунд бездействия
let idleSeconds = 0;
const checkIdle = () => {
idleSeconds += 1;
if (idleSeconds >= 45 && isMenuOpen) {
handleMenuToggle(false);
if (idleSeconds >= 60 && isMenuOpenRef.current) {
handleMenuToggleRef.current(false);
}
};
@@ -301,7 +306,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
window.removeEventListener(event, resetIdle);
});
};
}, [isMenuOpen, handleMenuToggle]);
}, []);
// Закрываем и открываем список достопримечательностей при изменении сортировки
const prevSortingByRef = useRef(sortingBy);

View File

@@ -26,15 +26,17 @@
position: fixed;
display: inline-flex;
border-radius: 10px;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset,
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
padding: 1px; /* Чтобы контент не прилипал к рамке */
background: linear-gradient(
background:
linear-gradient(
to bottom right,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4);
rgba(179, 165, 152, 0.4);
backdrop-filter: blur(10px);
pointer-events: auto;
z-index: 10000001;
@@ -50,7 +52,8 @@
height: 96px;
background-color: #fcd500;
color: black;
border-radius: 10px;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
display: flex;
justify-content: center;
align-items: center;

View File

@@ -31,7 +31,7 @@ export const ArticleListPage = observer(() => {
setIsLoading(false);
};
fetchArticles();
}, [language]);
}, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [
{

View File

@@ -1,6 +1,6 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, carrierStore, cityStore, languageStore, SearchInput } from "@shared";
import { authStore, carrierStore, cityStore, languageStore, selectedCityStore, SearchInput } from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
@@ -39,7 +39,7 @@ export const CarrierListPage = observer(() => {
setIsLoading(false);
};
fetchData();
}, [language]);
}, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [
{
@@ -169,6 +169,11 @@ export const CarrierListPage = observer(() => {
checkboxSelection={canWriteCarriers}
disableRowSelectionExcludeModel
disableRowSelectionOnClick
onRowDoubleClick={(params) => {
if (canWriteCarriers) {
navigate(`/carrier/${params.id}/edit`);
}
}}
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}

View File

@@ -172,6 +172,11 @@ export const CityListPage = observer(() => {
checkboxSelection={canWriteCities}
disableRowSelectionExcludeModel
disableRowSelectionOnClick
onRowDoubleClick={(params) => {
if (canWriteCities) {
navigate(`/city/${params.id}/edit`);
}
}}
loading={isLoading}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}

View File

@@ -15,6 +15,7 @@ import {
} from "@widgets";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
function a11yProps(index: number) {
return {
@@ -36,6 +37,16 @@ export const CreateSightPage = observer(() => {
return () => selectedCityStore.setIsLocked(false);
}, []);
useEffect(() => {
const { selectedCityId, selectedCity } = selectedCityStore;
if (selectedCityId && selectedCity && !createSightStore.sight.city_id) {
runInAction(() => {
createSightStore.sight.city_id = selectedCityId;
createSightStore.sight.city = selectedCity.name;
});
}
}, []);
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};

View File

@@ -43,7 +43,7 @@ export const RouteListPage = observer(() => {
loadCounts(routeIds);
};
fetchData();
}, [language]);
}, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [
{

View File

@@ -24,7 +24,9 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
useEffect(() => {
async function fetchCarrierData() {
if (routeData?.carrier_id) {
const carrier = (await authInstance.get(`/carrier/${routeData.carrier_id}`)).data;
const carrier = (
await authInstance.get(`/carrier/${routeData.carrier_id}`)
).data;
setCarrierLogo(carrier.logo);
setCarrierSlogan(carrier.slogan ?? null);
setCarrierShortName(carrier.short_name ?? null);
@@ -45,6 +47,7 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
<div
style={{
position: "relative",
width: 288,
height: "100%",
color: "#fff",
}}
@@ -165,7 +168,11 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
<div style={{ width: 170 }}>
<MediaViewer
media={{ id: carrierLogo, media_type: 1, filename: "carrier_logo" }}
media={{
id: carrierLogo,
media_type: 1,
filename: "carrier_logo",
}}
fullWidth
/>
</div>
@@ -190,7 +197,12 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
<img
src="/side-menu-photo.png"
alt=""
style={{ width: "100%", marginTop: 32, display: "block", pointerEvents: "none" }}
style={{
width: "288px",
marginTop: 32,
display: "block",
pointerEvents: "none",
}}
/>
</div>
</div>

View File

@@ -448,6 +448,22 @@ const StationLabel = observer(
anchor={dynamicAnchor}
zIndex={isHovered || isControlHovered ? 1000 : 0}
>
{ruLabelWidth > 0 && (
<pixiGraphics
draw={(g: Graphics) => {
g.clear();
const hasSecondLabel = !!(station.name && language !== "ru" && ruLabel);
const pad = 10 / scale;
const w = ruLabelWidth + pad * 2;
const top = -compensatedRuFontSize / 2 - pad;
const bottom = hasSecondLabel
? compensatedRuFontSize * 1.1 + compensatedNameFontSize / 2 + pad
: compensatedRuFontSize / 2 + pad;
g.rect(-w / 2, top, w, bottom - top);
g.fill({ color: 0x000000, alpha: 0.001 });
}}
/>
)}
{ruLabel && (
<pixiText
ref={ruLabelRef}

View File

@@ -54,7 +54,7 @@ export const RoutePreview = () => {
<Box
sx={{
position: "relative",
width: isLeftSidebarOpen ? 300 : 0,
width: isLeftSidebarOpen ? 288 : 0,
transition: "width 0.3s ease",
overflow: "visible",
height: "100%",
@@ -145,14 +145,14 @@ export const RouteMap = observer(() => {
) {
const coordinates = coordinatesToLocal(
originalRouteData?.center_latitude,
originalRouteData?.center_longitude
originalRouteData?.center_longitude,
);
setTransform(
coordinates.x,
coordinates.y,
originalRouteData?.rotate,
originalRouteData?.scale_min
originalRouteData?.scale_min,
);
setIsSetup(true);
}

View File

@@ -26,15 +26,17 @@
position: fixed;
display: inline-flex;
border-radius: 10px;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset,
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
padding: 1px; /* Чтобы контент не прилипал к рамке */
background: linear-gradient(
background:
linear-gradient(
to bottom right,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4);
rgba(179, 165, 152, 0.4);
backdrop-filter: blur(10px);
pointer-events: auto;
z-index: 10000001;
@@ -50,7 +52,8 @@
height: 96px;
background-color: #fcd500;
color: black;
border-radius: 10px;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
display: flex;
justify-content: center;
align-items: center;

View File

@@ -2405,12 +2405,6 @@ export const WebGLRouteMapPrototype = observer(() => {
resizingStationIconId === station.id;
const secondaryLineHeight = 1.2;
const secondaryHeight = showSecondary
? secondaryFontSize * secondaryLineHeight
: 0;
const menuPaddingTop = showSecondary
? Math.max(0, secondaryHeight - secondaryMarginTop) + 3
: 3;
return (
<div key={station.id}>
@@ -2440,22 +2434,25 @@ export const WebGLRouteMapPrototype = observer(() => {
color: "#fff",
fontFamily: "Roboto, sans-serif",
textAlign: "left",
pointerEvents: "auto",
pointerEvents: "none",
cursor: "grab",
userSelect: "none",
touchAction: "none",
lineHeight: 1,
}}
>
<div
style={{
pointerEvents: "auto",
display: "inline-block",
pointerEvents: "none",
transformOrigin: "left center",
transform: `rotate(${rotationCss})`,
}}
>
<div
style={{
pointerEvents: "auto",
display: "inline-block",
pointerEvents: "none",
transformOrigin: "left center",
transform: `rotate(${counterRotationCss})`,
}}
@@ -2549,15 +2546,16 @@ export const WebGLRouteMapPrototype = observer(() => {
) : null}
<div
style={{
position: "relative",
fontWeight: 700,
fontSize: primaryFontSize,
lineHeight: 1,
textShadow: "0 0 4px rgba(0,0,0,0.6)",
pointerEvents: "none",
whiteSpace: "nowrap",
}}
>
{station.name}
</div>
{showSecondary ? (
<div
style={{
@@ -2580,6 +2578,7 @@ export const WebGLRouteMapPrototype = observer(() => {
</div>
</div>
</div>
</div>
{hoveredStationId === station.id && (
<div
style={{
@@ -2587,9 +2586,9 @@ export const WebGLRouteMapPrototype = observer(() => {
top: "100%",
left: "50%",
transform: "translateX(-50%)",
paddingTop: menuPaddingTop,
paddingTop: "8px",
pointerEvents: "auto",
zIndex: 10,
zIndex: 1000000,
cursor: "default",
}}
onPointerDown={(e) => e.stopPropagation()}

View File

@@ -46,7 +46,7 @@ export const SightListPage = observer(() => {
setIsLoading(false);
};
fetchSights();
}, [language]);
}, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [
{

View File

@@ -49,7 +49,7 @@ export const StationListPage = observer(() => {
loadSightCounts(stationIds);
};
fetchStations();
}, [language]);
}, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [
{

View File

@@ -12,6 +12,8 @@ import {
VEHICLE_TYPES,
carrierStore,
languageStore,
cityStore,
selectedCityStore,
} from "@shared";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
@@ -26,11 +28,13 @@ export const VehicleCreatePage = observer(() => {
const [type, setType] = useState("");
const [carrierId, setCarrierId] = useState<number | null>(null);
const [model, setModel] = useState("");
const [cityId, setCityId] = useState<number | null>(selectedCityStore.selectedCityId);
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
carrierStore.getCarriers(language);
cityStore.getCities("ru");
}, [language]);
const handleCreate = async () => {
@@ -43,6 +47,7 @@ export const VehicleCreatePage = observer(() => {
?.full_name as string,
carrierId!,
model || undefined,
cityId ?? undefined,
);
toast.success("Транспорт успешно создан");
} catch (error) {
@@ -73,12 +78,11 @@ export const VehicleCreatePage = observer(() => {
onChange={(e) => setTailNumber(e.target.value)}
/>
<FormControl fullWidth>
<FormControl fullWidth required>
<InputLabel>Тип</InputLabel>
<Select
value={type}
label="Тип"
required
onChange={(e) => setType(e.target.value)}
>
{VEHICLE_TYPES.map((type) => (
@@ -89,12 +93,11 @@ export const VehicleCreatePage = observer(() => {
</Select>
</FormControl>
<FormControl fullWidth>
<FormControl fullWidth required>
<InputLabel>Перевозчик</InputLabel>
<Select
value={carrierId || ""}
label="Перевозчик"
required
onChange={(e) => setCarrierId(e.target.value as number)}
>
{carrierStore.carriers[language].data?.map((carrier) => (
@@ -113,12 +116,27 @@ export const VehicleCreatePage = observer(() => {
placeholder="Произвольное название модели"
/>
<FormControl fullWidth required>
<InputLabel>Город</InputLabel>
<Select
value={cityId ?? ""}
label="Город"
onChange={(e) => setCityId(e.target.value ? Number(e.target.value) : null)}
>
{cityStore.cities.ru.data.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={isLoading || !tailNumber || !type || !carrierId}
disabled={isLoading || !tailNumber || !type || !carrierId || !cityId}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />

View File

@@ -20,6 +20,8 @@ import {
VEHICLE_TYPES,
vehicleStore,
LoadingSpinner,
cityStore,
selectedCityStore,
} from "@shared";
import { toast } from "react-toastify";
@@ -54,6 +56,7 @@ export const VehicleEditPage = observer(() => {
try {
await getVehicle(Number(id));
await getCarriers(language);
await cityStore.getCities("ru");
setEditVehicleData({
tail_number: vehicle[Number(id)]?.vehicle.tail_number ?? "",
@@ -63,6 +66,7 @@ export const VehicleEditPage = observer(() => {
model: vehicle[Number(id)]?.vehicle.model ?? "",
snapshot_update_blocked:
vehicle[Number(id)]?.vehicle.snapshot_update_blocked ?? false,
city_id: vehicle[Number(id)]?.vehicle.city_id ?? selectedCityStore.selectedCityId ?? undefined,
});
} finally {
setIsLoadingData(false);
@@ -125,12 +129,11 @@ export const VehicleEditPage = observer(() => {
}
/>
<FormControl fullWidth>
<FormControl fullWidth required>
<InputLabel>Тип</InputLabel>
<Select
value={editVehicleData.type}
label="Тип"
required
onChange={(e) =>
setEditVehicleData({ ...editVehicleData, type: e.target.value })
}
@@ -143,12 +146,11 @@ export const VehicleEditPage = observer(() => {
</Select>
</FormControl>
<FormControl fullWidth>
<FormControl fullWidth required>
<InputLabel>Перевозчик</InputLabel>
<Select
value={editVehicleData.carrier_id}
label="Перевозчик"
required
onChange={(e) =>
setEditVehicleData({
...editVehicleData,
@@ -177,6 +179,26 @@ export const VehicleEditPage = observer(() => {
placeholder="Произвольное название модели"
/>
<FormControl fullWidth required>
<InputLabel>Город</InputLabel>
<Select
value={editVehicleData.city_id ?? ""}
label="Город"
onChange={(e) =>
setEditVehicleData({
...editVehicleData,
city_id: e.target.value ? Number(e.target.value) : undefined,
})
}
>
{cityStore.cities.ru.data.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControlLabel
control={
<Checkbox
@@ -202,7 +224,8 @@ export const VehicleEditPage = observer(() => {
isLoading ||
!editVehicleData.tail_number ||
!editVehicleData.type ||
!editVehicleData.carrier_id
!editVehicleData.carrier_id ||
!editVehicleData.city_id
}
>
{isLoading ? (

View File

@@ -1,6 +1,6 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, carrierStore, languageStore, vehicleStore, SearchInput } from "@shared";
import { authStore, carrierStore, languageStore, vehicleStore, SearchInput, selectedCityStore, cityStore } from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
@@ -11,7 +11,7 @@ import { Box, CircularProgress } from "@mui/material";
export const VehicleListPage = observer(() => {
const { vehicles, getVehicles, deleteVehicle } = vehicleStore;
const { carriers, getCarriers } = carrierStore;
const { getCarriers } = carrierStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
@@ -31,10 +31,11 @@ export const VehicleListPage = observer(() => {
setIsLoading(true);
await getVehicles();
await getCarriers(language);
await cityStore.getCities("ru");
setIsLoading(false);
};
fetchData();
}, [language]);
}, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [
{
@@ -137,9 +138,13 @@ export const VehicleListPage = observer(() => {
},
];
const { selectedCityId } = selectedCityStore;
const rows = useMemo(() => {
if (!selectedCityId) return [];
const query = searchQuery.trim().toLowerCase();
return (vehicles.data ?? [])
.filter((vehicle) => vehicle.vehicle.city_id === selectedCityId)
.filter(
(vehicle) =>
!query ||
@@ -151,11 +156,9 @@ export const VehicleListPage = observer(() => {
tail_number: vehicle.vehicle.tail_number,
type: vehicle.vehicle.type,
carrier: vehicle.vehicle.carrier,
city: carriers[language].data?.find(
(carrier) => carrier.id === vehicle.vehicle.carrier_id
)?.city,
city: cityStore.cities.ru.data.find((c) => c.id === vehicle.vehicle.city_id)?.name,
}));
}, [vehicles.data, carriers[language].data, searchQuery]);
}, [vehicles.data, selectedCityId, searchQuery]);
return (
<>
@@ -166,6 +169,7 @@ export const VehicleListPage = observer(() => {
<CreateButton
label="Создать транспортное средство"
path="/vehicle/create"
disabled={!selectedCityId}
/>
</div>
@@ -223,6 +227,8 @@ export const VehicleListPage = observer(() => {
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
{isLoading ? (
<CircularProgress size={20} />
) : !selectedCityId ? (
"Выберите город"
) : (
"Нет транспортных средств"
)}

View File

@@ -4,6 +4,7 @@ import { City } from "../CityStore";
class SelectedCityStore {
selectedCity: City | null = null;
isLocked: boolean = false;
cityVersion: number = 0;
constructor() {
makeAutoObservable(this);
@@ -25,6 +26,7 @@ class SelectedCityStore {
setSelectedCity = (city: City | null) => {
runInAction(() => {
this.selectedCity = city;
this.cityVersion += 1;
if (city) {
localStorage.setItem("selectedCity", JSON.stringify(city));
} else {

View File

@@ -120,6 +120,7 @@ class VehicleStore {
carrier: string,
carrierId: number,
model?: string,
cityId?: number,
) => {
const payload: Record<string, unknown> = {
tail_number: tailNumber.trim(),
@@ -129,6 +130,7 @@ class VehicleStore {
};
// TODO: когда будет бекенд — добавить model в payload и в ответ
if (model != null && model !== "") payload.model = model;
if (cityId != null) payload.city_id = cityId;
const response = await languageInstance("ru").post("/vehicle", payload);
const normalizedVehicle = this.normalizeVehicleItem(response.data);
@@ -147,6 +149,7 @@ class VehicleStore {
carrier_id: number;
model: string;
snapshot_update_blocked: boolean;
city_id?: number;
} = {
tail_number: "",
type: 0,
@@ -154,6 +157,7 @@ class VehicleStore {
carrier_id: 0,
model: "",
snapshot_update_blocked: false,
city_id: undefined,
};
setEditVehicleData = (data: {
@@ -163,6 +167,7 @@ class VehicleStore {
carrier_id: number;
model?: string;
snapshot_update_blocked?: boolean;
city_id?: number;
}) => {
this.editVehicleData = {
...this.editVehicleData,
@@ -179,6 +184,7 @@ class VehicleStore {
carrier_id: number;
model?: string;
snapshot_update_blocked?: boolean;
city_id?: number;
},
) => {
const payload: Record<string, unknown> = {
@@ -190,6 +196,7 @@ class VehicleStore {
if (data.model != null && data.model !== "") payload.model = data.model;
if (data.snapshot_update_blocked != null)
payload.snapshot_update_blocked = data.snapshot_update_blocked;
if (data.city_id != null) payload.city_id = data.city_id;
const response = await languageInstance("ru").patch(
`/vehicle/${id}`,
payload,

View File

@@ -8,7 +8,7 @@ import {
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { authStore, cityStore, selectedCityStore, type City } from "@shared";
import { authStore, cityStore, selectedCityStore, snapshotStore, type City } from "@shared";
import { MapPin } from "lucide-react";
export const CitySelector: React.FC = observer(() => {
@@ -46,12 +46,14 @@ export const CitySelector: React.FC = observer(() => {
const handleCityChange = (event: SelectChangeEvent<string>) => {
const cityId = event.target.value;
if (cityId === "") {
snapshotStore.clearStoreCache();
setSelectedCity(null);
return;
}
const city = currentCities.find((c) => c.id === Number(cityId));
if (city) {
snapshotStore.clearStoreCache();
setSelectedCity(city);
}
};

View File

@@ -9,7 +9,6 @@ import {
vehicleStore,
routeStore,
Vehicle,
carrierStore,
selectedCityStore,
menuStore,
VEHICLE_TYPES,
@@ -184,31 +183,14 @@ export const DevicesTable = observer(() => {
pageSize: 50,
});
const filterVehiclesBySelectedCity = (vehiclesList: Vehicle[]): Vehicle[] => {
const selectedCityId = selectedCityStore.selectedCityId;
const { selectedCityId } = selectedCityStore;
if (!selectedCityId) {
return vehiclesList;
}
const carriersInSelectedCityIds = new Set(
carrierStore.carriers.ru.data
.filter((carrier) => carrier.city_id === selectedCityId)
.map((carrier) => carrier.id),
);
if (carriersInSelectedCityIds.size === 0) {
return [];
}
return vehiclesList.filter((vehicle) =>
carriersInSelectedCityIds.has(vehicle.vehicle.carrier_id),
);
};
const filteredVehicles = filterVehiclesBySelectedCity(
vehicles.data as Vehicle[],
const filteredVehicles = useMemo((): Vehicle[] => {
if (!selectedCityId) return [];
return (vehicles.data as Vehicle[]).filter(
(vehicle) => vehicle.vehicle.city_id === selectedCityId,
);
}, [selectedCityId, vehicles.data]);
const rows = useMemo(
() => transformToRows(filteredVehicles),
@@ -748,13 +730,8 @@ export const DevicesTable = observer(() => {
setIsLoading(false);
};
fetchData();
}, [getDevices, getSnapshots, getVehicles, getRoutes, isMaintenanceOnly]);
}, [getDevices, getSnapshots, getVehicles, getRoutes, isMaintenanceOnly, selectedCityStore.cityVersion]);
useEffect(() => {
if (!isMaintenanceOnly) {
carrierStore.getCarriers("ru");
}
}, [isMaintenanceOnly]);
const handleOpenSendSnapshotModal = () => {
if (!canWriteDevices) {
@@ -888,6 +865,8 @@ export const DevicesTable = observer(() => {
>
{isLoading ? (
<CircularProgress size={20} />
) : !selectedCityId ? (
"Выберите город"
) : (
"Нет устройств для отображения"
)}
@@ -933,8 +912,12 @@ export const DevicesTable = observer(() => {
checkboxSelection={canWriteDevices}
disableRowSelectionExcludeModel
disableRowSelectionOnClick
loading={isLoading}
paginationModel={paginationModel}
onRowDoubleClick={(params) => {
if (canWriteDevices) {
navigate(`/vehicle/${params.row.vehicle_id}/edit`);
}
}}
loading={isLoading} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]}
onRowSelectionModelChange={

File diff suppressed because one or more lines are too long