import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import api from '../services/api'; import { format } from 'date-fns'; import { PlusIcon, PencilIcon, TrashIcon, ServerIcon, MapPinIcon, SignalIcon, BoltIcon } from '@heroicons/react/24/outline'; const Devices = () => { const navigate = useNavigate(); const [devices, setDevices] = useState([]); const [loading, setLoading] = useState(true); const [showAddModal, setShowAddModal] = useState(false); const [editingDevice, setEditingDevice] = useState(null); const [filter, setFilter] = useState('all'); // 'all', 'approved', 'pending' const [showDetailsModal, setShowDetailsModal] = useState(false); const [selectedDevice, setSelectedDevice] = useState(null); useEffect(() => { fetchDevices(); }, []); const fetchDevices = async () => { try { const response = await api.get('/devices?include_stats=true'); setDevices(response.data.data); } catch (error) { console.error('Error fetching devices:', error); } finally { setLoading(false); } }; const handleAddDevice = () => { setEditingDevice(null); setShowAddModal(true); }; const handleEditDevice = (device) => { setEditingDevice(device); setShowAddModal(true); }; const handleApproveDevice = async (deviceId) => { try { await api.post(`/devices/${deviceId}/approve`, { approved: true }); fetchDevices(); } catch (error) { console.error('Error approving device:', error); if (error.response?.status === 401 || error.response?.status === 403) { alert('Your session has expired. Please log in again.'); return; } alert('Error approving device: ' + (error.response?.data?.message || error.message)); } }; const handleRejectDevice = async (deviceId) => { if (window.confirm('Are you sure you want to reject this device?')) { try { await api.post(`/devices/${deviceId}/approve`, { approved: false }); fetchDevices(); } catch (error) { console.error('Error rejecting device:', error); if (error.response?.status === 401 || error.response?.status === 403) { alert('Your session has expired. Please log in again.'); return; } alert('Error rejecting device: ' + (error.response?.data?.message || error.message)); } } }; const handleViewDetails = (device) => { setSelectedDevice(device); setShowDetailsModal(true); }; const handleViewOnMap = (device) => { if (device.geo_lat && device.geo_lon) { // Navigate to map with device information navigate('/map', { state: { focusDevice: { id: device.id, name: device.name || `Device ${device.id}`, lat: device.geo_lat, lon: device.geo_lon, status: device.stats?.status || 'unknown' } } }); } else { alert('Device location coordinates are not available'); } }; const handleDeleteDevice = async (deviceId) => { if (window.confirm('Are you sure you want to deactivate this device?')) { try { await api.delete(`/devices/${deviceId}`); fetchDevices(); } catch (error) { console.error('Error deleting device:', error); } } }; const getStatusColor = (status) => { switch (status) { case 'online': return 'bg-green-100 text-green-800'; case 'offline': return 'bg-red-100 text-red-800'; default: return 'bg-gray-100 text-gray-800'; } }; const getSignalStrength = (lastHeartbeat) => { if (!lastHeartbeat) return 'Unknown'; const timeSince = (new Date() - new Date(lastHeartbeat)) / 1000 / 60; // minutes if (timeSince < 5) return 'Strong'; if (timeSince < 15) return 'Good'; if (timeSince < 60) return 'Weak'; return 'Lost'; }; const filteredDevices = devices.filter(device => { if (filter === 'approved') return device.is_approved; if (filter === 'pending') return !device.is_approved; return true; // 'all' }); const pendingCount = devices.filter(device => !device.is_approved).length; if (loading) { return (
); } return (

Devices

Manage your drone detection devices {pendingCount > 0 && ( {pendingCount} pending approval )}

{/* Filter Tabs */}
{/* Device Grid */}
{filteredDevices.map((device) => (

{device.name || `Device ${device.id}`}

{!device.is_approved && ( Needs Approval )}
Status {device.stats?.status || 'Unknown'}
Approval {device.is_approved ? 'Approved' : 'Pending'}
Device ID {device.id}
{device.location_description && (
Location {device.location_description}
)} {(device.geo_lat && device.geo_lon) && (
Coordinates {device.geo_lat}, {device.geo_lon}
)}
Signal {getSignalStrength(device.last_heartbeat)}
{device.stats && (
Detections (24h) 0 ? 'text-red-600' : 'text-green-600' }`}> {device.stats.detections_24h}
)} {device.last_heartbeat && (
Last Seen {format(new Date(device.last_heartbeat), 'MMM dd, HH:mm')}
)} {device.firmware_version && (
Firmware {device.firmware_version}
)}
{/* Device Actions */}
{!device.is_approved ? (
) : null}
))}
{filteredDevices.length === 0 && devices.length > 0 && (

No {filter === 'all' ? '' : filter} devices

{filter === 'pending' ? 'No devices are currently pending approval.' : filter === 'approved' ? 'No devices have been approved yet.' : 'No devices match the current filter.' }

)} {devices.length === 0 && (

No devices

Get started by adding your first drone detection device.

)} {/* Add/Edit Device Modal */} {showAddModal && ( setShowAddModal(false)} onSave={() => { setShowAddModal(false); fetchDevices(); }} /> )} {/* Device Details Modal */} {showDetailsModal && selectedDevice && (

Device Details

Device ID: {selectedDevice.id}
Name: {selectedDevice.name || 'Unnamed'}
Status: {selectedDevice.stats?.status || 'Unknown'}
Approved: {selectedDevice.is_approved ? 'Yes' : 'Pending'}
{selectedDevice.location_description && (
Location: {selectedDevice.location_description}
)} {(selectedDevice.geo_lat && selectedDevice.geo_lon) && (
Coordinates: {selectedDevice.geo_lat}, {selectedDevice.geo_lon}
)} {selectedDevice.last_heartbeat && (
Last Heartbeat: {format(new Date(selectedDevice.last_heartbeat), 'MMM dd, yyyy HH:mm')}
)} {selectedDevice.firmware_version && (
Firmware: {selectedDevice.firmware_version}
)} {selectedDevice.stats && (
Detections (24h): 0 ? 'text-red-600' : 'text-green-600' }`}> {selectedDevice.stats.detections_24h}
)} {selectedDevice.created_at && (
Created: {format(new Date(selectedDevice.created_at), 'MMM dd, yyyy HH:mm')}
)}
{!selectedDevice.is_approved && ( )} {(selectedDevice.geo_lat && selectedDevice.geo_lon) && ( )}
)}
); }; const DeviceModal = ({ device, onClose, onSave }) => { const [formData, setFormData] = useState({ id: device?.id || '', name: device?.name || '', geo_lat: device?.geo_lat || '', geo_lon: device?.geo_lon || '', location_description: device?.location_description || '', heartbeat_interval: device?.heartbeat_interval || 300, firmware_version: device?.firmware_version || '', notes: device?.notes || '' }); const [saving, setSaving] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setSaving(true); try { if (device) { // Update existing device - exclude read-only fields and filter out empty strings const updateData = { name: formData.name, location_description: formData.location_description || null, notes: formData.notes || null }; // Remove null values to avoid sending unnecessary data Object.keys(updateData).forEach(key => { if (updateData[key] === null || updateData[key] === '') { delete updateData[key]; } }); await api.put(`/devices/${device.id}`, updateData); } else { // Create new device - include all fields, convert empty strings to null const createData = { ...formData }; Object.keys(createData).forEach(key => { if (createData[key] === '') { createData[key] = null; } }); await api.post('/devices', createData); } onSave(); } catch (error) { console.error('Error saving device:', error); // Check if it's a token expiration error if (error.response?.status === 401 || error.response?.status === 403) { alert('Your session has expired. Please log in again.'); // The API interceptor will handle the logout automatically return; } alert('Error saving device: ' + (error.response?.data?.message || error.message)); } finally { setSaving(false); } }; const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; return (

{device ? 'Edit Device' : 'Add New Device'}

{!device && (
)}