feat: role system

This commit is contained in:
2026-03-18 20:11:07 +03:00
parent 73070fe233
commit c3127b8d47
47 changed files with 2425 additions and 768 deletions

View File

@@ -8,16 +8,40 @@ import {
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { cityStore, selectedCityStore } from "@shared";
import { authStore, cityStore, selectedCityStore, type City } from "@shared";
import { MapPin } from "lucide-react";
export const CitySelector: React.FC = observer(() => {
const { getCities, cities } = cityStore;
const { selectedCity, setSelectedCity } = selectedCityStore;
const canReadCities = authStore.canRead("cities");
useEffect(() => {
getCities("ru");
}, []);
if (canReadCities) {
cityStore.getCities("ru");
return;
}
authStore.fetchMeCities().catch(() => undefined);
}, [canReadCities]);
const baseCities: City[] = canReadCities
? cityStore.cities["ru"].data
: authStore.meCities["ru"].map((uc) => ({
id: uc.city_id,
name: uc.name,
country: "",
country_code: "",
arms: "",
}));
const currentCities: City[] = selectedCity?.id
? (() => {
const exists = baseCities.some((city) => city.id === selectedCity.id);
if (exists) {
return baseCities;
}
return [selectedCity, ...baseCities];
})()
: baseCities;
const handleCityChange = (event: SelectChangeEvent<string>) => {
const cityId = event.target.value;
@@ -26,14 +50,12 @@ export const CitySelector: React.FC = observer(() => {
return;
}
const city = cities["ru"].data.find((c) => c.id === Number(cityId));
const city = currentCities.find((c) => c.id === Number(cityId));
if (city) {
setSelectedCity(city);
}
};
const currentCities = cities["ru"].data;
return (
<Box className="flex items-center gap-2">
<MapPin size={16} className="text-white" />
@@ -51,16 +73,13 @@ export const CitySelector: React.FC = observer(() => {
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "rgba(255, 255, 255, 0.5)",
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
"&.Mui.focused .MuiOutlinedInput-notchedOutline": {
borderColor: "white",
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
MenuProps={{
PaperProps: {},
}}
>
<MenuItem value="">
<Typography variant="body2">Выберите город</Typography>

View File

@@ -0,0 +1,180 @@
import { Modal, vehicleStore } from "@shared";
import {
Box,
Button,
CircularProgress,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { useEffect } from "react";
interface VehicleSessionsModalProps {
open: boolean;
vehicleId: number | null;
tailNumber?: string | null;
onClose: () => void;
}
const formatDateTime = (value: string) => {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(date);
};
const formatDuration = (durationSeconds: number) => {
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) {
return "-";
}
const hours = Math.floor(durationSeconds / 3600);
const minutes = Math.floor((durationSeconds % 3600) / 60);
const seconds = durationSeconds % 60;
return [hours, minutes, seconds]
.map((part) => String(part).padStart(2, "0"))
.join(":");
};
export const VehicleSessionsModal = observer(
({ open, vehicleId, tailNumber, onClose }: VehicleSessionsModalProps) => {
const {
vehicleSessions,
vehicleSessionsLoading,
vehicleSessionsError,
getVehicleSessions,
} = vehicleStore;
useEffect(() => {
if (!open || vehicleId == null) return;
getVehicleSessions(vehicleId).catch(() => undefined);
}, [open, vehicleId, getVehicleSessions]);
const title =
tailNumber && tailNumber !== ""
? `Сессии ТО: ${tailNumber}`
: "Сессии ТО";
return (
<Modal
open={open}
onClose={onClose}
sx={{ width: "min(1080px, 95vw)", p: 3 }}
>
<div className="flex flex-col gap-4 max-h-[82vh]">
<Typography variant="h6" component="h2">
{title}
</Typography>
<Box sx={{ minHeight: 220 }}>
{vehicleSessionsLoading && (
<Box
sx={{
minHeight: 220,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress size={24} />
</Box>
)}
{!vehicleSessionsLoading && vehicleSessionsError && (
<Box
sx={{
minHeight: 220,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "error.main",
}}
>
{vehicleSessionsError}
</Box>
)}
{!vehicleSessionsLoading &&
!vehicleSessionsError &&
vehicleSessions &&
vehicleSessions.length === 0 && (
<Box
sx={{
minHeight: 220,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "text.secondary",
}}
>
По этому транспорту нет сессий ТО.
</Box>
)}
{!vehicleSessionsLoading &&
!vehicleSessionsError &&
vehicleSessions &&
vehicleSessions.length > 0 && (
<TableContainer
sx={{ maxHeight: "60vh", border: 1, borderColor: "divider" }}
>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Начало</TableCell>
<TableCell>Окончание</TableCell>
<TableCell align="right">Длительность</TableCell>
</TableRow>
</TableHead>
<TableBody>
{vehicleSessions.map((session) => (
<TableRow key={session.id} hover>
<TableCell>{session.id}</TableCell>
<TableCell>
{formatDateTime(session.started_at)}
</TableCell>
<TableCell>
{formatDateTime(session.ended_at)}
</TableCell>
<TableCell align="right">
{formatDuration(session.duration_seconds)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
<Button
variant="outlined"
color="inherit"
onClick={onClose}
fullWidth
>
Закрыть
</Button>
</div>
</Modal>
);
},
);

View File

@@ -1,11 +1,13 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import {
authStore,
authInstance,
devicesStore,
Modal,
snapshotStore,
vehicleStore,
routeStore,
Vehicle,
carrierStore,
selectedCityStore,
@@ -22,6 +24,7 @@ import {
RotateCcw,
ScrollText,
Trash2,
Wrench,
X,
} from "lucide-react";
import {
@@ -35,6 +38,7 @@ import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
import { DeleteModal } from "@widgets";
import { DeviceLogsModal } from "./DeviceLogsModal";
import { VehicleSessionsModal } from "./VehicleSessionsModal";
export type ConnectedDevice = string;
@@ -77,6 +81,13 @@ type RowData = {
snapshot_update_blocked: boolean;
maintenance_mode_on: boolean;
demo_mode_enabled: boolean;
current_route_id: number | null;
};
type PendingModeToggle = {
deviceUuid: string;
nextEnabled: boolean;
tailNumber: string;
};
function getVehicleTypeLabel(vehicle: Vehicle): string {
@@ -109,11 +120,13 @@ const transformToRows = (vehicles: Vehicle[]): RowData[] => {
snapshot_update_blocked: vehicle.vehicle.snapshot_update_blocked ?? false,
maintenance_mode_on: vehicle.vehicle.maintenance_mode_on ?? false,
demo_mode_enabled: vehicle.vehicle.demo_mode_enabled ?? false,
current_route_id: vehicle.device_status?.current_route_id ?? null,
};
});
};
export const DevicesTable = observer(() => {
const canWriteDevices = authStore.canWrite("devices");
const {
getDevices,
setSelectedDevice,
@@ -123,6 +136,7 @@ export const DevicesTable = observer(() => {
} = devicesStore;
const { snapshots, getSnapshots } = snapshotStore;
const { routes, getRoutes } = routeStore;
const {
getVehicles,
vehicles,
@@ -137,6 +151,12 @@ export const DevicesTable = observer(() => {
const [logsModalDeviceUuid, setLogsModalDeviceUuid] = useState<string | null>(
null,
);
const [sessionsModalOpen, setSessionsModalOpen] = useState(false);
const [sessionsModalVehicleId, setSessionsModalVehicleId] = useState<
number | null
>(null);
const [sessionsModalVehicleTailNumber, setSessionsModalVehicleTailNumber] =
useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [maintenanceLoadingUuids, setMaintenanceLoadingUuids] = useState<
Set<string>
@@ -144,6 +164,14 @@ export const DevicesTable = observer(() => {
const [demoLoadingUuids, setDemoLoadingUuids] = useState<Set<string>>(
new Set(),
);
const [maintenanceConfirm, setMaintenanceConfirm] =
useState<PendingModeToggle | null>(null);
const [demoConfirm, setDemoConfirm] = useState<PendingModeToggle | null>(
null,
);
const [maintenanceConfirmSubmitting, setMaintenanceConfirmSubmitting] =
useState(false);
const [demoConfirmSubmitting, setDemoConfirmSubmitting] = useState(false);
const [collapsedModels, setCollapsedModels] = useState<Set<string>>(
new Set(),
);
@@ -223,65 +251,108 @@ export const DevicesTable = observer(() => {
.map((r) => r.device_uuid as string);
}, [rows, selectedIds]);
const handleToggleMaintenanceMode = async (row: RowData) => {
if (!row.device_uuid) return;
const nextEnabled = !row.maintenance_mode_on;
const applyMaintenanceMode = async (toggle: PendingModeToggle) => {
setMaintenanceLoadingUuids((prev) => {
const next = new Set(prev);
next.add(row.device_uuid!);
next.add(toggle.deviceUuid);
return next;
});
try {
await setMaintenanceMode(row.device_uuid, nextEnabled);
await setMaintenanceMode(toggle.deviceUuid, toggle.nextEnabled);
await getVehicles();
await getDevices();
toast.success(
nextEnabled ? "Устройство отправлено на ТО" : "Режим ТО отключен",
toggle.nextEnabled
? "Устройство отправлено на ТО"
: "Режим ТО отключен",
);
} catch (error) {
console.error(
`Error toggling maintenance mode for ${row.device_uuid}:`,
`Error toggling maintenance mode for ${toggle.deviceUuid}:`,
error,
);
toast.error("Не удалось изменить режим ТО");
} finally {
setMaintenanceLoadingUuids((prev) => {
const next = new Set(prev);
next.delete(row.device_uuid!);
next.delete(toggle.deviceUuid);
return next;
});
}
};
const handleToggleDemoMode = async (row: RowData) => {
if (!row.device_uuid) return;
const nextEnabled = !row.demo_mode_enabled;
const applyDemoMode = async (toggle: PendingModeToggle) => {
setDemoLoadingUuids((prev) => {
const next = new Set(prev);
next.add(row.device_uuid!);
next.add(toggle.deviceUuid);
return next;
});
try {
await setDemoMode(row.device_uuid, nextEnabled);
await setDemoMode(toggle.deviceUuid, toggle.nextEnabled);
await getVehicles();
await getDevices();
toast.success(nextEnabled ? "Демо-режим включен" : "Демо-режим отключен");
toast.success(
toggle.nextEnabled ? "Демо-режим включен" : "Демо-режим отключен",
);
} catch (error) {
console.error(`Error toggling demo mode for ${row.device_uuid}:`, error);
console.error(
`Error toggling demo mode for ${toggle.deviceUuid}:`,
error,
);
toast.error("Не удалось изменить демо-режим");
} finally {
setDemoLoadingUuids((prev) => {
const next = new Set(prev);
next.delete(row.device_uuid!);
next.delete(toggle.deviceUuid);
return next;
});
}
};
const openMaintenanceConfirm = (row: RowData) => {
if (!row.device_uuid) return;
setMaintenanceConfirm({
deviceUuid: row.device_uuid,
nextEnabled: !row.maintenance_mode_on,
tailNumber: row.tail_number,
});
};
const openDemoConfirm = (row: RowData) => {
if (!row.device_uuid) return;
setDemoConfirm({
deviceUuid: row.device_uuid,
nextEnabled: !row.demo_mode_enabled,
tailNumber: row.tail_number,
});
};
const handleConfirmMaintenanceToggle = async () => {
if (!maintenanceConfirm) return;
setMaintenanceConfirmSubmitting(true);
try {
await applyMaintenanceMode(maintenanceConfirm);
setMaintenanceConfirm(null);
} finally {
setMaintenanceConfirmSubmitting(false);
}
};
const handleConfirmDemoToggle = async () => {
if (!demoConfirm) return;
setDemoConfirmSubmitting(true);
try {
await applyDemoMode(demoConfirm);
setDemoConfirm(null);
} finally {
setDemoConfirmSubmitting(false);
}
};
const columns: GridColDef[] = useMemo(
() => [
{
@@ -375,10 +446,14 @@ export const DevicesTable = observer(() => {
>
<Checkbox
checked={rowData.maintenance_mode_on}
disabled={!rowData.device_uuid || isMaintenanceLoading}
disabled={
!rowData.device_uuid ||
isMaintenanceLoading ||
maintenanceConfirmSubmitting
}
size="small"
sx={{ p: 0 }}
onChange={() => handleToggleMaintenanceMode(rowData)}
onChange={() => openMaintenanceConfirm(rowData)}
/>
</Box>
);
@@ -404,10 +479,12 @@ export const DevicesTable = observer(() => {
>
<Checkbox
checked={rowData.demo_mode_enabled}
disabled={!rowData.device_uuid || isDemoLoading}
disabled={
!rowData.device_uuid || isDemoLoading || demoConfirmSubmitting
}
size="small"
sx={{ p: 0 }}
onChange={() => handleToggleDemoMode(rowData)}
onChange={() => openDemoConfirm(rowData)}
/>
</Box>
);
@@ -436,6 +513,20 @@ export const DevicesTable = observer(() => {
return snapshot?.Name ?? uuid;
},
},
{
field: "current_route",
headerName: "Текущий маршрут",
flex: 1,
minWidth: 140,
filterable: true,
valueGetter: (_value, row) => {
const rowData = row as RowData;
const routeId = rowData.current_route_id;
if (!routeId) return "—";
const route = routes.data.find((r) => r.id === routeId);
return route?.route_number || "—";
},
},
{
field: "gps",
headerName: "GPS",
@@ -496,15 +587,17 @@ export const DevicesTable = observer(() => {
justifyContent: "center",
}}
>
<button
onClick={(e) => {
e.stopPropagation();
navigate(`/vehicle/${row.vehicle_id}/edit`);
}}
title="Редактировать транспорт"
>
<Pencil size={16} />
</button>
{canWriteDevices && (
<button
onClick={(e) => {
e.stopPropagation();
navigate(`/vehicle/${row.vehicle_id}/edit`);
}}
title="Редактировать транспорт"
>
<Pencil size={16} />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
@@ -529,6 +622,17 @@ export const DevicesTable = observer(() => {
>
<Copy size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
setSessionsModalVehicleId(row.vehicle_id);
setSessionsModalVehicleTailNumber(row.tail_number);
setSessionsModalOpen(true);
}}
title="Сессии ТО"
>
<Wrench size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
@@ -557,35 +661,44 @@ export const DevicesTable = observer(() => {
setLogsModalOpen,
maintenanceLoadingUuids,
demoLoadingUuids,
setMaintenanceMode,
setDemoMode,
handleToggleMaintenanceMode,
handleToggleDemoMode,
openMaintenanceConfirm,
openDemoConfirm,
maintenanceConfirmSubmitting,
demoConfirmSubmitting,
routes,
canWriteDevices,
],
);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
await getVehicles();
await getDevices();
await getSnapshots();
await Promise.all([
getVehicles(),
getDevices(),
getSnapshots(),
getRoutes(),
]);
setIsLoading(false);
};
fetchData();
}, [getDevices, getSnapshots, getVehicles]);
}, [getDevices, getSnapshots, getVehicles, getRoutes]);
useEffect(() => {
carrierStore.getCarriers("ru");
}, []);
const handleOpenSendSnapshotModal = () => {
if (!canWriteDevices) {
return;
}
if (selectedDeviceUuidsAllowed.length > 0) {
toggleSendSnapshotModal();
}
};
const handleSendSnapshotAction = async (snapshotId: string) => {
if (!canWriteDevices) return;
if (selectedDeviceUuidsAllowed.length === 0) return;
const blockedCount =
@@ -658,14 +771,16 @@ export const DevicesTable = observer(() => {
<>
<div className="w-full">
<div className="flex justify-end mb-5 gap-2 flex-wrap">
<Button
variant="contained"
color="primary"
size="small"
onClick={() => navigate("/vehicle/create")}
>
Добавить устройство
</Button>
{canWriteDevices && (
<Button
variant="contained"
color="primary"
size="small"
onClick={() => navigate("/vehicle/create")}
>
Добавить устройство
</Button>
)}
{selectedIds.length > 0 && (
<Button
variant="contained"
@@ -677,18 +792,21 @@ export const DevicesTable = observer(() => {
Удалить ({selectedIds.length})
</Button>
)}
<Button
variant="contained"
color="primary"
disabled={selectedDeviceUuidsAllowed.length === 0}
onClick={handleOpenSendSnapshotModal}
size="small"
>
Обновление ПО ({selectedDeviceUuidsAllowed.length}
{selectedDeviceUuids.length !== selectedDeviceUuidsAllowed.length &&
`/${selectedDeviceUuids.length}`}
)
</Button>
{canWriteDevices && (
<Button
variant="contained"
color="primary"
disabled={selectedDeviceUuidsAllowed.length === 0}
onClick={handleOpenSendSnapshotModal}
size="small"
>
Обновление ПО ({selectedDeviceUuidsAllowed.length}
{selectedDeviceUuids.length !==
selectedDeviceUuidsAllowed.length &&
`/${selectedDeviceUuids.length}`}
)
</Button>
)}
</div>
{groupsByModel.length === 0 ? (
@@ -787,52 +905,59 @@ export const DevicesTable = observer(() => {
)}
</div>
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
Обновление ПО
</Box>
<Box sx={{ mb: 2 }}>
Выбрано устройств для обновления:{" "}
<strong className="text-blue-600">
{selectedDeviceUuidsAllowed.length}
</strong>
{selectedDeviceUuids.length !== selectedDeviceUuidsAllowed.length && (
<span className="text-amber-600 ml-1">
(пропущено{" "}
{selectedDeviceUuids.length - selectedDeviceUuidsAllowed.length} с
блокировкой)
</span>
)}
</Box>
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
{snapshots && (snapshots as Snapshot[]).length > 0 ? (
(snapshots as Snapshot[]).map((snapshot) => (
<Button
variant="outlined"
fullWidth
onClick={() => handleSendSnapshotAction(snapshot.ID)}
key={snapshot.ID}
sx={{ justifyContent: "flex-start" }}
>
{snapshot.Name}
</Button>
))
) : (
<Box sx={{ typography: "body2", color: "text.secondary" }}>
Нет доступных экспортов медиа.
</Box>
)}
</div>
<Button
onClick={toggleSendSnapshotModal}
color="inherit"
variant="outlined"
sx={{ mt: 3 }}
fullWidth
{canWriteDevices && (
<Modal
open={sendSnapshotModalOpen}
onClose={toggleSendSnapshotModal}
sx={{ width: "min(760px, 94vw)", p: 3 }}
>
Отмена
</Button>
</Modal>
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
Обновление ПО
</Box>
<Box sx={{ mb: 2 }}>
Выбрано устройств для обновления:{" "}
<strong className="text-blue-600">
{selectedDeviceUuidsAllowed.length}
</strong>
{selectedDeviceUuids.length !==
selectedDeviceUuidsAllowed.length && (
<span className="text-amber-600 ml-1">
(пропущено{" "}
{selectedDeviceUuids.length - selectedDeviceUuidsAllowed.length}{" "}
с блокировкой)
</span>
)}
</Box>
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
{snapshots && (snapshots as Snapshot[]).length > 0 ? (
(snapshots as Snapshot[]).map((snapshot) => (
<Button
variant="outlined"
fullWidth
onClick={() => handleSendSnapshotAction(snapshot.ID)}
key={snapshot.ID}
sx={{ justifyContent: "flex-start" }}
>
{snapshot.Name}
</Button>
))
) : (
<Box sx={{ typography: "body2", color: "text.secondary" }}>
Нет доступных экспортов медиа.
</Box>
)}
</div>
<Button
onClick={toggleSendSnapshotModal}
color="inherit"
variant="outlined"
sx={{ mt: 3 }}
fullWidth
>
Отмена
</Button>
</Modal>
)}
<DeleteModal
open={isDeleteModalOpen}
@@ -840,6 +965,78 @@ export const DevicesTable = observer(() => {
onCancel={() => setIsDeleteModalOpen(false)}
/>
<Modal
open={maintenanceConfirm != null}
onClose={() => {
if (!maintenanceConfirmSubmitting) setMaintenanceConfirm(null);
}}
sx={{ width: "min(640px, 92vw)", p: 3 }}
>
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
Подтверждение режима ТО
</Box>
<Box sx={{ mb: 3 }}>
{maintenanceConfirm?.nextEnabled
? `Включить режим ТО для устройства ${maintenanceConfirm.tailNumber}?`
: `Отключить режим ТО для устройства ${maintenanceConfirm?.tailNumber}?`}
</Box>
<Box sx={{ display: "flex", gap: 1.5 }}>
<Button
fullWidth
variant="outlined"
color="inherit"
disabled={maintenanceConfirmSubmitting}
onClick={() => setMaintenanceConfirm(null)}
>
Отмена
</Button>
<Button
fullWidth
variant="contained"
disabled={maintenanceConfirmSubmitting}
onClick={handleConfirmMaintenanceToggle}
>
Подтвердить
</Button>
</Box>
</Modal>
<Modal
open={demoConfirm != null}
onClose={() => {
if (!demoConfirmSubmitting) setDemoConfirm(null);
}}
sx={{ width: "min(640px, 92vw)", p: 3 }}
>
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
Подтверждение демо-режима
</Box>
<Box sx={{ mb: 3 }}>
{demoConfirm?.nextEnabled
? `Включить демо-режим для устройства ${demoConfirm.tailNumber}?`
: `Отключить демо-режим для устройства ${demoConfirm?.tailNumber}?`}
</Box>
<Box sx={{ display: "flex", gap: 1.5 }}>
<Button
fullWidth
variant="outlined"
color="inherit"
disabled={demoConfirmSubmitting}
onClick={() => setDemoConfirm(null)}
>
Отмена
</Button>
<Button
fullWidth
variant="contained"
disabled={demoConfirmSubmitting}
onClick={handleConfirmDemoToggle}
>
Подтвердить
</Button>
</Box>
</Modal>
<DeviceLogsModal
open={logsModalOpen}
deviceUuid={logsModalDeviceUuid}
@@ -848,6 +1045,17 @@ export const DevicesTable = observer(() => {
setLogsModalDeviceUuid(null);
}}
/>
<VehicleSessionsModal
open={sessionsModalOpen}
vehicleId={sessionsModalVehicleId}
tailNumber={sessionsModalVehicleTailNumber}
onClose={() => {
setSessionsModalOpen(false);
setSessionsModalVehicleId(null);
setSessionsModalVehicleTailNumber(null);
}}
/>
</>
);
});

View File

@@ -8,7 +8,7 @@ import { AppBar } from "./ui/AppBar";
import { Drawer } from "./ui/Drawer";
import { DrawerHeader } from "./ui/DrawerHeader";
import { NavigationList } from "@features";
import { authStore, userStore, menuStore, isMediaIdEmpty } from "@shared";
import { authStore, menuStore, isMediaIdEmpty } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect } from "react";
import { Typography } from "@mui/material";
@@ -27,13 +27,10 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
setIsMenuOpen(open);
}, [open]);
const { getUsers, users } = userStore;
const { getMeAction, me } = authStore;
useEffect(() => {
const fetchUsers = async () => {
await getUsers();
};
fetchUsers();
getMeAction();
}, []);
const handleDrawerOpen = () => {
@@ -68,17 +65,13 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
<div className="flex gap-2 items-center">
{(() => {
const currentUser = users?.data?.find(
(user) => user.id === (authStore.payload as { user_id?: number })?.user_id,
);
const hasAvatar =
currentUser?.icon && !isMediaIdEmpty(currentUser.icon);
const hasAvatar = me?.icon && !isMediaIdEmpty(me.icon);
const token = localStorage.getItem("token");
return (
<>
<div className="flex flex-col gap-1">
<p className="text-white">{currentUser?.name}</p>
<p className="text-white">{me?.name}</p>
<div
className="text-center text-xs"
style={{
@@ -88,7 +81,7 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
padding: "2px 10px",
}}
>
{(authStore.payload as { is_admin?: boolean })?.is_admin
{me?.roles?.includes("admin")
? "Администратор"
: "Режим пользователя"}
</div>
@@ -98,7 +91,7 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
<img
src={`${
import.meta.env.VITE_KRBL_MEDIA
}${currentUser!.icon}/download?token=${token}`}
}${me?.icon}/download?token=${token}`}
alt="Аватар"
className="w-full h-full object-cover"
/>

View File

@@ -14,6 +14,7 @@ import {
BackButton,
TabPanel,
languageStore,
authStore,
Language,
cityStore,
isMediaIdEmpty,
@@ -40,7 +41,7 @@ import { SaveWithoutCityAgree } from "@widgets";
export const CreateInformationTab = observer(
({ value, index }: { value: number; index: number }) => {
const { cities } = cityStore;
const canReadCities = authStore.canRead("cities");
const [mediaId, setMediaId] = useState<string>("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
@@ -64,6 +65,30 @@ export const CreateInformationTab = observer(
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
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 =
sight.city_id && !baseCities.some((city) => city.id === sight.city_id)
? [
{
id: sight.city_id,
name: sight.city || `Город ${sight.city_id}`,
country: "",
country_code: "",
arms: "",
},
...baseCities,
]
: baseCities;
useEffect(() => {}, [hardcodeType]);
useEffect(() => {
@@ -208,17 +233,16 @@ export const CreateInformationTab = observer(
/>
<Autocomplete
options={cities["ru"]?.data ?? []}
options={availableCities}
value={
cities["ru"]?.data?.find(
(city) => city.id === sight.city_id
) ?? null
availableCities.find((city) => city.id === sight.city_id) ?? null
}
getOptionLabel={(option) => option.name}
onChange={(_, value) => {
setCity(value?.id ?? 0);
handleChange({
city_id: value?.id ?? 0,
city: value?.name ?? "",
});
}}
renderInput={(params) => (

View File

@@ -14,6 +14,7 @@ import {
BackButton,
TabPanel,
languageStore,
authStore,
Language,
cityStore,
editSightStore,
@@ -62,10 +63,35 @@ export const InformationTab = observer(
const [hardcodeType, setHardcodeType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
>(null);
const { cities } = cityStore;
const canReadCities = authStore.canRead("cities");
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
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 =
sight.common.city_id &&
!baseCities.some((city) => city.id === sight.common.city_id)
? [
{
id: sight.common.city_id,
name: sight.common.city || `Город ${sight.common.city_id}`,
country: "",
country_code: "",
arms: "",
},
...baseCities,
]
: baseCities;
useEffect(() => {}, [hardcodeType]);
useEffect(() => {
@@ -208,11 +234,9 @@ export const InformationTab = observer(
/>
<Autocomplete
options={cities["ru"]?.data ?? []}
options={availableCities}
value={
cities["ru"]?.data?.find(
(city) => city.id === sight.common.city_id
) ?? null
availableCities.find((city) => city.id === sight.common.city_id) ?? null
}
getOptionLabel={(option) => option.name}
onChange={(_, value) => {
@@ -221,6 +245,7 @@ export const InformationTab = observer(
language as Language,
{
city_id: value?.id ?? 0,
city: value?.name ?? "",
},
true
);