Add Map page
with basic logic
This commit is contained in:
@ -11,15 +11,25 @@ import {
|
||||
devicesStore,
|
||||
Modal,
|
||||
snapshotStore,
|
||||
vehicleStore,
|
||||
} from "@shared";
|
||||
vehicleStore, // Not directly used in this component's rendering logic anymore
|
||||
} from "@shared"; // Assuming @shared exports these
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Button, Checkbox, Typography } from "@mui/material"; // Import Typography for the modal message
|
||||
import { Button, Checkbox, Typography } from "@mui/material";
|
||||
import { Vehicle } from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const formatDate = (dateString: string | undefined) => {
|
||||
export type ConnectedDevice = string;
|
||||
|
||||
interface Snapshot {
|
||||
ID: string; // Assuming ID is string based on usage
|
||||
Name: string;
|
||||
// Add other snapshot properties if needed
|
||||
}
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return "Нет данных";
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
@ -32,244 +42,368 @@ const formatDate = (dateString: string | undefined) => {
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return "Некорректная дата";
|
||||
}
|
||||
};
|
||||
|
||||
type TableRowData = {
|
||||
tail_number: number;
|
||||
online: boolean;
|
||||
lastUpdate: string | null;
|
||||
gps: boolean;
|
||||
media: boolean;
|
||||
connection: boolean;
|
||||
device_uuid: string | null;
|
||||
};
|
||||
function createData(
|
||||
uuid: string,
|
||||
tail_number: number,
|
||||
online: boolean,
|
||||
lastUpdate: string,
|
||||
lastUpdate: string | null,
|
||||
gps: boolean,
|
||||
media: boolean,
|
||||
connection: boolean
|
||||
) {
|
||||
return { uuid, online, lastUpdate, gps, media, connection };
|
||||
connection: boolean,
|
||||
device_uuid: string | null
|
||||
): TableRowData {
|
||||
return {
|
||||
tail_number,
|
||||
online,
|
||||
lastUpdate,
|
||||
gps,
|
||||
media,
|
||||
connection,
|
||||
device_uuid,
|
||||
};
|
||||
}
|
||||
|
||||
// Keep the rows function as you provided it, without additional filters
|
||||
const rows = (vehicles: any[]) => {
|
||||
// This function transforms the raw device data (which includes vehicle and device_status)
|
||||
// into the format expected by the table. It now filters for devices that have a UUID.
|
||||
const transformDevicesToRows = (
|
||||
vehicles: Vehicle[]
|
||||
// devices: ConnectedDevice[]
|
||||
): TableRowData[] => {
|
||||
return vehicles.map((vehicle) => {
|
||||
const uuid = vehicle.vehicle.uuid;
|
||||
if (!uuid)
|
||||
return {
|
||||
tail_number: vehicle.vehicle.tail_number,
|
||||
online: false,
|
||||
lastUpdate: null,
|
||||
gps: false,
|
||||
media: false,
|
||||
connection: false,
|
||||
device_uuid: null,
|
||||
};
|
||||
return createData(
|
||||
vehicle?.vehicle?.tail_number ?? "1243000", // Using tail_number as UUID, as in your original code
|
||||
vehicle?.device_status?.online ?? false,
|
||||
vehicle?.device_status?.last_update,
|
||||
vehicle?.device_status?.gps_ok,
|
||||
vehicle?.device_status?.media_service_ok,
|
||||
vehicle?.device_status?.is_connected
|
||||
vehicle.vehicle.tail_number,
|
||||
vehicle.device_status?.online ?? false,
|
||||
vehicle.device_status?.last_update ?? null,
|
||||
vehicle.device_status?.gps_ok ?? false,
|
||||
vehicle.device_status?.media_service_ok ?? false,
|
||||
vehicle.device_status?.is_connected ?? false,
|
||||
uuid
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const DevicesTable = observer(() => {
|
||||
const {
|
||||
devices,
|
||||
getDevices,
|
||||
// uuid, // This 'uuid' from devicesStore refers to a *single* selected device, not for batch actions.
|
||||
setSelectedDevice, // Useful for individual device actions like 'Reload Status'
|
||||
setSelectedDevice,
|
||||
sendSnapshotModalOpen,
|
||||
toggleSendSnapshotModal,
|
||||
} = devicesStore;
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles } = vehicleStore;
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||
|
||||
// Get the current list of rows displayed in the table
|
||||
const currentRows = rows(devices);
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
|
||||
const { devices } = devicesStore;
|
||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||
|
||||
// Transform the raw devices data into rows suitable for the table
|
||||
// This will also filter out devices without a UUID, as those cannot be acted upon.
|
||||
const currentTableRows = transformDevicesToRows(
|
||||
vehicles as Vehicle[]
|
||||
// devices as ConnectedDevice[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await getVehicles();
|
||||
await getDevices();
|
||||
await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices
|
||||
await getDevices(); // This should fetch the combined vehicle/device_status data
|
||||
await getSnapshots();
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [getDevices, getSnapshots]); // Added dependencies
|
||||
|
||||
// Determine if all visible devices are selected
|
||||
const isAllSelected =
|
||||
currentRows.length > 0 && selectedDevices.length === currentRows.length;
|
||||
currentTableRows.length > 0 &&
|
||||
selectedDeviceUuids.length === currentTableRows.length;
|
||||
|
||||
const handleSelectAllDevices = () => {
|
||||
if (isAllSelected) {
|
||||
// If all are currently selected, deselect all
|
||||
setSelectedDevices([]);
|
||||
setSelectedDeviceUuids([]);
|
||||
} else {
|
||||
// Otherwise, select all device UUIDs from the current rows
|
||||
setSelectedDevices(currentRows.map((row) => row.uuid));
|
||||
// Select all device UUIDs from the *currently visible and selectable* rows
|
||||
setSelectedDeviceUuids(
|
||||
currentTableRows.map((row) => row.device_uuid ?? "")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectDevice = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const deviceUuid = event.target.value;
|
||||
const handleSelectDevice = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
deviceUuid: string
|
||||
) => {
|
||||
if (event.target.checked) {
|
||||
setSelectedDevices((prevSelected) => [...prevSelected, deviceUuid]);
|
||||
setSelectedDeviceUuids((prevSelected) => [...prevSelected, deviceUuid]);
|
||||
} else {
|
||||
setSelectedDevices((prevSelected) =>
|
||||
setSelectedDeviceUuids((prevSelected) =>
|
||||
prevSelected.filter((uuid) => uuid !== deviceUuid)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// This function now opens the modal for selected devices
|
||||
const handleOpenSendSnapshotModal = () => {
|
||||
if (selectedDevices.length > 0) {
|
||||
if (selectedDeviceUuids.length > 0) {
|
||||
toggleSendSnapshotModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReloadStatus = async (uuid: string) => {
|
||||
setSelectedDevice(uuid); // Set the active device in store for context if needed
|
||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||
await getDevices(); // Refresh devices after status request
|
||||
setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere
|
||||
try {
|
||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||
await getDevices(); // Refresh devices to show updated status
|
||||
} catch (error) {
|
||||
console.error(`Error requesting status for device ${uuid}:`, error);
|
||||
// Optionally: show a user-facing error message
|
||||
}
|
||||
};
|
||||
|
||||
// This function now handles sending snapshots to ALL selected devices
|
||||
const handleSendSnapshotAction = async (snapshotId: string) => {
|
||||
if (selectedDeviceUuids.length === 0) return;
|
||||
|
||||
try {
|
||||
for (const deviceUuid of selectedDevices) {
|
||||
// Create an array of promises for all snapshot requests
|
||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
|
||||
// Ensure you are using the correct API endpoint for force-snapshot
|
||||
await authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
|
||||
return authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
|
||||
snapshot_id: snapshotId,
|
||||
});
|
||||
}
|
||||
// After all requests are sent
|
||||
await getDevices(); // Refresh the device list to show updated status
|
||||
setSelectedDevices([]); // Clear the selection
|
||||
});
|
||||
|
||||
// Wait for all promises to settle (either resolve or reject)
|
||||
await Promise.allSettled(snapshotPromises);
|
||||
|
||||
// After all requests are attempted
|
||||
await getDevices(); // Refresh the device list
|
||||
setSelectedDeviceUuids([]); // Clear the selection
|
||||
toggleSendSnapshotModal(); // Close the modal
|
||||
} catch (error) {
|
||||
console.error("Error sending snapshots:", error);
|
||||
// You might want to show an error notification to the user here
|
||||
// This catch block might not be hit if Promise.allSettled is used,
|
||||
// as it doesn't reject on individual promise failures.
|
||||
// Individual errors should be handled if needed within the .map or by checking results.
|
||||
console.error("Error in snapshot sending process:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<div className="flex justify-end p-3 gap-3">
|
||||
{" "}
|
||||
{/* Changed gap to 3 for slightly less space */}
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<div className="flex justify-end p-3 gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
variant="outlined" // Changed to outlined for distinction
|
||||
onClick={handleSelectAllDevices}
|
||||
size="small"
|
||||
>
|
||||
{isAllSelected ? "Снять выбор со всех" : "Выбрать все"}
|
||||
{isAllSelected ? "Снять выбор" : "Выбрать все"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={selectedDevices.length === 0}
|
||||
onClick={handleOpenSendSnapshotModal} // Call the new handler
|
||||
disabled={selectedDeviceUuids.length === 0}
|
||||
onClick={handleOpenSendSnapshotModal}
|
||||
size="small"
|
||||
>
|
||||
Отправить снапшот ({selectedDevices.length})
|
||||
Отправить снапшот ({selectedDeviceUuids.length})
|
||||
</Button>
|
||||
</div>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<Table sx={{ minWidth: 650 }} aria-label="devices table" size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
{" "}
|
||||
{/* Added padding="checkbox" */}
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={
|
||||
selectedDeviceUuids.length > 0 &&
|
||||
selectedDeviceUuids.length < currentTableRows.length
|
||||
}
|
||||
checked={isAllSelected}
|
||||
onChange={handleSelectAllDevices}
|
||||
inputProps={{ "aria-label": "select all devices" }}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">Бортовой номер</TableCell>
|
||||
<TableCell align="center">Борт. номер</TableCell>
|
||||
<TableCell align="center">Онлайн</TableCell>
|
||||
<TableCell align="center">Последнее обновление</TableCell>
|
||||
<TableCell align="center">ГПС</TableCell>
|
||||
<TableCell align="center">Медиа-данные</TableCell>
|
||||
<TableCell align="center">Подключение</TableCell>
|
||||
<TableCell align="center">Перезапросить</TableCell>
|
||||
<TableCell align="center">Обновлено</TableCell>
|
||||
<TableCell align="center">GPS</TableCell>
|
||||
<TableCell align="center">Медиа</TableCell>
|
||||
<TableCell align="center">Связь</TableCell>
|
||||
<TableCell align="center">Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentRows.map((row) => (
|
||||
{currentTableRows.map((row) => (
|
||||
<TableRow
|
||||
key={row.uuid} // Use row.uuid as key for consistent rendering
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
key={row.tail_number}
|
||||
hover
|
||||
role="checkbox"
|
||||
aria-checked={selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
)}
|
||||
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
||||
onClick={(event) => {
|
||||
// Allow clicking row to toggle checkbox, if not clicking on button
|
||||
if (
|
||||
(event.target as HTMLElement).closest("button") === null &&
|
||||
(event.target as HTMLElement).closest(
|
||||
'input[type="checkbox"]'
|
||||
) === null
|
||||
) {
|
||||
handleSelectDevice(
|
||||
{
|
||||
target: {
|
||||
checked: !selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
),
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
||||
row.device_uuid ?? ""
|
||||
);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:last-child td, &:last-child th": { border: 0 },
|
||||
}}
|
||||
>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
{" "}
|
||||
{/* Added padding="checkbox" */}
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
className="h-full"
|
||||
onChange={handleSelectDevice}
|
||||
value={row.uuid}
|
||||
checked={selectedDevices.includes(row.uuid)} // THIS IS THE KEY CHANGE
|
||||
checked={selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
)}
|
||||
onChange={(event) =>
|
||||
handleSelectDevice(event, row.device_uuid ?? "")
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center" component="th" scope="row">
|
||||
{row.uuid}
|
||||
<TableCell
|
||||
align="center"
|
||||
component="th"
|
||||
scope="row"
|
||||
id={`device-label-${row.device_uuid}`}
|
||||
>
|
||||
{row.tail_number}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.online ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.online ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{formatDate(row.lastUpdate)}
|
||||
<div className="flex items-center justify-center">
|
||||
{formatDate(row.lastUpdate)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.gps ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.gps ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.media ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.media ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.connection ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.connection ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<button onClick={() => handleReloadStatus(row.uuid)}>
|
||||
<RotateCcw className="m-auto" />
|
||||
</button>
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
if (
|
||||
row.device_uuid &&
|
||||
devices.find((device) => device === row.device_uuid)
|
||||
) {
|
||||
await handleReloadStatus(row.device_uuid);
|
||||
await getDevices();
|
||||
toast.success("Статус устройства обновлен");
|
||||
} else {
|
||||
toast.error("Нет связи с устройством");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Ошибка сервера");
|
||||
}
|
||||
}}
|
||||
title="Перезапросить статус"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{currentTableRows.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center">
|
||||
Нет устройств для отображения.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
|
||||
<Typography variant="h6" component="p" sx={{ mb: 2 }}>
|
||||
Выбрать снапшот для{" "}
|
||||
<strong className="text-blue-600">{selectedDevices.length}</strong>{" "}
|
||||
устройств
|
||||
<Typography variant="h6" component="h2" sx={{ mb: 1 }}>
|
||||
Отправить снапшот
|
||||
</Typography>
|
||||
<div className="mt-5 flex flex-col gap-2 max-h-[300px] overflow-y-auto">
|
||||
{snapshots && snapshots.length > 0 ? (
|
||||
snapshots.map((snapshot) => (
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Выбрано устройств:{" "}
|
||||
<strong className="text-blue-600">
|
||||
{selectedDeviceUuids.length}
|
||||
</strong>
|
||||
</Typography>
|
||||
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
||||
{snapshots && (snapshots as Snapshot[]).length > 0 ? ( // Cast snapshots
|
||||
(snapshots as Snapshot[]).map((snapshot) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={() => handleSendSnapshotAction(snapshot.ID)}
|
||||
sx={{
|
||||
p: 1.5, // Adjust padding
|
||||
borderRadius: 2, // Adjust border radius
|
||||
backgroundColor: "white", // Ensure background is white
|
||||
"&:hover": {
|
||||
backgroundColor: "grey.100", // Light hover effect
|
||||
},
|
||||
}}
|
||||
key={snapshot.ID}
|
||||
sx={{ justifyContent: "flex-start" }}
|
||||
>
|
||||
{snapshot.Name}
|
||||
</Button>
|
||||
@ -280,6 +414,15 @@ export const DevicesTable = observer(() => {
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={toggleSendSnapshotModal}
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
sx={{ mt: 3 }}
|
||||
fullWidth
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
@ -9,7 +9,10 @@ export interface MediaData {
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) {
|
||||
export function MediaViewer({
|
||||
media,
|
||||
className,
|
||||
}: Readonly<{ media?: MediaData; className?: string }>) {
|
||||
const token = localStorage.getItem("token");
|
||||
return (
|
||||
<Box
|
||||
@ -22,6 +25,7 @@ export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) {
|
||||
justifyContent: "center",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{media?.media_type === 1 && (
|
||||
<img
|
||||
|
Reference in New Issue
Block a user