import { Button, FormControlLabel, Checkbox, 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"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { userStore, languageStore, LoadingSpinner, mediaStore, isMediaIdEmpty, 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, setEditUserRoles } = userStore; const canReadCities = authStore.canRead("cities"); const [localRoles, setLocalRoles] = useState([]); const [localCityIds, setLocalCityIds] = useState([]); const [initialUserCities, setInitialUserCities] = useState([]); const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const [mediaId, setMediaId] = useState(""); const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false); const [activeMenuType, setActiveMenuType] = useState< "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null >(null); useEffect(() => { languageStore.setLanguage("ru"); }, []); 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)); await userStore.addUserCityAction({ id: Number(id), city_ids: localCityIds, }); toast.success("Пользователь успешно обновлён"); navigate("/user"); } catch { toast.error("Ошибка при обновлении пользователя"); } finally { setIsLoading(false); } }; const handleMediaSelect = (media: { id: string; filename: string; media_name?: string; media_type: number; }) => { setEditUserData( editUserData.name || "", editUserData.email || "", editUserData.password || "", editUserData.is_admin || false, media.id, ); }; 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); const cityOptionsMap = new Map(); 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 ( ); } return ( {/* ── Основные данные ── */}
Основные данные setEditUserData( e.target.value, editUserData.email || "", editUserData.password || "", editUserData.is_admin || false, editUserData.icon, ) } /> setEditUserData( editUserData.name || "", e.target.value, editUserData.password || "", editUserData.is_admin || false, editUserData.icon, ) } /> setEditUserData( editUserData.name || "", editUserData.email || "", e.target.value, editUserData.is_admin || false, editUserData.icon, ) } />
{ setIsPreviewMediaOpen(true); setMediaId(effectiveIconUrl ?? ""); }} onDeleteImageClick={() => setIsDeleteIconModalOpen(true)} onSelectFileClick={() => { setActiveMenuType("image"); setIsSelectMediaOpen(true); }} setUploadMediaOpen={() => { setIsUploadMediaOpen(true); setActiveMenuType("image"); }} />
{/* ── Права доступа ── */}
Права доступа { 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)" /> Ресурс Нет доступа Чтение Чтение/Запись Создание (snapshot_create) {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 ( {label} handleChange(e.target.value)} sx={{ justifyContent: "center", flexWrap: "nowrap" }} > {isSnapshotResource ? ( - ) : ( handleChange(e.target.value)} sx={{ justifyContent: "center", flexWrap: "nowrap" }} > )} handleChange(e.target.value)} sx={{ justifyContent: "center", flexWrap: "nowrap" }} > {isSnapshotResource ? ( handleSnapshotCreateChange(e.target.checked) } size="small" /> ) : ( - )} ); })}
{/* ── Города ── */}
Города setLocalCityIds(ids as number[])} label="Города" placeholder="Выберите города" loading={canReadCities ? !cityStore.ruCities.loaded : isLoadingData} />
setIsSelectMediaOpen(false)} onSelectMedia={handleMediaSelect} mediaType={1} /> setIsUploadMediaOpen(false)} contextObjectName={editUserData.name || "Пользователь"} contextType="user" afterUpload={handleMediaSelect} hardcodeType={activeMenuType} /> setIsPreviewMediaOpen(false)} mediaId={mediaId} /> { setEditUserData( editUserData.name || "", editUserData.email || "", editUserData.password || "", editUserData.is_admin || false, "", ); setIsDeleteIconModalOpen(false); }} onCancel={() => setIsDeleteIconModalOpen(false)} edit />
); });