feat: update dashboard for diagnosis workflow

This commit is contained in:
Asep Haryana Saputra
2026-05-22 16:36:53 +00:00
parent 7712a5c6a1
commit 02b002b021
2 changed files with 72 additions and 78 deletions
+9 -1
View File
@@ -1,8 +1,9 @@
import { diseaseCatalogSeed } from '@zeavis/shared'; import { diseaseCatalogSeed } from '@zeavis/shared';
import type { InferInsertModel } from 'drizzle-orm'; import type { InferInsertModel } from 'drizzle-orm';
import { diseaseCatalog } from './schema'; import { diseaseCatalog, users } from './schema';
export type DiseaseCatalogRow = InferInsertModel<typeof diseaseCatalog>; export type DiseaseCatalogRow = InferInsertModel<typeof diseaseCatalog>;
export type UserRow = InferInsertModel<typeof users>;
export const diseaseCatalogRows: DiseaseCatalogRow[] = diseaseCatalogSeed.map((item) => ({ export const diseaseCatalogRows: DiseaseCatalogRow[] = diseaseCatalogSeed.map((item) => ({
slug: item.slug, slug: item.slug,
@@ -16,3 +17,10 @@ export const diseaseCatalogRows: DiseaseCatalogRow[] = diseaseCatalogSeed.map((i
accentColor: item.accentColor, accentColor: item.accentColor,
displayOrder: item.displayOrder, displayOrder: item.displayOrder,
})); }));
export const demoExpertUser = {
email: 'expert@zeavis.local',
name: 'ZeaVis Expert',
role: 'expert' as const,
password: 'password123',
};
+63 -77
View File
@@ -1,88 +1,74 @@
import { Elysia } from 'elysia'; import { Elysia } from 'elysia';
import type { DashboardSummary, RiskLevel } from '@zeavis/shared'; import type { DashboardSummary } from '@zeavis/shared';
import { isDiseaseSlug } from '@zeavis/shared';
import { eq, desc, count } from 'drizzle-orm';
import { createDbClient } from '../db/client'; import { createDbClient } from '../db/client';
import { diseaseCatalog, imageClassifications, manualClassifications } from '../db/schema'; import { diseaseCatalog, manualClassifications, diagnoses } from '../db/schema';
import { serviceUnavailable } from '../lib/http-errors'; import { serviceUnavailable } from '../lib/http-errors';
import { desc, eq } from 'drizzle-orm'; import { toDisease } from '../lib/disease-mappers';
import { getCurrentUser } from '../lib/auth';
import { loadDiagnosisRecord } from './diagnoses';
export const dashboardRoutes = new Elysia({ prefix: '/api/v1' }).get('/dashboard/summary', async () => { export const dashboardRoutes = new Elysia({ prefix: '/api/v1' })
try { .get('/dashboard/summary', async ({ request }) => {
const db = createDbClient(); try {
const db = createDbClient();
const user = await getCurrentUser(request.headers.get('cookie'));
const diseaseCount = await db.select().from(diseaseCatalog); const diseases = await db.select().from(diseaseCatalog).orderBy(diseaseCatalog.displayOrder);
const manualClassificationCount = await db.select().from(manualClassifications); const manualRows = await db.select().from(manualClassifications).orderBy(desc(manualClassifications.createdAt)).limit(1);
const imageClassificationCount = await db.select().from(imageClassifications);
const latestClassificationRow = await db const diagnosisFilters = user ? eq(diagnoses.userId, user.id) : undefined;
.select({ const diagnosisRows = diagnosisFilters
id: manualClassifications.id, ? await db.select({ id: diagnoses.id }).from(diagnoses).where(diagnosisFilters).orderBy(desc(diagnoses.createdAt)).limit(1)
diseaseSlug: manualClassifications.diseaseSlug, : [];
observation: manualClassifications.observation,
location: manualClassifications.location, const diagnosisCountRows = user
createdAt: manualClassifications.createdAt, ? await db.select({ value: count() }).from(diagnoses).where(eq(diagnoses.userId, user.id))
disease: { : [{ value: 0 }];
slug: diseaseCatalog.slug,
label: diseaseCatalog.label, const needsReviewRows = user?.role === 'expert'
commonName: diseaseCatalog.commonName, ? await db.select({ value: count() }).from(diagnoses).where(eq(diagnoses.status, 'needs_review'))
summary: diseaseCatalog.summary, : [{ value: 0 }];
description: diseaseCatalog.description,
symptoms: diseaseCatalog.symptoms, const latestDiagnosis = diagnosisRows[0]
recommendations: diseaseCatalog.recommendations, ? await loadDiagnosisRecord(diagnosisRows[0].id, user?.id ?? null, user?.role === 'expert')
riskLevel: diseaseCatalog.riskLevel, : null;
accentColor: diseaseCatalog.accentColor,
displayOrder: diseaseCatalog.displayOrder, const riskDistribution = diseases.reduce(
(acc, disease) => {
if (disease.riskLevel === 'high') acc.high += 1;
if (disease.riskLevel === 'medium') acc.medium += 1;
if (disease.riskLevel === 'low') acc.low += 1;
return acc;
}, },
}) { low: 0, medium: 0, high: 0 },
.from(manualClassifications) );
.innerJoin(diseaseCatalog, eq(manualClassifications.diseaseSlug, diseaseCatalog.slug))
.orderBy(desc(manualClassifications.createdAt))
.limit(1);
const latestClassification = latestClassificationRow[0] const latestManual = manualRows[0];
? { const latestDisease = latestManual
id: latestClassificationRow[0].id, ? diseases.find((disease) => disease.slug === latestManual.diseaseSlug)
diseaseSlug: latestClassificationRow[0].diseaseSlug as any, : null;
observation: latestClassificationRow[0].observation,
location: latestClassificationRow[0].location,
createdAt: latestClassificationRow[0].createdAt.toISOString(),
disease: {
slug: latestClassificationRow[0].disease.slug as any,
label: latestClassificationRow[0].disease.label as any,
commonName: latestClassificationRow[0].disease.commonName,
summary: latestClassificationRow[0].disease.summary,
description: latestClassificationRow[0].disease.description,
symptoms: latestClassificationRow[0].disease.symptoms,
recommendations: latestClassificationRow[0].disease.recommendations,
riskLevel: latestClassificationRow[0].disease.riskLevel as any,
accentColor: latestClassificationRow[0].disease.accentColor,
displayOrder: latestClassificationRow[0].disease.displayOrder,
},
}
: null;
const riskDistributionRows = await db.select().from(diseaseCatalog); const summary: DashboardSummary = {
const riskDistribution: Record<RiskLevel, number> = { diseaseCount: diseases.length,
low: 0, classificationCount: manualRows.length,
medium: 0, imageClassificationCount: diagnosisCountRows[0]?.value ?? 0,
high: 0, needsReviewCount: needsReviewRows[0]?.value ?? 0,
}; riskDistribution,
latestClassification: latestManual && latestDisease ? {
id: latestManual.id,
diseaseSlug: latestManual.diseaseSlug as any,
observation: latestManual.observation,
location: latestManual.location,
createdAt: latestManual.createdAt.toISOString(),
disease: toDisease(latestDisease),
} : null,
latestDiagnosis,
};
for (const row of riskDistributionRows) { return summary;
const risk = row.riskLevel as RiskLevel; } catch (error) {
if (risk in riskDistribution) { return serviceUnavailable('Database unavailable');
riskDistribution[risk]++;
}
} }
});
const summary: DashboardSummary = {
diseaseCount: diseaseCount.length,
classificationCount: manualClassificationCount.length + imageClassificationCount.length,
latestClassification,
riskDistribution,
};
return summary;
} catch (error) {
return serviceUnavailable('Database unavailable');
}
});