feat: update map, admin to and cache

This commit is contained in:
2026-05-08 13:33:41 +03:00
parent 4bda233b63
commit 193f53c029
22 changed files with 191 additions and 107 deletions

14
.env
View File

@@ -1,8 +1,8 @@
# VITE_API_URL='https://wn.st.unprism.ru' VITE_API_URL='https://wn.st.unprism.ru'
# VITE_REACT_APP ='https://wn.st.unprism.ru/' VITE_REACT_APP ='https://wn.st.unprism.ru/'
# VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' 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' 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); const textWrapperRef = useRef(null);
// Автозакрытие fullscreen 3D при бездействии (45 сек) // Автозакрытие fullscreen 3D при бездействии (60 сек)
useEffect(() => { useEffect(() => {
if (!isFullscreen3D) { if (!isFullscreen3D) {
if (idleTimerRef.current) { if (idleTimerRef.current) {
@@ -61,7 +61,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
const checkIdle = () => { const checkIdle = () => {
idleSeconds += 1; idleSeconds += 1;
if (idleSeconds >= 45) { if (idleSeconds >= 60) {
setIsFullscreen3D(false); 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(() => { useEffect(() => {
// Автоматическое закрытие сайд-меню после 45 секунд бездействия // Автоматическое закрытие сайд-меню после 60 секунд бездействия
let idleSeconds = 0; let idleSeconds = 0;
const checkIdle = () => { const checkIdle = () => {
idleSeconds += 1; idleSeconds += 1;
if (idleSeconds >= 45 && isMenuOpen) { if (idleSeconds >= 60 && isMenuOpenRef.current) {
handleMenuToggle(false); handleMenuToggleRef.current(false);
} }
}; };
@@ -301,7 +306,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
window.removeEventListener(event, resetIdle); window.removeEventListener(event, resetIdle);
}); });
}; };
}, [isMenuOpen, handleMenuToggle]); }, []);
// Закрываем и открываем список достопримечательностей при изменении сортировки // Закрываем и открываем список достопримечательностей при изменении сортировки
const prevSortingByRef = useRef(sortingBy); const prevSortingByRef = useRef(sortingBy);

View File

@@ -52,7 +52,8 @@
height: 96px; height: 96px;
background-color: #fcd500; background-color: #fcd500;
color: black; color: black;
border-radius: 10px; border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

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

View File

@@ -1,6 +1,6 @@
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, SearchInput } from "@shared"; import { authStore, carrierStore, cityStore, languageStore, selectedCityStore, SearchInput } 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";
import { Pencil, Trash2, Minus } from "lucide-react"; import { Pencil, Trash2, Minus } from "lucide-react";
@@ -39,7 +39,7 @@ export const CarrierListPage = observer(() => {
setIsLoading(false); setIsLoading(false);
}; };
fetchData(); fetchData();
}, [language]); }, [language, selectedCityStore.cityVersion]);
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
@@ -169,6 +169,11 @@ export const CarrierListPage = observer(() => {
checkboxSelection={canWriteCarriers} checkboxSelection={canWriteCarriers}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
disableRowSelectionOnClick disableRowSelectionOnClick
onRowDoubleClick={(params) => {
if (canWriteCarriers) {
navigate(`/carrier/${params.id}/edit`);
}
}}
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}

View File

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

View File

@@ -15,6 +15,7 @@ import {
} from "@widgets"; } from "@widgets";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
@@ -36,6 +37,16 @@ export const CreateSightPage = observer(() => {
return () => selectedCityStore.setIsLocked(false); 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) => { const handleChange = (_: React.SyntheticEvent, newValue: number) => {
setValue(newValue); setValue(newValue);
}; };

View File

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

View File

@@ -448,6 +448,22 @@ const StationLabel = observer(
anchor={dynamicAnchor} anchor={dynamicAnchor}
zIndex={isHovered || isControlHovered ? 1000 : 0} 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 && ( {ruLabel && (
<pixiText <pixiText
ref={ruLabelRef} ref={ruLabelRef}

View File

@@ -52,7 +52,8 @@
height: 96px; height: 96px;
background-color: #fcd500; background-color: #fcd500;
color: black; color: black;
border-radius: 10px; border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long