feat: add Diagnoses page and update routing; enhance Scan page with recent diagnoses list
This commit is contained in:
+14
-13
@@ -5,28 +5,22 @@ import {
|
||||
Navigate,
|
||||
} from "react-router-dom";
|
||||
import { DashboardPage } from "@/pages/dashboard-page";
|
||||
// import { LandingPage } from "@/pages/landing-page";
|
||||
import { ScanPage } from "@/pages/scan-page";
|
||||
import { LibraryPage } from "@/pages/library-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";
|
||||
import { DiagnosesPage } from "@/pages/diagnoses-page";
|
||||
import { MainLayout } from "@/components/layout/main-layout";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const router = createBrowserRouter([
|
||||
// {
|
||||
// path: "/",
|
||||
// element: (
|
||||
// <MainLayout>
|
||||
// <LandingPage />
|
||||
// </MainLayout>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
path: "/",
|
||||
element: <Navigate to="/dashboard" replace />,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <Navigate to="/dashboard" replace />,
|
||||
@@ -47,8 +41,7 @@ const router = createBrowserRouter([
|
||||
</MainLayout>
|
||||
),
|
||||
},
|
||||
// { path: "/login", element: <LoginPage /> },
|
||||
// { path: "/register", element: <RegisterPage /> },
|
||||
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: (
|
||||
@@ -61,6 +54,14 @@ const router = createBrowserRouter([
|
||||
path: "/diagnoses/:id",
|
||||
element: <DiagnosisDetailPage />,
|
||||
},
|
||||
{
|
||||
path: "/diagnoses",
|
||||
element: (
|
||||
<MainLayout>
|
||||
<DiagnosesPage />
|
||||
</MainLayout>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/expert/reviews",
|
||||
element: <ExpertReviewsPage />,
|
||||
|
||||
@@ -156,8 +156,11 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold">
|
||||
{summary.diseaseCount}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Link to="/diagnoses" className="text-emerald-600 ml-auto hover:underline">
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
@@ -169,8 +172,11 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold">
|
||||
{summary.imageClassificationCount}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Link to="/diagnoses" className="text-emerald-600 ml-auto hover:underline">
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
@@ -182,6 +188,9 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold text-amber-600">
|
||||
{summary.needsReviewCount}
|
||||
</div>
|
||||
<Link to="/diagnoses?status=needs_review" className="text-amber-600 ml-auto hover:underline">
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -195,6 +204,9 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold text-red-600">
|
||||
{summary.riskDistribution.high}
|
||||
</div>
|
||||
<Link to="/diagnoses?risk=high" className="text-red-600 ml-auto hover:underline">
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
|
||||
import { RiskBadge } from "@/components/risk-badge";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
|
||||
export function DiagnosesPage() {
|
||||
const [statusFilter, setStatusFilter] = useState<
|
||||
"all" | "needs_review" | "verified" | "failed"
|
||||
>("all");
|
||||
const [riskFilter, setRiskFilter] = useState<
|
||||
"all" | "high" | "medium" | "low"
|
||||
>("all");
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["diagnoses"],
|
||||
queryFn: () => apiClient.getDiagnoses(),
|
||||
});
|
||||
const diagnoses = query.data ?? [];
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return diagnoses.filter((d) => {
|
||||
if (statusFilter !== "all" && d.status !== statusFilter) return false;
|
||||
if (riskFilter !== "all") {
|
||||
const level = d.disease?.riskLevel ?? "low";
|
||||
if (level !== riskFilter) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [diagnoses, statusFilter, riskFilter]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-8">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-primary">
|
||||
Manajemen Diagnosis
|
||||
</p>
|
||||
<h1 className="text-3xl font-bold">Daftar Diagnosis</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link to="/dashboard">
|
||||
<Button variant="outline">Dashboard</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4 p-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="text-sm font-medium">Status</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
||||
className="rounded-md border border-border bg-background px-2 py-1"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="needs_review">Needs review</option>
|
||||
<option value="verified">Verified</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
|
||||
<label className="text-sm font-medium">Risiko</label>
|
||||
<select
|
||||
value={riskFilter}
|
||||
onChange={(e) => setRiskFilter(e.target.value as any)}
|
||||
className="rounded-md border border-border bg-background px-2 py-1"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
|
||||
<div className="ml-auto text-sm text-muted-foreground">
|
||||
Total: {filtered.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{query.isLoading ? (
|
||||
<div className="p-6 text-center text-muted-foreground">
|
||||
Memuat diagnosis...
|
||||
</div>
|
||||
) : query.isError ? (
|
||||
<div className="p-6 text-center text-red-600">
|
||||
Gagal memuat diagnosis
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="p-6 text-center text-muted-foreground">
|
||||
Tidak ada diagnosis sesuai filter
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{filtered.map((d) => (
|
||||
<Card key={d.id} className="h-full">
|
||||
<CardContent className="flex gap-4 p-4 items-start">
|
||||
<img
|
||||
src={d.imageUrl}
|
||||
alt="Daun"
|
||||
className="h-24 w-24 rounded-md object-cover"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{d.disease?.commonName ?? "Unknown"}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{d.predictedDiseaseSlug ?? ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<DiagnosisStatusBadge status={d.status} />
|
||||
<RiskBadge level={d.disease?.riskLevel ?? "low"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{new Date(d.createdAt).toLocaleString("id-ID")}
|
||||
</div>
|
||||
<Link
|
||||
to={`/diagnoses/${d.id}`}
|
||||
className="text-emerald-600 font-semibold"
|
||||
>
|
||||
Lihat detail
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiagnosesPage;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import { Modal } from "@/components/ui/modal";
|
||||
import type { DiagnosisRecord } from "@zeavis/shared";
|
||||
@@ -16,7 +16,7 @@ export function ScanPage() {
|
||||
onSuccess: (diagnosis) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["diagnoses"] });
|
||||
// show preview modal instead of immediate navigation
|
||||
setDiagnosisPreview(diagnosis as DiagnosisRecord);
|
||||
setDiagnosisPreview(diagnosis);
|
||||
setPreviewOpen(true);
|
||||
},
|
||||
});
|
||||
@@ -31,6 +31,12 @@ export function ScanPage() {
|
||||
const [diagnosisPreview, setDiagnosisPreview] =
|
||||
useState<DiagnosisRecord | null>(null);
|
||||
|
||||
const diagnosesQuery = useQuery({
|
||||
queryKey: ["diagnoses"],
|
||||
queryFn: () => apiClient.getDiagnoses(),
|
||||
enabled: previewOpen,
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-2xl font-semibold mb-4">Scan Tanaman</h1>
|
||||
@@ -135,6 +141,53 @@ export function ScanPage() {
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4 border-t">
|
||||
<h4 className="text-sm font-medium mb-2">
|
||||
Daftar Diagnosis Terbaru
|
||||
</h4>
|
||||
{diagnosesQuery.isLoading && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Memuat daftar...
|
||||
</div>
|
||||
)}
|
||||
{diagnosesQuery.isError && (
|
||||
<div className="text-sm text-red-600">
|
||||
Gagal memuat daftar diagnosis
|
||||
</div>
|
||||
)}
|
||||
{!diagnosesQuery.isLoading && !diagnosesQuery.isError && (
|
||||
<div className="space-y-2">
|
||||
{(diagnosesQuery.data ?? []).slice(0, 5).map((d) => (
|
||||
<div
|
||||
key={d.id}
|
||||
className="flex items-center justify-between rounded-md p-2 hover:bg-muted"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={d.imageUrl}
|
||||
alt="thumb"
|
||||
className="h-10 w-10 rounded object-cover"
|
||||
/>
|
||||
<div className="text-sm">
|
||||
<div className="font-medium">
|
||||
{d.disease?.commonName ?? d.predictedDiseaseSlug}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(d.createdAt).toLocaleString("id-ID")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={`/diagnoses/${d.id}`}
|
||||
className="text-emerald-600 text-sm font-semibold"
|
||||
>
|
||||
Lihat
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user