feat: update dashboard for diagnosis workflow
This commit is contained in:
@@ -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',
|
||||||
|
};
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user