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> | ||||
|     </> | ||||
|   ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user