feat: big major update
This commit is contained in:
@@ -8,8 +8,8 @@ import {
|
|||||||
InputLabel,
|
InputLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import {
|
import {
|
||||||
@@ -17,15 +17,14 @@ import {
|
|||||||
cityStore,
|
cityStore,
|
||||||
mediaStore,
|
mediaStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
useSelectedCity,
|
useSelectedCity,
|
||||||
} from "@shared";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
|
||||||
import {
|
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const CarrierCreatePage = observer(() => {
|
export const CarrierCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -56,7 +55,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
selectedCityId,
|
selectedCityId,
|
||||||
createCarrierData[language].slogan,
|
createCarrierData[language].slogan,
|
||||||
selectedMediaId || "",
|
selectedMediaId || "",
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [selectedCityId, createCarrierData.city_id]);
|
}, [selectedCityId, createCarrierData.city_id]);
|
||||||
@@ -88,13 +87,17 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
createCarrierData.city_id,
|
createCarrierData.city_id,
|
||||||
createCarrierData[language].slogan,
|
createCarrierData[language].slogan,
|
||||||
media.id,
|
media.id,
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedMedia = selectedMediaId
|
const selectedMedia =
|
||||||
|
selectedMediaId && !isMediaIdEmpty(selectedMediaId)
|
||||||
? mediaStore.media.find((m) => m.id === selectedMediaId)
|
? mediaStore.media.find((m) => m.id === selectedMediaId)
|
||||||
: null;
|
: null;
|
||||||
|
const effectiveLogoUrl = isMediaIdEmpty(selectedMediaId)
|
||||||
|
? null
|
||||||
|
: selectedMedia?.id ?? selectedMediaId ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
@@ -127,7 +130,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
e.target.value as number,
|
e.target.value as number,
|
||||||
createCarrierData[language].slogan,
|
createCarrierData[language].slogan,
|
||||||
selectedMediaId || "",
|
selectedMediaId || "",
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -151,7 +154,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
createCarrierData.city_id,
|
createCarrierData.city_id,
|
||||||
createCarrierData[language].slogan,
|
createCarrierData[language].slogan,
|
||||||
selectedMediaId || "",
|
selectedMediaId || "",
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +171,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
createCarrierData.city_id,
|
createCarrierData.city_id,
|
||||||
createCarrierData[language].slogan,
|
createCarrierData[language].slogan,
|
||||||
selectedMediaId || "",
|
selectedMediaId || "",
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -184,7 +187,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
createCarrierData.city_id,
|
createCarrierData.city_id,
|
||||||
e.target.value,
|
e.target.value,
|
||||||
selectedMediaId || "",
|
selectedMediaId || "",
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -193,10 +196,10 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Логотип перевозчика"
|
title="Логотип перевозчика"
|
||||||
imageKey="thumbnail"
|
imageKey="thumbnail"
|
||||||
imageUrl={selectedMedia?.id}
|
imageUrl={effectiveLogoUrl}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
setIsPreviewMediaOpen(true);
|
setIsPreviewMediaOpen(true);
|
||||||
setMediaId(selectedMedia?.id ?? "");
|
setMediaId(effectiveLogoUrl ?? "");
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
onDeleteImageClick={() => {
|
||||||
setSelectedMediaId(null);
|
setSelectedMediaId(null);
|
||||||
@@ -207,7 +210,7 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
createCarrierData.city_id,
|
createCarrierData.city_id,
|
||||||
createCarrierData[language].slogan,
|
createCarrierData[language].slogan,
|
||||||
"",
|
"",
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onSelectFileClick={() => {
|
onSelectFileClick={() => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
cityStore,
|
cityStore,
|
||||||
mediaStore,
|
mediaStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
@@ -123,9 +124,13 @@ export const CarrierEditPage = observer(() => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedMedia = editCarrierData.logo
|
const selectedMedia =
|
||||||
|
editCarrierData.logo && !isMediaIdEmpty(editCarrierData.logo)
|
||||||
? mediaStore.media.find((m) => m.id === editCarrierData.logo)
|
? mediaStore.media.find((m) => m.id === editCarrierData.logo)
|
||||||
: null;
|
: null;
|
||||||
|
const effectiveLogoUrl = isMediaIdEmpty(editCarrierData.logo)
|
||||||
|
? null
|
||||||
|
: (selectedMedia?.id ?? editCarrierData.logo);
|
||||||
|
|
||||||
if (isLoadingData) {
|
if (isLoadingData) {
|
||||||
return (
|
return (
|
||||||
@@ -238,10 +243,10 @@ export const CarrierEditPage = observer(() => {
|
|||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Логотип перевозчика"
|
title="Логотип перевозчика"
|
||||||
imageKey="thumbnail"
|
imageKey="thumbnail"
|
||||||
imageUrl={selectedMedia?.id}
|
imageUrl={effectiveLogoUrl}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
setIsPreviewMediaOpen(true);
|
setIsPreviewMediaOpen(true);
|
||||||
setMediaId(selectedMedia?.id ?? "");
|
setMediaId(effectiveLogoUrl ?? "");
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
onDeleteImageClick={() => {
|
||||||
setIsDeleteLogoModalOpen(true);
|
setIsDeleteLogoModalOpen(true);
|
||||||
|
|||||||
@@ -12,14 +12,18 @@ import { ArrowLeft, Save } from "lucide-react";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { cityStore, countryStore, languageStore, mediaStore } from "@shared";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
|
||||||
import {
|
import {
|
||||||
|
cityStore,
|
||||||
|
countryStore,
|
||||||
|
languageStore,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||||
|
|
||||||
export const CityCreatePage = observer(() => {
|
export const CityCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -72,9 +76,13 @@ export const CityCreatePage = observer(() => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedMedia = createCityData.arms
|
const selectedMedia =
|
||||||
|
createCityData.arms && !isMediaIdEmpty(createCityData.arms)
|
||||||
? mediaStore.media.find((m) => m.id === createCityData.arms)
|
? mediaStore.media.find((m) => m.id === createCityData.arms)
|
||||||
: null;
|
: null;
|
||||||
|
const effectiveArmsUrl = isMediaIdEmpty(createCityData.arms)
|
||||||
|
? null
|
||||||
|
: (selectedMedia?.id ?? createCityData.arms);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
@@ -135,10 +143,10 @@ export const CityCreatePage = observer(() => {
|
|||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Герб города"
|
title="Герб города"
|
||||||
imageKey="image"
|
imageKey="image"
|
||||||
imageUrl={selectedMedia?.id}
|
imageUrl={effectiveArmsUrl}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
setIsPreviewMediaOpen(true);
|
setIsPreviewMediaOpen(true);
|
||||||
setMediaId(selectedMedia?.id ?? "");
|
setMediaId(effectiveArmsUrl ?? "");
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
onDeleteImageClick={() => {
|
||||||
setCreateCityData(
|
setCreateCityData(
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import {
|
import {
|
||||||
@@ -18,16 +17,15 @@ import {
|
|||||||
countryStore,
|
countryStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
mediaStore,
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
CashedCities,
|
CashedCities,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
} from "@shared";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
|
||||||
import {
|
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||||
|
|
||||||
export const CityEditPage = observer(() => {
|
export const CityEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -99,13 +97,17 @@ export const CityEditPage = observer(() => {
|
|||||||
editCityData[language].name,
|
editCityData[language].name,
|
||||||
editCityData.country_code,
|
editCityData.country_code,
|
||||||
media.id,
|
media.id,
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedMedia = editCityData.arms
|
const selectedMedia =
|
||||||
|
editCityData.arms && !isMediaIdEmpty(editCityData.arms)
|
||||||
? mediaStore.media.find((m) => m.id === editCityData.arms)
|
? mediaStore.media.find((m) => m.id === editCityData.arms)
|
||||||
: null;
|
: null;
|
||||||
|
const effectiveArmsUrl = isMediaIdEmpty(editCityData.arms)
|
||||||
|
? null
|
||||||
|
: selectedMedia?.id ?? editCityData.arms;
|
||||||
|
|
||||||
if (isLoadingData) {
|
if (isLoadingData) {
|
||||||
return (
|
return (
|
||||||
@@ -149,7 +151,7 @@ export const CityEditPage = observer(() => {
|
|||||||
e.target.value,
|
e.target.value,
|
||||||
editCityData.country_code,
|
editCityData.country_code,
|
||||||
editCityData.arms,
|
editCityData.arms,
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -165,7 +167,7 @@ export const CityEditPage = observer(() => {
|
|||||||
editCityData[language].name,
|
editCityData[language].name,
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCityData.arms,
|
editCityData.arms,
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -181,17 +183,17 @@ export const CityEditPage = observer(() => {
|
|||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Герб города"
|
title="Герб города"
|
||||||
imageKey="image"
|
imageKey="image"
|
||||||
imageUrl={selectedMedia?.id}
|
imageUrl={effectiveArmsUrl}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
setIsPreviewMediaOpen(true);
|
setIsPreviewMediaOpen(true);
|
||||||
setMediaId(selectedMedia?.id ?? "");
|
setMediaId(effectiveArmsUrl ?? "");
|
||||||
}}
|
}}
|
||||||
onDeleteImageClick={() => {
|
onDeleteImageClick={() => {
|
||||||
setEditCityData(
|
setEditCityData(
|
||||||
editCityData[language].name,
|
editCityData[language].name,
|
||||||
editCityData.country_code,
|
editCityData.country_code,
|
||||||
"",
|
"",
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
setActiveMenuType(null);
|
setActiveMenuType(null);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const saveHiddenRoutes = (hiddenRoutes: Set<number>): void => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
HIDDEN_ROUTES_KEY,
|
HIDDEN_ROUTES_KEY,
|
||||||
JSON.stringify(Array.from(hiddenRoutes))
|
JSON.stringify(Array.from(hiddenRoutes)),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to save hidden routes:", error);
|
console.warn("Failed to save hidden routes:", error);
|
||||||
@@ -221,7 +221,7 @@ class MapStore {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY,
|
HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY,
|
||||||
JSON.stringify(!!val)
|
JSON.stringify(!!val),
|
||||||
);
|
);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ class MapStore {
|
|||||||
|
|
||||||
private sortFeatures<T extends ApiStation | ApiSight>(
|
private sortFeatures<T extends ApiStation | ApiSight>(
|
||||||
features: T[],
|
features: T[],
|
||||||
sortType: SortType
|
sortType: SortType,
|
||||||
): T[] {
|
): T[] {
|
||||||
const sorted = [...features];
|
const sorted = [...features];
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
@@ -324,7 +324,7 @@ class MapStore {
|
|||||||
return this.sortedStations;
|
return this.sortedStations;
|
||||||
}
|
}
|
||||||
return this.sortedStations.filter(
|
return this.sortedStations.filter(
|
||||||
(station) => station.city_id === selectedCityId
|
(station) => station.city_id === selectedCityId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ class MapStore {
|
|||||||
const response = await languageInstance("ru").get("/route");
|
const response = await languageInstance("ru").get("/route");
|
||||||
const routesIds = response.data.map((route: any) => route.id);
|
const routesIds = response.data.map((route: any) => route.id);
|
||||||
const routePromises = routesIds.map((id: number) =>
|
const routePromises = routesIds.map((id: number) =>
|
||||||
languageInstance("ru").get(`/route/${id}`)
|
languageInstance("ru").get(`/route/${id}`),
|
||||||
);
|
);
|
||||||
const routeResponses = await Promise.all(routePromises);
|
const routeResponses = await Promise.all(routePromises);
|
||||||
this.routes = routeResponses.map((res) => ({
|
this.routes = routeResponses.map((res) => ({
|
||||||
@@ -379,7 +379,7 @@ class MapStore {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this.routes = this.routes.sort((a, b) =>
|
this.routes = this.routes.sort((a, b) =>
|
||||||
a.route_number.localeCompare(b.route_number)
|
a.route_number.localeCompare(b.route_number),
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.preloadRouteStations(routesIds);
|
await this.preloadRouteStations(routesIds);
|
||||||
@@ -391,14 +391,14 @@ class MapStore {
|
|||||||
const stationPromises = routesIds.map(async (routeId) => {
|
const stationPromises = routesIds.map(async (routeId) => {
|
||||||
try {
|
try {
|
||||||
const stationsResponse = await languageInstance("ru").get(
|
const stationsResponse = await languageInstance("ru").get(
|
||||||
`/route/${routeId}/station`
|
`/route/${routeId}/station`,
|
||||||
);
|
);
|
||||||
const stationIds = stationsResponse.data.map((s: any) => s.id);
|
const stationIds = stationsResponse.data.map((s: any) => s.id);
|
||||||
this.routeStationsCache.set(routeId, stationIds);
|
this.routeStationsCache.set(routeId, stationIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to preload stations for route ${routeId}:`,
|
`Failed to preload stations for route ${routeId}:`,
|
||||||
error
|
error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -409,7 +409,7 @@ class MapStore {
|
|||||||
const sightPromises = routesIds.map(async (routeId) => {
|
const sightPromises = routesIds.map(async (routeId) => {
|
||||||
try {
|
try {
|
||||||
const sightsResponse = await languageInstance("ru").get(
|
const sightsResponse = await languageInstance("ru").get(
|
||||||
`/route/${routeId}/sight`
|
`/route/${routeId}/sight`,
|
||||||
);
|
);
|
||||||
const sightIds = sightsResponse.data.map((s: any) => s.id);
|
const sightIds = sightsResponse.data.map((s: any) => s.id);
|
||||||
this.routeSightsCache.set(routeId, sightIds);
|
this.routeSightsCache.set(routeId, sightIds);
|
||||||
@@ -493,7 +493,7 @@ class MapStore {
|
|||||||
|
|
||||||
if (selectedCityStore.selectedCityId) {
|
if (selectedCityStore.selectedCityId) {
|
||||||
const carriersInCity = carrierStore.carriers.ru.data.filter(
|
const carriersInCity = carrierStore.carriers.ru.data.filter(
|
||||||
(c: any) => c.city_id === selectedCityStore.selectedCityId
|
(c: any) => c.city_id === selectedCityStore.selectedCityId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (carriersInCity.length > 0) {
|
if (carriersInCity.length > 0) {
|
||||||
@@ -521,7 +521,7 @@ class MapStore {
|
|||||||
|
|
||||||
if (!carrier_id && selectedCityStore.selectedCityId) {
|
if (!carrier_id && selectedCityStore.selectedCityId) {
|
||||||
toast.error(
|
toast.error(
|
||||||
"В выбранном городе нет доступных перевозчиков, маршрут отображается в общем списке"
|
"В выбранном городе нет доступных перевозчиков, маршрут отображается в общем списке",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
createdItem = routeStore.routes.data[routeStore.routes.data.length - 1];
|
createdItem = routeStore.routes.data[routeStore.routes.data.length - 1];
|
||||||
@@ -573,7 +573,7 @@ class MapStore {
|
|||||||
const centerCoords = getCenter(lineGeom.getExtent());
|
const centerCoords = getCenter(lineGeom.getExtent());
|
||||||
const [center_longitude, center_latitude] = toLonLat(
|
const [center_longitude, center_latitude] = toLonLat(
|
||||||
centerCoords,
|
centerCoords,
|
||||||
"EPSG:3857"
|
"EPSG:3857",
|
||||||
);
|
);
|
||||||
data = {
|
data = {
|
||||||
route_number: properties.name,
|
route_number: properties.name,
|
||||||
@@ -606,7 +606,7 @@ class MapStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not find old data for ${featureType} with id ${numericId}`
|
`Could not find old data for ${featureType} with id ${numericId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@ class MapStore {
|
|||||||
|
|
||||||
const response = await languageInstance("ru").patch(
|
const response = await languageInstance("ru").patch(
|
||||||
`/${featureType}/${numericId}`,
|
`/${featureType}/${numericId}`,
|
||||||
requestBody
|
requestBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateStore = (store: any[], updatedItem: any) => {
|
const updateStore = (store: any[], updatedItem: any) => {
|
||||||
@@ -745,7 +745,7 @@ class MapService {
|
|||||||
private selectInteraction: Select;
|
private selectInteraction: Select;
|
||||||
private hoveredFeatureId: string | number | null;
|
private hoveredFeatureId: string | number | null;
|
||||||
private boundHandlePointerMove: (
|
private boundHandlePointerMove: (
|
||||||
event: MapBrowserEvent<PointerEvent>
|
event: MapBrowserEvent<PointerEvent>,
|
||||||
) => void;
|
) => void;
|
||||||
private boundHandlePointerLeave: () => void;
|
private boundHandlePointerLeave: () => void;
|
||||||
private boundHandleContextMenu: (event: MouseEvent) => void;
|
private boundHandleContextMenu: (event: MouseEvent) => void;
|
||||||
@@ -784,7 +784,7 @@ class MapService {
|
|||||||
onFeaturesChange: (features: Feature<Geometry>[]) => void,
|
onFeaturesChange: (features: Feature<Geometry>[]) => void,
|
||||||
onFeatureSelect: (feature: Feature<Geometry> | null) => void,
|
onFeatureSelect: (feature: Feature<Geometry> | null) => void,
|
||||||
tooltipElement: HTMLElement,
|
tooltipElement: HTMLElement,
|
||||||
onSelectionChange?: (ids: Set<string | number>) => void
|
onSelectionChange?: (ids: Set<string | number>) => void,
|
||||||
) {
|
) {
|
||||||
this.map = null;
|
this.map = null;
|
||||||
this.tooltipElement = tooltipElement;
|
this.tooltipElement = tooltipElement;
|
||||||
@@ -933,7 +933,7 @@ class MapService {
|
|||||||
style: (featureLike: FeatureLike) => {
|
style: (featureLike: FeatureLike) => {
|
||||||
const clusterFeature = featureLike as Feature<Point>;
|
const clusterFeature = featureLike as Feature<Point>;
|
||||||
const featuresInCluster = clusterFeature.get(
|
const featuresInCluster = clusterFeature.get(
|
||||||
"features"
|
"features",
|
||||||
) as Feature<Point>[];
|
) as Feature<Point>[];
|
||||||
const size = featuresInCluster.length;
|
const size = featuresInCluster.length;
|
||||||
|
|
||||||
@@ -991,18 +991,18 @@ class MapService {
|
|||||||
|
|
||||||
this.pointSource.on(
|
this.pointSource.on(
|
||||||
"addfeature",
|
"addfeature",
|
||||||
this.handleFeatureEvent.bind(this) as any
|
this.handleFeatureEvent.bind(this) as any,
|
||||||
);
|
);
|
||||||
this.pointSource.on("removefeature", () => this.updateFeaturesInReact());
|
this.pointSource.on("removefeature", () => this.updateFeaturesInReact());
|
||||||
this.pointSource.on(
|
this.pointSource.on(
|
||||||
"changefeature",
|
"changefeature",
|
||||||
this.handleFeatureChange.bind(this) as any
|
this.handleFeatureChange.bind(this) as any,
|
||||||
);
|
);
|
||||||
this.lineSource.on("addfeature", this.handleFeatureEvent.bind(this) as any);
|
this.lineSource.on("addfeature", this.handleFeatureEvent.bind(this) as any);
|
||||||
this.lineSource.on("removefeature", () => this.updateFeaturesInReact());
|
this.lineSource.on("removefeature", () => this.updateFeaturesInReact());
|
||||||
this.lineSource.on(
|
this.lineSource.on(
|
||||||
"changefeature",
|
"changefeature",
|
||||||
this.handleFeatureChange.bind(this) as any
|
this.handleFeatureChange.bind(this) as any,
|
||||||
);
|
);
|
||||||
|
|
||||||
let renderCompleteHandled = false;
|
let renderCompleteHandled = false;
|
||||||
@@ -1056,7 +1056,7 @@ class MapService {
|
|||||||
if (center && zoom !== undefined && this.map) {
|
if (center && zoom !== undefined && this.map) {
|
||||||
const [lon, lat] = toLonLat(
|
const [lon, lat] = toLonLat(
|
||||||
center,
|
center,
|
||||||
this.map.getView().getProjection()
|
this.map.getView().getProjection(),
|
||||||
);
|
);
|
||||||
saveMapPosition({ center: [lon, lat], zoom });
|
saveMapPosition({ center: [lon, lat], zoom });
|
||||||
}
|
}
|
||||||
@@ -1068,7 +1068,7 @@ class MapService {
|
|||||||
if (center && zoom !== undefined && this.map) {
|
if (center && zoom !== undefined && this.map) {
|
||||||
const [lon, lat] = toLonLat(
|
const [lon, lat] = toLonLat(
|
||||||
center,
|
center,
|
||||||
this.map.getView().getProjection()
|
this.map.getView().getProjection(),
|
||||||
);
|
);
|
||||||
saveMapPosition({ center: [lon, lat], zoom });
|
saveMapPosition({ center: [lon, lat], zoom });
|
||||||
}
|
}
|
||||||
@@ -1189,7 +1189,7 @@ class MapService {
|
|||||||
const feature = this.map?.forEachFeatureAtPixel(
|
const feature = this.map?.forEachFeatureAtPixel(
|
||||||
event.pixel,
|
event.pixel,
|
||||||
(f: FeatureLike) => f as Feature<Geometry>,
|
(f: FeatureLike) => f as Feature<Geometry>,
|
||||||
{ layerFilter, hitTolerance: 5 }
|
{ layerFilter, hitTolerance: 5 },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!feature) return;
|
if (!feature) return;
|
||||||
@@ -1227,7 +1227,7 @@ class MapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newCoordinates = coordinates.filter(
|
const newCoordinates = coordinates.filter(
|
||||||
(_, index) => index !== closestIndex
|
(_, index) => index !== closestIndex,
|
||||||
);
|
);
|
||||||
lineString.setCoordinates(newCoordinates);
|
lineString.setCoordinates(newCoordinates);
|
||||||
this.saveModifiedFeature(feature);
|
this.saveModifiedFeature(feature);
|
||||||
@@ -1270,7 +1270,7 @@ class MapService {
|
|||||||
selected.add(f.getId()!);
|
selected.add(f.getId()!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setSelectedIds(selected);
|
this.setSelectedIds(selected);
|
||||||
@@ -1417,7 +1417,7 @@ class MapService {
|
|||||||
public loadFeaturesFromApi(
|
public loadFeaturesFromApi(
|
||||||
_apiStations: typeof mapStore.stations,
|
_apiStations: typeof mapStore.stations,
|
||||||
_apiRoutes: typeof mapStore.routes,
|
_apiRoutes: typeof mapStore.routes,
|
||||||
_apiSights: typeof mapStore.sights
|
_apiSights: typeof mapStore.sights,
|
||||||
): void {
|
): void {
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
||||||
@@ -1450,8 +1450,8 @@ class MapService {
|
|||||||
transform(
|
transform(
|
||||||
[station.longitude, station.latitude],
|
[station.longitude, station.latitude],
|
||||||
"EPSG:4326",
|
"EPSG:4326",
|
||||||
projection
|
projection,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const feature = new Feature({ geometry: point, name: station.name });
|
const feature = new Feature({ geometry: point, name: station.name });
|
||||||
feature.setId(`station-${station.id}`);
|
feature.setId(`station-${station.id}`);
|
||||||
@@ -1462,7 +1462,7 @@ class MapService {
|
|||||||
filteredSights.forEach((sight) => {
|
filteredSights.forEach((sight) => {
|
||||||
if (sight.longitude == null || sight.latitude == null) return;
|
if (sight.longitude == null || sight.latitude == null) return;
|
||||||
const point = new Point(
|
const point = new Point(
|
||||||
transform([sight.longitude, sight.latitude], "EPSG:4326", projection)
|
transform([sight.longitude, sight.latitude], "EPSG:4326", projection),
|
||||||
);
|
);
|
||||||
const feature = new Feature({
|
const feature = new Feature({
|
||||||
geometry: point,
|
geometry: point,
|
||||||
@@ -1482,7 +1482,7 @@ class MapService {
|
|||||||
const coordinates = route.path
|
const coordinates = route.path
|
||||||
.filter((c) => c && c[0] != null && c[1] != null)
|
.filter((c) => c && c[0] != null && c[1] != null)
|
||||||
.map((c: [number, number]) =>
|
.map((c: [number, number]) =>
|
||||||
transform([c[1], c[0]], "EPSG:4326", projection)
|
transform([c[1], c[0]], "EPSG:4326", projection),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (coordinates.length === 0) return;
|
if (coordinates.length === 0) return;
|
||||||
@@ -1568,7 +1568,7 @@ class MapService {
|
|||||||
|
|
||||||
public startDrawing(
|
public startDrawing(
|
||||||
type: "Point" | "LineString",
|
type: "Point" | "LineString",
|
||||||
featureType: FeatureType
|
featureType: FeatureType,
|
||||||
): void {
|
): void {
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
||||||
@@ -1732,7 +1732,7 @@ class MapService {
|
|||||||
this.map.forEachFeatureAtPixel(
|
this.map.forEachFeatureAtPixel(
|
||||||
event.pixel,
|
event.pixel,
|
||||||
(f: FeatureLike) => f as Feature<Geometry>,
|
(f: FeatureLike) => f as Feature<Geometry>,
|
||||||
{ layerFilter, hitTolerance: 5 }
|
{ layerFilter, hitTolerance: 5 },
|
||||||
);
|
);
|
||||||
|
|
||||||
let finalFeature: Feature<Geometry> | null = null;
|
let finalFeature: Feature<Geometry> | null = null;
|
||||||
@@ -1807,7 +1807,7 @@ class MapService {
|
|||||||
|
|
||||||
public deleteFeature(
|
public deleteFeature(
|
||||||
featureId: string | number | undefined,
|
featureId: string | number | undefined,
|
||||||
recourse: string
|
recourse: string,
|
||||||
): void {
|
): void {
|
||||||
if (featureId === undefined) return;
|
if (featureId === undefined) return;
|
||||||
|
|
||||||
@@ -1863,7 +1863,7 @@ class MapService {
|
|||||||
const lineFeature = this.lineSource.getFeatureById(id);
|
const lineFeature = this.lineSource.getFeatureById(id);
|
||||||
if (lineFeature)
|
if (lineFeature)
|
||||||
this.lineSource.removeFeature(
|
this.lineSource.removeFeature(
|
||||||
lineFeature as Feature<LineString>
|
lineFeature as Feature<LineString>,
|
||||||
);
|
);
|
||||||
const pointFeature = this.pointSource.getFeatureById(id);
|
const pointFeature = this.pointSource.getFeatureById(id);
|
||||||
if (pointFeature)
|
if (pointFeature)
|
||||||
@@ -1890,11 +1890,11 @@ class MapService {
|
|||||||
if (targetEl instanceof HTMLElement) {
|
if (targetEl instanceof HTMLElement) {
|
||||||
targetEl.removeEventListener(
|
targetEl.removeEventListener(
|
||||||
"contextmenu",
|
"contextmenu",
|
||||||
this.boundHandleContextMenu
|
this.boundHandleContextMenu,
|
||||||
);
|
);
|
||||||
targetEl.removeEventListener(
|
targetEl.removeEventListener(
|
||||||
"pointerleave",
|
"pointerleave",
|
||||||
this.boundHandlePointerLeave
|
this.boundHandlePointerLeave,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.map.un("pointermove", this.boundHandlePointerMove as any);
|
this.map.un("pointermove", this.boundHandlePointerMove as any);
|
||||||
@@ -1907,7 +1907,7 @@ class MapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleFeatureEvent(
|
private handleFeatureEvent(
|
||||||
event: VectorSourceEvent<Feature<Geometry>>
|
event: VectorSourceEvent<Feature<Geometry>>,
|
||||||
): void {
|
): void {
|
||||||
if (!event.feature) return;
|
if (!event.feature) return;
|
||||||
const feature = event.feature;
|
const feature = event.feature;
|
||||||
@@ -1918,7 +1918,7 @@ class MapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleFeatureChange(
|
private handleFeatureChange(
|
||||||
event: VectorSourceEvent<Feature<Geometry>>
|
event: VectorSourceEvent<Feature<Geometry>>,
|
||||||
): void {
|
): void {
|
||||||
if (!event.feature) return;
|
if (!event.feature) return;
|
||||||
this.updateFeaturesInReact();
|
this.updateFeaturesInReact();
|
||||||
@@ -1956,7 +1956,7 @@ class MapService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.modifyInteraction.setActive(
|
this.modifyInteraction.setActive(
|
||||||
this.selectInteraction.getFeatures().getLength() > 0
|
this.selectInteraction.getFeatures().getLength() > 0,
|
||||||
);
|
);
|
||||||
this.clusterLayer.changed();
|
this.clusterLayer.changed();
|
||||||
this.routeLayer.changed();
|
this.routeLayer.changed();
|
||||||
@@ -2026,7 +2026,7 @@ class MapService {
|
|||||||
if (typeof featureId === "number" || !String(featureId).includes("-")) {
|
if (typeof featureId === "number" || !String(featureId).includes("-")) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Skipping save for feature with non-standard ID:",
|
"Skipping save for feature with non-standard ID:",
|
||||||
featureId
|
featureId,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2074,7 +2074,7 @@ class MapService {
|
|||||||
try {
|
try {
|
||||||
const createdFeatureData = await mapStore.createFeature(
|
const createdFeatureData = await mapStore.createFeature(
|
||||||
featureType,
|
featureType,
|
||||||
featureGeoJSON
|
featureGeoJSON,
|
||||||
);
|
);
|
||||||
|
|
||||||
const newFeatureId = `${featureType}-${createdFeatureData.id}`;
|
const newFeatureId = `${featureType}-${createdFeatureData.id}`;
|
||||||
@@ -2093,8 +2093,8 @@ class MapService {
|
|||||||
|
|
||||||
const lineGeom = new LineString(
|
const lineGeom = new LineString(
|
||||||
routeData.path.map((c) =>
|
routeData.path.map((c) =>
|
||||||
transform([c[1], c[0]], "EPSG:4326", projection)
|
transform([c[1], c[0]], "EPSG:4326", projection),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
feature.setGeometry(lineGeom);
|
feature.setGeometry(lineGeom);
|
||||||
} else {
|
} else {
|
||||||
@@ -2247,7 +2247,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
|
|
||||||
const actualFeatures = useMemo(
|
const actualFeatures = useMemo(
|
||||||
() => mapFeatures.filter((f) => !f.get("isProxy")),
|
() => mapFeatures.filter((f) => !f.get("isProxy")),
|
||||||
[mapFeatures]
|
[mapFeatures],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allFeatures = useMemo(() => {
|
const allFeatures = useMemo(() => {
|
||||||
@@ -2257,8 +2257,8 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
transform(
|
transform(
|
||||||
[station.longitude, station.latitude],
|
[station.longitude, station.latitude],
|
||||||
"EPSG:4326",
|
"EPSG:4326",
|
||||||
"EPSG:3857"
|
"EPSG:3857",
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
name: station.name,
|
name: station.name,
|
||||||
description: station.description || "",
|
description: station.description || "",
|
||||||
@@ -2275,8 +2275,8 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
transform(
|
transform(
|
||||||
[sight.longitude, sight.latitude],
|
[sight.longitude, sight.latitude],
|
||||||
"EPSG:4326",
|
"EPSG:4326",
|
||||||
"EPSG:3857"
|
"EPSG:3857",
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
name: sight.name,
|
name: sight.name,
|
||||||
description: sight.description,
|
description: sight.description,
|
||||||
@@ -2320,7 +2320,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
(f.get("routeNumber") as string) || "",
|
(f.get("routeNumber") as string) || "",
|
||||||
];
|
];
|
||||||
return candidates.some((value) =>
|
return candidates.some((value) =>
|
||||||
value.toLowerCase().includes(normalizedQuery)
|
value.toLowerCase().includes(normalizedQuery),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [allFeatures, searchQuery]);
|
}, [allFeatures, searchQuery]);
|
||||||
@@ -2343,7 +2343,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
mapService.selectFeature(id);
|
mapService.selectFeature(id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mapService, selectedIds, setSelectedIds]
|
[mapService, selectedIds, setSelectedIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteFeature = useCallback(
|
const handleDeleteFeature = useCallback(
|
||||||
@@ -2353,7 +2353,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
mapService.deleteFeature(id, resource);
|
mapService.deleteFeature(id, resource);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mapService]
|
[mapService],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCheckboxChange = useCallback(
|
const handleCheckboxChange = useCallback(
|
||||||
@@ -2365,14 +2365,14 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
setSelectedIds(newSet);
|
setSelectedIds(newSet);
|
||||||
mapService.setSelectedIds(newSet);
|
mapService.setSelectedIds(newSet);
|
||||||
},
|
},
|
||||||
[mapService, selectedIds, setSelectedIds]
|
[mapService, selectedIds, setSelectedIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBulkDelete = useCallback(() => {
|
const handleBulkDelete = useCallback(() => {
|
||||||
if (!mapService || selectedIds.size === 0) return;
|
if (!mapService || selectedIds.size === 0) return;
|
||||||
if (
|
if (
|
||||||
window.confirm(
|
window.confirm(
|
||||||
`Вы уверены, что хотите удалить ${selectedIds.size} объект(ов)?`
|
`Вы уверены, что хотите удалить ${selectedIds.size} объект(ов)?`,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
mapService.deleteMultipleFeatures(Array.from(selectedIds));
|
mapService.deleteMultipleFeatures(Array.from(selectedIds));
|
||||||
@@ -2386,7 +2386,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
if (!featureType || !numericId) return;
|
if (!featureType || !numericId) return;
|
||||||
navigate(`/${featureType}/${numericId}/edit`);
|
navigate(`/${featureType}/${numericId}/edit`);
|
||||||
},
|
},
|
||||||
[navigate]
|
[navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHideRoute = useCallback(
|
const handleHideRoute = useCallback(
|
||||||
@@ -2413,7 +2413,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
const coordinates = route.path
|
const coordinates = route.path
|
||||||
.filter((c) => c && c[0] != null && c[1] != null)
|
.filter((c) => c && c[0] != null && c[1] != null)
|
||||||
.map((c: [number, number]) =>
|
.map((c: [number, number]) =>
|
||||||
transform([c[1], c[0]], "EPSG:4326", projection)
|
transform([c[1], c[0]], "EPSG:4326", projection),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (coordinates.length > 0) {
|
if (coordinates.length > 0) {
|
||||||
@@ -2435,7 +2435,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
|
|
||||||
const visibleRouteIds = allRouteIds.filter(
|
const visibleRouteIds = allRouteIds.filter(
|
||||||
(id: number) =>
|
(id: number) =>
|
||||||
id !== numericRouteId && !mapStore.hiddenRoutes.has(id)
|
id !== numericRouteId && !mapStore.hiddenRoutes.has(id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const stationsInVisibleRoutes = new Set<number>();
|
const stationsInVisibleRoutes = new Set<number>();
|
||||||
@@ -2443,12 +2443,12 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
const stationIds =
|
const stationIds =
|
||||||
mapStore.routeStationsCache.get(otherRouteId) || [];
|
mapStore.routeStationsCache.get(otherRouteId) || [];
|
||||||
stationIds.forEach((id: number) =>
|
stationIds.forEach((id: number) =>
|
||||||
stationsInVisibleRoutes.add(id)
|
stationsInVisibleRoutes.add(id),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const stationsToShow = routeStationIds.filter(
|
const stationsToShow = routeStationIds.filter(
|
||||||
(id: number) => !stationsInVisibleRoutes.has(id)
|
(id: number) => !stationsInVisibleRoutes.has(id),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const stationId of stationsToShow) {
|
for (const stationId of stationsToShow) {
|
||||||
@@ -2459,8 +2459,8 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
transform(
|
transform(
|
||||||
[station.longitude, station.latitude],
|
[station.longitude, station.latitude],
|
||||||
"EPSG:4326",
|
"EPSG:4326",
|
||||||
projection
|
projection,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const feature = new Feature({
|
const feature = new Feature({
|
||||||
geometry: point,
|
geometry: point,
|
||||||
@@ -2470,7 +2470,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
feature.set("featureType", "station");
|
feature.set("featureType", "station");
|
||||||
|
|
||||||
const existingFeature = mapService.pointSource.getFeatureById(
|
const existingFeature = mapService.pointSource.getFeatureById(
|
||||||
`station-${station.id}`
|
`station-${station.id}`,
|
||||||
);
|
);
|
||||||
if (!existingFeature) {
|
if (!existingFeature) {
|
||||||
mapService.pointSource.addFeature(feature);
|
mapService.pointSource.addFeature(feature);
|
||||||
@@ -2487,7 +2487,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
|
|
||||||
const visibleRouteIds = allRouteIds.filter(
|
const visibleRouteIds = allRouteIds.filter(
|
||||||
(id: number) =>
|
(id: number) =>
|
||||||
id !== numericRouteId && !mapStore.hiddenRoutes.has(id)
|
id !== numericRouteId && !mapStore.hiddenRoutes.has(id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const stationsInVisibleRoutes = new Set<number>();
|
const stationsInVisibleRoutes = new Set<number>();
|
||||||
@@ -2495,21 +2495,21 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
const stationIds =
|
const stationIds =
|
||||||
mapStore.routeStationsCache.get(otherRouteId) || [];
|
mapStore.routeStationsCache.get(otherRouteId) || [];
|
||||||
stationIds.forEach((id: number) =>
|
stationIds.forEach((id: number) =>
|
||||||
stationsInVisibleRoutes.add(id)
|
stationsInVisibleRoutes.add(id),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const stationsToHide = routeStationIds.filter(
|
const stationsToHide = routeStationIds.filter(
|
||||||
(id: number) => !stationsInVisibleRoutes.has(id)
|
(id: number) => !stationsInVisibleRoutes.has(id),
|
||||||
);
|
);
|
||||||
|
|
||||||
stationsToHide.forEach((stationId: number) => {
|
stationsToHide.forEach((stationId: number) => {
|
||||||
const pointFeature = mapService.pointSource.getFeatureById(
|
const pointFeature = mapService.pointSource.getFeatureById(
|
||||||
`station-${stationId}`
|
`station-${stationId}`,
|
||||||
);
|
);
|
||||||
if (pointFeature) {
|
if (pointFeature) {
|
||||||
mapService.pointSource.removeFeature(
|
mapService.pointSource.removeFeature(
|
||||||
pointFeature as Feature<Point>
|
pointFeature as Feature<Point>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2517,7 +2517,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
const lineFeature = mapService.lineSource.getFeatureById(routeId);
|
const lineFeature = mapService.lineSource.getFeatureById(routeId);
|
||||||
if (lineFeature) {
|
if (lineFeature) {
|
||||||
mapService.lineSource.removeFeature(
|
mapService.lineSource.removeFeature(
|
||||||
lineFeature as Feature<LineString>
|
lineFeature as Feature<LineString>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2529,31 +2529,31 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"[handleHideRoute] Error toggling route visibility:",
|
"[handleHideRoute] Error toggling route visibility:",
|
||||||
error
|
error,
|
||||||
);
|
);
|
||||||
toast.error("Ошибка при изменении видимости маршрута");
|
toast.error("Ошибка при изменении видимости маршрута");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mapService]
|
[mapService],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortFeaturesByType = <T extends Feature<Geometry>>(
|
const sortFeaturesByType = <T extends Feature<Geometry>>(
|
||||||
features: T[],
|
features: T[],
|
||||||
sortType: SortType
|
sortType: SortType,
|
||||||
): T[] => {
|
): T[] => {
|
||||||
const sorted = [...features];
|
const sorted = [...features];
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case "name_asc":
|
case "name_asc":
|
||||||
return sorted.sort((a, b) =>
|
return sorted.sort((a, b) =>
|
||||||
((a.get("name") as string) || "").localeCompare(
|
((a.get("name") as string) || "").localeCompare(
|
||||||
(b.get("name") as string) || ""
|
(b.get("name") as string) || "",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
case "name_desc":
|
case "name_desc":
|
||||||
return sorted.sort((a, b) =>
|
return sorted.sort((a, b) =>
|
||||||
((b.get("name") as string) || "").localeCompare(
|
((b.get("name") as string) || "").localeCompare(
|
||||||
(a.get("name") as string) || ""
|
(a.get("name") as string) || "",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
case "created_asc":
|
case "created_asc":
|
||||||
return sorted.sort((a, b) => {
|
return sorted.sort((a, b) => {
|
||||||
@@ -2609,13 +2609,13 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stations = filteredFeatures.filter(
|
const stations = filteredFeatures.filter(
|
||||||
(f) => f.get("featureType") === "station"
|
(f) => f.get("featureType") === "station",
|
||||||
);
|
);
|
||||||
const lines = filteredFeatures.filter(
|
const lines = filteredFeatures.filter(
|
||||||
(f) => f.get("featureType") === "route"
|
(f) => f.get("featureType") === "route",
|
||||||
);
|
);
|
||||||
const sights = filteredFeatures.filter(
|
const sights = filteredFeatures.filter(
|
||||||
(f) => f.get("featureType") === "sight"
|
(f) => f.get("featureType") === "sight",
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedStations = sortFeaturesByType(stations, stationSort);
|
const sortedStations = sortFeaturesByType(stations, stationSort);
|
||||||
@@ -2624,7 +2624,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
const renderFeatureList = (
|
const renderFeatureList = (
|
||||||
features: Feature<Geometry>[],
|
features: Feature<Geometry>[],
|
||||||
featureType: "station" | "route" | "sight",
|
featureType: "station" | "route" | "sight",
|
||||||
IconComponent: React.ElementType
|
IconComponent: React.ElementType,
|
||||||
) => (
|
) => (
|
||||||
<div className="space-y-1 pr-1">
|
<div className="space-y-1 pr-1">
|
||||||
{features.length > 0 ? (
|
{features.length > 0 ? (
|
||||||
@@ -2897,7 +2897,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
mapStore.setHideSightsByHiddenRoutes(
|
mapStore.setHideSightsByHiddenRoutes(
|
||||||
!mapStore.hideSightsByHiddenRoutes
|
!mapStore.hideSightsByHiddenRoutes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -2992,7 +2992,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -3009,7 +3009,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MapPage: React.FC = observer(() => {
|
export const MapPage: React.FC = observer(() => {
|
||||||
@@ -3025,7 +3025,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
const [selectedFeatureForSidebar, setSelectedFeatureForSidebar] =
|
const [selectedFeatureForSidebar, setSelectedFeatureForSidebar] =
|
||||||
useState<Feature<Geometry> | null>(null);
|
useState<Feature<Geometry> | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string | number>>(
|
const [selectedIds, setSelectedIds] = useState<Set<string | number>>(
|
||||||
new Set()
|
new Set(),
|
||||||
);
|
);
|
||||||
const [isLassoActive, setIsLassoActive] = useState<boolean>(false);
|
const [isLassoActive, setIsLassoActive] = useState<boolean>(false);
|
||||||
const [showHelp, setShowHelp] = useState<boolean>(false);
|
const [showHelp, setShowHelp] = useState<boolean>(false);
|
||||||
@@ -3037,7 +3037,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
|
|
||||||
const handleFeaturesChange = useCallback(
|
const handleFeaturesChange = useCallback(
|
||||||
(feats: Feature<Geometry>[]) => setMapFeatures([...feats]),
|
(feats: Feature<Geometry>[]) => setMapFeatures([...feats]),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFeatureSelectForSidebar = useCallback(
|
const handleFeatureSelectForSidebar = useCallback(
|
||||||
@@ -3059,7 +3059,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -3080,7 +3080,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
mapService.loadFeaturesFromApi(
|
mapService.loadFeaturesFromApi(
|
||||||
mapStore.stations,
|
mapStore.stations,
|
||||||
mapStore.routes,
|
mapStore.routes,
|
||||||
mapStore.sights
|
mapStore.sights,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load initial map data:", e);
|
console.error("Failed to load initial map data:", e);
|
||||||
@@ -3099,7 +3099,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
handleFeaturesChange,
|
handleFeaturesChange,
|
||||||
handleFeatureSelectForSidebar,
|
handleFeatureSelectForSidebar,
|
||||||
tooltipRef.current,
|
tooltipRef.current,
|
||||||
setSelectedIds
|
setSelectedIds,
|
||||||
);
|
);
|
||||||
setMapServiceInstance(service);
|
setMapServiceInstance(service);
|
||||||
|
|
||||||
@@ -3110,7 +3110,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
loadInitialData(service);
|
loadInitialData(service);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(
|
setError(
|
||||||
`Ошибка инициализации карты: ${e.message || "Неизвестная ошибка"}.`
|
`Ошибка инициализации карты: ${e.message || "Неизвестная ошибка"}.`,
|
||||||
);
|
);
|
||||||
setIsMapLoading(false);
|
setIsMapLoading(false);
|
||||||
setIsDataLoading(false);
|
setIsDataLoading(false);
|
||||||
@@ -3203,7 +3203,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
mapServiceInstance.loadFeaturesFromApi(
|
mapServiceInstance.loadFeaturesFromApi(
|
||||||
mapStore.stations,
|
mapStore.stations,
|
||||||
mapStore.routes,
|
mapStore.routes,
|
||||||
mapStore.sights
|
mapStore.sights,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [selectedCityId, mapServiceInstance, isDataLoading]);
|
}, [selectedCityId, mapServiceInstance, isDataLoading]);
|
||||||
@@ -3216,7 +3216,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
mapServiceInstance.loadFeaturesFromApi(
|
mapServiceInstance.loadFeaturesFromApi(
|
||||||
mapStore.stations,
|
mapStore.stations,
|
||||||
mapStore.routes,
|
mapStore.routes,
|
||||||
mapStore.sights
|
mapStore.sights,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -3232,7 +3232,7 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
selectedFeatureForSidebar !== null || selectedIds.size > 0;
|
selectedFeatureForSidebar !== null || selectedIds.size > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row font-sans bg-gray-100 h-[90vh] overflow-hidden">
|
<div className="-mb-4 flex flex-col md:flex-row font-sans bg-gray-100 h-[90vh] overflow-hidden">
|
||||||
<div className="relative flex-grow flex">
|
<div className="relative flex-grow flex">
|
||||||
<div
|
<div
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
@@ -3279,35 +3279,87 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setShowHelp(!showHelp)}
|
|
||||||
className="absolute bottom-4 right-4 z-20 p-2 bg-white rounded-full shadow-md hover:bg-gray-100"
|
|
||||||
title="Помощь по клавишам"
|
|
||||||
>
|
|
||||||
<InfoIcon size={20} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showHelp && (
|
{showHelp && (
|
||||||
<div className="absolute bottom-16 right-4 z-20 p-4 bg-white rounded-lg shadow-xl max-w-xs">
|
<div className="absolute bottom-16 right-4 z-20 p-4 bg-white rounded-lg shadow-xl max-w-md max-h-[30vh] overflow-y-auto scrollbar-visible">
|
||||||
<h4 className="font-bold mb-2">Горячие клавиши:</h4>
|
<h4 className="font-bold mb-2">Управление картой</h4>
|
||||||
<ul className="text-sm space-y-2">
|
<div className="text-sm space-y-3">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold mb-1">Перемещение и масштаб:</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li>Колесо мыши — приблизить / отдалить.</li>
|
||||||
|
<li>
|
||||||
|
Средняя кнопка мыши (колесо зажато) — перетаскивание карты.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold mb-1">Выделение объектов:</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li>Одинарный клик по объекту — выделить и центрировать.</li>
|
||||||
|
<li>
|
||||||
|
<span className="font-mono bg-gray-100 px-1 rounded">
|
||||||
|
Ctrl
|
||||||
|
</span>{" "}
|
||||||
|
+ клик — добавить / убрать объект из выделения.
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="font-mono bg-gray-100 px-1 rounded">
|
<span className="font-mono bg-gray-100 px-1 rounded">
|
||||||
Shift
|
Shift
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
- Режим выделения (лассо)
|
— временно включить режим лассо (выделение области).
|
||||||
</li>
|
</li>
|
||||||
|
<li>Клик по пустому месту карты — снять выделение.</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="font-mono bg-gray-100 px-1 rounded">
|
<span className="font-mono bg-gray-100 px-1 rounded">
|
||||||
Ctrl + клик
|
Esc
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
- Добавить/убрать из выделения
|
— снять выделение всех объектов.
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className="font-mono bg-gray-100 px-1 rounded">Esc</span>{" "}
|
|
||||||
- Отменить выделение
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold mb-1">
|
||||||
|
Рисование и редактирование:
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li>
|
||||||
|
Кнопки в верхней панели — выбор режима: редактирование,
|
||||||
|
добавление остановки, достопримечательности или маршрута.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
При рисовании маршрута: правый клик — завершить линию.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
В режиме редактирования: перетаскивайте точки маршрута для
|
||||||
|
изменения траектории.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Двойной клик по внутренней точке маршрута — удалить эту
|
||||||
|
точку (при наличии не менее 2 точек).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold mb-1">Боковая панель:</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li>Клик по строке в списке — перейти к объекту на карте.</li>
|
||||||
|
<li>
|
||||||
|
Иконка карандаша — открыть объект в режиме редактирования.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Иконка карты у маршрутов — открыть предпросмотр маршрута.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Иконка глаза у маршрутов — скрыть / показать маршрут и
|
||||||
|
связанные остановки.
|
||||||
|
</li>
|
||||||
|
<li>Иконка корзины — удалить объект.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowHelp(false)}
|
onClick={() => setShowHelp(false)}
|
||||||
className="mt-3 text-xs text-blue-600 hover:text-blue-800"
|
className="mt-3 text-xs text-blue-600 hover:text-blue-800"
|
||||||
@@ -3316,6 +3368,14 @@ export const MapPage: React.FC = observer(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHelp(!showHelp)}
|
||||||
|
className="absolute bottom-4 right-4 z-20 p-2 bg-white rounded-full shadow-md hover:bg-gray-100"
|
||||||
|
title="Помощь по управлению картой"
|
||||||
|
>
|
||||||
|
<InfoIcon size={20} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showContent && (
|
{showContent && (
|
||||||
<MapSightbar
|
<MapSightbar
|
||||||
|
|||||||
@@ -13,22 +13,30 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
import {
|
||||||
|
MediaViewer,
|
||||||
|
VideoPreviewCard,
|
||||||
|
ImageUploadCard,
|
||||||
|
} from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react";
|
import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { carrierStore } from "../../../shared/store/CarrierStore";
|
|
||||||
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
|
||||||
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
|
||||||
import {
|
import {
|
||||||
|
carrierStore,
|
||||||
|
articlesStore,
|
||||||
|
routeStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
ArticleSelectOrCreateDialog,
|
ArticleSelectOrCreateDialog,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
import type { Route } from "@shared";
|
||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -45,18 +53,27 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const [centerLat, setCenterLat] = useState("");
|
const [centerLat, setCenterLat] = useState("");
|
||||||
const [centerLng, setCenterLng] = useState("");
|
const [centerLng, setCenterLng] = useState("");
|
||||||
const [videoPreview, setVideoPreview] = useState<string>("");
|
const [videoPreview, setVideoPreview] = useState<string>("");
|
||||||
|
const [icon, setIcon] = useState<string>("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||||
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||||
|
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
|
||||||
|
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
|
||||||
|
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
|
||||||
|
const [previewIconId, setPreviewIconId] = useState("");
|
||||||
|
const [activeIconMenuType, setActiveIconMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
|
>(null);
|
||||||
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
carrierStore.getCarriers(language);
|
carrierStore.getCarriers(language);
|
||||||
articlesStore.getArticleList();
|
articlesStore.getArticleList();
|
||||||
|
mediaStore.getMedia();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const filteredCarriers = useMemo(() => {
|
const filteredCarriers = useMemo(() => {
|
||||||
@@ -150,6 +167,23 @@ export const RouteCreatePage = observer(() => {
|
|||||||
setIsVideoPreviewOpen(true);
|
setIsVideoPreviewOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleIconSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setIcon(media.id);
|
||||||
|
setIsSelectIconDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedIconMedia =
|
||||||
|
icon && !isMediaIdEmpty(icon)
|
||||||
|
? mediaStore.media.find((m) => m.id === icon)
|
||||||
|
: null;
|
||||||
|
const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon;
|
||||||
|
const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview;
|
||||||
|
|
||||||
const handleCreateRoute = async () => {
|
const handleCreateRoute = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -243,8 +277,8 @@ export const RouteCreatePage = observer(() => {
|
|||||||
center_latitude,
|
center_latitude,
|
||||||
center_longitude,
|
center_longitude,
|
||||||
path,
|
path,
|
||||||
video_preview:
|
video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined,
|
||||||
videoPreview && videoPreview !== "" ? videoPreview : undefined,
|
icon: !isMediaIdEmpty(icon) ? icon : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (governor_appeal !== undefined) {
|
if (governor_appeal !== undefined) {
|
||||||
@@ -403,16 +437,41 @@ export const RouteCreatePage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box className="w-full flex justify-center gap-4 flex-wrap">
|
||||||
|
<div className="flex flex-col gap-4 max-w-[300px]">
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Иконка маршрута"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={effectiveIconUrl}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewIconOpen(true);
|
||||||
|
setPreviewIconId(selectedIconMedia?.id ?? icon ?? "");
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => {
|
||||||
|
setIcon("");
|
||||||
|
setActiveIconMenuType(null);
|
||||||
|
}}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveIconMenuType("image");
|
||||||
|
setIsSelectIconDialogOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadIconDialogOpen(true);
|
||||||
|
setActiveIconMenuType("image");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 max-w-[300px]">
|
||||||
<VideoPreviewCard
|
<VideoPreviewCard
|
||||||
title="Видеозаставка"
|
title="Видеозаставка"
|
||||||
videoId={videoPreview}
|
videoId={effectiveVideoId}
|
||||||
onVideoClick={handleVideoPreviewClick}
|
onVideoClick={handleVideoPreviewClick}
|
||||||
onDeleteVideoClick={() => {
|
onDeleteVideoClick={() => setVideoPreview("")}
|
||||||
setVideoPreview("");
|
|
||||||
}}
|
|
||||||
onSelectVideoClick={handleVideoFileSelect}
|
onSelectVideoClick={handleVideoFileSelect}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<FormControl fullWidth required>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
@@ -522,7 +581,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onSelectMedia={handleVideoSelect}
|
onSelectMedia={handleVideoSelect}
|
||||||
mediaType={2}
|
mediaType={2}
|
||||||
/>
|
/>
|
||||||
{videoPreview && videoPreview !== "" && (
|
{effectiveVideoId && (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isVideoPreviewOpen}
|
open={isVideoPreviewOpen}
|
||||||
onClose={() => setIsVideoPreviewOpen(false)}
|
onClose={() => setIsVideoPreviewOpen(false)}
|
||||||
@@ -534,7 +593,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
<Box className="flex justify-center items-center p-4">
|
<Box className="flex justify-center items-center p-4">
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: videoPreview,
|
id: effectiveVideoId,
|
||||||
media_type: 2,
|
media_type: 2,
|
||||||
filename: "video_preview",
|
filename: "video_preview",
|
||||||
}}
|
}}
|
||||||
@@ -560,6 +619,28 @@ export const RouteCreatePage = observer(() => {
|
|||||||
initialFile={fileToUpload || undefined}
|
initialFile={fileToUpload || undefined}
|
||||||
afterUpload={handleVideoUpload}
|
afterUpload={handleVideoUpload}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectIconDialogOpen}
|
||||||
|
onClose={() => setIsSelectIconDialogOpen(false)}
|
||||||
|
onSelectMedia={handleIconSelect}
|
||||||
|
mediaType={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadIconDialogOpen}
|
||||||
|
onClose={() => setIsUploadIconDialogOpen(false)}
|
||||||
|
contextObjectName={routeName || "Маршрут"}
|
||||||
|
contextType="route"
|
||||||
|
afterUpload={handleIconSelect}
|
||||||
|
hardcodeType={activeIconMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewIconOpen}
|
||||||
|
onClose={() => setIsPreviewIconOpen(false)}
|
||||||
|
mediaId={previewIconId}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,24 +13,31 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
import {
|
||||||
|
MediaViewer,
|
||||||
|
VideoPreviewCard,
|
||||||
|
ImageUploadCard,
|
||||||
|
DeleteModal,
|
||||||
|
} from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Copy, Save, Plus, X } from "lucide-react";
|
import { ArrowLeft, Copy, Save, Plus, X } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import { carrierStore } from "../../../shared/store/CarrierStore";
|
|
||||||
import { articlesStore } from "../../../shared/store/ArticlesStore";
|
|
||||||
import {
|
import {
|
||||||
|
carrierStore,
|
||||||
|
articlesStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
|
stationsStore,
|
||||||
ArticleSelectOrCreateDialog,
|
ArticleSelectOrCreateDialog,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { stationsStore } from "@shared";
|
|
||||||
import { LinkedItems } from "../LinekedStations";
|
import { LinkedItems } from "../LinekedStations";
|
||||||
|
|
||||||
export const RouteEditPage = observer(() => {
|
export const RouteEditPage = observer(() => {
|
||||||
@@ -44,6 +51,14 @@ export const RouteEditPage = observer(() => {
|
|||||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||||
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||||
|
const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false);
|
||||||
|
const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false);
|
||||||
|
const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false);
|
||||||
|
const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false);
|
||||||
|
const [previewIconId, setPreviewIconId] = useState("");
|
||||||
|
const [activeIconMenuType, setActiveIconMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
|
>(null);
|
||||||
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
@@ -71,10 +86,32 @@ export const RouteEditPage = observer(() => {
|
|||||||
await carrierStore.getCarriers(language);
|
await carrierStore.getCarriers(language);
|
||||||
await stationsStore.getStations();
|
await stationsStore.getStations();
|
||||||
await articlesStore.getArticleList();
|
await articlesStore.getArticleList();
|
||||||
|
await mediaStore.getMedia();
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id, language]);
|
}, [id, language]);
|
||||||
|
|
||||||
|
const handleIconSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
routeStore.setEditRouteData({ icon: media.id });
|
||||||
|
setIsSelectIconDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedIconMedia =
|
||||||
|
editRouteData.icon && !isMediaIdEmpty(editRouteData.icon)
|
||||||
|
? mediaStore.media.find((m) => m.id === editRouteData.icon)
|
||||||
|
: null;
|
||||||
|
const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon)
|
||||||
|
? null
|
||||||
|
: (selectedIconMedia?.id ?? editRouteData.icon);
|
||||||
|
const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview)
|
||||||
|
? null
|
||||||
|
: editRouteData.video_preview;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editRouteData.path && editRouteData.path.length > 0) {
|
if (editRouteData.path && editRouteData.path.length > 0) {
|
||||||
const formattedPath = editRouteData.path
|
const formattedPath = editRouteData.path
|
||||||
@@ -552,9 +589,33 @@ export const RouteEditPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box className="w-full flex justify-center gap-4 flex-wrap">
|
||||||
|
<div className="flex flex-col gap-4 max-w-[300px]">
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Иконка маршрута"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={effectiveIconUrl}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewIconOpen(true);
|
||||||
|
setPreviewIconId(
|
||||||
|
selectedIconMedia?.id ?? editRouteData.icon ?? ""
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => setIsDeleteIconModalOpen(true)}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveIconMenuType("image");
|
||||||
|
setIsSelectIconDialogOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadIconDialogOpen(true);
|
||||||
|
setActiveIconMenuType("image");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 max-w-[300px]">
|
||||||
<VideoPreviewCard
|
<VideoPreviewCard
|
||||||
title="Видеозаставка"
|
title="Видеозаставка"
|
||||||
videoId={editRouteData.video_preview}
|
videoId={effectiveVideoId}
|
||||||
onVideoClick={handleVideoPreviewClick}
|
onVideoClick={handleVideoPreviewClick}
|
||||||
onDeleteVideoClick={() => {
|
onDeleteVideoClick={() => {
|
||||||
routeStore.setEditRouteData({ video_preview: "" });
|
routeStore.setEditRouteData({ video_preview: "" });
|
||||||
@@ -562,6 +623,8 @@ export const RouteEditPage = observer(() => {
|
|||||||
onSelectVideoClick={handleVideoFileSelect}
|
onSelectVideoClick={handleVideoFileSelect}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<LinkedItems
|
<LinkedItems
|
||||||
@@ -621,10 +684,10 @@ export const RouteEditPage = observer(() => {
|
|||||||
<DialogTitle>Предпросмотр видео</DialogTitle>
|
<DialogTitle>Предпросмотр видео</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box className="flex justify-center items-center p-4">
|
<Box className="flex justify-center items-center p-4">
|
||||||
{editRouteData.video_preview && (
|
{effectiveVideoId && (
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: editRouteData.video_preview,
|
id: effectiveVideoId,
|
||||||
media_type: 2,
|
media_type: 2,
|
||||||
filename: "video_preview",
|
filename: "video_preview",
|
||||||
}}
|
}}
|
||||||
@@ -648,6 +711,38 @@ export const RouteEditPage = observer(() => {
|
|||||||
initialFile={fileToUpload || undefined}
|
initialFile={fileToUpload || undefined}
|
||||||
afterUpload={handleVideoUpload}
|
afterUpload={handleVideoUpload}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectIconDialogOpen}
|
||||||
|
onClose={() => setIsSelectIconDialogOpen(false)}
|
||||||
|
onSelectMedia={handleIconSelect}
|
||||||
|
mediaType={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadIconDialogOpen}
|
||||||
|
onClose={() => setIsUploadIconDialogOpen(false)}
|
||||||
|
contextObjectName={editRouteData.route_name || "Маршрут"}
|
||||||
|
contextType="route"
|
||||||
|
afterUpload={handleIconSelect}
|
||||||
|
hardcodeType={activeIconMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewIconOpen}
|
||||||
|
onClose={() => setIsPreviewIconOpen(false)}
|
||||||
|
mediaId={previewIconId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isDeleteIconModalOpen}
|
||||||
|
onDelete={() => {
|
||||||
|
routeStore.setEditRouteData({ icon: "" });
|
||||||
|
setIsDeleteIconModalOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsDeleteIconModalOpen(false)}
|
||||||
|
edit
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MediaViewer } from "@widgets";
|
|||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { authInstance } from "@shared";
|
import { authInstance, isMediaIdEmpty } from "@shared";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import LanguageSelector from "./web-gl/LanguageSelector";
|
import LanguageSelector from "./web-gl/LanguageSelector";
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{carrierThumbnail && (
|
{carrierThumbnail && !isMediaIdEmpty(carrierThumbnail) && (
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: carrierThumbnail,
|
id: carrierThumbnail,
|
||||||
@@ -143,7 +143,7 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
>
|
>
|
||||||
{carrierLogo && (
|
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: carrierLogo,
|
id: carrierLogo,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface StationData {
|
|||||||
address: string;
|
address: string;
|
||||||
city_id?: number;
|
city_id?: number;
|
||||||
description: string;
|
description: string;
|
||||||
|
icon?: string;
|
||||||
id: number;
|
id: number;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
|||||||
@@ -1960,6 +1960,11 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
? { right: 0, transform: "none" }
|
? { right: 0, transform: "none" }
|
||||||
: { left: "50%", transform: "translateX(-50%)" };
|
: { left: "50%", transform: "translateX(-50%)" };
|
||||||
|
|
||||||
|
const iconUrl = station.icon
|
||||||
|
? `${import.meta.env.VITE_KRBL_MEDIA}${station.icon}/download?token=${localStorage.getItem("token") ?? ""}`
|
||||||
|
: null;
|
||||||
|
const iconSizePx = Math.round(primaryFontSize * 1.2);
|
||||||
|
|
||||||
const secondaryLineHeight = 1.2;
|
const secondaryLineHeight = 1.2;
|
||||||
const secondaryHeight = showSecondary
|
const secondaryHeight = showSecondary
|
||||||
? secondaryFontSize * secondaryLineHeight
|
? secondaryFontSize * secondaryLineHeight
|
||||||
@@ -2019,10 +2024,24 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "inline-block",
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: iconUrl ? 6 : 0,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{iconUrl ? (
|
||||||
|
<img
|
||||||
|
src={iconUrl}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
width: iconSizePx,
|
||||||
|
height: iconSizePx,
|
||||||
|
flexShrink: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
Назад
|
Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold">Создание снапшота</h1>
|
<h1 className="text-2xl font-bold">Создание экспорта медиа</h1>
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -53,7 +53,7 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (snapshotStore.snapshotStatus?.Status === "done") {
|
if (snapshotStore.snapshotStatus?.Status === "done") {
|
||||||
toast.success("Снапшот успешно создан");
|
toast.success("Экспорт медиа успешно создан");
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
snapshotStore.snapshotStatus = null;
|
snapshotStore.snapshotStatus = null;
|
||||||
@@ -63,7 +63,7 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error("Ошибка при создании снапшота");
|
toast.error("Ошибка при создании экспорта медиа");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ export const SnapshotListPage = observer(() => {
|
|||||||
fetchSnapshots();
|
fetchSnapshots();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
const formatCreationTime = (isoString: string | undefined) => {
|
||||||
|
if (!isoString) return "";
|
||||||
|
const [datePart, timePartWithMs] = isoString.split("T");
|
||||||
|
if (!datePart || !timePartWithMs) return isoString;
|
||||||
|
const timePart = timePartWithMs.split(".")[0];
|
||||||
|
return `${datePart} - ${timePart}`;
|
||||||
|
};
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: "name",
|
field: "name",
|
||||||
@@ -41,7 +49,14 @@ export const SnapshotListPage = observer(() => {
|
|||||||
headerName: "Родитель",
|
headerName: "Родитель",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "created_at",
|
||||||
|
headerName: "Дата создания",
|
||||||
|
flex: 1,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
return <div>{params.value ? params.value : "-"}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Действия",
|
headerName: "Действия",
|
||||||
@@ -79,14 +94,15 @@ export const SnapshotListPage = observer(() => {
|
|||||||
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),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
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 label="Создать снапшот" path="/snapshot/create" />
|
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
||||||
</div>
|
</div>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -99,7 +115,11 @@ export const SnapshotListPage = observer(() => {
|
|||||||
slots={{
|
slots={{
|
||||||
noRowsOverlay: () => (
|
noRowsOverlay: () => (
|
||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? <CircularProgress size={20} /> : "Нет снапшотов"}
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : (
|
||||||
|
"Нет экспортов медиа"
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import {
|
import {
|
||||||
stationsStore,
|
stationsStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
cityStore,
|
cityStore,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
useSelectedCity,
|
useSelectedCity,
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import {
|
||||||
import { SaveWithoutCityAgree } from "@widgets";
|
ImageUploadCard,
|
||||||
|
LanguageSwitcher,
|
||||||
|
SaveWithoutCityAgree,
|
||||||
|
} from "@widgets";
|
||||||
|
|
||||||
export const StationCreatePage = observer(() => {
|
export const StationCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -35,6 +42,13 @@ export const StationCreatePage = observer(() => {
|
|||||||
const { cities, getCities } = cityStore;
|
const { cities, getCities } = cityStore;
|
||||||
const { selectedCityId, selectedCity } = useSelectedCity();
|
const { selectedCityId, selectedCity } = useSelectedCity();
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
@@ -96,8 +110,27 @@ export const StationCreatePage = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchCities();
|
fetchCities();
|
||||||
|
mediaStore.getMedia();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setCreateCommonData({ icon: media.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMedia =
|
||||||
|
createStationData.common.icon &&
|
||||||
|
!isMediaIdEmpty(createStationData.common.icon)
|
||||||
|
? mediaStore.media.find((m) => m.id === createStationData.common.icon)
|
||||||
|
: null;
|
||||||
|
const effectiveIconUrl = isMediaIdEmpty(createStationData.common.icon)
|
||||||
|
? null
|
||||||
|
: selectedMedia?.id ?? createStationData.common.icon;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
||||||
setCreateCommonData({
|
setCreateCommonData({
|
||||||
@@ -108,7 +141,7 @@ export const StationCreatePage = observer(() => {
|
|||||||
}, [selectedCityId, selectedCity, createStationData.common.city_id]);
|
}, [selectedCityId, selectedCity, createStationData.common.city_id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Box className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
@@ -213,6 +246,30 @@ export const StationCreatePage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Иконка остановки"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={effectiveIconUrl}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(effectiveIconUrl ?? "");
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => {
|
||||||
|
setCreateCommonData({ icon: "" });
|
||||||
|
setActiveMenuType(null);
|
||||||
|
}}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveMenuType("image");
|
||||||
|
setIsSelectMediaOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadMediaOpen(true);
|
||||||
|
setActiveMenuType("image");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
@@ -229,6 +286,28 @@ export const StationCreatePage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
contextObjectName={createStationData[language].name || "Остановка"}
|
||||||
|
contextType="station"
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
|
|
||||||
{isSaveWarningOpen && (
|
{isSaveWarningOpen && (
|
||||||
<SaveWithoutCityAgree
|
<SaveWithoutCityAgree
|
||||||
blocker={{
|
blocker={{
|
||||||
@@ -237,6 +316,6 @@ export const StationCreatePage = observer(() => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -9,20 +8,28 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import {
|
import {
|
||||||
stationsStore,
|
stationsStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
cityStore,
|
cityStore,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
SelectMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import {
|
||||||
|
ImageUploadCard,
|
||||||
|
LanguageSwitcher,
|
||||||
|
SaveWithoutCityAgree,
|
||||||
|
DeleteModal,
|
||||||
|
} from "@widgets";
|
||||||
import { LinkedSights } from "../LinkedSights";
|
import { LinkedSights } from "../LinkedSights";
|
||||||
import { SaveWithoutCityAgree } from "@widgets";
|
|
||||||
|
|
||||||
export const StationEditPage = observer(() => {
|
export const StationEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -39,6 +46,14 @@ export const StationEditPage = observer(() => {
|
|||||||
} = stationsStore;
|
} = stationsStore;
|
||||||
const { cities, getCities } = cityStore;
|
const { cities, getCities } = cityStore;
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false);
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
@@ -95,6 +110,23 @@ export const StationEditPage = observer(() => {
|
|||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setEditCommonData({ icon: media.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMedia =
|
||||||
|
editStationData.common.icon && !isMediaIdEmpty(editStationData.common.icon)
|
||||||
|
? mediaStore.media.find((m) => m.id === editStationData.common.icon)
|
||||||
|
: null;
|
||||||
|
const effectiveIconUrl = isMediaIdEmpty(editStationData.common.icon)
|
||||||
|
? null
|
||||||
|
: selectedMedia?.id ?? editStationData.common.icon;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAndSetStationData = async () => {
|
const fetchAndSetStationData = async () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -109,6 +141,7 @@ export const StationEditPage = observer(() => {
|
|||||||
await getCities("ru");
|
await getCities("ru");
|
||||||
await getCities("en");
|
await getCities("en");
|
||||||
await getCities("zh");
|
await getCities("zh");
|
||||||
|
await mediaStore.getMedia();
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingData(false);
|
setIsLoadingData(false);
|
||||||
}
|
}
|
||||||
@@ -133,7 +166,7 @@ export const StationEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Box className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -239,6 +272,29 @@ export const StationEditPage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Иконка остановки"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={effectiveIconUrl}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(effectiveIconUrl ?? "");
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => {
|
||||||
|
setIsDeleteIconModalOpen(true);
|
||||||
|
}}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveMenuType("image");
|
||||||
|
setIsSelectMediaOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadMediaOpen(true);
|
||||||
|
setActiveMenuType("image");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{id && (
|
{id && (
|
||||||
<LinkedSights
|
<LinkedSights
|
||||||
parentId={Number(id)}
|
parentId={Number(id)}
|
||||||
@@ -262,6 +318,38 @@ export const StationEditPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
contextObjectName={editStationData[language].name || "Остановка"}
|
||||||
|
contextType="station"
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isDeleteIconModalOpen}
|
||||||
|
onDelete={() => {
|
||||||
|
setEditCommonData({ icon: "" });
|
||||||
|
setIsDeleteIconModalOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsDeleteIconModalOpen(false)}
|
||||||
|
edit
|
||||||
|
/>
|
||||||
|
|
||||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||||
{isSaveWarningOpen && (
|
{isSaveWarningOpen && (
|
||||||
<SaveWithoutCityAgree
|
<SaveWithoutCityAgree
|
||||||
@@ -271,6 +359,6 @@ export const StationEditPage = observer(() => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,17 +6,35 @@ import {
|
|||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { userStore } from "@shared";
|
import {
|
||||||
import { useState } from "react";
|
userStore,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
} from "@shared";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ImageUploadCard } from "@widgets";
|
||||||
|
|
||||||
export const UserCreatePage = observer(() => {
|
export const UserCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createUserData, setCreateUserData, createUser } = userStore;
|
const { createUserData, setCreateUserData, createUser } = userStore;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mediaStore.getMedia();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -31,6 +49,29 @@ export const UserCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
createUserData.email || "",
|
||||||
|
createUserData.password || "",
|
||||||
|
createUserData.is_admin || false,
|
||||||
|
media.id
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMedia =
|
||||||
|
createUserData.icon && !isMediaIdEmpty(createUserData.icon)
|
||||||
|
? mediaStore.media.find((m) => m.id === createUserData.icon)
|
||||||
|
: null;
|
||||||
|
const effectiveIconUrl = isMediaIdEmpty(createUserData.icon)
|
||||||
|
? null
|
||||||
|
: selectedMedia?.id ?? createUserData.icon ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -54,7 +95,8 @@ export const UserCreatePage = observer(() => {
|
|||||||
e.target.value,
|
e.target.value,
|
||||||
createUserData.email || "",
|
createUserData.email || "",
|
||||||
createUserData.password || "",
|
createUserData.password || "",
|
||||||
createUserData.is_admin || false
|
createUserData.is_admin || false,
|
||||||
|
createUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -69,7 +111,8 @@ export const UserCreatePage = observer(() => {
|
|||||||
createUserData.name || "",
|
createUserData.name || "",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
createUserData.password || "",
|
createUserData.password || "",
|
||||||
createUserData.is_admin || false
|
createUserData.is_admin || false,
|
||||||
|
createUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -84,7 +127,8 @@ export const UserCreatePage = observer(() => {
|
|||||||
createUserData.name || "",
|
createUserData.name || "",
|
||||||
createUserData.email || "",
|
createUserData.email || "",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
createUserData.is_admin || false
|
createUserData.is_admin || false,
|
||||||
|
createUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -99,7 +143,8 @@ export const UserCreatePage = observer(() => {
|
|||||||
createUserData.name || "",
|
createUserData.name || "",
|
||||||
createUserData.email || "",
|
createUserData.email || "",
|
||||||
createUserData.password || "",
|
createUserData.password || "",
|
||||||
e.target.checked
|
e.target.checked,
|
||||||
|
createUserData.icon
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -108,6 +153,36 @@ export const UserCreatePage = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Аватар"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={effectiveIconUrl}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(effectiveIconUrl ?? "");
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => {
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
createUserData.email || "",
|
||||||
|
createUserData.password || "",
|
||||||
|
createUserData.is_admin || false,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
setActiveMenuType(null);
|
||||||
|
}}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveMenuType("image");
|
||||||
|
setIsSelectMediaOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadMediaOpen(true);
|
||||||
|
setActiveMenuType("image");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
@@ -124,6 +199,28 @@ export const UserCreatePage = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
contextObjectName={createUserData.name || "Пользователь"}
|
||||||
|
contextType="user"
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,12 +7,21 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { userStore, languageStore, LoadingSpinner } from "@shared";
|
import {
|
||||||
|
userStore,
|
||||||
|
languageStore,
|
||||||
|
LoadingSpinner,
|
||||||
|
mediaStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
|
PreviewMediaDialog,
|
||||||
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { ImageUploadCard, DeleteModal } from "@widgets";
|
||||||
|
|
||||||
export const UserEditPage = observer(() => {
|
export const UserEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -22,8 +31,16 @@ export const UserEditPage = observer(() => {
|
|||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { editUserData, editUser, getUser, setEditUserData } = userStore;
|
const { editUserData, editUser, getUser, setEditUserData } = userStore;
|
||||||
|
|
||||||
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||||
|
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false);
|
||||||
|
const [activeMenuType, setActiveMenuType] = useState<
|
||||||
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -40,19 +57,38 @@ export const UserEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMediaSelect = (media: {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
media_name?: string;
|
||||||
|
media_type: number;
|
||||||
|
}) => {
|
||||||
|
setEditUserData(
|
||||||
|
editUserData.name || "",
|
||||||
|
editUserData.email || "",
|
||||||
|
editUserData.password || "",
|
||||||
|
editUserData.is_admin || false,
|
||||||
|
media.id
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
setIsLoadingData(true);
|
setIsLoadingData(true);
|
||||||
try {
|
try {
|
||||||
|
await mediaStore.getMedia();
|
||||||
const data = await getUser(Number(id));
|
const data = await getUser(Number(id));
|
||||||
|
|
||||||
|
if (data) {
|
||||||
setEditUserData(
|
setEditUserData(
|
||||||
data?.name || "",
|
data.name || "",
|
||||||
data?.email || "",
|
data.email || "",
|
||||||
data?.password || "",
|
data.password || "",
|
||||||
data?.is_admin || false
|
data.is_admin || false,
|
||||||
|
data.icon || ""
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingData(false);
|
setIsLoadingData(false);
|
||||||
}
|
}
|
||||||
@@ -62,6 +98,14 @@ export const UserEditPage = observer(() => {
|
|||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const selectedMedia =
|
||||||
|
editUserData.icon && !isMediaIdEmpty(editUserData.icon)
|
||||||
|
? mediaStore.media.find((m) => m.id === editUserData.icon)
|
||||||
|
: null;
|
||||||
|
const effectiveIconUrl = isMediaIdEmpty(editUserData.icon)
|
||||||
|
? null
|
||||||
|
: selectedMedia?.id ?? editUserData.icon ?? null;
|
||||||
|
|
||||||
if (isLoadingData) {
|
if (isLoadingData) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -100,7 +144,8 @@ export const UserEditPage = observer(() => {
|
|||||||
e.target.value,
|
e.target.value,
|
||||||
editUserData.email || "",
|
editUserData.email || "",
|
||||||
editUserData.password || "",
|
editUserData.password || "",
|
||||||
editUserData.is_admin || false
|
editUserData.is_admin || false,
|
||||||
|
editUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -114,7 +159,8 @@ export const UserEditPage = observer(() => {
|
|||||||
editUserData.name || "",
|
editUserData.name || "",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editUserData.password || "",
|
editUserData.password || "",
|
||||||
editUserData.is_admin || false
|
editUserData.is_admin || false,
|
||||||
|
editUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -129,7 +175,8 @@ export const UserEditPage = observer(() => {
|
|||||||
editUserData.name || "",
|
editUserData.name || "",
|
||||||
editUserData.email || "",
|
editUserData.email || "",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editUserData.is_admin || false
|
editUserData.is_admin || false,
|
||||||
|
editUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +189,8 @@ export const UserEditPage = observer(() => {
|
|||||||
editUserData.name || "",
|
editUserData.name || "",
|
||||||
editUserData.email || "",
|
editUserData.email || "",
|
||||||
editUserData.password || "",
|
editUserData.password || "",
|
||||||
e.target.checked
|
e.target.checked,
|
||||||
|
editUserData.icon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -150,6 +198,27 @@ export const UserEditPage = observer(() => {
|
|||||||
label="Администратор"
|
label="Администратор"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-4 max-w-[300px]">
|
||||||
|
<ImageUploadCard
|
||||||
|
title="Аватар"
|
||||||
|
imageKey="thumbnail"
|
||||||
|
imageUrl={effectiveIconUrl}
|
||||||
|
onImageClick={() => {
|
||||||
|
setIsPreviewMediaOpen(true);
|
||||||
|
setMediaId(effectiveIconUrl ?? "");
|
||||||
|
}}
|
||||||
|
onDeleteImageClick={() => setIsDeleteIconModalOpen(true)}
|
||||||
|
onSelectFileClick={() => {
|
||||||
|
setActiveMenuType("image");
|
||||||
|
setIsSelectMediaOpen(true);
|
||||||
|
}}
|
||||||
|
setUploadMediaOpen={() => {
|
||||||
|
setIsUploadMediaOpen(true);
|
||||||
|
setActiveMenuType("image");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center self-end"
|
className="w-min flex gap-2 items-center self-end"
|
||||||
@@ -164,6 +233,44 @@ export const UserEditPage = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SelectMediaDialog
|
||||||
|
open={isSelectMediaOpen}
|
||||||
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
|
onSelectMedia={handleMediaSelect}
|
||||||
|
mediaType={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadMediaOpen}
|
||||||
|
onClose={() => setIsUploadMediaOpen(false)}
|
||||||
|
contextObjectName={editUserData.name || "Пользователь"}
|
||||||
|
contextType="user"
|
||||||
|
afterUpload={handleMediaSelect}
|
||||||
|
hardcodeType={activeMenuType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewMediaDialog
|
||||||
|
open={isPreviewMediaOpen}
|
||||||
|
onClose={() => setIsPreviewMediaOpen(false)}
|
||||||
|
mediaId={mediaId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
open={isDeleteIconModalOpen}
|
||||||
|
onDelete={() => {
|
||||||
|
setEditUserData(
|
||||||
|
editUserData.name || "",
|
||||||
|
editUserData.email || "",
|
||||||
|
editUserData.password || "",
|
||||||
|
editUserData.is_admin || false,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
setIsDeleteIconModalOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsDeleteIconModalOpen(false)}
|
||||||
|
edit
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
const [tailNumber, setTailNumber] = useState("");
|
const [tailNumber, setTailNumber] = useState("");
|
||||||
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 [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
Number(type),
|
Number(type),
|
||||||
carrierStore.carriers[language].data?.find((c) => c.id === carrierId)
|
carrierStore.carriers[language].data?.find((c) => c.id === carrierId)
|
||||||
?.full_name as string,
|
?.full_name as string,
|
||||||
carrierId!
|
carrierId!,
|
||||||
|
model || undefined,
|
||||||
);
|
);
|
||||||
toast.success("Транспорт успешно создан");
|
toast.success("Транспорт успешно создан");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,6 +105,14 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Модель ТС"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder="Произвольное название модели"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
InputLabel,
|
InputLabel,
|
||||||
Button,
|
Button,
|
||||||
Box,
|
Box,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
@@ -54,10 +56,13 @@ export const VehicleEditPage = observer(() => {
|
|||||||
await getCarriers(language);
|
await getCarriers(language);
|
||||||
|
|
||||||
setEditVehicleData({
|
setEditVehicleData({
|
||||||
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
|
tail_number: vehicle[Number(id)]?.vehicle.tail_number ?? "",
|
||||||
type: vehicle[Number(id)]?.vehicle.type,
|
type: vehicle[Number(id)]?.vehicle.type ?? 0,
|
||||||
carrier: vehicle[Number(id)]?.vehicle.carrier,
|
carrier: vehicle[Number(id)]?.vehicle.carrier ?? "",
|
||||||
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
|
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id ?? 0,
|
||||||
|
model: vehicle[Number(id)]?.vehicle.model ?? "",
|
||||||
|
snapshot_update_blocked:
|
||||||
|
vehicle[Number(id)]?.vehicle.snapshot_update_blocked ?? false,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingData(false);
|
setIsLoadingData(false);
|
||||||
@@ -159,6 +164,35 @@ export const VehicleEditPage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Модель ТС"
|
||||||
|
value={editVehicleData.model}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditVehicleData({
|
||||||
|
...editVehicleData,
|
||||||
|
model: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Произвольное название модели"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={editVehicleData.snapshot_update_blocked}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditVehicleData({
|
||||||
|
...editVehicleData,
|
||||||
|
snapshot_update_blocked: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Блокировка обновления ПО"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const NAVIGATION_ITEMS: {
|
|||||||
primary: [
|
primary: [
|
||||||
{
|
{
|
||||||
id: "snapshots",
|
id: "snapshots",
|
||||||
label: "Снапшоты",
|
label: "Экспорт",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
path: "/snapshot",
|
path: "/snapshot",
|
||||||
for_admin: true,
|
for_admin: true,
|
||||||
@@ -124,6 +124,16 @@ export const NAVIGATION_ITEMS: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const VEHICLE_TYPES = [
|
export const VEHICLE_TYPES = [
|
||||||
{ label: "Трамвай", value: 1 },
|
{ label: "Автобус", value: 3 },
|
||||||
{ label: "Троллейбус", value: 2 },
|
{ label: "Троллейбус", value: 2 },
|
||||||
|
{ label: "Трамвай", value: 1 },
|
||||||
|
{ label: "Электробус", value: 4 },
|
||||||
|
{ label: "Электричка", value: 5 },
|
||||||
|
{ label: "Вагон метро", value: 6 },
|
||||||
|
{ label: "Вагон ЖД", value: 7 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const VEHICLE_MODELS = [
|
||||||
|
{ label: "71-431P «Довлатов»", value: "71-431P «Довлатов»" },
|
||||||
|
{ label: "71-638M-02 «Альтаир»", value: "71-638M-02 «Альтаир»" },
|
||||||
|
] as const;
|
||||||
|
|||||||
@@ -33,3 +33,12 @@ export const generateDefaultMediaName = (
|
|||||||
|
|
||||||
return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
|
return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Медиа-id считается пустым, если строка пустая или состоит только из нулей (с дефисами или без). */
|
||||||
|
export const isMediaIdEmpty = (
|
||||||
|
id: string | null | undefined
|
||||||
|
): boolean => {
|
||||||
|
if (id == null || id === "") return true;
|
||||||
|
const digits = id.replace(/-/g, "");
|
||||||
|
return digits === "" || /^0+$/.test(digits);
|
||||||
|
};
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ interface UploadMediaDialogProps {
|
|||||||
| "carrier"
|
| "carrier"
|
||||||
| "country"
|
| "country"
|
||||||
| "vehicle"
|
| "vehicle"
|
||||||
| "station";
|
| "station"
|
||||||
|
| "route"
|
||||||
|
| "user";
|
||||||
isArticle?: boolean;
|
isArticle?: boolean;
|
||||||
articleName?: string;
|
articleName?: string;
|
||||||
initialFile?: File;
|
initialFile?: File;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
import { authInstance, languageInstance, languageStore } from "@shared";
|
import {
|
||||||
|
authInstance,
|
||||||
|
languageInstance,
|
||||||
|
languageStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
route_name: string;
|
route_name: string;
|
||||||
@@ -9,6 +14,7 @@ export type Route = {
|
|||||||
center_longitude: number;
|
center_longitude: number;
|
||||||
governor_appeal: number;
|
governor_appeal: number;
|
||||||
id: number;
|
id: number;
|
||||||
|
icon: string;
|
||||||
path: number[][];
|
path: number[][];
|
||||||
rotate: number;
|
rotate: number;
|
||||||
route_direction: boolean;
|
route_direction: boolean;
|
||||||
@@ -137,6 +143,7 @@ class RouteStore {
|
|||||||
center_longitude: "",
|
center_longitude: "",
|
||||||
governor_appeal: 0,
|
governor_appeal: 0,
|
||||||
id: 0,
|
id: 0,
|
||||||
|
icon: "",
|
||||||
path: [] as number[][],
|
path: [] as number[][],
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
route_direction: false,
|
route_direction: false,
|
||||||
@@ -152,9 +159,15 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editRoute = async (id: number) => {
|
editRoute = async (id: number) => {
|
||||||
if (!this.editRouteData.video_preview) {
|
if (
|
||||||
|
!this.editRouteData.video_preview ||
|
||||||
|
isMediaIdEmpty(this.editRouteData.video_preview)
|
||||||
|
) {
|
||||||
delete this.editRouteData.video_preview;
|
delete this.editRouteData.video_preview;
|
||||||
}
|
}
|
||||||
|
if (!this.editRouteData.icon || isMediaIdEmpty(this.editRouteData.icon)) {
|
||||||
|
delete (this.editRouteData as any).icon;
|
||||||
|
}
|
||||||
const dataToSend: any = {
|
const dataToSend: any = {
|
||||||
...this.editRouteData,
|
...this.editRouteData,
|
||||||
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type User = {
|
|||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserStore {
|
class UserStore {
|
||||||
@@ -57,15 +58,23 @@ class UserStore {
|
|||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
|
icon: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
setCreateUserData = (
|
setCreateUserData = (
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
is_admin: boolean
|
is_admin: boolean,
|
||||||
|
icon?: string
|
||||||
) => {
|
) => {
|
||||||
this.createUserData = { name, email, password, is_admin };
|
this.createUserData = {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
is_admin,
|
||||||
|
icon: icon ?? "",
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
createUser = async () => {
|
createUser = async () => {
|
||||||
@@ -73,7 +82,9 @@ class UserStore {
|
|||||||
if (this.users.data.length > 0) {
|
if (this.users.data.length > 0) {
|
||||||
id = this.users.data[this.users.data.length - 1].id + 1;
|
id = this.users.data[this.users.data.length - 1].id + 1;
|
||||||
}
|
}
|
||||||
const response = await authInstance.post("/user", this.createUserData);
|
const payload = { ...this.createUserData };
|
||||||
|
if (!payload.icon) delete payload.icon;
|
||||||
|
const response = await authInstance.post("/user", payload);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users.data.push({
|
this.users.data.push({
|
||||||
@@ -88,19 +99,29 @@ class UserStore {
|
|||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
|
icon: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditUserData = (
|
setEditUserData = (
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
is_admin: boolean
|
is_admin: boolean,
|
||||||
|
icon?: string
|
||||||
) => {
|
) => {
|
||||||
this.editUserData = { name, email, password, is_admin };
|
this.editUserData = {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
is_admin,
|
||||||
|
icon: icon ?? "",
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
editUser = async (id: number) => {
|
editUser = async (id: number) => {
|
||||||
const response = await authInstance.patch(`/user/${id}`, this.editUserData);
|
const payload = { ...this.editUserData };
|
||||||
|
if (!payload.icon) delete payload.icon;
|
||||||
|
const response = await authInstance.patch(`/user/${id}`, payload);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.users.data = this.users.data.map((user) =>
|
this.users.data = this.users.data.map((user) =>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export type Vehicle = {
|
|||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
carrier: string;
|
carrier: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
model?: string;
|
||||||
|
current_snapshot_uuid?: string;
|
||||||
|
snapshot_update_blocked?: boolean;
|
||||||
};
|
};
|
||||||
device_status?: {
|
device_status?: {
|
||||||
device_uuid: string;
|
device_uuid: string;
|
||||||
@@ -65,14 +68,18 @@ class VehicleStore {
|
|||||||
tailNumber: string,
|
tailNumber: string,
|
||||||
type: number,
|
type: number,
|
||||||
carrier: string,
|
carrier: string,
|
||||||
carrierId: number
|
carrierId: number,
|
||||||
|
model?: string
|
||||||
) => {
|
) => {
|
||||||
const response = await languageInstance("ru").post("/vehicle", {
|
const payload: Record<string, unknown> = {
|
||||||
tail_number: tailNumber,
|
tail_number: tailNumber,
|
||||||
type,
|
type,
|
||||||
carrier,
|
carrier,
|
||||||
carrier_id: carrierId,
|
carrier_id: carrierId,
|
||||||
});
|
};
|
||||||
|
// TODO: когда будет бекенд — добавить model в payload и в ответ
|
||||||
|
if (model != null && model !== "") payload.model = model;
|
||||||
|
const response = await languageInstance("ru").post("/vehicle", payload);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.vehicles.data.push({
|
this.vehicles.data.push({
|
||||||
@@ -83,6 +90,7 @@ class VehicleStore {
|
|||||||
carrier_id: response.data.carrier_id,
|
carrier_id: response.data.carrier_id,
|
||||||
carrier: response.data.carrier,
|
carrier: response.data.carrier,
|
||||||
uuid: response.data.uuid,
|
uuid: response.data.uuid,
|
||||||
|
model: response.data.model ?? model,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -93,11 +101,15 @@ class VehicleStore {
|
|||||||
type: number;
|
type: number;
|
||||||
carrier: string;
|
carrier: string;
|
||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
|
model: string;
|
||||||
|
snapshot_update_blocked: boolean;
|
||||||
} = {
|
} = {
|
||||||
tail_number: "",
|
tail_number: "",
|
||||||
type: 0,
|
type: 0,
|
||||||
carrier: "",
|
carrier: "",
|
||||||
carrier_id: 0,
|
carrier_id: 0,
|
||||||
|
model: "",
|
||||||
|
snapshot_update_blocked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditVehicleData = (data: {
|
setEditVehicleData = (data: {
|
||||||
@@ -105,6 +117,8 @@ class VehicleStore {
|
|||||||
type: number;
|
type: number;
|
||||||
carrier: string;
|
carrier: string;
|
||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
|
model?: string;
|
||||||
|
snapshot_update_blocked?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
this.editVehicleData = {
|
this.editVehicleData = {
|
||||||
...this.editVehicleData,
|
...this.editVehicleData,
|
||||||
@@ -119,27 +133,45 @@ class VehicleStore {
|
|||||||
type: number;
|
type: number;
|
||||||
carrier: string;
|
carrier: string;
|
||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
|
model?: string;
|
||||||
|
snapshot_update_blocked?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const response = await languageInstance("ru").patch(`/vehicle/${id}`, {
|
const payload: Record<string, unknown> = {
|
||||||
tail_number: data.tail_number,
|
tail_number: data.tail_number,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
carrier: data.carrier,
|
carrier: data.carrier,
|
||||||
carrier_id: data.carrier_id,
|
carrier_id: data.carrier_id,
|
||||||
});
|
};
|
||||||
|
if (data.model != null && data.model !== "") payload.model = data.model;
|
||||||
|
if (data.snapshot_update_blocked != null)
|
||||||
|
payload.snapshot_update_blocked = data.snapshot_update_blocked;
|
||||||
|
const response = await languageInstance("ru").patch(
|
||||||
|
`/vehicle/${id}`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
const updated = {
|
||||||
|
...response.data,
|
||||||
|
model: response.data.model ?? data.model,
|
||||||
|
snapshot_update_blocked:
|
||||||
|
response.data.snapshot_update_blocked ?? data.snapshot_update_blocked,
|
||||||
|
};
|
||||||
this.vehicle[id] = {
|
this.vehicle[id] = {
|
||||||
vehicle: {
|
vehicle: {
|
||||||
...this.vehicle[id].vehicle,
|
...this.vehicle[id].vehicle,
|
||||||
...response.data,
|
...updated,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.vehicles.data = this.vehicles.data.map((vehicle) =>
|
this.vehicles.data = this.vehicles.data.map((vehicle) =>
|
||||||
vehicle.vehicle.id === id
|
vehicle.vehicle.id === id
|
||||||
? {
|
? {
|
||||||
...vehicle,
|
...vehicle,
|
||||||
...response.data,
|
vehicle: {
|
||||||
|
...vehicle.vehicle,
|
||||||
|
...updated,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: vehicle
|
: vehicle
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Modal as MuiModal, Typography, Box } from "@mui/material";
|
import { Modal as MuiModal, Typography, Box, SxProps, Theme } from "@mui/material";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
@@ -19,7 +20,7 @@ const style = {
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Modal = ({ open, onClose, children, title }: ModalProps) => {
|
export const Modal = ({ open, onClose, children, title, sx }: ModalProps) => {
|
||||||
return (
|
return (
|
||||||
<MuiModal
|
<MuiModal
|
||||||
open={open}
|
open={open}
|
||||||
@@ -27,7 +28,7 @@ export const Modal = ({ open, onClose, children, title }: ModalProps) => {
|
|||||||
aria-labelledby="modal-modal-title"
|
aria-labelledby="modal-modal-title"
|
||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
<Box sx={style}>
|
<Box sx={{ ...style, ...sx }}>
|
||||||
{title && (
|
{title && (
|
||||||
<Typography
|
<Typography
|
||||||
id="modal-modal-title"
|
id="modal-modal-title"
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
declare const __APP_VERSION__: string;
|
||||||
|
|||||||
132
src/widgets/DevicesTable/DeviceLogsModal.tsx
Normal file
132
src/widgets/DevicesTable/DeviceLogsModal.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { API_URL, authInstance, Modal } from "@shared";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
interface DeviceLogChunk {
|
||||||
|
date?: string;
|
||||||
|
lines?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceLogsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
deviceUuid: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date: Date) => {
|
||||||
|
return new Intl.DateTimeFormat("ru-RU", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLogTimestamp = (timestampStr: string) => {
|
||||||
|
return timestampStr.replace(/^\[|\]$/g, "").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLogLine = (line: string, index: number) => {
|
||||||
|
const match = line.match(/^(\[[^\]]+\])\s*(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
return { id: index, time: match[1], text: match[2].trim() || line };
|
||||||
|
}
|
||||||
|
return { id: index, time: "", text: line };
|
||||||
|
};
|
||||||
|
|
||||||
|
const toYYYYMMDD = (d: Date) => d.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
export const DeviceLogsModal = ({
|
||||||
|
open,
|
||||||
|
deviceUuid,
|
||||||
|
onClose,
|
||||||
|
}: DeviceLogsModalProps) => {
|
||||||
|
const [chunks, setChunks] = useState<DeviceLogChunk[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const today = new Date();
|
||||||
|
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
const dateStr = toYYYYMMDD(today);
|
||||||
|
const dateStrYesterday = toYYYYMMDD(yesterday);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !deviceUuid) return;
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const { data } = await authInstance.get<DeviceLogChunk[]>(
|
||||||
|
`${API_URL}/devices/${deviceUuid}/logs`,
|
||||||
|
{ params: { from: dateStrYesterday, to: dateStr } }
|
||||||
|
);
|
||||||
|
setChunks(Array.isArray(data) ? data : []);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg =
|
||||||
|
err && typeof err === "object" && "message" in err
|
||||||
|
? String((err as { message?: string }).message)
|
||||||
|
: "Ошибка загрузки логов";
|
||||||
|
setError(msg);
|
||||||
|
toast.error(msg);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLogs();
|
||||||
|
}, [open, deviceUuid, dateStr]);
|
||||||
|
|
||||||
|
const logs = chunks.flatMap((chunk, chunkIdx) =>
|
||||||
|
(chunk.lines ?? []).map((line, i) =>
|
||||||
|
parseLogLine(line, chunkIdx * 10000 + i)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose} sx={{ width: "80vw", p: 1.5 }}>
|
||||||
|
<div className="flex flex-col gap-6 h-[85vh]">
|
||||||
|
<div className="flex gap-3 items-center justify-between w-full">
|
||||||
|
<h2 className="text-2xl font-semibold text-[#000000BF]">Логи</h2>
|
||||||
|
<span className="text-lg text-[#00000040]">{formatDate(today)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col min-h-0 w-full">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="w-full h-full flex items-center justify-center bg-white rounded-xl shadow-inner">
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && error && (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-red-500">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && (
|
||||||
|
<div className="w-full bg-white p-4 h-full overflow-y-auto rounded-xl shadow-inner">
|
||||||
|
<div className="flex flex-col gap-2 font-mono">
|
||||||
|
{logs.length > 0 ? (
|
||||||
|
logs.map((log) => (
|
||||||
|
<div key={log.id} className="flex gap-3 items-start p-2">
|
||||||
|
<div className="text-sm text-[#00000050] shrink-0 whitespace-nowrap pt-px">
|
||||||
|
{log.time ? `[${formatLogTimestamp(log.time)}]` : null}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-[#000000BF] break-words w-full">
|
||||||
|
{log.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="h-full flex items-center justify-center text-gray-500">
|
||||||
|
Логи отсутствуют.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
import Table from "@mui/material/Table";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import TableBody from "@mui/material/TableBody";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import TableCell from "@mui/material/TableCell";
|
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
|
||||||
import TableHead from "@mui/material/TableHead";
|
|
||||||
import TableRow from "@mui/material/TableRow";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import { Check, Copy, Pencil, RotateCcw, Trash2, X } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
authInstance,
|
authInstance,
|
||||||
devicesStore,
|
devicesStore,
|
||||||
@@ -13,13 +7,34 @@ import {
|
|||||||
snapshotStore,
|
snapshotStore,
|
||||||
vehicleStore,
|
vehicleStore,
|
||||||
Vehicle,
|
Vehicle,
|
||||||
|
carrierStore,
|
||||||
|
selectedCityStore,
|
||||||
|
VEHICLE_TYPES,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Button, Checkbox, Typography } from "@mui/material";
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Copy,
|
||||||
|
Pencil,
|
||||||
|
RotateCcw,
|
||||||
|
ScrollText,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
} from "@mui/material";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DeleteModal } from "@widgets";
|
import { DeleteModal } from "@widgets";
|
||||||
|
import { DeviceLogsModal } from "./DeviceLogsModal";
|
||||||
|
|
||||||
export type ConnectedDevice = string;
|
export type ConnectedDevice = string;
|
||||||
|
|
||||||
@@ -41,13 +56,16 @@ const formatDate = (dateString: string | null) => {
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
hour12: false,
|
hour12: false,
|
||||||
}).format(date);
|
}).format(date);
|
||||||
} catch (error) {
|
} catch {
|
||||||
return "Некорректная дата";
|
return "Некорректная дата";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type TableRowData = {
|
type RowData = {
|
||||||
|
id: number;
|
||||||
vehicle_id: number;
|
vehicle_id: number;
|
||||||
|
type: string;
|
||||||
|
model: string;
|
||||||
tail_number: string;
|
tail_number: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
lastUpdate: string | null;
|
lastUpdate: string | null;
|
||||||
@@ -55,53 +73,39 @@ type TableRowData = {
|
|||||||
media: boolean;
|
media: boolean;
|
||||||
connection: boolean;
|
connection: boolean;
|
||||||
device_uuid: string | null;
|
device_uuid: string | null;
|
||||||
|
current_snapshot_uuid: string | null;
|
||||||
|
snapshot_update_blocked: boolean;
|
||||||
};
|
};
|
||||||
function createData(
|
|
||||||
vehicle_id: number,
|
function getVehicleTypeLabel(vehicle: Vehicle): string {
|
||||||
tail_number: string,
|
return (
|
||||||
online: boolean,
|
VEHICLE_TYPES.find((t) => t.value === vehicle.vehicle.type)?.label ?? "—"
|
||||||
lastUpdate: string | null,
|
);
|
||||||
gps: boolean,
|
|
||||||
media: boolean,
|
|
||||||
connection: boolean,
|
|
||||||
device_uuid: string | null
|
|
||||||
): TableRowData {
|
|
||||||
return {
|
|
||||||
vehicle_id,
|
|
||||||
tail_number,
|
|
||||||
online,
|
|
||||||
lastUpdate,
|
|
||||||
gps,
|
|
||||||
media,
|
|
||||||
connection,
|
|
||||||
device_uuid,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => {
|
const transformToRows = (vehicles: Vehicle[]): RowData[] => {
|
||||||
return vehicles.map((vehicle) => {
|
return vehicles.map((vehicle) => {
|
||||||
const uuid = vehicle.vehicle.uuid;
|
const uuid = vehicle.vehicle.uuid;
|
||||||
if (!uuid)
|
const model =
|
||||||
|
vehicle.vehicle.model != null && vehicle.vehicle.model !== ""
|
||||||
|
? vehicle.vehicle.model
|
||||||
|
: "—";
|
||||||
|
const type = getVehicleTypeLabel(vehicle);
|
||||||
return {
|
return {
|
||||||
|
id: vehicle.vehicle.id,
|
||||||
vehicle_id: vehicle.vehicle.id,
|
vehicle_id: vehicle.vehicle.id,
|
||||||
|
type,
|
||||||
|
model,
|
||||||
tail_number: vehicle.vehicle.tail_number,
|
tail_number: vehicle.vehicle.tail_number,
|
||||||
online: false,
|
online: uuid ? vehicle.device_status?.online ?? false : false,
|
||||||
lastUpdate: null,
|
lastUpdate: vehicle.device_status?.last_update ?? null,
|
||||||
gps: false,
|
gps: uuid ? vehicle.device_status?.gps_ok ?? false : false,
|
||||||
media: false,
|
media: uuid ? vehicle.device_status?.media_service_ok ?? false : false,
|
||||||
connection: false,
|
connection: uuid ? vehicle.device_status?.is_connected ?? false : false,
|
||||||
device_uuid: null,
|
device_uuid: uuid ?? null,
|
||||||
|
current_snapshot_uuid: vehicle.vehicle.current_snapshot_uuid ?? null,
|
||||||
|
snapshot_update_blocked: vehicle.vehicle.snapshot_update_blocked ?? false,
|
||||||
};
|
};
|
||||||
return createData(
|
|
||||||
vehicle.vehicle.id,
|
|
||||||
vehicle.vehicle.tail_number,
|
|
||||||
vehicle.device_status?.online ?? false,
|
|
||||||
vehicle.device_status?.last_update ?? null,
|
|
||||||
vehicle.device_status?.gps_ok ?? false,
|
|
||||||
vehicle.device_status?.media_service_ok ?? false,
|
|
||||||
vehicle.device_status?.is_connected ?? false,
|
|
||||||
uuid
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,130 +115,409 @@ export const DevicesTable = observer(() => {
|
|||||||
setSelectedDevice,
|
setSelectedDevice,
|
||||||
sendSnapshotModalOpen,
|
sendSnapshotModalOpen,
|
||||||
toggleSendSnapshotModal,
|
toggleSendSnapshotModal,
|
||||||
|
devices,
|
||||||
} = devicesStore;
|
} = devicesStore;
|
||||||
|
|
||||||
const { snapshots, getSnapshots } = snapshotStore;
|
const { snapshots, getSnapshots } = snapshotStore;
|
||||||
const { getVehicles, vehicles, deleteVehicle } = vehicleStore;
|
const { getVehicles, vehicles, deleteVehicle } = vehicleStore;
|
||||||
const { devices } = devicesStore;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [logsModalOpen, setLogsModalOpen] = useState(false);
|
||||||
|
const [logsModalDeviceUuid, setLogsModalDeviceUuid] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [collapsedModels, setCollapsedModels] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [paginationModel, setPaginationModel] = useState({
|
||||||
|
page: 0,
|
||||||
|
pageSize: 50,
|
||||||
|
});
|
||||||
|
|
||||||
const currentTableRows = transformDevicesToRows(vehicles.data as Vehicle[]);
|
const filterVehiclesBySelectedCity = (vehiclesList: Vehicle[]): Vehicle[] => {
|
||||||
|
const selectedCityId = selectedCityStore.selectedCityId;
|
||||||
|
|
||||||
|
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 rows = useMemo(
|
||||||
|
() => transformToRows(filteredVehicles),
|
||||||
|
[filteredVehicles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupsByModel = useMemo(() => {
|
||||||
|
const map = new Map<string, RowData[]>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const list = map.get(row.model) ?? [];
|
||||||
|
list.push(row);
|
||||||
|
map.set(row.model, list);
|
||||||
|
}
|
||||||
|
return Array.from(map.entries()).map(([model, groupRows]) => ({
|
||||||
|
model,
|
||||||
|
rows: groupRows,
|
||||||
|
}));
|
||||||
|
}, [rows]);
|
||||||
|
|
||||||
|
const toggleModelCollapsed = (model: string) => {
|
||||||
|
setCollapsedModels((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(model)) next.delete(model);
|
||||||
|
else next.add(model);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDeviceUuids = useMemo(() => {
|
||||||
|
return rows
|
||||||
|
.filter((r) => selectedIds.includes(r.id))
|
||||||
|
.map((r) => r.device_uuid)
|
||||||
|
.filter((u): u is string => u != null);
|
||||||
|
}, [rows, selectedIds]);
|
||||||
|
|
||||||
|
const selectedDeviceUuidsAllowed = useMemo(() => {
|
||||||
|
return rows
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
selectedIds.includes(r.id) &&
|
||||||
|
r.device_uuid != null &&
|
||||||
|
!r.snapshot_update_blocked
|
||||||
|
)
|
||||||
|
.map((r) => r.device_uuid as string);
|
||||||
|
}, [rows, selectedIds]);
|
||||||
|
|
||||||
|
const columns: GridColDef[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
field: "model",
|
||||||
|
headerName: "Модель",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 120,
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tail_number",
|
||||||
|
headerName: "Бортовой номер",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 120,
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "can_send_update",
|
||||||
|
headerName: "Обновление",
|
||||||
|
width: 90,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
sortable: false,
|
||||||
|
disableColumnMenu: true,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
const rowData = params.row as RowData;
|
||||||
|
const canSend =
|
||||||
|
!rowData.snapshot_update_blocked && rowData.device_uuid !== null;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
canSend ? "Можно отправить запрос" : "Блокировка обновления"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={canSend as unknown as boolean}
|
||||||
|
disabled
|
||||||
|
size="small"
|
||||||
|
sx={{ p: 0 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "online",
|
||||||
|
headerName: "Онлайн",
|
||||||
|
width: 90,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
type: "boolean",
|
||||||
|
filterable: true,
|
||||||
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", justifyContent: "center", width: "100%" }}
|
||||||
|
>
|
||||||
|
{params.value ? (
|
||||||
|
<Check size={18} className="text-green-600" />
|
||||||
|
) : (
|
||||||
|
<X size={18} className="text-red-600" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "lastUpdate",
|
||||||
|
headerName: "Обновлено",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 140,
|
||||||
|
filterable: true,
|
||||||
|
renderCell: (params: GridRenderCellParams) =>
|
||||||
|
formatDate(params.value as string | null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "snapshot_name",
|
||||||
|
headerName: "Экспорт на устройстве",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 140,
|
||||||
|
filterable: true,
|
||||||
|
valueGetter: (_value, row, _apiRef) => {
|
||||||
|
const rowData = row as RowData;
|
||||||
|
const uuid = rowData.current_snapshot_uuid;
|
||||||
|
if (!uuid) return "—";
|
||||||
|
const snapshot = (snapshots as Snapshot[]).find((s) => s.ID === uuid);
|
||||||
|
return snapshot?.Name ?? uuid;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "gps",
|
||||||
|
headerName: "GPS",
|
||||||
|
width: 70,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
type: "boolean",
|
||||||
|
filterable: true,
|
||||||
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", justifyContent: "center", width: "100%" }}
|
||||||
|
>
|
||||||
|
{params.value ? (
|
||||||
|
<Check size={18} className="text-green-600" />
|
||||||
|
) : (
|
||||||
|
<X size={18} className="text-red-600" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "actions",
|
||||||
|
headerName: "Действия",
|
||||||
|
width: 160,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
const row = params.row as RowData;
|
||||||
|
|
||||||
|
const handleReloadStatus = async () => {
|
||||||
|
if (!row.device_uuid) return;
|
||||||
|
setSelectedDevice(row.device_uuid);
|
||||||
|
try {
|
||||||
|
await authInstance.post(
|
||||||
|
`/devices/${row.device_uuid}/request-status`
|
||||||
|
);
|
||||||
|
await getVehicles();
|
||||||
|
await getDevices();
|
||||||
|
toast.success("Статус устройства обновлен");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error requesting status for device ${row.device_uuid}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
toast.error("Ошибка сервера");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/vehicle/${row.vehicle_id}/edit`);
|
||||||
|
}}
|
||||||
|
title="Редактировать транспорт"
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleReloadStatus();
|
||||||
|
}}
|
||||||
|
title="Перезапросить статус"
|
||||||
|
disabled={
|
||||||
|
!row.device_uuid || !devices.includes(row.device_uuid)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (row.device_uuid) {
|
||||||
|
navigator.clipboard.writeText(row.device_uuid);
|
||||||
|
toast.success("UUID скопирован");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Копировать UUID"
|
||||||
|
>
|
||||||
|
<Copy size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (row.device_uuid) {
|
||||||
|
setLogsModalDeviceUuid(row.device_uuid);
|
||||||
|
setLogsModalOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Логи устройства"
|
||||||
|
>
|
||||||
|
<ScrollText size={16} />
|
||||||
|
</button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
devices,
|
||||||
|
getDevices,
|
||||||
|
getVehicles,
|
||||||
|
navigate,
|
||||||
|
setSelectedDevice,
|
||||||
|
snapshots,
|
||||||
|
setLogsModalDeviceUuid,
|
||||||
|
setLogsModalOpen,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
await getVehicles();
|
await getVehicles();
|
||||||
await getDevices();
|
await getDevices();
|
||||||
await getSnapshots();
|
await getSnapshots();
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [getDevices, getSnapshots]);
|
}, [getDevices, getSnapshots, getVehicles]);
|
||||||
|
|
||||||
const isAllSelected =
|
useEffect(() => {
|
||||||
currentTableRows.length > 0 &&
|
carrierStore.getCarriers("ru");
|
||||||
selectedDeviceUuids.length === currentTableRows.length;
|
}, []);
|
||||||
|
|
||||||
const handleSelectAllDevices = () => {
|
|
||||||
if (isAllSelected) {
|
|
||||||
setSelectedDeviceUuids([]);
|
|
||||||
} else {
|
|
||||||
setSelectedDeviceUuids(
|
|
||||||
currentTableRows.map((row) => row.device_uuid ?? "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectDevice = (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
deviceUuid: string
|
|
||||||
) => {
|
|
||||||
if (event.target.checked) {
|
|
||||||
setSelectedDeviceUuids((prevSelected) => [...prevSelected, deviceUuid]);
|
|
||||||
} else {
|
|
||||||
setSelectedDeviceUuids((prevSelected) =>
|
|
||||||
prevSelected.filter((uuid) => uuid !== deviceUuid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenSendSnapshotModal = () => {
|
const handleOpenSendSnapshotModal = () => {
|
||||||
if (selectedDeviceUuids.length > 0) {
|
if (selectedDeviceUuidsAllowed.length > 0) {
|
||||||
toggleSendSnapshotModal();
|
toggleSendSnapshotModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReloadStatus = async (uuid: string) => {
|
|
||||||
setSelectedDevice(uuid);
|
|
||||||
try {
|
|
||||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
|
||||||
await getVehicles();
|
|
||||||
await getDevices();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error requesting status for device ${uuid}:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendSnapshotAction = async (snapshotId: string) => {
|
const handleSendSnapshotAction = async (snapshotId: string) => {
|
||||||
if (selectedDeviceUuids.length === 0) return;
|
if (selectedDeviceUuidsAllowed.length === 0) return;
|
||||||
|
|
||||||
|
const blockedCount =
|
||||||
|
selectedDeviceUuids.length - selectedDeviceUuidsAllowed.length;
|
||||||
|
if (blockedCount > 0) {
|
||||||
|
toast.info(
|
||||||
|
`Обновление ПО не отправлено на ${blockedCount} устройств (блокировка)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const send = async (deviceUuid: string) => {
|
const send = async (deviceUuid: string) => {
|
||||||
try {
|
try {
|
||||||
await authInstance.post(
|
await authInstance.post(
|
||||||
`/devices/${deviceUuid}/force-snapshot-update`,
|
`/devices/${deviceUuid}/force-snapshot-update`,
|
||||||
{
|
{ snapshot_id: snapshotId }
|
||||||
snapshot_id: snapshotId,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
toast.success(`Снапшот отправлен на устройство `);
|
toast.success("Обновление ПО отправлено на устройство");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error sending snapshot to device ${deviceUuid}:`, error);
|
console.error(`Error sending snapshot to device ${deviceUuid}:`, error);
|
||||||
toast.error(`Не удалось отправить снапшот на устройство`);
|
toast.error("Не удалось отправить обновление ПО на устройство");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
|
||||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
|
||||||
return send(deviceUuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.allSettled(snapshotPromises);
|
|
||||||
|
|
||||||
|
await Promise.allSettled(selectedDeviceUuidsAllowed.map(send));
|
||||||
await getDevices();
|
await getDevices();
|
||||||
setSelectedDeviceUuids([]);
|
setSelectedIds([]);
|
||||||
toggleSendSnapshotModal();
|
toggleSendSnapshotModal();
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in snapshot sending process:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVehicleIdsByUuids = (uuids: string[]): number[] => {
|
|
||||||
return vehicles.data
|
|
||||||
.filter((vehicle) => uuids.includes(vehicle.vehicle.uuid ?? ""))
|
|
||||||
.map((vehicle) => vehicle.vehicle.id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteVehicles = async () => {
|
const handleDeleteVehicles = async () => {
|
||||||
if (selectedDeviceUuids.length === 0) return;
|
if (selectedIds.length === 0) return;
|
||||||
|
|
||||||
const vehicleIds = getVehicleIdsByUuids(selectedDeviceUuids);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(vehicleIds.map((id) => deleteVehicle(id)));
|
await Promise.all(selectedIds.map((id) => deleteVehicle(id)));
|
||||||
await getVehicles();
|
await getVehicles();
|
||||||
await getDevices();
|
await getDevices();
|
||||||
setSelectedDeviceUuids([]);
|
setSelectedIds([]);
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
toast.success(`Удалено устройств: ${vehicleIds.length}`);
|
toast.success(`Удалено устройств: ${selectedIds.length}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting vehicles:", error);
|
console.error("Error deleting vehicles:", error);
|
||||||
toast.error("Ошибка при удалении устройств");
|
toast.error("Ошибка при удалении устройств");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createSelectionHandler = (groupRowIds: number[]) => {
|
||||||
|
const groupIdSet = new Set(groupRowIds);
|
||||||
|
return (newSelection: { ids: Set<number | string> } | number[]) => {
|
||||||
|
let newIds: number[] = [];
|
||||||
|
if (Array.isArray(newSelection)) {
|
||||||
|
newIds = newSelection.map((id) => Number(id));
|
||||||
|
} else if (
|
||||||
|
newSelection &&
|
||||||
|
typeof newSelection === "object" &&
|
||||||
|
"ids" in newSelection
|
||||||
|
) {
|
||||||
|
const idsSet = newSelection.ids as Set<number | string>;
|
||||||
|
newIds = Array.from(idsSet)
|
||||||
|
.map((id) => (typeof id === "string" ? Number.parseInt(id, 10) : id))
|
||||||
|
.filter((id) => !Number.isNaN(id));
|
||||||
|
}
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const fromOtherGroups = prev.filter((id) => !groupIdSet.has(id));
|
||||||
|
return [...fromOtherGroups, ...newIds];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
<div className="w-full">
|
||||||
<div className="flex justify-end p-3 gap-2 ">
|
<div className="flex justify-end mb-5 gap-2 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -243,16 +526,7 @@ export const DevicesTable = observer(() => {
|
|||||||
>
|
>
|
||||||
Добавить устройство
|
Добавить устройство
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
{selectedIds.length > 0 && (
|
||||||
<div className="flex justify-end p-3 gap-2 items-center">
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleSelectAllDevices}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{isAllSelected ? "Снять выбор" : "Выбрать все"}
|
|
||||||
</Button>
|
|
||||||
{selectedDeviceUuids.length > 0 && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="error"
|
color="error"
|
||||||
@@ -260,227 +534,136 @@ export const DevicesTable = observer(() => {
|
|||||||
size="small"
|
size="small"
|
||||||
startIcon={<Trash2 size={16} />}
|
startIcon={<Trash2 size={16} />}
|
||||||
>
|
>
|
||||||
Удалить ({selectedDeviceUuids.length})
|
Удалить ({selectedIds.length})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={selectedDeviceUuids.length === 0}
|
disabled={selectedDeviceUuidsAllowed.length === 0}
|
||||||
onClick={handleOpenSendSnapshotModal}
|
onClick={handleOpenSendSnapshotModal}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
Отправить снапшот ({selectedDeviceUuids.length})
|
Обновление ПО ({selectedDeviceUuidsAllowed.length}
|
||||||
|
{selectedDeviceUuids.length !== selectedDeviceUuidsAllowed.length &&
|
||||||
|
`/${selectedDeviceUuids.length}`}
|
||||||
|
)
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table sx={{ minWidth: 650 }} aria-label="devices table" size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox
|
|
||||||
indeterminate={
|
|
||||||
selectedDeviceUuids.length > 0 &&
|
|
||||||
selectedDeviceUuids.length < currentTableRows.length
|
|
||||||
}
|
|
||||||
checked={isAllSelected}
|
|
||||||
onChange={handleSelectAllDevices}
|
|
||||||
inputProps={{ "aria-label": "select all devices" }}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">Борт. номер</TableCell>
|
|
||||||
<TableCell align="center">Онлайн</TableCell>
|
|
||||||
<TableCell align="center">Обновлено</TableCell>
|
|
||||||
<TableCell align="center">GPS</TableCell>
|
|
||||||
<TableCell align="center">Медиа</TableCell>
|
|
||||||
<TableCell align="center">Связь</TableCell>
|
|
||||||
<TableCell align="center">Действия</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{currentTableRows.map((row) => (
|
|
||||||
<TableRow
|
|
||||||
key={row.tail_number}
|
|
||||||
hover
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked={selectedDeviceUuids.includes(
|
|
||||||
row.device_uuid ?? ""
|
|
||||||
)}
|
|
||||||
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (
|
|
||||||
(event.target as HTMLElement).closest("button") === null &&
|
|
||||||
(event.target as HTMLElement).closest(
|
|
||||||
'input[type="checkbox"]'
|
|
||||||
) === null
|
|
||||||
) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
if (row.device_uuid) {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(row.device_uuid)
|
|
||||||
.then(() => {
|
|
||||||
toast.success(`UUID скопирован`);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Не удалось скопировать UUID");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.warning("Устройство не имеет UUID");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.shiftKey) {
|
{groupsByModel.length === 0 ? (
|
||||||
handleSelectDevice(
|
<Box
|
||||||
{
|
sx={{
|
||||||
target: {
|
mt: 5,
|
||||||
checked: !selectedDeviceUuids.includes(
|
py: 4,
|
||||||
row.device_uuid ?? ""
|
textAlign: "center",
|
||||||
),
|
color: "text.secondary",
|
||||||
},
|
}}
|
||||||
} as React.ChangeEvent<HTMLInputElement>,
|
>
|
||||||
row.device_uuid ?? ""
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : (
|
||||||
|
"Нет устройств для отображения"
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-6 mt-4">
|
||||||
|
{groupsByModel.map(({ model: groupModel, rows: groupRows }) => {
|
||||||
|
const isCollapsed = collapsedModels.has(groupModel);
|
||||||
|
const groupRowIds = groupRows.map((r) => r.id);
|
||||||
|
const selectedInGroup = selectedIds.filter((id) =>
|
||||||
|
groupRowIds.includes(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={groupModel}
|
||||||
|
className="border border-gray-200 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleModelCollapsed(groupModel)}
|
||||||
|
className="w-full flex items-center gap-2 px-4 py-3 bg-gray-50 hover:bg-gray-100 text-left border-b border-gray-200"
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<ChevronRight size={20} className="text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown size={20} className="text-gray-600" />
|
||||||
|
)}
|
||||||
|
<Typography variant="h6" component="span" fontWeight={600}>
|
||||||
|
{groupModel}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
({groupRows.length}{" "}
|
||||||
|
{groupRows.length === 1 ? "устройство" : "устройств"})
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<Box sx={{ p: 0 }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={groupRows}
|
||||||
|
columns={columns}
|
||||||
|
checkboxSelection
|
||||||
|
disableRowSelectionExcludeModel
|
||||||
|
loading={isLoading}
|
||||||
|
paginationModel={paginationModel}
|
||||||
|
onPaginationModelChange={setPaginationModel}
|
||||||
|
pageSizeOptions={[50]}
|
||||||
|
onRowSelectionModelChange={
|
||||||
|
createSelectionHandler(groupRowIds) as (
|
||||||
|
ids: unknown
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
rowSelectionModel={{
|
||||||
|
type: "include",
|
||||||
|
ids: new Set(selectedInGroup),
|
||||||
|
}}
|
||||||
|
localeText={
|
||||||
|
ruRU.components.MuiDataGrid.defaultProps.localeText
|
||||||
}
|
}
|
||||||
|
autoHeight
|
||||||
|
slots={{
|
||||||
|
noRowsOverlay: () => null,
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
border: "none",
|
||||||
"&:last-child td, &:last-child th": { border: 0 },
|
"& .MuiDataGrid-columnHeaders": {
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
},
|
||||||
|
"& .MuiDataGrid-cell": {
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedDeviceUuids.includes(
|
|
||||||
row.device_uuid ?? ""
|
|
||||||
)}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleSelectDevice(event, row.device_uuid ?? "")
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</Box>
|
||||||
<TableCell
|
|
||||||
align="center"
|
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
id={`device-label-${row.device_uuid}`}
|
|
||||||
>
|
|
||||||
{row.tail_number}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{row.online ? (
|
|
||||||
<Check size={18} className="text-green-600" />
|
|
||||||
) : (
|
|
||||||
<X size={18} className="text-red-600" />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
);
|
||||||
<TableCell align="center">
|
})}
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{formatDate(row.lastUpdate)}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{row.gps ? (
|
|
||||||
<Check size={18} className="text-green-600" />
|
|
||||||
) : (
|
|
||||||
<X size={18} className="text-red-600" />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{row.media ? (
|
|
||||||
<Check size={18} className="text-green-600" />
|
|
||||||
) : (
|
|
||||||
<X size={18} className="text-red-600" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{row.connection ? (
|
|
||||||
<Check size={18} className="text-green-600" />
|
|
||||||
) : (
|
|
||||||
<X size={18} className="text-red-600" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<div className="flex items-center justify-center gap-1">
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
navigate(`/vehicle/${row.vehicle_id}/edit`);
|
|
||||||
}}
|
|
||||||
title="Редактировать транспорт"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
<Pencil size={16} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
row.device_uuid &&
|
|
||||||
devices.find((device) => device === row.device_uuid)
|
|
||||||
) {
|
|
||||||
await handleReloadStatus(row.device_uuid);
|
|
||||||
toast.success("Статус устройства обновлен");
|
|
||||||
} else {
|
|
||||||
toast.error("Нет связи с устройством");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Ошибка сервера");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Перезапросить статус"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
<RotateCcw size={16} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
navigator.clipboard.writeText(row.device_uuid ?? "");
|
|
||||||
toast.success("UUID скопирован");
|
|
||||||
}}
|
|
||||||
title="Копировать UUID"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
<Copy size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
{currentTableRows.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} align="center">
|
|
||||||
Нет устройств для отображения.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
|
|
||||||
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
|
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
|
||||||
<Typography variant="h6" component="h2" sx={{ mb: 1 }}>
|
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
|
||||||
Отправить снапшот
|
Обновление ПО
|
||||||
</Typography>
|
</Box>
|
||||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
Выбрано устройств:{" "}
|
Выбрано устройств для обновления:{" "}
|
||||||
<strong className="text-blue-600">
|
<strong className="text-blue-600">
|
||||||
{selectedDeviceUuids.length}
|
{selectedDeviceUuidsAllowed.length}
|
||||||
</strong>
|
</strong>
|
||||||
</Typography>
|
{selectedDeviceUuids.length !== selectedDeviceUuidsAllowed.length && (
|
||||||
|
<span className="text-amber-600 ml-1">
|
||||||
|
(пропущено{" "}
|
||||||
|
{selectedDeviceUuids.length - selectedDeviceUuidsAllowed.length} с
|
||||||
|
блокировкой)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
||||||
{snapshots && (snapshots as Snapshot[]).length > 0 ? (
|
{snapshots && (snapshots as Snapshot[]).length > 0 ? (
|
||||||
(snapshots as Snapshot[]).map((snapshot) => (
|
(snapshots as Snapshot[]).map((snapshot) => (
|
||||||
@@ -495,9 +678,9 @@ export const DevicesTable = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2" color="textSecondary">
|
<Box sx={{ typography: "body2", color: "text.secondary" }}>
|
||||||
Нет доступных снапшотов.
|
Нет доступных экспортов медиа.
|
||||||
</Typography>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -516,6 +699,15 @@ export const DevicesTable = observer(() => {
|
|||||||
onDelete={handleDeleteVehicles}
|
onDelete={handleDeleteVehicles}
|
||||||
onCancel={() => setIsDeleteModalOpen(false)}
|
onCancel={() => setIsDeleteModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeviceLogsModal
|
||||||
|
open={logsModalOpen}
|
||||||
|
deviceUuid={logsModalDeviceUuid}
|
||||||
|
onClose={() => {
|
||||||
|
setLogsModalOpen(false);
|
||||||
|
setLogsModalDeviceUuid(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { AppBar } from "./ui/AppBar";
|
|||||||
import { Drawer } from "./ui/Drawer";
|
import { Drawer } from "./ui/Drawer";
|
||||||
import { DrawerHeader } from "./ui/DrawerHeader";
|
import { DrawerHeader } from "./ui/DrawerHeader";
|
||||||
import { NavigationList } from "@features";
|
import { NavigationList } from "@features";
|
||||||
import { authStore, userStore, menuStore } from "@shared";
|
import { authStore, userStore, menuStore, isMediaIdEmpty } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
@@ -67,18 +67,18 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{(() => {
|
{(() => {
|
||||||
|
const currentUser = users?.data?.find(
|
||||||
|
(user) => user.id === (authStore.payload as { user_id?: number })?.user_id,
|
||||||
|
);
|
||||||
|
const hasAvatar =
|
||||||
|
currentUser?.icon && !isMediaIdEmpty(currentUser.icon);
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className=" text-white">
|
<div className="flex flex-col gap-1">
|
||||||
{
|
<p className="text-white">{currentUser?.name}</p>
|
||||||
users?.data?.find(
|
|
||||||
// @ts-ignore
|
|
||||||
(user) => user.id === authStore.payload?.user_id
|
|
||||||
)?.name
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
<div
|
<div
|
||||||
className="text-center text-xs"
|
className="text-center text-xs"
|
||||||
style={{
|
style={{
|
||||||
@@ -88,19 +88,28 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
|||||||
padding: "2px 10px",
|
padding: "2px 10px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* @ts-ignore */}
|
{(authStore.payload as { is_admin?: boolean })?.is_admin
|
||||||
{authStore.payload?.is_admin
|
|
||||||
? "Администратор"
|
? "Администратор"
|
||||||
: "Режим пользователя"}
|
: "Режим пользователя"}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-10 h-10 rounded-full flex items-center justify-center overflow-hidden bg-gray-600 shrink-0">
|
||||||
|
{hasAvatar ? (
|
||||||
|
<img
|
||||||
|
src={`${
|
||||||
|
import.meta.env.VITE_KRBL_MEDIA
|
||||||
|
}${currentUser!.icon}/download?token=${token}`}
|
||||||
|
alt="Аватар"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<User className="text-white" size={20} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 bg-gray-600 rounded-full flex items-center justify-center">
|
|
||||||
<User />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Drawer variant="permanent" open={open}>
|
<Drawer variant="permanent" open={open}>
|
||||||
@@ -138,6 +147,9 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
|||||||
)}
|
)}
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<NavigationList open={open} onDrawerOpen={handleDrawerOpen} />
|
<NavigationList open={open} onDrawerOpen={handleDrawerOpen} />
|
||||||
|
<div className="mt-auto flex justify-center items-center pb-5 text-sm text-gray-300">
|
||||||
|
v.{__APP_VERSION__}
|
||||||
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
Language,
|
Language,
|
||||||
cityStore,
|
cityStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
SightLanguageInfo,
|
SightLanguageInfo,
|
||||||
@@ -308,7 +309,7 @@ export const CreateInformationTab = observer(
|
|||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Водяной знак (левый верхний)"
|
title="Водяной знак (левый верхний)"
|
||||||
imageKey="watermark_lu"
|
imageKey="watermark_lu"
|
||||||
imageUrl={sight.watermark_lu}
|
imageUrl={isMediaIdEmpty(sight.watermark_lu) ? null : sight.watermark_lu}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
setIsPreviewMediaOpen(true);
|
setIsPreviewMediaOpen(true);
|
||||||
setMediaId(sight.watermark_lu ?? "");
|
setMediaId(sight.watermark_lu ?? "");
|
||||||
@@ -363,7 +364,7 @@ export const CreateInformationTab = observer(
|
|||||||
|
|
||||||
<VideoPreviewCard
|
<VideoPreviewCard
|
||||||
title="Видеозаставка"
|
title="Видеозаставка"
|
||||||
videoId={sight.video_preview}
|
videoId={isMediaIdEmpty(sight.video_preview) ? null : sight.video_preview}
|
||||||
onVideoClick={handleVideoPreviewClick}
|
onVideoClick={handleVideoPreviewClick}
|
||||||
onDeleteVideoClick={() => {
|
onDeleteVideoClick={() => {
|
||||||
handleChange({
|
handleChange({
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Language,
|
Language,
|
||||||
cityStore,
|
cityStore,
|
||||||
editSightStore,
|
editSightStore,
|
||||||
|
isMediaIdEmpty,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
SightLanguageInfo,
|
SightLanguageInfo,
|
||||||
@@ -334,7 +335,7 @@ export const InformationTab = observer(
|
|||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Водяной знак (левый верхний)"
|
title="Водяной знак (левый верхний)"
|
||||||
imageKey="watermark_lu"
|
imageKey="watermark_lu"
|
||||||
imageUrl={sight.common.watermark_lu}
|
imageUrl={isMediaIdEmpty(sight.common.watermark_lu) ? null : sight.common.watermark_lu}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
setIsPreviewMediaOpen(true);
|
setIsPreviewMediaOpen(true);
|
||||||
setMediaId(sight.common.watermark_lu ?? "");
|
setMediaId(sight.common.watermark_lu ?? "");
|
||||||
@@ -396,7 +397,7 @@ export const InformationTab = observer(
|
|||||||
|
|
||||||
<VideoPreviewCard
|
<VideoPreviewCard
|
||||||
title="Видеозаставка"
|
title="Видеозаставка"
|
||||||
videoId={sight.common.video_preview}
|
videoId={isMediaIdEmpty(sight.common.video_preview) ? null : sight.common.video_preview}
|
||||||
onVideoClick={handleVideoPreviewClick}
|
onVideoClick={handleVideoPreviewClick}
|
||||||
onDeleteVideoClick={() => {
|
onDeleteVideoClick={() => {
|
||||||
handleChange(
|
handleChange(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const SnapshotRestore = ({
|
|||||||
>
|
>
|
||||||
<div className="bg-white p-4 w-140 rounded-lg flex flex-col gap-4 items-center">
|
<div className="bg-white p-4 w-140 rounded-lg flex flex-col gap-4 items-center">
|
||||||
<p className="text-black w-110 text-center">
|
<p className="text-black w-110 text-center">
|
||||||
Вы уверены, что хотите восстановить этот снапшот?
|
Вы уверены, что хотите восстановить этот экспорт медиа?
|
||||||
</p>
|
</p>
|
||||||
<p className="text-black w-100 text-center">
|
<p className="text-black w-100 text-center">
|
||||||
Это действие нельзя будет отменить.
|
Это действие нельзя будет отменить.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,12 +2,10 @@ import { defineConfig, type UserConfigExport } from "vite";
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import pkg from "./package.json";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react(), tailwindcss()],
|
||||||
react(),
|
|
||||||
tailwindcss(),
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@shared": path.resolve(__dirname, "src/shared"),
|
"@shared": path.resolve(__dirname, "src/shared"),
|
||||||
@@ -18,9 +16,11 @@ export default defineConfig({
|
|||||||
"@app": path.resolve(__dirname, "src/app"),
|
"@app": path.resolve(__dirname, "src/app"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 5000,
|
chunkSizeWarningLimit: 5000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user