feat: role system
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { articlesStore, languageStore } from "@shared";
|
||||
import { authStore, articlesStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Trash2, Eye, Minus } from "lucide-react";
|
||||
@@ -51,13 +51,12 @@ export const ArticleListPage = observer(() => {
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
{authStore.canWrite("sights") && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -66,9 +65,9 @@ export const ArticleListPage = observer(() => {
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { toast } from "react-toastify";
|
||||
import {
|
||||
carrierStore,
|
||||
cityStore,
|
||||
authStore,
|
||||
mediaStore,
|
||||
languageStore,
|
||||
isMediaIdEmpty,
|
||||
@@ -30,7 +31,8 @@ export const CarrierCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const { createCarrierData, setCreateCarrierData } = carrierStore;
|
||||
const { language } = languageStore;
|
||||
const { selectedCityId } = useSelectedCity();
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
const { selectedCityId, selectedCity } = useSelectedCity();
|
||||
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
@@ -42,11 +44,37 @@ export const CarrierCreatePage = observer(() => {
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
cityStore.getCities("ru");
|
||||
const fetchCities = async () => {
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await cityStore.getCities("ru");
|
||||
return;
|
||||
}
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
};
|
||||
|
||||
fetchCities();
|
||||
mediaStore.getMedia();
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
const baseCities = canReadCities
|
||||
? cityStore.cities["ru"].data
|
||||
: authStore.meCities["ru"].map((city) => ({
|
||||
id: city.city_id,
|
||||
name: city.name,
|
||||
country: "",
|
||||
country_code: "",
|
||||
arms: "",
|
||||
}));
|
||||
|
||||
const availableCities =
|
||||
selectedCity?.id && !baseCities.some((city) => city.id === selectedCity.id)
|
||||
? [selectedCity, ...baseCities]
|
||||
: baseCities;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCityId && !createCarrierData.city_id) {
|
||||
setCreateCarrierData(
|
||||
@@ -134,7 +162,7 @@ export const CarrierCreatePage = observer(() => {
|
||||
)
|
||||
}
|
||||
>
|
||||
{cityStore.cities["ru"].data.map((city) => (
|
||||
{availableCities.map((city) => (
|
||||
<MenuItem key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</MenuItem>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { toast } from "react-toastify";
|
||||
import {
|
||||
carrierStore,
|
||||
cityStore,
|
||||
authStore,
|
||||
mediaStore,
|
||||
languageStore,
|
||||
isMediaIdEmpty,
|
||||
@@ -34,6 +35,7 @@ export const CarrierEditPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { getCarrier, setEditCarrierData, editCarrierData } = carrierStore;
|
||||
const { language } = languageStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
@@ -42,6 +44,7 @@ export const CarrierEditPage = observer(() => {
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
const [mediaId, setMediaId] = useState("");
|
||||
const [isDeleteLogoModalOpen, setIsDeleteLogoModalOpen] = useState(false);
|
||||
const [initialCityName, setInitialCityName] = useState("");
|
||||
const [activeMenuType, setActiveMenuType] = useState<
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||
>(null);
|
||||
@@ -54,9 +57,14 @@ export const CarrierEditPage = observer(() => {
|
||||
}
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await cityStore.getCities("ru");
|
||||
await cityStore.getCities("en");
|
||||
await cityStore.getCities("zh");
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await cityStore.getCities("ru");
|
||||
} else {
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
}
|
||||
const carrierData = await getCarrier(Number(id));
|
||||
|
||||
if (carrierData) {
|
||||
@@ -84,6 +92,7 @@ export const CarrierEditPage = observer(() => {
|
||||
carrierData.zh?.logo || "",
|
||||
"zh"
|
||||
);
|
||||
setInitialCityName(carrierData.ru?.city || "");
|
||||
}
|
||||
|
||||
await mediaStore.getMedia();
|
||||
@@ -132,6 +141,31 @@ export const CarrierEditPage = observer(() => {
|
||||
? null
|
||||
: (selectedMedia?.id ?? editCarrierData.logo);
|
||||
|
||||
const baseCities = canReadCities
|
||||
? cityStore.cities["ru"].data
|
||||
: authStore.meCities["ru"].map((city) => ({
|
||||
id: city.city_id,
|
||||
name: city.name,
|
||||
country: "",
|
||||
country_code: "",
|
||||
arms: "",
|
||||
}));
|
||||
|
||||
const availableCities =
|
||||
editCarrierData.city_id &&
|
||||
!baseCities.some((city) => city.id === editCarrierData.city_id)
|
||||
? [
|
||||
{
|
||||
id: editCarrierData.city_id,
|
||||
name: initialCityName || `Город ${editCarrierData.city_id}`,
|
||||
country: "",
|
||||
country_code: "",
|
||||
arms: "",
|
||||
},
|
||||
...baseCities,
|
||||
]
|
||||
: baseCities;
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
@@ -181,7 +215,7 @@ export const CarrierEditPage = observer(() => {
|
||||
)
|
||||
}
|
||||
>
|
||||
{cityStore.cities["ru"].data?.map((city) => (
|
||||
{availableCities.map((city) => (
|
||||
<MenuItem key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { carrierStore, cityStore, languageStore } from "@shared";
|
||||
import { authStore, carrierStore, cityStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||
@@ -10,7 +10,6 @@ import { Box, CircularProgress } from "@mui/material";
|
||||
|
||||
export const CarrierListPage = observer(() => {
|
||||
const { carriers, getCarriers, deleteCarrier } = carrierStore;
|
||||
const { getCities, cities } = cityStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||
@@ -22,13 +21,19 @@ export const CarrierListPage = observer(() => {
|
||||
pageSize: 50,
|
||||
});
|
||||
const { language } = languageStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
await getCities("ru");
|
||||
await getCities("en");
|
||||
await getCities("zh");
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await cityStore.getCities(language);
|
||||
} else {
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
}
|
||||
await getCarriers(language);
|
||||
setIsLoading(false);
|
||||
};
|
||||
@@ -73,56 +78,57 @@ export const CarrierListPage = observer(() => {
|
||||
headerName: "Город",
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const city = cities[language]?.data.find(
|
||||
(city) => city.id == params.value
|
||||
);
|
||||
const lang = language as "ru" | "en" | "zh";
|
||||
const cityName = canReadCities
|
||||
? cityStore.cities[lang]?.data.find((c) => c.id === params.value)?.name
|
||||
: authStore.meCities[lang]?.find((c) => c.city_id === params.value)?.name;
|
||||
return (
|
||||
<div className="w-full h-full flex items-center">
|
||||
{city && city.name ? (
|
||||
city.name
|
||||
) : (
|
||||
<Minus size={20} className="text-red-500" />
|
||||
)}
|
||||
{cityName ?? <Minus size={20} className="text-red-500" />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...(authStore.canWrite("carriers") ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
headerAlign: "center",
|
||||
headerAlign: "center" as const,
|
||||
width: 200,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
{/* <button onClick={() => navigate(`/carrier/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button> */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const rows = carriers[language].data?.map((carrier) => ({
|
||||
id: carrier.id,
|
||||
full_name: carrier.full_name,
|
||||
short_name: carrier.short_name,
|
||||
city_id: carrier.city_id,
|
||||
}));
|
||||
const allowedCityIds = canReadCities
|
||||
? null
|
||||
: authStore.meCities["ru"].map((c) => c.city_id);
|
||||
|
||||
const canWriteCarriers = authStore.canWrite("carriers");
|
||||
|
||||
const rows = carriers[language].data
|
||||
?.filter((carrier) =>
|
||||
!allowedCityIds || allowedCityIds.includes(carrier.city_id),
|
||||
)
|
||||
.map((carrier) => ({
|
||||
id: carrier.id,
|
||||
full_name: carrier.full_name,
|
||||
short_name: carrier.short_name,
|
||||
city_id: carrier.city_id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -130,10 +136,12 @@ export const CarrierListPage = observer(() => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Перевозчики</h1>
|
||||
<CreateButton label="Создать перевозчика" path="/carrier/create" />
|
||||
{canWriteCarriers && (
|
||||
<CreateButton label="Создать перевозчика" path="/carrier/create" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ids.length > 0 && (
|
||||
{canWriteCarriers && ids.length > 0 && (
|
||||
<div className="flex justify-end mb-5 duration-300">
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||
@@ -148,25 +156,33 @@ export const CarrierListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={canWriteCarriers}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
pageSizeOptions={[50]}
|
||||
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
|
||||
onRowSelectionModelChange={(newSelection: any) => {
|
||||
if (Array.isArray(newSelection)) {
|
||||
const selectedIds = newSelection.map((id: string | number) => Number(id));
|
||||
setIds(selectedIds);
|
||||
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) {
|
||||
const idsSet = newSelection.ids as Set<string | number>;
|
||||
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id));
|
||||
setIds(selectedIds);
|
||||
} else {
|
||||
setIds([]);
|
||||
}
|
||||
}}
|
||||
onRowSelectionModelChange={
|
||||
canWriteCarriers
|
||||
? (newSelection: any) => {
|
||||
if (Array.isArray(newSelection)) {
|
||||
const selectedIds = newSelection.map(Number);
|
||||
setIds(selectedIds);
|
||||
} else if (
|
||||
newSelection &&
|
||||
typeof newSelection === "object" &&
|
||||
"ids" in newSelection
|
||||
) {
|
||||
const idsSet = newSelection.ids as Set<string | number>;
|
||||
const selectedIds = Array.from(idsSet).map(Number);
|
||||
setIds(selectedIds);
|
||||
} else {
|
||||
setIds([]);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
slots={{
|
||||
noRowsOverlay: () => (
|
||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { languageStore, cityStore, countryStore } from "@shared";
|
||||
import { authStore, languageStore, cityStore, countryStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||
@@ -23,6 +23,7 @@ export const CityListPage = observer(() => {
|
||||
pageSize: 50,
|
||||
});
|
||||
const { language } = languageStore;
|
||||
const canWriteCities = authStore.canWrite("cities");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -91,35 +92,30 @@ export const CityListPage = observer(() => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...(authStore.canWrite("cities") ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
width: 200,
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
{/* <button onClick={() => navigate(`/city/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button> */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -129,7 +125,9 @@ export const CityListPage = observer(() => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Города</h1>
|
||||
<CreateButton label="Создать город" path="/city/create" />
|
||||
{canWriteCities && (
|
||||
<CreateButton label="Создать город" path="/city/create" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ids.length > 0 && (
|
||||
@@ -147,7 +145,7 @@ export const CityListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={authStore.canWrite("cities")}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { countryStore, languageStore } from "@shared";
|
||||
import { authStore, countryStore, languageStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Trash2, Minus } from "lucide-react";
|
||||
@@ -21,6 +21,7 @@ export const CountryListPage = observer(() => {
|
||||
pageSize: 50,
|
||||
});
|
||||
const { language } = languageStore;
|
||||
const canWriteCountries = authStore.canWrite("countries");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCountries = async () => {
|
||||
@@ -48,37 +49,27 @@ export const CountryListPage = observer(() => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...(authStore.canWrite("countries") ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
width: 200,
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
{/* <button
|
||||
onClick={() => navigate(`/country/${params.row.code}/edit`)}
|
||||
>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button> */}
|
||||
{/* <button onClick={() => navigate(`/country/${params.row.code}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button> */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.code);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.code);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const rows = countries[language]?.data.map((country) => ({
|
||||
@@ -94,7 +85,9 @@ export const CountryListPage = observer(() => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Страны</h1>
|
||||
<CreateButton label="Добавить страну" path="/country/add" />
|
||||
{canWriteCountries && (
|
||||
<CreateButton label="Добавить страну" path="/country/add" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ids.length > 0 && (
|
||||
@@ -112,7 +105,7 @@ export const CountryListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows || []}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={authStore.canWrite("countries")}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Tab, Tabs } from "@mui/material";
|
||||
import {
|
||||
articlesStore,
|
||||
authStore,
|
||||
cityStore,
|
||||
createSightStore,
|
||||
languageStore,
|
||||
@@ -40,7 +41,14 @@ export const CreateSightPage = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await getCities("ru");
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await getCities("ru");
|
||||
} else {
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
}
|
||||
await getArticles(languageStore.language);
|
||||
};
|
||||
fetchData();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import {
|
||||
articlesStore,
|
||||
authStore,
|
||||
cityStore,
|
||||
editSightStore,
|
||||
LoadingSpinner,
|
||||
@@ -41,7 +42,14 @@ export const EditSightPage = observer(() => {
|
||||
if (id) {
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await getCities("ru");
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await getCities("ru");
|
||||
} else {
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
}
|
||||
await getSightInfo(+id, "ru");
|
||||
await getSightInfo(+id, "en");
|
||||
await getSightInfo(+id, "zh");
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
import * as React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export const MainPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ marginBottom: 2 }}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus
|
||||
non enim praesent elementum facilisis leo vel. Risus at ultrices mi
|
||||
tempus imperdiet. Semper risus in hendrerit gravida rutrum quisque non
|
||||
tellus. Convallis convallis tellus id interdum velit laoreet id donec
|
||||
ultrices. Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl
|
||||
suscipit adipiscing bibendum est ultricies integer quis. Cursus euismod
|
||||
quis viverra nibh cras. Metus vulputate eu scelerisque felis imperdiet
|
||||
proin fermentum leo. Mauris commodo quis imperdiet massa tincidunt. Cras
|
||||
tincidunt lobortis feugiat vivamus at augue. At augue eget arcu dictum
|
||||
varius duis at consectetur lorem. Velit sed ullamcorper morbi tincidunt.
|
||||
Lorem donec massa sapien faucibus et molestie ac.
|
||||
</Typography>
|
||||
<Typography sx={{ marginBottom: 2 }}>
|
||||
Consequat mauris nunc congue nisi vitae suscipit. Fringilla est
|
||||
ullamcorper eget nulla facilisi etiam dignissim diam. Pulvinar elementum
|
||||
integer enim neque volutpat ac tincidunt. Ornare suspendisse sed nisi
|
||||
lacus sed viverra tellus. Purus sit amet volutpat consequat mauris.
|
||||
Elementum eu facilisis sed odio morbi. Euismod lacinia at quis risus sed
|
||||
vulputate odio. Morbi tincidunt ornare massa eget egestas purus viverra
|
||||
accumsan in. In hendrerit gravida rutrum quisque non tellus orci ac.
|
||||
Pellentesque nec nam aliquam sem et tortor. Habitant morbi tristique
|
||||
senectus et. Adipiscing elit duis tristique sollicitudin nibh sit.
|
||||
Ornare aenean euismod elementum nisi quis eleifend. Commodo viverra
|
||||
maecenas accumsan lacus vel facilisis. Nulla posuere sollicitudin
|
||||
aliquam ultrices sagittis orci a.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
||||
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2, Minus } from "lucide-react";
|
||||
@@ -71,16 +71,15 @@ export const MediaListPage = observer(() => {
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 200,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
{authStore.canWrite("sights") && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -89,9 +88,9 @@ export const MediaListPage = observer(() => {
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -119,7 +118,7 @@ export const MediaListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={authStore.canWrite("sights")}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { carrierStore, languageStore, routeStore } from "@shared";
|
||||
import { authStore, carrierStore, languageStore, routeStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Map, Pencil, Trash2, Minus } from "lucide-react";
|
||||
@@ -108,27 +108,37 @@ export const RouteListPage = observer(() => {
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 250,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const canWrite = authStore.canWrite("routes");
|
||||
const canShowRoutePreview =
|
||||
authStore.canRead("stations") &&
|
||||
authStore.canRead("sights") &&
|
||||
authStore.canRead("routes");
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||
<Map size={20} className="text-purple-500" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
{canWrite && (
|
||||
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
{canShowRoutePreview && (
|
||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||
<Map size={20} className="text-purple-500" />
|
||||
</button>
|
||||
)}
|
||||
{canWrite && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -168,7 +178,7 @@ export const RouteListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={authStore.canWrite("routes")}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import {
|
||||
authStore,
|
||||
cityStore,
|
||||
languageStore,
|
||||
sightsStore,
|
||||
@@ -15,7 +16,6 @@ import { Box, CircularProgress } from "@mui/material";
|
||||
|
||||
export const SightListPage = observer(() => {
|
||||
const { sights, getSights, deleteListSight } = sightsStore;
|
||||
const { cities, getCities } = cityStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||
@@ -27,13 +27,20 @@ export const SightListPage = observer(() => {
|
||||
pageSize: 50,
|
||||
});
|
||||
const { language } = languageStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSights = async () => {
|
||||
setIsLoading(true);
|
||||
await getCities(language);
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await cityStore.getCities(language);
|
||||
} else {
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
}
|
||||
await getSights();
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchSights();
|
||||
@@ -61,54 +68,59 @@ export const SightListPage = observer(() => {
|
||||
headerName: "Город",
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const lang = language as "ru" | "en" | "zh";
|
||||
const cityName = canReadCities
|
||||
? cityStore.cities[lang]?.data.find((c) => c.id === params.value)?.name
|
||||
: authStore.meCities[lang]?.find((c) => c.city_id === params.value)?.name;
|
||||
return (
|
||||
<div className="w-full h-full flex items-center">
|
||||
{params.value ? (
|
||||
cities[language].data.find((el) => el.id == params.value)?.name
|
||||
) : (
|
||||
<Minus size={20} className="text-red-500" />
|
||||
)}
|
||||
{cityName ?? <Minus size={20} className="text-red-500" />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...(authStore.canWrite("sights") ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
{/* <button onClick={() => navigate(`/sight/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button> */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const filteredSights = useMemo(() => {
|
||||
const { selectedCityId } = selectedCityStore;
|
||||
if (!selectedCityId) {
|
||||
return sights;
|
||||
}
|
||||
return sights.filter((sight: any) => sight.city_id === selectedCityId);
|
||||
}, [sights, selectedCityStore.selectedCityId]);
|
||||
const allowedCityIds = canReadCities
|
||||
? null
|
||||
: authStore.meCities["ru"].map((c) => c.city_id);
|
||||
|
||||
return sights.filter((sight: any) => {
|
||||
if (allowedCityIds && !allowedCityIds.includes(sight.city_id)) {
|
||||
return false;
|
||||
}
|
||||
if (selectedCityId && sight.city_id !== selectedCityId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
|
||||
|
||||
const canWriteSights = authStore.canWrite("sights");
|
||||
|
||||
const rows = filteredSights.map((sight) => ({
|
||||
id: sight.id,
|
||||
@@ -123,13 +135,15 @@ export const SightListPage = observer(() => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Достопримечательности</h1>
|
||||
<CreateButton
|
||||
label="Создать достопримечательность"
|
||||
path="/sight/create"
|
||||
/>
|
||||
{canWriteSights && (
|
||||
<CreateButton
|
||||
label="Создать достопримечательность"
|
||||
path="/sight/create"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ids.length > 0 && (
|
||||
{canWriteSights && ids.length > 0 && (
|
||||
<div className="flex justify-end mb-5 duration-300">
|
||||
<button
|
||||
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
|
||||
@@ -144,25 +158,33 @@ export const SightListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={canWriteSights}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
pageSizeOptions={[50]}
|
||||
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
|
||||
onRowSelectionModelChange={(newSelection: any) => {
|
||||
if (Array.isArray(newSelection)) {
|
||||
const selectedIds = newSelection.map((id: string | number) => Number(id));
|
||||
setIds(selectedIds);
|
||||
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) {
|
||||
const idsSet = newSelection.ids as Set<string | number>;
|
||||
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id));
|
||||
setIds(selectedIds);
|
||||
} else {
|
||||
setIds([]);
|
||||
}
|
||||
}}
|
||||
onRowSelectionModelChange={
|
||||
canWriteSights
|
||||
? (newSelection: any) => {
|
||||
if (Array.isArray(newSelection)) {
|
||||
const selectedIds = newSelection.map(Number);
|
||||
setIds(selectedIds);
|
||||
} else if (
|
||||
newSelection &&
|
||||
typeof newSelection === "object" &&
|
||||
"ids" in newSelection
|
||||
) {
|
||||
const idsSet = newSelection.ids as Set<string | number>;
|
||||
const selectedIds = Array.from(idsSet).map(Number);
|
||||
setIds(selectedIds);
|
||||
} else {
|
||||
setIds([]);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
slots={{
|
||||
noRowsOverlay: () => (
|
||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { languageStore, snapshotStore } from "@shared";
|
||||
import { authStore, languageStore, snapshotStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DatabaseBackup, Trash2 } from "lucide-react";
|
||||
@@ -10,6 +10,9 @@ import { Box, CircularProgress } from "@mui/material";
|
||||
export const SnapshotListPage = observer(() => {
|
||||
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
|
||||
snapshotStore;
|
||||
const canWriteDevices = authStore.canWrite("devices");
|
||||
const canCreateSnapshot = authStore.hasRole("snapshot_create") && canWriteDevices;
|
||||
const canManageSnapshots = authStore.canWrite("snapshot") && canWriteDevices;
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null);
|
||||
@@ -57,37 +60,33 @@ export const SnapshotListPage = observer(() => {
|
||||
return <div>{params.value ? params.value : "-"}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
...(canManageSnapshots ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 300,
|
||||
headerAlign: "center",
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRestoreModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<DatabaseBackup size={20} className="text-blue-500" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRestoreModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<DatabaseBackup size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const rows = snapshots.map((snapshot) => ({
|
||||
@@ -102,7 +101,9 @@ export const SnapshotListPage = observer(() => {
|
||||
<div style={{ width: "100%" }}>
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
||||
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
||||
{canCreateSnapshot && (
|
||||
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
||||
)}
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
stationsStore,
|
||||
languageStore,
|
||||
cityStore,
|
||||
authStore,
|
||||
mediaStore,
|
||||
isMediaIdEmpty,
|
||||
useSelectedCity,
|
||||
@@ -39,7 +40,8 @@ export const StationCreatePage = observer(() => {
|
||||
createStation,
|
||||
setLanguageCreateStationData,
|
||||
} = stationsStore;
|
||||
const { cities, getCities } = cityStore;
|
||||
const { getCities } = cityStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
const { selectedCityId, selectedCity } = useSelectedCity();
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
@@ -104,15 +106,35 @@ export const StationCreatePage = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCities = async () => {
|
||||
await getCities("ru");
|
||||
await getCities("en");
|
||||
await getCities("zh");
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await getCities("ru");
|
||||
return;
|
||||
}
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
};
|
||||
|
||||
fetchCities();
|
||||
mediaStore.getMedia();
|
||||
}, []);
|
||||
|
||||
const baseCities = canReadCities
|
||||
? cityStore.cities["ru"].data
|
||||
: authStore.meCities["ru"].map((city) => ({
|
||||
id: city.city_id,
|
||||
name: city.name,
|
||||
country: "",
|
||||
country_code: "",
|
||||
arms: "",
|
||||
}));
|
||||
|
||||
const availableCities =
|
||||
selectedCity?.id && !baseCities.some((city) => city.id === selectedCity.id)
|
||||
? [selectedCity, ...baseCities]
|
||||
: baseCities;
|
||||
|
||||
const handleMediaSelect = (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
@@ -229,7 +251,7 @@ export const StationCreatePage = observer(() => {
|
||||
value={createStationData.common.city_id || ""}
|
||||
label="Город"
|
||||
onChange={(e) => {
|
||||
const selectedCity = cities["ru"].data.find(
|
||||
const selectedCity = availableCities.find(
|
||||
(city) => city.id === e.target.value
|
||||
);
|
||||
setCreateCommonData({
|
||||
@@ -238,7 +260,7 @@ export const StationCreatePage = observer(() => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{cities["ru"].data.map((city) => (
|
||||
{availableCities.map((city) => (
|
||||
<MenuItem key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</MenuItem>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
stationsStore,
|
||||
languageStore,
|
||||
cityStore,
|
||||
authStore,
|
||||
mediaStore,
|
||||
isMediaIdEmpty,
|
||||
LoadingSpinner,
|
||||
@@ -44,7 +45,8 @@ export const StationEditPage = observer(() => {
|
||||
editStation,
|
||||
setLanguageEditStationData,
|
||||
} = stationsStore;
|
||||
const { cities, getCities } = cityStore;
|
||||
const { getCities } = cityStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
@@ -138,9 +140,14 @@ export const StationEditPage = observer(() => {
|
||||
try {
|
||||
const stationId = Number(id);
|
||||
await getEditStation(stationId);
|
||||
await getCities("ru");
|
||||
await getCities("en");
|
||||
await getCities("zh");
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
if (authStore.canRead("cities")) {
|
||||
await getCities("ru");
|
||||
} else {
|
||||
await authStore.fetchMeCities().catch(() => undefined);
|
||||
}
|
||||
await mediaStore.getMedia();
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
@@ -150,6 +157,31 @@ export const StationEditPage = observer(() => {
|
||||
fetchAndSetStationData();
|
||||
}, [id]);
|
||||
|
||||
const baseCities = canReadCities
|
||||
? cityStore.cities["ru"].data
|
||||
: authStore.meCities["ru"].map((city) => ({
|
||||
id: city.city_id,
|
||||
name: city.name,
|
||||
country: "",
|
||||
country_code: "",
|
||||
arms: "",
|
||||
}));
|
||||
|
||||
const availableCities =
|
||||
editStationData.common.city_id &&
|
||||
!baseCities.some((city) => city.id === editStationData.common.city_id)
|
||||
? [
|
||||
{
|
||||
id: editStationData.common.city_id,
|
||||
name: editStationData.common.city || `Город ${editStationData.common.city_id}`,
|
||||
country: "",
|
||||
country_code: "",
|
||||
arms: "",
|
||||
},
|
||||
...baseCities,
|
||||
]
|
||||
: baseCities;
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
@@ -255,7 +287,7 @@ export const StationEditPage = observer(() => {
|
||||
value={editStationData.common.city_id || ""}
|
||||
label="Город"
|
||||
onChange={(e) => {
|
||||
const selectedCity = cities["ru"].data.find(
|
||||
const selectedCity = availableCities.find(
|
||||
(city) => city.id === e.target.value
|
||||
);
|
||||
setEditCommonData({
|
||||
@@ -264,7 +296,7 @@ export const StationEditPage = observer(() => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{cities["ru"].data.map((city) => (
|
||||
{availableCities.map((city) => (
|
||||
<MenuItem key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import {
|
||||
authStore,
|
||||
languageStore,
|
||||
stationsStore,
|
||||
selectedCityStore,
|
||||
cityStore,
|
||||
} from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Pencil, Trash2, Minus, Route } from "lucide-react";
|
||||
import { Pencil, Trash2, Minus, Route } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CreateButton,
|
||||
@@ -35,11 +35,11 @@ export const StationListPage = observer(() => {
|
||||
pageSize: 50,
|
||||
});
|
||||
const { language } = languageStore;
|
||||
const canWriteStations = authStore.canWrite("stations");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStations = async () => {
|
||||
setIsLoading(true);
|
||||
await cityStore.getCities(language);
|
||||
await getStationList();
|
||||
setIsLoading(false);
|
||||
};
|
||||
@@ -83,36 +83,38 @@ export const StationListPage = observer(() => {
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 200,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedStationId(params.row.id);
|
||||
setIsTransfersModalOpen(true);
|
||||
}}
|
||||
title="Редактировать пересадки"
|
||||
>
|
||||
<Route size={20} className="text-purple-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
{canWriteStations && (
|
||||
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
{canWriteStations && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedStationId(params.row.id);
|
||||
setIsTransfersModalOpen(true);
|
||||
}}
|
||||
title="Редактировать пересадки"
|
||||
>
|
||||
<Route size={20} className="text-purple-500" />
|
||||
</button>
|
||||
)}
|
||||
{canWriteStations && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -142,7 +144,9 @@ export const StationListPage = observer(() => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Станции</h1>
|
||||
<CreateButton label="Создать остановки" path="/station/create" />
|
||||
{canWriteStations && (
|
||||
<CreateButton label="Создать остановки" path="/station/create" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-5 duration-300">
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
} from "@mui/material";
|
||||
import { Button, Paper, TextField } from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -133,26 +127,6 @@ export const UserCreatePage = observer(() => {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col items-start">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={createUserData.is_admin || false}
|
||||
onChange={(e) => {
|
||||
setCreateUserData(
|
||||
createUserData.name || "",
|
||||
createUserData.email || "",
|
||||
createUserData.password || "",
|
||||
e.target.checked,
|
||||
createUserData.icon
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Администратор"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||
<ImageUploadCard
|
||||
title="Аватар"
|
||||
|
||||
@@ -5,6 +5,15 @@ import {
|
||||
Paper,
|
||||
TextField,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
@@ -19,17 +28,61 @@ import {
|
||||
SelectMediaDialog,
|
||||
UploadMediaDialog,
|
||||
PreviewMediaDialog,
|
||||
authStore,
|
||||
cityStore,
|
||||
MultiSelect,
|
||||
type User,
|
||||
type UserCity,
|
||||
} from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ImageUploadCard, DeleteModal } 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 UserEditPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
|
||||
const { id } = useParams();
|
||||
const { editUserData, editUser, getUser, setEditUserData } = userStore;
|
||||
const { editUserData, editUser, getUser, setEditUserData, setEditUserRoles } = userStore;
|
||||
const canReadCities = authStore.canRead("cities");
|
||||
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [localCityIds, setLocalCityIds] = useState<number[]>([]);
|
||||
const [initialUserCities, setInitialUserCities] = useState<UserCity[]>([]);
|
||||
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
@@ -44,13 +97,65 @@ export const UserEditPage = observer(() => {
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
const handleEdit = async () => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
if (!authStore.me) {
|
||||
await authStore.getMeAction().catch(() => undefined);
|
||||
}
|
||||
await Promise.all([
|
||||
mediaStore.getMedia(),
|
||||
authStore.canRead("cities")
|
||||
? cityStore.getRuCities()
|
||||
: authStore.fetchMeCities().catch(() => undefined),
|
||||
]);
|
||||
const data = (await getUser(Number(id))) as User | undefined;
|
||||
|
||||
if (data) {
|
||||
setEditUserData(
|
||||
data.name || "",
|
||||
data.email || "",
|
||||
data.password || "",
|
||||
data.is_admin || false,
|
||||
data.icon || "",
|
||||
);
|
||||
|
||||
const roles = data.roles ?? [];
|
||||
setLocalRoles(roles);
|
||||
setEditUserRoles(roles);
|
||||
|
||||
const cityIds = (data.cities ?? []).map((c) => c.city_id);
|
||||
setLocalCityIds(cityIds);
|
||||
setInitialUserCities(data.cities ?? []);
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const mandatoryRoles = ["articles_ro", "articles_rw", "media_ro", "media_rw"];
|
||||
const rolesToSave = Array.from(new Set([...localRoles, ...mandatoryRoles]));
|
||||
setEditUserRoles(rolesToSave);
|
||||
await editUser(Number(id));
|
||||
toast.success("Пользователь успешно обновлен");
|
||||
|
||||
await userStore.addUserCityAction({
|
||||
id: Number(id),
|
||||
city_ids: localCityIds,
|
||||
});
|
||||
|
||||
toast.success("Пользователь успешно обновлён");
|
||||
navigate("/user");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error("Ошибка при обновлении пользователя");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -68,43 +173,43 @@ export const UserEditPage = observer(() => {
|
||||
editUserData.email || "",
|
||||
editUserData.password || "",
|
||||
editUserData.is_admin || false,
|
||||
media.id
|
||||
media.id,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await mediaStore.getMedia();
|
||||
const data = await getUser(Number(id));
|
||||
|
||||
if (data) {
|
||||
setEditUserData(
|
||||
data.name || "",
|
||||
data.email || "",
|
||||
data.password || "",
|
||||
data.is_admin || false,
|
||||
data.icon || ""
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
}, [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;
|
||||
: (selectedMedia?.id ?? editUserData.icon ?? null);
|
||||
|
||||
const cityOptionsMap = new Map<number, string>();
|
||||
|
||||
const sourceCities: UserCity[] = canReadCities
|
||||
? cityStore.ruCities.data
|
||||
.filter((city) => city.id !== undefined)
|
||||
.map((city) => ({
|
||||
city_id: city.id as number,
|
||||
name: city.name,
|
||||
}))
|
||||
: authStore.meCities.ru;
|
||||
|
||||
for (const city of sourceCities) {
|
||||
cityOptionsMap.set(city.city_id, city.name);
|
||||
}
|
||||
|
||||
for (const city of initialUserCities) {
|
||||
if (!cityOptionsMap.has(city.city_id)) {
|
||||
cityOptionsMap.set(city.city_id, city.name);
|
||||
}
|
||||
}
|
||||
|
||||
const cityOptions = Array.from(cityOptionsMap.entries()).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
@@ -122,18 +227,16 @@ export const UserEditPage = observer(() => {
|
||||
}
|
||||
|
||||
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-start">
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Имя"
|
||||
@@ -145,7 +248,7 @@ export const UserEditPage = observer(() => {
|
||||
editUserData.email || "",
|
||||
editUserData.password || "",
|
||||
editUserData.is_admin || false,
|
||||
editUserData.icon
|
||||
editUserData.icon,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -160,11 +263,10 @@ export const UserEditPage = observer(() => {
|
||||
e.target.value,
|
||||
editUserData.password || "",
|
||||
editUserData.is_admin || false,
|
||||
editUserData.icon
|
||||
editUserData.icon,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Пароль"
|
||||
@@ -176,27 +278,10 @@ export const UserEditPage = observer(() => {
|
||||
editUserData.email || "",
|
||||
e.target.value,
|
||||
editUserData.is_admin || false,
|
||||
editUserData.icon
|
||||
editUserData.icon,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={editUserData.is_admin || false}
|
||||
onChange={(e) =>
|
||||
setEditUserData(
|
||||
editUserData.name || "",
|
||||
editUserData.email || "",
|
||||
editUserData.password || "",
|
||||
e.target.checked,
|
||||
editUserData.icon
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Администратор"
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-4 max-w-[300px]">
|
||||
<ImageUploadCard
|
||||
@@ -218,21 +303,189 @@ export const UserEditPage = observer(() => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="w-min flex gap-2 items-center self-end"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleEdit}
|
||||
disabled={isLoading || !editUserData.name || !editUserData.email}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Сохранить"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
{/* ── Права доступа ── */}
|
||||
<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");
|
||||
}
|
||||
next.push("admin");
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setLocalRoles((prev) => prev.filter((r) => r !== "admin"));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Полный доступ (admin)"
|
||||
/>
|
||||
|
||||
<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 }}>
|
||||
Создание (snapshot_create)
|
||||
</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,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSnapshotCreateChange = (checked: boolean) => {
|
||||
if (!isSnapshotResource) {
|
||||
return;
|
||||
}
|
||||
setLocalRoles((prev) => {
|
||||
const withoutSnapshotCreate = prev.filter(
|
||||
(role) => role !== "snapshot_create"
|
||||
);
|
||||
return checked
|
||||
? [...withoutSnapshotCreate, "snapshot_create"]
|
||||
: withoutSnapshotCreate;
|
||||
});
|
||||
};
|
||||
|
||||
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"
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── Города ── */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<Typography variant="h6">Города</Typography>
|
||||
<MultiSelect
|
||||
options={cityOptions}
|
||||
value={localCityIds}
|
||||
onChange={(ids) => setLocalCityIds(ids as number[])}
|
||||
label="Города"
|
||||
placeholder="Выберите города"
|
||||
loading={canReadCities ? !cityStore.ruCities.loaded : isLoadingData}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
className="self-end"
|
||||
startIcon={isLoading ? <Loader2 size={20} className="animate-spin" /> : <Save size={20} />}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || !editUserData.name || !editUserData.email}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
|
||||
<SelectMediaDialog
|
||||
open={isSelectMediaOpen}
|
||||
@@ -240,7 +493,6 @@ export const UserEditPage = observer(() => {
|
||||
onSelectMedia={handleMediaSelect}
|
||||
mediaType={1}
|
||||
/>
|
||||
|
||||
<UploadMediaDialog
|
||||
open={isUploadMediaOpen}
|
||||
onClose={() => setIsUploadMediaOpen(false)}
|
||||
@@ -249,13 +501,11 @@ export const UserEditPage = observer(() => {
|
||||
afterUpload={handleMediaSelect}
|
||||
hardcodeType={activeMenuType}
|
||||
/>
|
||||
|
||||
<PreviewMediaDialog
|
||||
open={isPreviewMediaOpen}
|
||||
onClose={() => setIsPreviewMediaOpen(false)}
|
||||
mediaId={mediaId}
|
||||
/>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteIconModalOpen}
|
||||
onDelete={() => {
|
||||
@@ -264,7 +514,7 @@ export const UserEditPage = observer(() => {
|
||||
editUserData.email || "",
|
||||
editUserData.password || "",
|
||||
editUserData.is_admin || false,
|
||||
""
|
||||
"",
|
||||
);
|
||||
setIsDeleteIconModalOpen(false);
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { userStore } from "@shared";
|
||||
import { authStore, userStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||
@@ -20,6 +20,7 @@ export const UserListPage = observer(() => {
|
||||
page: 0,
|
||||
pageSize: 50,
|
||||
});
|
||||
const canWriteUsers = authStore.canWrite("users");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
@@ -81,44 +82,35 @@ export const UserListPage = observer(() => {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
...(canWriteUsers ? [{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button>
|
||||
<Pencil
|
||||
size={20}
|
||||
className="text-blue-500"
|
||||
onClick={() => {
|
||||
navigate(`/user/${params.row.id}/edit`);
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/user/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const rows = users.data?.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
is_admin: user.is_admin,
|
||||
is_admin: user.is_admin || (user.roles ?? []).includes("admin"),
|
||||
name: user.name,
|
||||
}));
|
||||
|
||||
@@ -127,7 +119,9 @@ export const UserListPage = observer(() => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Пользователи</h1>
|
||||
<CreateButton label="Создать пользователя" path="/user/create" />
|
||||
{canWriteUsers && (
|
||||
<CreateButton label="Создать пользователя" path="/user/create" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ids.length > 0 && (
|
||||
@@ -145,7 +139,7 @@ export const UserListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={canWriteUsers}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { ruRU } from "@mui/x-data-grid/locales";
|
||||
import { carrierStore, languageStore, vehicleStore } from "@shared";
|
||||
import { authStore, carrierStore, languageStore, vehicleStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
||||
@@ -104,27 +104,31 @@ export const VehicleListPage = observer(() => {
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 200,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
align: "center" as const,
|
||||
headerAlign: "center" as const,
|
||||
sortable: false,
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const canWrite = authStore.canWrite("devices");
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
{canWrite && (
|
||||
<button onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
{canWrite && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -167,7 +171,7 @@ export const VehicleListPage = observer(() => {
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
checkboxSelection={authStore.canWrite("devices")}
|
||||
disableRowSelectionExcludeModel
|
||||
loading={isLoading}
|
||||
paginationModel={paginationModel}
|
||||
|
||||
Reference in New Issue
Block a user