feat: big update 07.05.26
This commit is contained in:
@@ -6,8 +6,6 @@ import {
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
@@ -29,13 +27,12 @@ import {
|
||||
import { useState, useEffect } from "react";
|
||||
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
||||
|
||||
type ColorFields = { main_color: string; left_color: string; right_color: string; rgb_color: string };
|
||||
type ColorFields = { main_color: string; left_color: string; right_color: string };
|
||||
|
||||
const colorFields = (data: ColorFields) => ({
|
||||
main_color: data.main_color,
|
||||
left_color: data.left_color,
|
||||
right_color: data.right_color,
|
||||
rgb_color: data.rgb_color,
|
||||
});
|
||||
|
||||
const ColorPickerField = ({
|
||||
@@ -81,7 +78,6 @@ const ColorPickerField = ({
|
||||
);
|
||||
|
||||
export const CarrierCreatePage = observer(() => {
|
||||
const [colorMode, setColorMode] = useState<"rgb" | "three">("three");
|
||||
const navigate = useNavigate();
|
||||
const { createCarrierData, setCreateCarrierData } = carrierStore;
|
||||
const { language } = languageStore;
|
||||
@@ -274,51 +270,11 @@ export const CarrierCreatePage = observer(() => {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium">Режим цвета:</span>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
exclusive
|
||||
value={colorMode}
|
||||
onChange={(_, val) => {
|
||||
if (!val) return;
|
||||
setColorMode(val);
|
||||
if (val === "rgb") {
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ main_color: "", left_color: "", right_color: "", rgb_color: createCarrierData.rgb_color }
|
||||
);
|
||||
} else {
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ main_color: createCarrierData.main_color, left_color: createCarrierData.left_color, right_color: createCarrierData.right_color, rgb_color: "" }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="rgb">Один цвет</ToggleButton>
|
||||
<ToggleButton value="three">Три цвета</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">* при переключении цвет сбрасывается</span>
|
||||
</div>
|
||||
|
||||
{colorMode === "rgb" ? (
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<ColorPickerField
|
||||
label="Один цвет"
|
||||
value={createCarrierData.rgb_color}
|
||||
label="Основной цвет"
|
||||
value={createCarrierData.main_color}
|
||||
onChange={(val) =>
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
@@ -327,59 +283,54 @@ export const CarrierCreatePage = observer(() => {
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ ...colorFields(createCarrierData), rgb_color: val }
|
||||
{ ...colorFields(createCarrierData), main_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ColorPickerField
|
||||
label="Основной цвет"
|
||||
value={createCarrierData.main_color}
|
||||
onChange={(val) =>
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ ...colorFields(createCarrierData), main_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="Левый цвет"
|
||||
value={createCarrierData.left_color}
|
||||
onChange={(val) =>
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ ...colorFields(createCarrierData), left_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="Правый цвет"
|
||||
value={createCarrierData.right_color}
|
||||
onChange={(val) =>
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ ...colorFields(createCarrierData), right_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 pl-1">
|
||||
Используется в: виджет маршрута, виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<ColorPickerField
|
||||
label="Левый цвет"
|
||||
value={createCarrierData.left_color}
|
||||
onChange={(val) =>
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ ...colorFields(createCarrierData), left_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 pl-1">
|
||||
Используется в: боковое меню, левый виджет достопримечательности
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<ColorPickerField
|
||||
label="Правый цвет"
|
||||
value={createCarrierData.right_color}
|
||||
onChange={(val) =>
|
||||
setCreateCarrierData(
|
||||
createCarrierData[language].full_name,
|
||||
createCarrierData[language].short_name,
|
||||
createCarrierData.city_id,
|
||||
createCarrierData[language].slogan,
|
||||
selectedMediaId || "",
|
||||
language,
|
||||
{ ...colorFields(createCarrierData), right_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 pl-1">
|
||||
Используется в: список достопримечательностей, страница достопримечательности
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
@@ -32,13 +30,12 @@ import {
|
||||
UploadMediaDialog,
|
||||
} from "@shared";
|
||||
|
||||
type ColorFields = { main_color: string; left_color: string; right_color: string; rgb_color: string };
|
||||
type ColorFields = { main_color: string; left_color: string; right_color: string };
|
||||
|
||||
const colorFields = (data: ColorFields) => ({
|
||||
main_color: data.main_color,
|
||||
left_color: data.left_color,
|
||||
right_color: data.right_color,
|
||||
rgb_color: data.rgb_color,
|
||||
});
|
||||
|
||||
const ColorPickerField = ({
|
||||
@@ -90,7 +87,6 @@ export const CarrierEditPage = observer(() => {
|
||||
const { language } = languageStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
|
||||
const [colorMode, setColorMode] = useState<"rgb" | "three">("rgb");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
@@ -126,13 +122,7 @@ export const CarrierEditPage = observer(() => {
|
||||
main_color: carrierData.ru?.main_color || "",
|
||||
left_color: carrierData.ru?.left_color || "",
|
||||
right_color: carrierData.ru?.right_color || "",
|
||||
rgb_color: carrierData.ru?.rgb_color || "",
|
||||
};
|
||||
if (colors.rgb_color) {
|
||||
setColorMode("rgb");
|
||||
} else {
|
||||
setColorMode("three");
|
||||
}
|
||||
setEditCarrierData(
|
||||
carrierData.ru?.full_name || "",
|
||||
carrierData.ru?.short_name || "",
|
||||
@@ -339,51 +329,11 @@ export const CarrierEditPage = observer(() => {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium">Режим цвета:</span>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
exclusive
|
||||
value={colorMode}
|
||||
onChange={(_, val) => {
|
||||
if (!val) return;
|
||||
setColorMode(val);
|
||||
if (val === "rgb") {
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ main_color: "", left_color: "", right_color: "", rgb_color: editCarrierData.rgb_color }
|
||||
);
|
||||
} else {
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ main_color: editCarrierData.main_color, left_color: editCarrierData.left_color, right_color: editCarrierData.right_color, rgb_color: "" }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="rgb">Один цвет</ToggleButton>
|
||||
<ToggleButton value="three">Три цвета</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">* при переключении цвет сбрасывается</span>
|
||||
</div>
|
||||
|
||||
{colorMode === "rgb" ? (
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<ColorPickerField
|
||||
label="Один цвет"
|
||||
value={editCarrierData.rgb_color}
|
||||
label="Основной цвет"
|
||||
value={editCarrierData.main_color}
|
||||
onChange={(val) =>
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
@@ -392,59 +342,54 @@ export const CarrierEditPage = observer(() => {
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ ...colorFields(editCarrierData), rgb_color: val }
|
||||
{ ...colorFields(editCarrierData), main_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ColorPickerField
|
||||
label="Основной цвет"
|
||||
value={editCarrierData.main_color}
|
||||
onChange={(val) =>
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ ...colorFields(editCarrierData), main_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="Левый цвет"
|
||||
value={editCarrierData.left_color}
|
||||
onChange={(val) =>
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ ...colorFields(editCarrierData), left_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ColorPickerField
|
||||
label="Правый цвет"
|
||||
value={editCarrierData.right_color}
|
||||
onChange={(val) =>
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ ...colorFields(editCarrierData), right_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 pl-1">
|
||||
Используется в: виджет маршрута, виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<ColorPickerField
|
||||
label="Левый цвет"
|
||||
value={editCarrierData.left_color}
|
||||
onChange={(val) =>
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ ...colorFields(editCarrierData), left_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 pl-1">
|
||||
Используется в: боковое меню, левый виджет достопримечательности
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<ColorPickerField
|
||||
label="Правый цвет"
|
||||
value={editCarrierData.right_color}
|
||||
onChange={(val) =>
|
||||
setEditCarrierData(
|
||||
editCarrierData[language].full_name,
|
||||
editCarrierData[language].short_name,
|
||||
editCarrierData.city_id,
|
||||
editCarrierData[language].slogan,
|
||||
editCarrierData.logo,
|
||||
language,
|
||||
{ ...colorFields(editCarrierData), right_color: val }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 pl-1">
|
||||
Используется в: список достопримечательностей, страница достопримечательности
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||
|
||||
@@ -57,7 +57,7 @@ export const RouteCreatePage = observer(() => {
|
||||
const [turn, setTurn] = useState("");
|
||||
const [centerLat, setCenterLat] = useState("");
|
||||
const [centerLng, setCenterLng] = useState("");
|
||||
const [videoTimer, setVideoTimer] = useState(60);
|
||||
const [videoTimer, setVideoTimer] = useState(420);
|
||||
const [videoPreview, setVideoPreview] = useState<string>("");
|
||||
const [icon, setIcon] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -557,7 +557,7 @@ export const RouteEditPage = observer(() => {
|
||||
className="w-full"
|
||||
label="Таймер видео заставки (сек)"
|
||||
type="number"
|
||||
value={editRouteData.video_timer ?? 60}
|
||||
value={editRouteData.video_timer ?? 420}
|
||||
onChange={(e) => {
|
||||
const val = Math.max(1, Math.round(Number(e.target.value)));
|
||||
if (Number.isFinite(val)) {
|
||||
|
||||
@@ -139,7 +139,7 @@ export const RouteListPage = observer(() => {
|
||||
headerAlign: "center" as const,
|
||||
sortable: true,
|
||||
renderHeader: (params: any) => (
|
||||
<Tooltip title="Количество привязанных достопримечательностей">
|
||||
<Tooltip title="Отображает количество привязанных достопримечательностей">
|
||||
<span>{params.colDef.headerName}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -157,7 +157,7 @@ export const RouteListPage = observer(() => {
|
||||
headerAlign: "center" as const,
|
||||
sortable: true,
|
||||
renderHeader: (params: any) => (
|
||||
<Tooltip title="Количество привязанных остановок">
|
||||
<Tooltip title="Отображает количество привязанных остановок">
|
||||
<span>{params.colDef.headerName}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import { Button } from "@mui/material";
|
||||
import { useNavigate, useNavigationType } from "react-router";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
@@ -15,22 +15,22 @@ type LeftSidebarProps = {
|
||||
|
||||
export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
||||
const navigate = useNavigate();
|
||||
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||
const navigationType = useNavigationType();
|
||||
const { routeData } = useMapData();
|
||||
const [carrierThumbnail, setCarrierThumbnail] = useState<string | null>(null);
|
||||
const [carrierLogo, setCarrierLogo] = useState<string | null>(null);
|
||||
const [carrierSlogan, setCarrierSlogan] = useState<string | null>(null);
|
||||
const [carrierShortName, setCarrierShortName] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchCarrierThumbnail() {
|
||||
async function fetchCarrierData() {
|
||||
if (routeData?.carrier_id) {
|
||||
const { city_id, logo } = (
|
||||
await authInstance.get(`/carrier/${routeData.carrier_id}`)
|
||||
).data;
|
||||
const { arms } = (await authInstance.get(`/city/${city_id}`)).data;
|
||||
setCarrierThumbnail(arms);
|
||||
setCarrierLogo(logo);
|
||||
const carrier = (await authInstance.get(`/carrier/${routeData.carrier_id}`)).data;
|
||||
setCarrierLogo(carrier.logo);
|
||||
setCarrierSlogan(carrier.slogan ?? null);
|
||||
setCarrierShortName(carrier.short_name ?? null);
|
||||
}
|
||||
}
|
||||
fetchCarrierThumbnail();
|
||||
fetchCarrierData();
|
||||
}, [routeData?.carrier_id]);
|
||||
|
||||
const handleBack = () => {
|
||||
@@ -42,131 +42,162 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
color: "#fff",
|
||||
transition: "padding 0.3s ease",
|
||||
p: open ? 2 : 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "stretch",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="column"
|
||||
height="100%"
|
||||
width="100%"
|
||||
spacing={4}
|
||||
alignItems="stretch"
|
||||
justifyContent="space-between"
|
||||
sx={{
|
||||
{/* Кнопка назад — вне основного меню */}
|
||||
<div style={{ padding: "12px 12px 0" }}>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: "#222",
|
||||
color: "#fff",
|
||||
borderRadius: 1.5,
|
||||
px: 2,
|
||||
py: 1,
|
||||
"&:hover": { backgroundColor: "#2d2d2d" },
|
||||
}}
|
||||
fullWidth
|
||||
startIcon={<ArrowBackIcon />}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Основное меню — повторяет .side-menu */}
|
||||
<div
|
||||
style={{
|
||||
boxSizing: "border-box",
|
||||
paddingTop: 46,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
height: "calc(100% - 56px)",
|
||||
position: "relative",
|
||||
opacity: open ? 1 : 0,
|
||||
transition: "opacity 0.25s ease",
|
||||
pointerEvents: open ? "auto" : "none",
|
||||
display: open ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
backgroundColor: "#222",
|
||||
color: "#fff",
|
||||
borderRadius: 1.5,
|
||||
px: 2,
|
||||
py: 1,
|
||||
marginBottom: 10,
|
||||
"&:hover": {
|
||||
backgroundColor: "#2d2d2d",
|
||||
},
|
||||
{/* Герб — .side-menu-crest */}
|
||||
<div
|
||||
style={{
|
||||
width: 170,
|
||||
height: 170,
|
||||
alignSelf: "flex-start",
|
||||
marginLeft: 20,
|
||||
backgroundColor: "rgba(255,255,255,0.15)",
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Герб
|
||||
</div>
|
||||
|
||||
{/* Слоган — .side-menu-label */}
|
||||
{carrierSlogan && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "left",
|
||||
fontSize: 15,
|
||||
padding: "0 20px",
|
||||
alignSelf: "flex-start",
|
||||
fontWeight: 400,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
fullWidth
|
||||
startIcon={<ArrowBackIcon />}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
{carrierSlogan}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
spacing={3}
|
||||
{/* Кнопки — .side-menu-buttons */}
|
||||
<div style={{ width: 220, marginTop: 260 }}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
color: "#000",
|
||||
textAlign: "center",
|
||||
padding: "8px 16px",
|
||||
marginBottom: 16,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{carrierThumbnail && !isMediaIdEmpty(carrierThumbnail) && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierThumbnail,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
<Typography sx={{ color: "#fff" }} textAlign="center">
|
||||
При поддержке Правительства
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-2 mt-10">
|
||||
<button className="bg-[#fcd500] text-black px-4 py-2 rounded-md w-full font-medium my-10">
|
||||
Обращение губернатора
|
||||
</button>
|
||||
<button className="bg-white text-black px-4 py-2 rounded-md w-full font-medium mx-5">
|
||||
Достопримечательности
|
||||
</button>
|
||||
<button className="bg-white text-black px-4 py-2 rounded-md w-full font-medium mx-5">
|
||||
Остановки
|
||||
</button>
|
||||
Достопримечательности
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
color: "#000",
|
||||
textAlign: "center",
|
||||
padding: "8px 16px",
|
||||
marginBottom: 16,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
Остановки
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
maxHeight={150}
|
||||
justifyContent="center"
|
||||
flexGrow={1}
|
||||
{/* Нижняя секция — .side-menu-bottom-section */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierLogo,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail_logo",
|
||||
}}
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{/* .side-menu-carrier-block */}
|
||||
<div style={{ padding: "0 20px" }}>
|
||||
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
|
||||
<div style={{ width: 170 }}>
|
||||
<MediaViewer
|
||||
media={{ id: carrierLogo, media_type: 1, filename: "carrier_logo" }}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{carrierShortName && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
textAlign: "left",
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
lineHeight: "150%",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{carrierShortName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign="center"
|
||||
sx={{ color: "#fff", marginTop: "auto" }}
|
||||
>
|
||||
#ВсемПоПути
|
||||
</Typography>
|
||||
</Stack>
|
||||
{/* .side-menu-bottom-photo */}
|
||||
<img
|
||||
src="/side-menu-photo.png"
|
||||
alt=""
|
||||
style={{ width: "100%", marginTop: 32, display: "block", pointerEvents: "none" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[20px] -right-[520px] z-10">
|
||||
<LanguageSelector onBack={onToggle} isSidebarOpen={open} />
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
),
|
||||
rgba(179, 165, 152, 0.4);
|
||||
rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
pointer-events: auto;
|
||||
z-index: 10000001;
|
||||
|
||||
@@ -76,6 +76,26 @@ export const SnapshotListPage = observer(() => {
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "color",
|
||||
headerName: "",
|
||||
width: 28,
|
||||
sortable: false,
|
||||
disableColumnMenu: true,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 12,
|
||||
height: 12,
|
||||
backgroundColor: params.value,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Название",
|
||||
@@ -150,12 +170,13 @@ export const SnapshotListPage = observer(() => {
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
.map((snapshot) => ({
|
||||
.map((snapshot, index) => ({
|
||||
id: snapshot.ID,
|
||||
name: snapshot.Name,
|
||||
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
||||
created_at: formatCreationTime(snapshot.CreationTime),
|
||||
occupied_disk_space_gb: snapshot.occupied_disk_space_gb,
|
||||
color: SEGMENT_COLORS[index % SEGMENT_COLORS.length],
|
||||
}));
|
||||
}, [snapshots, searchQuery]);
|
||||
|
||||
@@ -181,7 +202,7 @@ export const SnapshotListPage = observer(() => {
|
||||
setIsEmptySnapshotModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Создать пустой снапшот
|
||||
Создать пустой экспорт
|
||||
</Button>
|
||||
)}
|
||||
{canCreateSnapshot && (
|
||||
@@ -203,7 +224,7 @@ export const SnapshotListPage = observer(() => {
|
||||
</div>
|
||||
|
||||
<div className="flex w-full h-3 rounded-lg overflow-hidden bg-gray-100">
|
||||
{rows.map((row, i) => {
|
||||
{rows.map((row) => {
|
||||
const pct =
|
||||
row.occupied_disk_space_gb != null && totalGB > 0
|
||||
? (row.occupied_disk_space_gb / totalGB) * 100
|
||||
@@ -214,8 +235,7 @@ export const SnapshotListPage = observer(() => {
|
||||
key={row.id}
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
backgroundColor:
|
||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
||||
backgroundColor: row.color,
|
||||
}}
|
||||
title={`${row.name}: ${row.occupied_disk_space_gb?.toFixed(1)} ГБ`}
|
||||
/>
|
||||
@@ -233,7 +253,7 @@ export const SnapshotListPage = observer(() => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3">
|
||||
{rows.map((row, i) => {
|
||||
{rows.map((row) => {
|
||||
if (row.occupied_disk_space_gb == null || row.occupied_disk_space_gb <= 0)
|
||||
return null;
|
||||
return (
|
||||
@@ -243,10 +263,7 @@ export const SnapshotListPage = observer(() => {
|
||||
>
|
||||
<span
|
||||
className="inline-block w-2.5 h-2.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
||||
}}
|
||||
style={{ backgroundColor: row.color }}
|
||||
/>
|
||||
{row.name}
|
||||
</div>
|
||||
@@ -325,7 +342,7 @@ export const SnapshotListPage = observer(() => {
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>Создать пустой снапшот</DialogTitle>
|
||||
<DialogTitle>Создать пустой экспорт</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
|
||||
@@ -86,13 +86,13 @@ export const StationListPage = observer(() => {
|
||||
},
|
||||
{
|
||||
field: "sightCount",
|
||||
headerName: "Достопримечательности",
|
||||
headerName: "Привязки",
|
||||
width: 180,
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: true,
|
||||
renderHeader: (params) => (
|
||||
<Tooltip title="Количество привязанных достопримечательностей">
|
||||
<Tooltip title="Отображает количество привязок">
|
||||
<span>{params.colDef.headerName}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -114,7 +114,7 @@ export const StationListPage = observer(() => {
|
||||
headerAlign: "center" as const,
|
||||
sortable: true,
|
||||
renderHeader: (params) => (
|
||||
<Tooltip title="Подтверждение добавленных пересадок">
|
||||
<Tooltip title="Отображает подтверждение добавленных пересадок">
|
||||
<span>{params.colDef.headerName}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
import { Button, Paper, TextField } from "@mui/material";
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Checkbox,
|
||||
Typography,
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -14,6 +29,40 @@ import {
|
||||
import { useState, useEffect } from "react";
|
||||
import { ImageUploadCard } from "@widgets";
|
||||
|
||||
const ROLE_RESOURCES = [
|
||||
{ key: "snapshot", label: "Экспорт" },
|
||||
{ key: "devices", label: "Устройства" },
|
||||
{ key: "vehicles", label: "Транспорт" },
|
||||
{ key: "users", label: "Пользователи" },
|
||||
{ key: "sights", label: "Достопримечательности" },
|
||||
{ key: "stations", label: "Остановки" },
|
||||
{ key: "routes", label: "Маршруты" },
|
||||
{ key: "countries", label: "Страны" },
|
||||
{ key: "cities", label: "Города" },
|
||||
{ key: "carriers", label: "Перевозчики" },
|
||||
] as const;
|
||||
|
||||
type PermissionLevel = "none" | "ro" | "rw";
|
||||
|
||||
function getPermissionLevel(roles: string[], resource: string): PermissionLevel {
|
||||
if (roles.includes(`${resource}_rw`)) return "rw";
|
||||
if (roles.includes(`${resource}_ro`)) return "ro";
|
||||
return "none";
|
||||
}
|
||||
|
||||
function applyPermissionChange(
|
||||
roles: string[],
|
||||
resource: string,
|
||||
level: PermissionLevel,
|
||||
): string[] {
|
||||
const filtered = roles.filter(
|
||||
(r) => r !== `${resource}_ro` && r !== `${resource}_rw`,
|
||||
);
|
||||
if (level === "ro") return [...filtered, `${resource}_ro`];
|
||||
if (level === "rw") return [...filtered, `${resource}_rw`];
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export const UserCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const { createUserData, setCreateUserData, createUser } = userStore;
|
||||
@@ -26,13 +75,33 @@ export const UserCreatePage = observer(() => {
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||
>(null);
|
||||
|
||||
const [localRoles, setLocalRoles] = useState<string[]>(
|
||||
createUserData.roles ?? ["articles_ro", "articles_rw", "media_ro", "media_rw"]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
mediaStore.getMedia();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const allRw = ROLE_RESOURCES.every(({ key }) => localRoles.includes(`${key}_rw`));
|
||||
const isAdmin = allRw && !localRoles.includes("devices_maintenance_rw");
|
||||
if (isAdmin !== createUserData.is_admin) {
|
||||
setCreateUserData(
|
||||
createUserData.name || "",
|
||||
createUserData.email || "",
|
||||
createUserData.password || "",
|
||||
isAdmin,
|
||||
createUserData.icon
|
||||
);
|
||||
}
|
||||
}, [localRoles]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Убеждаемся, что роли в сторе обновлены перед созданием
|
||||
userStore.createUserData.roles = localRoles;
|
||||
await createUser();
|
||||
toast.success("Пользователь успешно создан");
|
||||
navigate("/user");
|
||||
@@ -67,18 +136,15 @@ export const UserCreatePage = observer(() => {
|
||||
: selectedMedia?.id ?? createUserData.icon ?? null;
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
<Paper className="w-full p-6 flex flex-col gap-8">
|
||||
<button className="flex items-center gap-2 self-start" onClick={() => navigate(-1)}>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<Typography variant="h6">Основные данные</Typography>
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Имя"
|
||||
@@ -116,6 +182,7 @@ export const UserCreatePage = observer(() => {
|
||||
label="Пароль"
|
||||
value={createUserData.password || ""}
|
||||
required
|
||||
type="password"
|
||||
onChange={(e) =>
|
||||
setCreateUserData(
|
||||
createUserData.name || "",
|
||||
@@ -127,7 +194,7 @@ export const UserCreatePage = observer(() => {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px]">
|
||||
<ImageUploadCard
|
||||
title="Аватар"
|
||||
imageKey="thumbnail"
|
||||
@@ -156,23 +223,197 @@ export const UserCreatePage = observer(() => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
isLoading || !createUserData.name || !createUserData.password
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<Typography variant="h6">Права доступа</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setCreateUserData(
|
||||
createUserData.name || "",
|
||||
createUserData.email || "",
|
||||
createUserData.password || "",
|
||||
true,
|
||||
createUserData.icon
|
||||
);
|
||||
const next: string[] = [];
|
||||
for (const { key } of ROLE_RESOURCES) {
|
||||
next.push(`${key}_rw`);
|
||||
}
|
||||
next.push("snapshot_create");
|
||||
setLocalRoles(next);
|
||||
}}
|
||||
>
|
||||
Полный доступ (admin)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setCreateUserData(
|
||||
createUserData.name || "",
|
||||
createUserData.email || "",
|
||||
createUserData.password || "",
|
||||
false,
|
||||
createUserData.icon
|
||||
);
|
||||
setLocalRoles(["devices_ro", "vehicles_ro", "devices_maintenance_rw"]);
|
||||
}}
|
||||
>
|
||||
Администратор ТО
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ border: "1px solid", borderColor: "divider", borderRadius: 1 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ bgcolor: "action.hover" }}>
|
||||
<TableCell sx={{ fontWeight: 600, width: 220 }}>Ресурс</TableCell>
|
||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Нет доступа</TableCell>
|
||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение</TableCell>
|
||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение/Запись</TableCell>
|
||||
<TableCell align="center" sx={{ fontWeight: 600 }}>
|
||||
Доп. права
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ROLE_RESOURCES.map(({ key, label }) => {
|
||||
const level = getPermissionLevel(localRoles, key);
|
||||
const isSnapshotResource = key === "snapshot";
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
setLocalRoles((prev) => {
|
||||
let updated = applyPermissionChange(prev, key, val as PermissionLevel);
|
||||
|
||||
if (key === "devices") {
|
||||
updated = applyPermissionChange(
|
||||
updated,
|
||||
"vehicles",
|
||||
val as PermissionLevel,
|
||||
);
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const isDevicesResource = key === "devices";
|
||||
|
||||
const handleSnapshotCreateChange = (checked: boolean) => {
|
||||
if (!isSnapshotResource) {
|
||||
return;
|
||||
}
|
||||
setLocalRoles((prev) => {
|
||||
const withoutSnapshotCreate = prev.filter(
|
||||
(role) => role !== "snapshot_create"
|
||||
);
|
||||
return checked
|
||||
? [...withoutSnapshotCreate, "snapshot_create"]
|
||||
: withoutSnapshotCreate;
|
||||
});
|
||||
};
|
||||
|
||||
const handleMaintenanceChange = (checked: boolean) => {
|
||||
setLocalRoles((prev) => {
|
||||
const without = prev.filter((r) => r !== "devices_maintenance_rw");
|
||||
return checked ? [...without, "devices_maintenance_rw"] : without;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow key={key} hover>
|
||||
<TableCell>{label}</TableCell>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
<RadioGroup
|
||||
row
|
||||
value={level}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
||||
>
|
||||
<Radio value="none" size="small" />
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
{isSnapshotResource ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
-
|
||||
</Typography>
|
||||
) : (
|
||||
<RadioGroup
|
||||
row
|
||||
value={level}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
||||
>
|
||||
<Radio value="ro" size="small" />
|
||||
</RadioGroup>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
<RadioGroup
|
||||
row
|
||||
value={level}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
||||
>
|
||||
<Radio value="rw" size="small" />
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
{isSnapshotResource ? (
|
||||
<Checkbox
|
||||
checked={localRoles.includes("snapshot_create")}
|
||||
onChange={(e) =>
|
||||
handleSnapshotCreateChange(e.target.checked)
|
||||
}
|
||||
size="small"
|
||||
title="Разрешает создавать новые снапшоты"
|
||||
/>
|
||||
) : isDevicesResource ? (
|
||||
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "center" }}>
|
||||
<Checkbox
|
||||
checked={localRoles.includes("devices_maintenance_rw")}
|
||||
onChange={(e) => handleMaintenanceChange(e.target.checked)}
|
||||
size="small"
|
||||
title="Техническое обслуживание (ТО)"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</section>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="self-end w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
isLoading || !createUserData.name || !createUserData.password || !createUserData.email
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<SelectMediaDialog
|
||||
open={isSelectMediaOpen}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Paper,
|
||||
TextField,
|
||||
@@ -97,6 +96,20 @@ export const UserEditPage = observer(() => {
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const allRw = ROLE_RESOURCES.every(({ key }) => localRoles.includes(`${key}_rw`));
|
||||
const isAdmin = allRw && !localRoles.includes("devices_maintenance_rw");
|
||||
if (isAdmin !== editUserData.is_admin) {
|
||||
setEditUserData(
|
||||
editUserData.name || "",
|
||||
editUserData.email || "",
|
||||
editUserData.password || "",
|
||||
isAdmin,
|
||||
editUserData.icon || ""
|
||||
);
|
||||
}
|
||||
}, [localRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
@@ -311,35 +324,33 @@ export const UserEditPage = observer(() => {
|
||||
<section className="flex flex-col gap-4">
|
||||
<Typography variant="h6">Права доступа</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={localRoles.includes("admin")}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setLocalRoles((prev) => {
|
||||
let next = prev.filter((r) => r !== "admin");
|
||||
for (const { key } of ROLE_RESOURCES) {
|
||||
next = next.filter((r) => r !== `${key}_ro` && r !== `${key}_rw`);
|
||||
next.push(`${key}_rw`);
|
||||
}
|
||||
if (!next.includes("snapshot_create")) {
|
||||
next.push("snapshot_create");
|
||||
}
|
||||
if (!next.includes("devices_maintenance_rw")) {
|
||||
next.push("devices_maintenance_rw");
|
||||
}
|
||||
next.push("admin");
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setLocalRoles((prev) => prev.filter((r) => r !== "admin"));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Полный доступ (admin)"
|
||||
/>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditUserData(editUserData.name || "", editUserData.email || "", editUserData.password || "", true, editUserData.icon || "");
|
||||
const next: string[] = [];
|
||||
for (const { key } of ROLE_RESOURCES) {
|
||||
next.push(`${key}_rw`);
|
||||
}
|
||||
next.push("snapshot_create");
|
||||
setLocalRoles(next);
|
||||
}}
|
||||
>
|
||||
Полный доступ (admin)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditUserData(editUserData.name || "", editUserData.email || "", editUserData.password || "", false, editUserData.icon || "");
|
||||
setLocalRoles(["devices_ro", "vehicles_ro", "devices_maintenance_rw"]);
|
||||
}}
|
||||
>
|
||||
Администратор ТО
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ border: "1px solid", borderColor: "divider", borderRadius: 1 }}>
|
||||
<Table size="small">
|
||||
@@ -371,20 +382,6 @@ export const UserEditPage = observer(() => {
|
||||
);
|
||||
}
|
||||
|
||||
const allRw = ROLE_RESOURCES.every(({ key: k }) =>
|
||||
updated.includes(`${k}_rw`),
|
||||
);
|
||||
if (allRw && !updated.includes("admin")) {
|
||||
const next = [...updated];
|
||||
if (!next.includes("snapshot_create")) {
|
||||
next.push("snapshot_create");
|
||||
}
|
||||
next.push("admin");
|
||||
return next;
|
||||
}
|
||||
if (!allRw) {
|
||||
return updated.filter((r) => r !== "admin");
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
@@ -462,12 +459,14 @@ export const UserEditPage = observer(() => {
|
||||
title="Разрешает создавать новые снапшоты"
|
||||
/>
|
||||
) : isDevicesResource ? (
|
||||
<Checkbox
|
||||
checked={localRoles.includes("devices_maintenance_rw")}
|
||||
onChange={(e) => handleMaintenanceChange(e.target.checked)}
|
||||
size="small"
|
||||
title="Разрешает переводить устройства в режим технического обслуживания"
|
||||
/>
|
||||
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "center" }}>
|
||||
<Checkbox
|
||||
checked={localRoles.includes("devices_maintenance_rw")}
|
||||
onChange={(e) => handleMaintenanceChange(e.target.checked)}
|
||||
size="small"
|
||||
title="Техническое обслуживание (ТО)"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
-
|
||||
|
||||
Reference in New Issue
Block a user