feat: Add Approve Identified page and API for pending identifications
This commit introduces a new Approve Identified page in the frontend, allowing users to view and manage pending identifications. The page fetches data from a new API endpoint that lists pending identifications from the auth database. Additionally, the necessary API routes and database session management for handling pending identifications have been implemented. The Layout component has been updated to include navigation to the new page, enhancing the user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
@@ -11,6 +11,7 @@ import AutoMatch from './pages/AutoMatch'
|
||||
import Modify from './pages/Modify'
|
||||
import Tags from './pages/Tags'
|
||||
import FacesMaintenance from './pages/FacesMaintenance'
|
||||
import ApproveIdentified from './pages/ApproveIdentified'
|
||||
import Settings from './pages/Settings'
|
||||
import Help from './pages/Help'
|
||||
import Layout from './components/Layout'
|
||||
@@ -44,6 +45,7 @@ function AppRoutes() {
|
||||
<Route path="modify" element={<Modify />} />
|
||||
<Route path="tags" element={<Tags />} />
|
||||
<Route path="faces-maintenance" element={<FacesMaintenance />} />
|
||||
<Route path="approve-identified" element={<ApproveIdentified />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="help" element={<Help />} />
|
||||
</Route>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import apiClient from './client'
|
||||
|
||||
export interface PendingIdentification {
|
||||
id: number
|
||||
face_id: number
|
||||
user_id: number
|
||||
user_name?: string | null
|
||||
user_email: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
middle_name?: string | null
|
||||
maiden_name?: string | null
|
||||
date_of_birth?: string | null
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PendingIdentificationsListResponse {
|
||||
items: PendingIdentification[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export const pendingIdentificationsApi = {
|
||||
list: async (): Promise<PendingIdentificationsListResponse> => {
|
||||
const res = await apiClient.get<PendingIdentificationsListResponse>(
|
||||
'/api/v1/pending-identifications'
|
||||
)
|
||||
return res.data
|
||||
},
|
||||
}
|
||||
|
||||
export default pendingIdentificationsApi
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function Layout() {
|
||||
{ path: '/modify', label: 'Modify', icon: '✏️' },
|
||||
{ path: '/tags', label: 'Tag', icon: '🏷️' },
|
||||
{ path: '/faces-maintenance', label: 'Faces Maintenance', icon: '🔧' },
|
||||
{ path: '/approve-identified', label: 'Approve identified', icon: '✅' },
|
||||
{ path: '/settings', label: 'Settings', icon: '⚙️' },
|
||||
{ path: '/help', label: 'Help', icon: '📚' },
|
||||
]
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import pendingIdentificationsApi, { PendingIdentification } from '../api/pendingIdentifications'
|
||||
|
||||
export default function ApproveIdentified() {
|
||||
const [pendingIdentifications, setPendingIdentifications] = useState<PendingIdentification[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadPendingIdentifications()
|
||||
}, [])
|
||||
|
||||
const loadPendingIdentifications = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await pendingIdentificationsApi.list()
|
||||
setPendingIdentifications(response.items)
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to load pending identifications')
|
||||
console.error('Error loading pending identifications:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string | null | undefined): string => {
|
||||
if (!dateString) return '-'
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString()
|
||||
} catch {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
const formatName = (pending: PendingIdentification): string => {
|
||||
const parts = [
|
||||
pending.first_name,
|
||||
pending.middle_name,
|
||||
pending.last_name,
|
||||
].filter(Boolean)
|
||||
if (pending.maiden_name) {
|
||||
parts.push(`(${pending.maiden_name})`)
|
||||
}
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Approve Identified</h1>
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-600">Loading identified people...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded text-red-700">
|
||||
<p className="font-semibold">Error loading data</p>
|
||||
<p className="text-sm mt-1">{error}</p>
|
||||
<button
|
||||
onClick={loadPendingIdentifications}
|
||||
className="mt-3 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
<div className="mb-4 text-sm text-gray-600">
|
||||
Total pending identifications: <span className="font-semibold">{pendingIdentifications.length}</span>
|
||||
</div>
|
||||
|
||||
{pendingIdentifications.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>No pending identifications found.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date of Birth
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Face ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
User
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{pendingIdentifications.map((pending) => (
|
||||
<tr key={pending.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{formatName(pending)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500">
|
||||
{formatDate(pending.date_of_birth)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500">
|
||||
{pending.face_id}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500">
|
||||
{pending.user_name || pending.user_email}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500">
|
||||
{formatDate(pending.created_at)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user