diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index 1b77ccc..29a38cc 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -6,6 +6,7 @@ import { LandingPage } from '@/pages/landing-page'; import { CatalogPage } from '@/pages/catalog-page'; import { DiseaseDetailPage } from '@/pages/disease-detail-page'; import { DiagnosisDetailPage } from '@/pages/diagnosis-detail-page'; +import { ExpertReviewsPage } from '@/pages/expert-reviews-page'; import { LoginPage } from '@/pages/login-page'; import { RegisterPage } from '@/pages/register-page'; @@ -31,6 +32,14 @@ const router = createBrowserRouter([ ), }, + { + path: '/expert/reviews', + element: ( + + + + ), + }, { path: '/catalog', element: }, { path: '/catalog/:slug', element: }, ]); diff --git a/apps/web/src/pages/expert-reviews-page.tsx b/apps/web/src/pages/expert-reviews-page.tsx new file mode 100644 index 0000000..4b58dc3 --- /dev/null +++ b/apps/web/src/pages/expert-reviews-page.tsx @@ -0,0 +1,262 @@ +import { FormEvent, useMemo, useState } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query'; +import type { DiseaseSlug } from '@zeavis/shared'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge'; +import { apiClient } from '@/lib/api-client'; + +export function ExpertReviewsPage() { + const [searchParams, setSearchParams] = useSearchParams(); + const selectedId = searchParams.get('diagnosis'); + const [verdict, setVerdict] = useState<'verified' | 'corrected'>('verified'); + const [correctedDiseaseSlug, setCorrectedDiseaseSlug] = useState(''); + const [notes, setNotes] = useState(''); + const queryClient = useQueryClient(); + + const [reviewsQuery, diseasesQuery] = useQueries({ + queries: [ + { queryKey: ['expert-reviews'], queryFn: () => apiClient.getExpertReviews() }, + { queryKey: ['diseases'], queryFn: () => apiClient.getDiseases() }, + ], + }); + + const reviews = reviewsQuery.data || []; + const diseases = diseasesQuery.data || []; + const selected = useMemo(() => reviews.find((item) => item.id === selectedId) ?? reviews[0] ?? null, [reviews, selectedId]); + + const mutation = useMutation({ + mutationFn: () => apiClient.reviewDiagnosis(selected!.id, { + verdict, + correctedDiseaseSlug: verdict === 'corrected' ? correctedDiseaseSlug || undefined : undefined, + notes, + }), + onSuccess: () => { + setNotes(''); + setVerdict('verified'); + setCorrectedDiseaseSlug(''); + queryClient.invalidateQueries({ queryKey: ['expert-reviews'] }); + queryClient.invalidateQueries({ queryKey: ['dashboard-summary'] }); + }, + }); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!selected) return; + await mutation.mutateAsync(); + } + + return ( +
+
+
+

Expert Reviews

+ + + +
+
+ +
+
+ {/* Left: List of reviews */} +
+
+

Diagnoses to Review

+ {reviews.length} +
+ +
+ {reviews.length === 0 ? ( +

No diagnoses pending review

+ ) : ( + reviews.map((review) => ( + + )) + )} +
+
+ + {/* Right: Detail and form */} +
+ {selected ? ( + <> + {/* Detail card */} + + + {selected.disease?.commonName ?? 'Diagnosis gagal'} + + {new Date(selected.createdAt).toLocaleString('id-ID')} + + + + Daun jagung + +
+
+ Status + +
+
+ Confidence + + {selected.confidence === null ? 'N/A' : `${(selected.confidence * 100).toFixed(1)}%`} + +
+
+ + {/* Top predictions */} + {selected.predictions.length > 0 && ( +
+

Top Predictions

+
+ {selected.predictions.map((pred) => ( +
+ {pred.modelLabel} + {(pred.confidence * 100).toFixed(1)}% +
+ ))} +
+
+ )} +
+
+ + {/* Review form */} + + + Expert Review + + +
+ {/* Verdict selection */} +
+ +
+ + +
+
+ + {/* Disease selection (only if corrected) */} + {verdict === 'corrected' && ( +
+ + +
+ )} + + {/* Notes */} +
+ +