From 1b5c16b135585d1c6aa5a0d0d53f829c4d8ab863 Mon Sep 17 00:00:00 2001 From: Asep Haryana Saputra <90584806+MythEclipse@users.noreply.github.com> Date: Fri, 22 May 2026 17:03:26 +0000 Subject: [PATCH] feat: migrate dashboard to diagnosis workflow --- .../components/image-classification-form.tsx | 204 +++++++----------- apps/web/src/pages/dashboard-page.tsx | 172 ++++++--------- 2 files changed, 149 insertions(+), 227 deletions(-) diff --git a/apps/web/src/components/image-classification-form.tsx b/apps/web/src/components/image-classification-form.tsx index 568d4bf..e55b914 100644 --- a/apps/web/src/components/image-classification-form.tsx +++ b/apps/web/src/components/image-classification-form.tsx @@ -1,14 +1,15 @@ -import { useRef, useState } from 'react'; -import type { ImageClassificationRecord } from '@zeavis/shared'; +import { ChangeEvent, FormEvent, useRef, useState } from 'react'; +import type { DiagnosisRecord } from '@zeavis/shared'; +import { Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { RiskBadge } from '@/components/risk-badge'; +import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge'; -export interface ImageClassificationFormProps { +type ImageClassificationFormProps = { onSubmit: (file: File) => Promise; isSubmitting: boolean; - latestResult: ImageClassificationRecord | null; -} + latestResult: DiagnosisRecord | null; +}; export function ImageClassificationForm({ onSubmit, @@ -16,140 +17,95 @@ export function ImageClassificationForm({ latestResult, }: ImageClassificationFormProps) { const fileInputRef = useRef(null); - const [selectedFile, setSelectedFile] = useState(null); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); const [error, setError] = useState(null); - const handleFileChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - if (!file.type.startsWith('image/')) { - setError('Silakan pilih file gambar'); - setSelectedFile(null); - return; - } - setError(null); - setSelectedFile(file); - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + function handleFileChange(event: ChangeEvent) { + const selectedFile = event.target.files?.[0] ?? null; setError(null); + setFile(selectedFile); + setPreviewUrl(selectedFile ? URL.createObjectURL(selectedFile) : null); + } - if (!selectedFile) { - setError('Silakan pilih file gambar'); + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!file) { + setError('Pilih gambar terlebih dahulu'); return; } try { - await onSubmit(selectedFile); - setSelectedFile(null); + await onSubmit(file); + setFile(null); + setPreviewUrl(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } } catch (err) { - setError(err instanceof Error ? err.message : 'Terjadi kesalahan saat mengunggah gambar'); + setError(err instanceof Error ? err.message : 'Gagal mengirim gambar'); } - }; - - const isFormValid = selectedFile !== null; + } return ( -
- - - Klasifikasi Gambar - Unggah foto daun jagung untuk klasifikasi otomatis - - -
- {error && ( -
{error}
- )} - -
- - - {selectedFile && ( -

- File dipilih: {selectedFile.name} -

- )} -
- - -
-
-
- - {latestResult && ( - - - Hasil Klasifikasi Terbaru - Prediksi penyakit dari gambar terakhir - - -
- Uploaded corn leaf -
- -
-
-
-

{latestResult.disease.commonName}

-

{latestResult.disease.label}

-
- -
- -
-

- Kepercayaan: {(latestResult.confidence * 100).toFixed(1)}% -

-
- -
-

Rekomendasi awal:

-
    - {latestResult.disease.recommendations.slice(0, 3).map((recommendation) => ( -
  • • {recommendation}
  • - ))} -
-
- -

- {new Date(latestResult.createdAt).toLocaleDateString('id-ID', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - })} + + + + Diagnosis Gambar + + + Upload gambar daun jagung untuk klasifikasi AI dan review pakar jika confidence rendah. + + + +

+
+ + + {file && ( +

+ File dipilih: {file.name}

+ )} +
+ + {previewUrl && ( + Preview + )} + + {error &&

{error}

} + + +
+ + {latestResult && ( +
+
+

+ {latestResult.disease?.commonName ?? 'Diagnosis gagal'} +

+
- - - )} -
+

+ {latestResult.confidence === null + ? 'Tidak ada confidence' + : `Confidence ${(latestResult.confidence * 100).toFixed(1)}%`} +

+
+ )} +
+
); } diff --git a/apps/web/src/pages/dashboard-page.tsx b/apps/web/src/pages/dashboard-page.tsx index d4ebf06..a352735 100644 --- a/apps/web/src/pages/dashboard-page.tsx +++ b/apps/web/src/pages/dashboard-page.tsx @@ -1,19 +1,22 @@ import { useQueries, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Link } from 'react-router-dom'; -import { BookOpen, History, Leaf, LayoutDashboard, TrendingUp, Image } from 'lucide-react'; +import { Link, useNavigate } from 'react-router-dom'; +import { BookOpen, History, LayoutDashboard, TrendingUp, LogOut, CheckCircle2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { ManualClassificationForm } from '@/components/manual-classification-form'; import { ImageClassificationForm } from '@/components/image-classification-form'; -import { RiskBadge } from '@/components/risk-badge'; +import { DiagnosisCard } from '@/components/diagnosis-card'; +import { useAuthStore } from '@/store/auth-store'; import { useUiStore } from '@/store/ui-store'; import { apiClient } from '@/lib/api-client'; export function DashboardPage() { + const { user } = useAuthStore(); const { dashboardCompact, toggleDashboardCompact } = useUiStore(); const queryClient = useQueryClient(); + const navigate = useNavigate(); - const [diseasesQuery, summaryQuery, classificationsQuery, imageClassificationsQuery] = useQueries({ + const [diseasesQuery, summaryQuery, diagnosesQuery, classificationsQuery] = useQueries({ queries: [ { queryKey: ['diseases'], @@ -23,17 +26,27 @@ export function DashboardPage() { queryKey: ['dashboard-summary'], queryFn: () => apiClient.getDashboardSummary(), }, + { + queryKey: ['diagnoses'], + queryFn: () => apiClient.getDiagnoses(), + }, { queryKey: ['manual-classifications'], queryFn: () => apiClient.getManualClassifications(), }, - { - queryKey: ['image-classifications'], - queryFn: () => apiClient.getImageClassifications(), - }, ], }); + const createDiagnosisMutation = useMutation({ + mutationFn: async (file: File) => { + return await apiClient.createDiagnosis(file); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['diagnoses'] }); + queryClient.invalidateQueries({ queryKey: ['dashboard-summary'] }); + }, + }); + const createClassificationMutation = useMutation({ mutationFn: async (payload: Parameters[0]) => { await apiClient.createManualClassification(payload); @@ -44,20 +57,21 @@ export function DashboardPage() { }, }); - const createImageClassificationMutation = useMutation({ - mutationFn: async (file: File) => { - return await apiClient.createImageClassification(file); + const logoutMutation = useMutation({ + mutationFn: async () => { + await apiClient.logout(); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['image-classifications'] }); - queryClient.invalidateQueries({ queryKey: ['dashboard-summary'] }); + useAuthStore.setState({ user: null }); + queryClient.clear(); + navigate('/login'); }, }); const diseases = diseasesQuery.data || []; const summary = summaryQuery.data; + const diagnoses = diagnosesQuery.data || []; const classifications = classificationsQuery.data || []; - const imageClassifications = imageClassificationsQuery.data || []; const isLoadingData = diseasesQuery.isLoading || summaryQuery.isLoading; const hasError = diseasesQuery.error || summaryQuery.error; @@ -72,15 +86,28 @@ export function DashboardPage() {

ZeaVis Edu Workspace

- Pantau penyakit daun jagung dan laporkan pengamatan Anda + {user?.name ? `Selamat datang, ${user.name}` : 'Pantau penyakit daun jagung dan laporkan pengamatan Anda'}

+ {user?.role === 'expert' && ( + + )} -
@@ -115,11 +142,24 @@ export function DashboardPage() { - Total Laporan + Total Diagnosis -
{summary.classificationCount}
+
{summary.imageClassificationCount}
+
+
+ + + + + Menunggu Review + + + +
+ {summary.needsReviewCount} +
@@ -135,19 +175,6 @@ export function DashboardPage() { - - - - - Risiko Sedang - - - -
- {summary.riskDistribution.medium} -
-
-
)} @@ -200,48 +227,12 @@ export function DashboardPage() { - {summary?.latestClassification && ( - - - - - Laporan Terbaru - - - Pengamatan penyakit terakhir yang dilaporkan - - - -
-
-
-

{summary.latestClassification.disease.commonName}

-

- {summary.latestClassification.disease.label} -

-
- -
-

- {summary.latestClassification.observation} -

-

- Lokasi: {summary.latestClassification.location} -

-

- {new Date(summary.latestClassification.createdAt).toLocaleDateString('id-ID')} -

-
-
-
- )} - { - await createImageClassificationMutation.mutateAsync(file); + await createDiagnosisMutation.mutateAsync(file); }} - isSubmitting={createImageClassificationMutation.isPending} - latestResult={imageClassifications[0] ?? null} + isSubmitting={createDiagnosisMutation.isPending} + latestResult={diagnoses[0] ?? null} /> - {imageClassifications.length > 0 && ( + {diagnoses.length > 0 && ( - - Riwayat Klasifikasi Gambar + + Riwayat Diagnosis - {imageClassifications.length} gambar yang diklasifikasi + {diagnoses.length} diagnosis yang telah dibuat -
- {imageClassifications.slice(0, 6).map((classification) => ( -
- Classified corn leaf -
-
-
-

- {classification.disease.commonName} -

-

- {classification.disease.label} -

-
- -
-
- Kepercayaan: {(classification.confidence * 100).toFixed(1)}% -
-

- {new Date(classification.createdAt).toLocaleDateString('id-ID')} -

-
-
+
+ {diagnoses.slice(0, 6).map((diagnosis) => ( + ))}