Compare commits
2 Commits
d758dbffa6
...
193f53c029
| Author | SHA1 | Date | |
|---|---|---|---|
| 193f53c029 | |||
| 4bda233b63 |
14
.env
14
.env
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ArticleListPage = observer(() => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchArticles();
|
||||
}, [language]);
|
||||
}, [language, selectedCityStore.cityVersion]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export const RouteListPage = observer(() => {
|
||||
loadCounts(routeIds);
|
||||
};
|
||||
fetchData();
|
||||
}, [language]);
|
||||
}, [language, selectedCityStore.cityVersion]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,34 +2546,36 @@ 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}
|
||||
{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>
|
||||
{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>
|
||||
@@ -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()}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const SightListPage = observer(() => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchSights();
|
||||
}, [language]);
|
||||
}, [language, selectedCityStore.cityVersion]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ export const StationListPage = observer(() => {
|
||||
loadSightCounts(stationIds);
|
||||
};
|
||||
fetchStations();
|
||||
}, [language]);
|
||||
}, [language, selectedCityStore.cityVersion]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
"Выберите город"
|
||||
) : (
|
||||
"Нет транспортных средств"
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
const filteredVehicles = useMemo((): Vehicle[] => {
|
||||
if (!selectedCityId) return [];
|
||||
return (vehicles.data as Vehicle[]).filter(
|
||||
(vehicle) => vehicle.vehicle.city_id === selectedCityId,
|
||||
);
|
||||
|
||||
if (carriersInSelectedCityIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return vehiclesList.filter((vehicle) =>
|
||||
carriersInSelectedCityIds.has(vehicle.vehicle.carrier_id),
|
||||
);
|
||||
};
|
||||
|
||||
const filteredVehicles = filterVehiclesBySelectedCity(
|
||||
vehicles.data as Vehicle[],
|
||||
);
|
||||
}, [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 ? (
|
||||
"Выберите город"
|
||||
) : (
|
||||
"Нет устройств для отображения"
|
||||
)}
|
||||
@@ -928,13 +907,17 @@ export const DevicesTable = observer(() => {
|
||||
{!isCollapsed && (
|
||||
<Box sx={{ p: 0 }}>
|
||||
<DataGrid
|
||||
rows={groupRows}
|
||||
columns={visibleColumns}
|
||||
checkboxSelection={canWriteDevices}
|
||||
disableRowSelectionExcludeModel
|
||||
disableRowSelectionOnClick
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
rows={groupRows}
|
||||
columns={visibleColumns}
|
||||
checkboxSelection={canWriteDevices}
|
||||
disableRowSelectionExcludeModel
|
||||
disableRowSelectionOnClick
|
||||
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
Reference in New Issue
Block a user