diff --git a/frontend/src/api/pendingIdentifications.ts b/frontend/src/api/pendingIdentifications.ts index e266ecc..2109dfa 100644 --- a/frontend/src/api/pendingIdentifications.ts +++ b/frontend/src/api/pendingIdentifications.ts @@ -37,6 +37,27 @@ export interface ApproveDenyResponse { errors: string[] } +export interface UserIdentificationStats { + user_id: number + username: string + full_name: string + email: string + face_count: number + first_identification_date: string | null + last_identification_date: string | null +} + +export interface IdentificationReportResponse { + items: UserIdentificationStats[] + total_faces: number + total_users: number +} + +export interface ClearDatabaseResponse { + deleted_records: number + errors: string[] +} + export const pendingIdentificationsApi = { list: async (includeDenied: boolean = false): Promise => { const res = await apiClient.get( @@ -52,6 +73,22 @@ export const pendingIdentificationsApi = { ) return res.data }, + getReport: async (dateFrom?: string, dateTo?: string): Promise => { + const params: Record = {} + if (dateFrom) params.date_from = dateFrom + if (dateTo) params.date_to = dateTo + const res = await apiClient.get( + '/api/v1/pending-identifications/report', + { params } + ) + return res.data + }, + clearDenied: async (): Promise => { + const res = await apiClient.post( + '/api/v1/pending-identifications/clear-denied' + ) + return res.data + }, } export default pendingIdentificationsApi diff --git a/frontend/src/pages/ApproveIdentified.tsx b/frontend/src/pages/ApproveIdentified.tsx index 64be17f..45fb6a1 100644 --- a/frontend/src/pages/ApproveIdentified.tsx +++ b/frontend/src/pages/ApproveIdentified.tsx @@ -1,5 +1,9 @@ import { useEffect, useState, useCallback } from 'react' -import pendingIdentificationsApi, { PendingIdentification } from '../api/pendingIdentifications' +import pendingIdentificationsApi, { + PendingIdentification, + IdentificationReportResponse, + UserIdentificationStats +} from '../api/pendingIdentifications' import { apiClient } from '../api/client' export default function ApproveIdentified() { @@ -9,6 +13,13 @@ export default function ApproveIdentified() { const [decisions, setDecisions] = useState>({}) const [submitting, setSubmitting] = useState(false) const [includeDenied, setIncludeDenied] = useState(false) + const [showReport, setShowReport] = useState(false) + const [reportData, setReportData] = useState(null) + const [reportLoading, setReportLoading] = useState(false) + const [reportError, setReportError] = useState(null) + const [dateFrom, setDateFrom] = useState('') + const [dateTo, setDateTo] = useState('') + const [clearing, setClearing] = useState(false) const loadPendingIdentifications = useCallback(async () => { setLoading(true) @@ -51,10 +62,20 @@ export default function ApproveIdentified() { } const handleDecisionChange = (id: number, decision: 'approve' | 'deny') => { - setDecisions(prev => ({ - ...prev, - [id]: decision - })) + setDecisions(prev => { + const currentDecision = prev[id] + // If clicking the same checkbox, deselect it + if (currentDecision === decision) { + const updated = { ...prev } + delete updated[id] + return updated + } + // Otherwise, set the new decision (this will automatically deselect the other) + return { + ...prev, + [id]: decision + } + }) } const handleSubmit = async () => { @@ -109,6 +130,78 @@ export default function ApproveIdentified() { } } + const loadReport = useCallback(async () => { + setReportLoading(true) + setReportError(null) + try { + const response = await pendingIdentificationsApi.getReport( + dateFrom || undefined, + dateTo || undefined + ) + setReportData(response) + } catch (err: any) { + setReportError(err.response?.data?.detail || err.message || 'Failed to load report') + console.error('Error loading report:', err) + } finally { + setReportLoading(false) + } + }, [dateFrom, dateTo]) + + const handleOpenReport = () => { + setShowReport(true) + loadReport() + } + + const handleCloseReport = () => { + setShowReport(false) + setReportData(null) + setReportError(null) + setDateFrom('') + setDateTo('') + } + + const formatDateTime = (dateString: string | null | undefined): string => { + if (!dateString) return '-' + try { + const date = new Date(dateString) + return date.toLocaleString() + } catch { + return dateString + } + } + + const handleClearDenied = async () => { + if (!confirm('Are you sure you want to delete all denied records? This action cannot be undone.')) { + return + } + + setClearing(true) + try { + const response = await pendingIdentificationsApi.clearDenied() + + const message = [ + `✅ Deleted ${response.deleted_records} denied record(s)`, + response.errors.length > 0 ? `âš ī¸ Errors: ${response.errors.length}` : '' + ].filter(Boolean).join('\n') + + alert(message) + + if (response.errors.length > 0) { + console.error('Errors:', response.errors) + alert('Errors:\n' + response.errors.join('\n')) + } + + // Reload the list to reflect changes + await loadPendingIdentifications() + } catch (err: any) { + const errorMessage = err.response?.data?.detail || err.message || 'Failed to clear denied records' + alert(`Error: ${errorMessage}`) + console.error('Error clearing denied records:', err) + } finally { + setClearing(false) + } + } + return (
@@ -126,7 +219,7 @@ export default function ApproveIdentified() {

{error}

@@ -140,6 +233,20 @@ export default function ApproveIdentified() { Total pending identifications: {pendingIdentifications.length}
+ +
+ + {/* Report Modal */} + {showReport && ( +
+
+
+

Identification Report

+ +
+ +
+
+
+ + setDateFrom(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setDateTo(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ +
+
+
+ +
+ {reportLoading && ( +
+

Loading report...

+
+ )} + + {reportError && ( +
+

Error loading report

+

{reportError}

+
+ )} + + {!reportLoading && !reportError && reportData && ( + <> +
+
+
+ Total Users:{' '} + {reportData.total_users} +
+
+ Total Faces:{' '} + {reportData.total_faces} +
+
+ Average per User:{' '} + + {reportData.total_users > 0 + ? Math.round((reportData.total_faces / reportData.total_users) * 10) / 10 + : 0} + +
+
+
+ + {reportData.items.length === 0 ? ( +
+

No identifications found for the selected date range.

+
+ ) : ( +
+ + + + + + + + + + + + {reportData.items.map((stat: UserIdentificationStats) => ( + + + + + + + + ))} + +
+ User + + Email + + Faces Identified + + First Identification + + Last Identification +
+
+ {stat.full_name} +
+
{stat.username}
+
+
{stat.email}
+
+
+ {stat.face_count} +
+
+
+ {formatDateTime(stat.first_identification_date)} +
+
+
+ {formatDateTime(stat.last_identification_date)} +
+
+
+ )} + + )} +
+
+
+ )}
) } diff --git a/frontend/src/pages/PendingPhotos.tsx b/frontend/src/pages/PendingPhotos.tsx index d4fa389..a9e3de1 100644 --- a/frontend/src/pages/PendingPhotos.tsx +++ b/frontend/src/pages/PendingPhotos.tsx @@ -427,24 +427,6 @@ export default function PendingPhotos() { - {isAdmin && ( -
- - -
- )}
{pendingPhotos.filter((p) => p.status === 'pending').length > 0 && ( <> @@ -462,6 +444,24 @@ export default function PendingPhotos() { )} + {isAdmin && ( + <> + + + + )}