Files
drone-detector/client/src/pages/Detections.jsx
2025-09-18 06:43:30 +02:00

334 lines
13 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import {
MagnifyingGlassIcon,
FunnelIcon,
EyeIcon
} from '@heroicons/react/24/outline';
const Detections = () => {
const [detections, setDetections] = useState([]);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState({});
const [filters, setFilters] = useState({
device_id: '',
drone_id: '',
start_date: '',
end_date: '',
limit: 50,
offset: 0
});
const [showFilters, setShowFilters] = useState(false);
useEffect(() => {
fetchDetections();
}, [filters]);
const fetchDetections = async () => {
try {
setLoading(true);
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) params.append(key, value);
});
console.log('🔍 Fetching detections with params:', params.toString());
const response = await api.get(`/detections?${params}`);
console.log('✅ Detections response:', response.data);
setDetections(response.data.data?.detections || []);
setPagination(response.data.data?.pagination || {});
} catch (error) {
console.error('❌ Error fetching detections:', error);
setDetections([]); // Ensure detections is always an array
setPagination({});
} finally {
setLoading(false);
}
};
const handleFilterChange = (key, value) => {
setFilters(prev => ({
...prev,
[key]: value,
offset: 0 // Reset to first page when filtering
}));
};
const handlePageChange = (newOffset) => {
setFilters(prev => ({
...prev,
offset: newOffset
}));
};
const clearFilters = () => {
setFilters({
device_id: '',
drone_id: '',
start_date: '',
end_date: '',
limit: 50,
offset: 0
});
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
Drone Detections
</h3>
<p className="mt-1 text-sm text-gray-500">
History of all drone detections from your devices
</p>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className="btn btn-secondary flex items-center space-x-2"
>
<FunnelIcon className="h-4 w-4" />
<span>Filters</span>
</button>
</div>
{/* Filters */}
{showFilters && (
<div className="bg-white p-6 rounded-lg shadow border">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Device ID
</label>
<input
type="number"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Device ID"
value={filters.device_id}
onChange={(e) => handleFilterChange('device_id', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Drone ID
</label>
<input
type="number"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Drone ID"
value={filters.drone_id}
onChange={(e) => handleFilterChange('drone_id', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="datetime-local"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={filters.start_date}
onChange={(e) => handleFilterChange('start_date', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="datetime-local"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={filters.end_date}
onChange={(e) => handleFilterChange('end_date', e.target.value)}
/>
</div>
</div>
<div className="mt-4 flex space-x-2">
<button
onClick={clearFilters}
className="btn btn-secondary"
>
Clear Filters
</button>
</div>
</div>
)}
{/* Detection List */}
<div className="bg-white rounded-lg shadow overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
) : (
<>
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Device</th>
<th>Drone ID</th>
<th>Type</th>
<th>Frequency</th>
<th>RSSI</th>
<th>Location</th>
<th>Detected At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{detections.map((detection) => (
<tr key={detection.id} className="hover:bg-gray-50">
<td>
<div>
<div className="text-sm font-medium text-gray-900">
{detection.device?.name || `Device ${detection.device_id}`}
</div>
<div className="text-sm text-gray-500">
ID: {detection.device_id}
</div>
</div>
</td>
<td>
<span className="px-2 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
{detection.drone_id}
</span>
</td>
<td>
<span className="text-sm text-gray-900">
{detection.drone_type_info?.name || `Type ${detection.drone_type}`}
</span>
{detection.drone_type_info?.category && (
<div className="text-xs text-gray-500">
{detection.drone_type_info.category}
</div>
)}
</td>
<td>
<span className="text-sm text-gray-900">
{detection.freq} MHz
</span>
</td>
<td>
<span className={`text-sm font-medium ${
detection.rssi > -60 ? 'text-red-600' :
detection.rssi > -80 ? 'text-yellow-600' : 'text-green-600'
}`}>
{detection.rssi} dBm
</span>
</td>
<td>
<div className="text-sm text-gray-900">
{detection.device?.location_description ||
(detection.geo_lat && detection.geo_lon ?
`${detection.geo_lat}, ${detection.geo_lon}` :
'Unknown')}
</div>
</td>
<td>
<div className="text-sm text-gray-900">
{format(new Date(detection.server_timestamp), 'MMM dd, yyyy')}
</div>
<div className="text-sm text-gray-500">
{format(new Date(detection.server_timestamp), 'HH:mm:ss')}
</div>
</td>
<td>
<button
className="text-primary-600 hover:text-primary-900 text-sm"
onClick={() => {
// TODO: Open detection details modal
console.log('View detection details:', detection);
}}
>
<EyeIcon className="h-4 w-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{detections.length === 0 && !loading && (
<div className="text-center py-12">
<MagnifyingGlassIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No detections found</h3>
<p className="mt-1 text-sm text-gray-500">
Try adjusting your search filters.
</p>
</div>
)}
{/* Pagination */}
{pagination.total > 0 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => handlePageChange(Math.max(0, filters.offset - filters.limit))}
disabled={filters.offset === 0}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(filters.offset + filters.limit)}
disabled={filters.offset + filters.limit >= pagination.total}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing{' '}
<span className="font-medium">{filters.offset + 1}</span>
{' '}to{' '}
<span className="font-medium">
{Math.min(filters.offset + filters.limit, pagination.total)}
</span>
{' '}of{' '}
<span className="font-medium">{pagination.total}</span>
{' '}results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => handlePageChange(Math.max(0, filters.offset - filters.limit))}
disabled={filters.offset === 0}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(filters.offset + filters.limit)}
disabled={filters.offset + filters.limit >= pagination.total}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
);
};
export default Detections;