diff --git a/alpr-service/main.py b/alpr-service/main.py index bef376f..26b9edc 100644 --- a/alpr-service/main.py +++ b/alpr-service/main.py @@ -348,6 +348,22 @@ def dataset_list(): def dataset_image(filename): return send_from_directory(DATASET_DIR, filename) +@app.route("/dataset/images/", methods=['DELETE']) +def delete_dataset_image(filename): + """Elimina una imagen del dataset""" + try: + filepath = os.path.join(DATASET_DIR, filename) + if os.path.exists(filepath): + os.remove(filepath) + # Invalidar cache + dataset_cache['timestamp'] = 0 + print(f"🗑️ Deleted from dataset: {filename}") + return {"success": True, "message": f"Deleted {filename}"} + else: + return {"success": False, "message": "File not found"}, 404 + except Exception as e: + return {"success": False, "message": str(e)}, 500 + if __name__ == "__main__": t = threading.Thread(target=camera_loop, daemon=True) t.start() diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index d48248c..28bb091 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -31,9 +31,11 @@ function AdminDashboard({ token }) { const [showDatasetModal, setShowDatasetModal] = useState(false); const [datasetImages, setDatasetImages] = useState([]); const [selectedImage, setSelectedImage] = useState(null); + const [selectedImageIndex, setSelectedImageIndex] = useState(-1); const [datasetPage, setDatasetPage] = useState(1); const [datasetTotalPages, setDatasetTotalPages] = useState(1); const [datasetTotal, setDatasetTotal] = useState(0); + const [deletingImage, setDeletingImage] = useState(false); useEffect(() => { fetchData(); @@ -67,6 +69,35 @@ function AdminDashboard({ token }) { }; }, [token]); + // Keyboard navigation for dataset gallery + useEffect(() => { + const handleKeyDown = (e) => { + if (!showDatasetModal || !selectedImage) return; + + if (e.key === 'ArrowLeft' && selectedImageIndex > 0) { + e.preventDefault(); + setSelectedImageIndex(prev => { + const newIndex = prev - 1; + setSelectedImage(datasetImages[newIndex]); + return newIndex; + }); + } else if (e.key === 'ArrowRight' && selectedImageIndex < datasetImages.length - 1) { + e.preventDefault(); + setSelectedImageIndex(prev => { + const newIndex = prev + 1; + setSelectedImage(datasetImages[newIndex]); + return newIndex; + }); + } else if (e.key === 'Escape') { + setSelectedImage(null); + setSelectedImageIndex(-1); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [showDatasetModal, selectedImage, selectedImageIndex, datasetImages]); + useEffect(() => { if (viewMode === 'history' && activeTab === 'monitor') { fetchHistory(selectedDate); @@ -138,6 +169,52 @@ function AdminDashboard({ token }) { } }; + const selectImageByIndex = (index) => { + if (index >= 0 && index < datasetImages.length) { + setSelectedImage(datasetImages[index]); + setSelectedImageIndex(index); + } + }; + + const navigateImage = (direction) => { + const newIndex = selectedImageIndex + direction; + if (newIndex >= 0 && newIndex < datasetImages.length) { + selectImageByIndex(newIndex); + } + }; + + const deleteCurrentImage = async () => { + if (!selectedImage || deletingImage) return; + + if (!confirm(`¿Eliminar imagen de ${selectedImage.plate}?`)) return; + + setDeletingImage(true); + try { + await axios.delete(`/dataset/images/${selectedImage.filename}`); + + // Remover de la lista local + const newImages = datasetImages.filter((_, idx) => idx !== selectedImageIndex); + setDatasetImages(newImages); + setDatasetTotal(prev => prev - 1); + setDatasetCount(prev => prev - 1); + + // Navegar a la siguiente imagen o cerrar si no hay más + if (newImages.length === 0) { + setSelectedImage(null); + setSelectedImageIndex(-1); + } else if (selectedImageIndex >= newImages.length) { + selectImageByIndex(newImages.length - 1); + } else { + setSelectedImage(newImages[selectedImageIndex]); + } + } catch (err) { + console.error('Error deleting image:', err); + alert('Error al eliminar la imagen'); + } finally { + setDeletingImage(false); + } + }; + const handleSearchRut = (e) => { e.preventDefault(); const normalizedRut = searchRut.replace(/\./g, '').toUpperCase(); @@ -539,23 +616,67 @@ function AdminDashboard({ token }) { {/* Content */}
{selectedImage ? ( - /* Image Preview */ + /* Image Preview with Navigation */
- - {selectedImage.plate} + {/* Top Controls */} +
+ +
+ {selectedImageIndex + 1} de {datasetImages.length} +
+ +
+ + {/* Image with Navigation Arrows */} +
+ {/* Previous Button */} + + + {/* Image */} + {selectedImage.plate} + + {/* Next Button */} + +
+ + {/* Plate Info */}
Patente: {selectedImage.plate}
+ + {/* Keyboard hint */} +
+ Usa ← → para navegar +
) : ( /* Image Grid */ @@ -563,7 +684,7 @@ function AdminDashboard({ token }) { {datasetImages.map((img, idx) => (
setSelectedImage(img)} + onClick={() => selectImageByIndex(idx)} className="cursor-pointer group relative aspect-video bg-slate-800 rounded-lg overflow-hidden border border-slate-700 hover:border-emerald-500 transition-all" >