feat: support custom maintenance mode duration
This commit was merged in pull request #22.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { authInstance, languageInstance, mobxFetch } from "@shared";
|
import { authInstance, languageInstance, mobxFetch } from "@shared";
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
import { getVehicleSessionsApi } from "./api";
|
import { getVehicleSessionsApi } from "./api";
|
||||||
import { Vehicle, VehicleMaintenanceSession } from "./types";
|
import { Vehicle, VehicleMaintenanceSession, SetMaintenanceModeResponse } from "./types";
|
||||||
|
|
||||||
export type { Vehicle, VehicleMaintenanceSession } from "./types";
|
export type { Vehicle, VehicleMaintenanceSession } from "./types";
|
||||||
|
|
||||||
@@ -218,23 +218,32 @@ class VehicleStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
setMaintenanceMode = async (uuid: string, enabled: boolean) => {
|
setMaintenanceMode = async (
|
||||||
const response = await authInstance.post(
|
uuid: string,
|
||||||
|
enabled: boolean,
|
||||||
|
durationSeconds?: number,
|
||||||
|
): Promise<SetMaintenanceModeResponse> => {
|
||||||
|
const response = await authInstance.post<SetMaintenanceModeResponse>(
|
||||||
`/devices/${uuid}/maintenance-mode`,
|
`/devices/${uuid}/maintenance-mode`,
|
||||||
{
|
{
|
||||||
enabled,
|
enabled,
|
||||||
|
...(enabled && durationSeconds !== undefined
|
||||||
|
? { duration_seconds: durationSeconds }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const normalizedVehicle = this.normalizeVehicleItem(response.data);
|
if (!response.data.saved) {
|
||||||
|
throw new Error("Maintenance mode was not saved");
|
||||||
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.mergeVehicleInCaches({
|
this.mergeVehicleInCaches({
|
||||||
...normalizedVehicle.vehicle,
|
|
||||||
uuid,
|
uuid,
|
||||||
maintenance_mode_on:
|
maintenance_mode_on: response.data.enabled,
|
||||||
normalizedVehicle.vehicle.maintenance_mode_on ?? enabled,
|
maintenance_mode_duration_seconds: response.data.duration_seconds,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
setDemoMode = async (uuid: string, enabled: boolean) => {
|
setDemoMode = async (uuid: string, enabled: boolean) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type Vehicle = {
|
|||||||
snapshot_update_blocked?: boolean;
|
snapshot_update_blocked?: boolean;
|
||||||
demo_mode_enabled?: boolean;
|
demo_mode_enabled?: boolean;
|
||||||
maintenance_mode_on?: boolean;
|
maintenance_mode_on?: boolean;
|
||||||
|
maintenance_mode_duration_seconds?: number;
|
||||||
city_id?: number;
|
city_id?: number;
|
||||||
};
|
};
|
||||||
device_status?: {
|
device_status?: {
|
||||||
@@ -21,13 +22,24 @@ export type Vehicle = {
|
|||||||
last_update: string;
|
last_update: string;
|
||||||
is_connected: boolean;
|
is_connected: boolean;
|
||||||
current_route_id?: number;
|
current_route_id?: number;
|
||||||
|
maintenance_mode_on?: boolean;
|
||||||
|
maintenance_mode_duration_seconds?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VehicleMaintenanceSession = {
|
export type VehicleMaintenanceSession = {
|
||||||
duration_seconds: number;
|
duration_seconds?: number | null;
|
||||||
ended_at: string;
|
ended_at?: string | null;
|
||||||
id: number;
|
id: number;
|
||||||
started_at: string;
|
started_at: string;
|
||||||
vehicle_id: number;
|
vehicle_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SetMaintenanceModeResponse = {
|
||||||
|
message: string;
|
||||||
|
enabled: boolean;
|
||||||
|
duration_seconds: number;
|
||||||
|
saved: boolean;
|
||||||
|
sent: boolean;
|
||||||
|
warning?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface VehicleSessionsModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDateTime = (value: string) => {
|
const formatDateTime = (value?: string | null) => {
|
||||||
if (!value) return "-";
|
if (!value) return "-";
|
||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
@@ -38,8 +38,8 @@ const formatDateTime = (value: string) => {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDuration = (durationSeconds: number) => {
|
const formatDuration = (durationSeconds?: number | null) => {
|
||||||
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) {
|
if (durationSeconds == null || !Number.isFinite(durationSeconds) || durationSeconds < 0) {
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +152,10 @@ export const VehicleSessionsModal = observer(
|
|||||||
{formatDateTime(session.started_at)}
|
{formatDateTime(session.started_at)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{formatDateTime(session.ended_at)}
|
{session.ended_at ? formatDateTime(session.ended_at) : "В процессе"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{formatDuration(session.duration_seconds)}
|
{session.ended_at ? formatDuration(session.duration_seconds) : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -34,12 +34,26 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
TextField,
|
||||||
|
MenuItem,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DeleteModal } from "@widgets";
|
import { DeleteModal } from "@widgets";
|
||||||
import { DeviceLogsModal } from "./DeviceLogsModal";
|
import { DeviceLogsModal } from "./DeviceLogsModal";
|
||||||
import { VehicleSessionsModal } from "./VehicleSessionsModal";
|
import { VehicleSessionsModal } from "./VehicleSessionsModal";
|
||||||
|
import { isAxiosError } from "axios";
|
||||||
|
|
||||||
|
const maintenanceDurations = [0, 900, 1800, 3600, 7200];
|
||||||
|
const MAX_MAINTENANCE_DURATION_SECONDS = 2_147_483_647;
|
||||||
|
const formatMaintenanceDuration = (seconds: number) => {
|
||||||
|
if (seconds === 0) return "Без автоматического отключения";
|
||||||
|
return [
|
||||||
|
Math.floor(seconds / 3600) ? `${Math.floor(seconds / 3600)} ч` : "",
|
||||||
|
Math.floor((seconds % 3600) / 60) ? `${Math.floor((seconds % 3600) / 60)} мин` : "",
|
||||||
|
seconds % 60 ? `${seconds % 60} с` : "",
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
export type ConnectedDevice = string;
|
export type ConnectedDevice = string;
|
||||||
|
|
||||||
@@ -81,6 +95,7 @@ type RowData = {
|
|||||||
current_snapshot_uuid: string | null;
|
current_snapshot_uuid: string | null;
|
||||||
snapshot_update_blocked: boolean;
|
snapshot_update_blocked: boolean;
|
||||||
maintenance_mode_on: boolean;
|
maintenance_mode_on: boolean;
|
||||||
|
maintenance_mode_duration_seconds: number;
|
||||||
demo_mode_enabled: boolean;
|
demo_mode_enabled: boolean;
|
||||||
current_route_id: number | null;
|
current_route_id: number | null;
|
||||||
};
|
};
|
||||||
@@ -120,6 +135,7 @@ const transformToRows = (vehicles: Vehicle[]): RowData[] => {
|
|||||||
current_snapshot_uuid: vehicle.vehicle.current_snapshot_uuid ?? null,
|
current_snapshot_uuid: vehicle.vehicle.current_snapshot_uuid ?? null,
|
||||||
snapshot_update_blocked: vehicle.vehicle.snapshot_update_blocked ?? false,
|
snapshot_update_blocked: vehicle.vehicle.snapshot_update_blocked ?? false,
|
||||||
maintenance_mode_on: vehicle.vehicle.maintenance_mode_on ?? false,
|
maintenance_mode_on: vehicle.vehicle.maintenance_mode_on ?? false,
|
||||||
|
maintenance_mode_duration_seconds: vehicle.vehicle.maintenance_mode_duration_seconds ?? 0,
|
||||||
demo_mode_enabled: vehicle.vehicle.demo_mode_enabled ?? false,
|
demo_mode_enabled: vehicle.vehicle.demo_mode_enabled ?? false,
|
||||||
current_route_id: vehicle.device_status?.current_route_id ?? null,
|
current_route_id: vehicle.device_status?.current_route_id ?? null,
|
||||||
};
|
};
|
||||||
@@ -169,6 +185,11 @@ export const DevicesTable = observer(() => {
|
|||||||
);
|
);
|
||||||
const [maintenanceConfirm, setMaintenanceConfirm] =
|
const [maintenanceConfirm, setMaintenanceConfirm] =
|
||||||
useState<PendingModeToggle | null>(null);
|
useState<PendingModeToggle | null>(null);
|
||||||
|
const [maintenanceDuration, setMaintenanceDuration] = useState(0);
|
||||||
|
const [customMaintenanceDuration, setCustomMaintenanceDuration] = useState(false);
|
||||||
|
const [customMaintenanceHours, setCustomMaintenanceHours] = useState(0);
|
||||||
|
const [customMaintenanceMinutes, setCustomMaintenanceMinutes] = useState(0);
|
||||||
|
const [savedMaintenanceDuration, setSavedMaintenanceDuration] = useState(0);
|
||||||
const [demoConfirm, setDemoConfirm] = useState<PendingModeToggle | null>(
|
const [demoConfirm, setDemoConfirm] = useState<PendingModeToggle | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -238,6 +259,7 @@ export const DevicesTable = observer(() => {
|
|||||||
}, [rows, selectedIds]);
|
}, [rows, selectedIds]);
|
||||||
|
|
||||||
const applyMaintenanceMode = async (toggle: PendingModeToggle) => {
|
const applyMaintenanceMode = async (toggle: PendingModeToggle) => {
|
||||||
|
if (!canMaintenanceDevices) return false;
|
||||||
setMaintenanceLoadingUuids((prev) => {
|
setMaintenanceLoadingUuids((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.add(toggle.deviceUuid);
|
next.add(toggle.deviceUuid);
|
||||||
@@ -245,20 +267,42 @@ export const DevicesTable = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setMaintenanceMode(toggle.deviceUuid, toggle.nextEnabled);
|
const result = await setMaintenanceMode(
|
||||||
await getVehicles();
|
toggle.deviceUuid,
|
||||||
await getDevices();
|
toggle.nextEnabled,
|
||||||
toast.success(
|
toggle.nextEnabled ? maintenanceDuration : undefined,
|
||||||
toggle.nextEnabled
|
|
||||||
? "Устройство отправлено на ТО"
|
|
||||||
: "Режим ТО отключен",
|
|
||||||
);
|
);
|
||||||
|
if (!result.sent) {
|
||||||
|
toast.warning(
|
||||||
|
result.enabled
|
||||||
|
? "Настройка сохранена. Режим ТО включится после подключения устройства."
|
||||||
|
: "Настройка сохранена. Режим ТО отключится после подключения устройства.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(result.enabled
|
||||||
|
? `Команда включения ТО отправлена. Длительность: ${formatMaintenanceDuration(result.duration_seconds)}`
|
||||||
|
: "Команда отключения ТО отправлена");
|
||||||
|
}
|
||||||
|
const refresh = await Promise.allSettled([getVehicles(), getDevices()]);
|
||||||
|
if (refresh.some((result) => result.status === "rejected")) {
|
||||||
|
toast.warning("Настройка ТО сохранена, но обновить таблицу не удалось. Обновите страницу.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error toggling maintenance mode for ${toggle.deviceUuid}:`,
|
`Error toggling maintenance mode for ${toggle.deviceUuid}:`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
toast.error("Не удалось изменить режим ТО");
|
const status = isAxiosError(error) ? error.response?.status : undefined;
|
||||||
|
const messages: Record<number, string> = {
|
||||||
|
400: "Некорректные параметры режима ТО",
|
||||||
|
401: "Войдите в систему, чтобы изменить режим ТО",
|
||||||
|
403: "Нет права на управление режимом ТО",
|
||||||
|
404: "Устройство не найдено",
|
||||||
|
500: "Сервер не смог сохранить настройку ТО",
|
||||||
|
};
|
||||||
|
toast.error((status && messages[status]) || "Не удалось изменить режим ТО");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setMaintenanceLoadingUuids((prev) => {
|
setMaintenanceLoadingUuids((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -298,7 +342,12 @@ export const DevicesTable = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openMaintenanceConfirm = (row: RowData) => {
|
const openMaintenanceConfirm = (row: RowData) => {
|
||||||
if (!row.device_uuid) return;
|
if (!row.device_uuid || !canMaintenanceDevices || maintenanceConfirmSubmitting) return;
|
||||||
|
setMaintenanceDuration(row.maintenance_mode_duration_seconds);
|
||||||
|
setCustomMaintenanceDuration(false);
|
||||||
|
setCustomMaintenanceHours(Math.floor(row.maintenance_mode_duration_seconds / 3600));
|
||||||
|
setCustomMaintenanceMinutes(Math.floor((row.maintenance_mode_duration_seconds % 3600) / 60));
|
||||||
|
setSavedMaintenanceDuration(row.maintenance_mode_duration_seconds);
|
||||||
setMaintenanceConfirm({
|
setMaintenanceConfirm({
|
||||||
deviceUuid: row.device_uuid,
|
deviceUuid: row.device_uuid,
|
||||||
nextEnabled: !row.maintenance_mode_on,
|
nextEnabled: !row.maintenance_mode_on,
|
||||||
@@ -316,12 +365,13 @@ export const DevicesTable = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmMaintenanceToggle = async () => {
|
const handleConfirmMaintenanceToggle = async () => {
|
||||||
if (!maintenanceConfirm) return;
|
if (!maintenanceConfirm || !canMaintenanceDevices || maintenanceConfirmSubmitting) return;
|
||||||
|
|
||||||
setMaintenanceConfirmSubmitting(true);
|
setMaintenanceConfirmSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await applyMaintenanceMode(maintenanceConfirm);
|
if (await applyMaintenanceMode(maintenanceConfirm)) {
|
||||||
setMaintenanceConfirm(null);
|
setMaintenanceConfirm(null);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setMaintenanceConfirmSubmitting(false);
|
setMaintenanceConfirmSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -441,9 +491,13 @@ export const DevicesTable = observer(() => {
|
|||||||
sx={{ display: "flex", justifyContent: "center", width: "100%" }}
|
sx={{ display: "flex", justifyContent: "center", width: "100%" }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
<Tooltip title={`Сохранённая длительность ТО: ${formatMaintenanceDuration(rowData.maintenance_mode_duration_seconds)}`}>
|
||||||
|
<span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={rowData.maintenance_mode_on}
|
checked={rowData.maintenance_mode_on}
|
||||||
|
inputProps={{ "aria-label": `Режим ТО: ${rowData.tail_number}` }}
|
||||||
disabled={
|
disabled={
|
||||||
|
!canMaintenanceDevices ||
|
||||||
!rowData.device_uuid ||
|
!rowData.device_uuid ||
|
||||||
isMaintenanceLoading ||
|
isMaintenanceLoading ||
|
||||||
maintenanceConfirmSubmitting
|
maintenanceConfirmSubmitting
|
||||||
@@ -452,6 +506,8 @@ export const DevicesTable = observer(() => {
|
|||||||
sx={{ p: 0 }}
|
sx={{ p: 0 }}
|
||||||
onChange={() => openMaintenanceConfirm(rowData)}
|
onChange={() => openMaintenanceConfirm(rowData)}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -694,6 +750,7 @@ export const DevicesTable = observer(() => {
|
|||||||
demoConfirmSubmitting,
|
demoConfirmSubmitting,
|
||||||
routes,
|
routes,
|
||||||
canWriteDevices,
|
canWriteDevices,
|
||||||
|
canMaintenanceDevices,
|
||||||
isMaintenanceOnly,
|
isMaintenanceOnly,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -708,7 +765,7 @@ export const DevicesTable = observer(() => {
|
|||||||
}
|
}
|
||||||
if (!canWriteDevices) {
|
if (!canWriteDevices) {
|
||||||
return columns.filter(
|
return columns.filter(
|
||||||
(c) => c.field !== "maintenance_mode_on" && c.field !== "demo_mode_enabled",
|
(c) => c.field !== "demo_mode_enabled",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
@@ -732,6 +789,29 @@ export const DevicesTable = observer(() => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [getDevices, getSnapshots, getVehicles, getRoutes, isMaintenanceOnly, selectedCityStore.cityVersion]);
|
}, [getDevices, getSnapshots, getVehicles, getRoutes, isMaintenanceOnly, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let refreshing = false;
|
||||||
|
const refreshMaintenance = async () => {
|
||||||
|
if (document.visibilityState !== "visible" || refreshing || maintenanceConfirmSubmitting) return;
|
||||||
|
refreshing = true;
|
||||||
|
try {
|
||||||
|
const results = await Promise.allSettled([getVehicles(), getDevices()]);
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
console.error("Failed to refresh maintenance state:", result.reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
refreshing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const timer = window.setInterval(refreshMaintenance, 15_000);
|
||||||
|
document.addEventListener("visibilitychange", refreshMaintenance);
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timer);
|
||||||
|
document.removeEventListener("visibilitychange", refreshMaintenance);
|
||||||
|
};
|
||||||
|
}, [getVehicles, getDevices, maintenanceConfirmSubmitting]);
|
||||||
|
|
||||||
const handleOpenSendSnapshotModal = () => {
|
const handleOpenSendSnapshotModal = () => {
|
||||||
if (!canWriteDevices) {
|
if (!canWriteDevices) {
|
||||||
@@ -1027,13 +1107,83 @@ export const DevicesTable = observer(() => {
|
|||||||
sx={{ width: "min(640px, 92vw)", p: 3 }}
|
sx={{ width: "min(640px, 92vw)", p: 3 }}
|
||||||
>
|
>
|
||||||
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
|
<Box component="h2" sx={{ mb: 1, typography: "h6" }}>
|
||||||
Подтверждение режима ТО
|
{maintenanceConfirm?.nextEnabled ? "Включить режим ТО" : "Отключить режим ТО"}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
{maintenanceConfirm?.nextEnabled
|
{maintenanceConfirm?.nextEnabled
|
||||||
? `Включить режим ТО для устройства ${maintenanceConfirm.tailNumber}?`
|
? `Включить режим ТО для устройства ${maintenanceConfirm.tailNumber}?`
|
||||||
: `Отключить режим ТО для устройства ${maintenanceConfirm?.tailNumber}?`}
|
: `Отключить режим ТО для устройства ${maintenanceConfirm?.tailNumber}?`}
|
||||||
</Box>
|
</Box>
|
||||||
|
{maintenanceConfirm?.nextEnabled && (
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
label="Длительность"
|
||||||
|
value={customMaintenanceDuration ? -1 : maintenanceDuration}
|
||||||
|
disabled={maintenanceConfirmSubmitting}
|
||||||
|
onChange={(event) => {
|
||||||
|
const seconds = Number(event.target.value);
|
||||||
|
if (seconds === -1) {
|
||||||
|
setCustomMaintenanceDuration(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCustomMaintenanceDuration(false);
|
||||||
|
setMaintenanceDuration(seconds);
|
||||||
|
}}
|
||||||
|
helperText="Автоотключение выполняется на устройстве после применения команды."
|
||||||
|
>
|
||||||
|
{maintenanceDurations.map((seconds) => (
|
||||||
|
<MenuItem key={seconds} value={seconds}>
|
||||||
|
{formatMaintenanceDuration(seconds)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
{!maintenanceDurations.includes(savedMaintenanceDuration) && (
|
||||||
|
<MenuItem value={savedMaintenanceDuration}>
|
||||||
|
{formatMaintenanceDuration(savedMaintenanceDuration)} (сохранено)
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem value={-1}>Своё время</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
{customMaintenanceDuration && (
|
||||||
|
<Box sx={{ display: "flex", gap: 1.5, mt: 2 }}>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
label="Часы"
|
||||||
|
value={customMaintenanceHours}
|
||||||
|
disabled={maintenanceConfirmSubmitting}
|
||||||
|
inputProps={{ min: 0, max: Math.floor(MAX_MAINTENANCE_DURATION_SECONDS / 3600), step: 1 }}
|
||||||
|
onChange={(event) => {
|
||||||
|
const hours = Math.max(0, Math.floor(Number(event.target.value) || 0));
|
||||||
|
setCustomMaintenanceHours(hours);
|
||||||
|
setMaintenanceDuration(hours * 3600 + customMaintenanceMinutes * 60);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
label="Минуты"
|
||||||
|
value={customMaintenanceMinutes}
|
||||||
|
disabled={maintenanceConfirmSubmitting}
|
||||||
|
inputProps={{ min: 0, max: 59, step: 1 }}
|
||||||
|
onChange={(event) => {
|
||||||
|
const minutes = Math.min(59, Math.max(0, Math.floor(Number(event.target.value) || 0)));
|
||||||
|
setCustomMaintenanceMinutes(minutes);
|
||||||
|
setMaintenanceDuration(customMaintenanceHours * 3600 + minutes * 60);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{customMaintenanceDuration && (maintenanceDuration < 60 || maintenanceDuration > MAX_MAINTENANCE_DURATION_SECONDS) && (
|
||||||
|
<Typography color="error" variant="caption" sx={{ display: "block", mt: 1 }}>
|
||||||
|
{maintenanceDuration < 60
|
||||||
|
? "Укажите длительность не меньше одной минуты."
|
||||||
|
: "Указанная длительность превышает допустимый максимум."}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box sx={{ display: "flex", gap: 1.5 }}>
|
<Box sx={{ display: "flex", gap: 1.5 }}>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -1047,10 +1197,15 @@ export const DevicesTable = observer(() => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={maintenanceConfirmSubmitting}
|
disabled={
|
||||||
|
maintenanceConfirmSubmitting ||
|
||||||
|
(Boolean(maintenanceConfirm?.nextEnabled) &&
|
||||||
|
customMaintenanceDuration &&
|
||||||
|
(maintenanceDuration < 60 || maintenanceDuration > MAX_MAINTENANCE_DURATION_SECONDS))
|
||||||
|
}
|
||||||
onClick={handleConfirmMaintenanceToggle}
|
onClick={handleConfirmMaintenanceToggle}
|
||||||
>
|
>
|
||||||
Подтвердить
|
{maintenanceConfirmSubmitting ? "Сохранение…" : maintenanceConfirm?.nextEnabled ? "Включить" : "Отключить"}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user