diff --git a/frontend/src/api/reportedPhotos.ts b/frontend/src/api/reportedPhotos.ts index d4252e2..07ae8ef 100644 --- a/frontend/src/api/reportedPhotos.ts +++ b/frontend/src/api/reportedPhotos.ts @@ -11,6 +11,7 @@ export interface ReportedPhotoResponse { reviewed_at: string | null reviewed_by: number | null review_notes: string | null + report_comment: string | null photo_path: string | null photo_filename: string | null } @@ -36,6 +37,12 @@ export interface ReviewResponse { errors: string[] } +export interface ReportedCleanupResponse { + deleted_records: number + errors: string[] + warnings?: string[] +} + export const reportedPhotosApi = { listReportedPhotos: async (statusFilter?: string): Promise => { const { data } = await apiClient.get( @@ -54,5 +61,13 @@ export const reportedPhotosApi = { ) return data }, + + cleanupReportedPhotos: async (): Promise => { + const { data } = await apiClient.post( + '/api/v1/reported-photos/cleanup', + {}, + ) + return data + }, } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 014b2aa..ecd9388 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -27,7 +27,6 @@ export default function Layout() { { path: '/auto-match', label: 'Auto-Match', icon: '🤖', adminOnly: false }, { path: '/modify', label: 'Modify', icon: 'âœī¸', adminOnly: false }, { path: '/tags', label: 'Tag', icon: 'đŸˇī¸', adminOnly: false }, - { path: '/manage-photos', label: 'Manage Photos', icon: '📷', adminOnly: false }, { path: '/faces-maintenance', label: 'Faces Maintenance', icon: '🔧', adminOnly: false }, { path: '/approve-identified', label: 'Approve identified', icon: '✅', adminOnly: true }, { path: '/manage-users', label: 'Manage users', icon: 'đŸ‘Ĩ', adminOnly: true }, diff --git a/frontend/src/pages/ReportedPhotos.tsx b/frontend/src/pages/ReportedPhotos.tsx index a02f1b0..3b33bf4 100644 --- a/frontend/src/pages/ReportedPhotos.tsx +++ b/frontend/src/pages/ReportedPhotos.tsx @@ -13,6 +13,7 @@ export default function ReportedPhotos() { const [decisions, setDecisions] = useState>({}) const [reviewNotes, setReviewNotes] = useState>({}) const [submitting, setSubmitting] = useState(false) + const [clearing, setClearing] = useState(false) const [statusFilter, setStatusFilter] = useState('pending') const loadReportedPhotos = useCallback(async () => { @@ -55,10 +56,14 @@ export default function ReportedPhotos() { } const handleDecisionChange = (id: number, decision: 'keep' | 'remove') => { - setDecisions((prev) => ({ - ...prev, - [id]: decision, - })) + setDecisions((prev) => { + const currentDecision = prev[id] ?? null + const nextDecision = currentDecision === decision ? null : decision + return { + ...prev, + [id]: nextDecision, + } + }) } const handleReviewNotesChange = (id: number, notes: string) => { @@ -124,18 +129,21 @@ export default function ReportedPhotos() { decisions: decisionsList, }) - const message = [ + const messageParts = [ `✅ Kept: ${response.kept}`, `❌ Removed: ${response.removed}`, - response.errors.length > 0 ? `âš ī¸ Errors: ${response.errors.length}` : '', ] - .filter(Boolean) - .join('\n') + + if (import.meta.env.DEV && response.errors.length > 0) { + messageParts.push(`âš ī¸ Errors: ${response.errors.length}`) + } + + const message = messageParts.join('\n') alert(message) if (response.errors.length > 0) { - console.error('Errors:', response.errors) + console.error('Reported photo review errors:', response.errors) } // Reload the list to show updated status @@ -153,6 +161,51 @@ export default function ReportedPhotos() { } } + const handleClearDatabase = async () => { + const confirmMessage = [ + 'Delete all kept and removed reported photo records from the auth database?', + '', + 'Only photos with Pending status will remain.', + 'This action cannot be undone.', + ].join('\n') + + if (!confirm(confirmMessage)) { + return + } + + setClearing(true) + try { + const response = await reportedPhotosApi.cleanupReportedPhotos() + const summary = [ + `✅ Deleted ${response.deleted_records} record(s)`, + response.warnings && response.warnings.length > 0 + ? `â„šī¸ ${response.warnings.join('; ')}` + : '', + response.errors.length > 0 ? `âš ī¸ ${response.errors.join('; ')}` : '', + ] + .filter(Boolean) + .join('\n') + + alert(summary || 'Cleanup complete.') + + if (response.errors.length > 0) { + console.error('Cleanup errors:', response.errors) + } + if (response.warnings && response.warnings.length > 0) { + console.info('Cleanup warnings:', response.warnings) + } + + await loadReportedPhotos() + } catch (err: any) { + const errorMessage = + err.response?.data?.detail || err.message || 'Failed to cleanup reported photos' + alert(`Error: ${errorMessage}`) + console.error('Error clearing reported photos:', err) + } finally { + setClearing(false) + } + } + return (
@@ -179,32 +232,47 @@ export default function ReportedPhotos() { {!loading && !error && ( <> -
-
-
- Total reported photos: {reportedPhotos.length} +
+
+
+
+ Total reported photos:{' '} + {reportedPhotos.length} +
+
- + {submitting ? 'Submitting...' : 'Submit Decisions'} + +
+
+ + + Clear kept/removed records +
-
{reportedPhotos.length === 0 ? ( @@ -225,6 +293,9 @@ export default function ReportedPhotos() { Reported At + + Report Comment + Status @@ -299,6 +370,15 @@ export default function ReportedPhotos() { {formatDate(reported.reported_at)}
+ + {reported.report_comment ? ( +
+ {reported.report_comment} +
+ ) : ( + - + )} +