Files
hristudio/src/server/api/routers/dashboard.ts

338 lines
9.8 KiB
TypeScript
Executable File

import { and, count, desc, eq, gte, inArray, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import {
activityLogs,
experiments,
participants,
studies,
studyMembers,
trials,
users,
userSystemRoles,
} from "~/server/db/schema";
export const dashboardRouter = createTRPCRouter({
getRecentActivity: protectedProcedure
.input(
z.object({
limit: z.number().min(1).max(20).default(10),
studyId: z.string().uuid().optional(),
}),
)
.query(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
// Get studies the user has access to
const accessibleStudies = await ctx.db
.select({ studyId: studyMembers.studyId })
.from(studyMembers)
.where(eq(studyMembers.userId, userId));
const studyIds = accessibleStudies.map((s) => s.studyId);
// If no accessible studies, return empty
if (studyIds.length === 0) {
return [];
}
// Build where conditions
const whereConditions = input.studyId
? eq(activityLogs.studyId, input.studyId)
: inArray(activityLogs.studyId, studyIds);
// Get recent activity logs
const activities = await ctx.db
.select({
id: activityLogs.id,
action: activityLogs.action,
description: activityLogs.description,
createdAt: activityLogs.createdAt,
user: {
name: users.name,
email: users.email,
},
study: {
name: studies.name,
},
})
.from(activityLogs)
.innerJoin(users, eq(activityLogs.userId, users.id))
.innerJoin(studies, eq(activityLogs.studyId, studies.id))
.where(whereConditions)
.orderBy(desc(activityLogs.createdAt))
.limit(input.limit);
return activities.map((activity) => ({
id: activity.id,
type: activity.action,
title: activity.description,
description: `${activity.study.name} - ${activity.user.name}`,
time: activity.createdAt,
status: "info" as const,
}));
}),
getStudyProgress: protectedProcedure
.input(
z.object({
limit: z.number().min(1).max(10).default(5),
studyId: z.string().uuid().optional(),
}),
)
.query(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
// Build where conditions
const whereConditions = input.studyId
? and(
eq(studyMembers.userId, userId),
eq(studies.status, "active"),
eq(studies.id, input.studyId),
)
: and(eq(studyMembers.userId, userId), eq(studies.status, "active"));
// Get studies the user has access to with participant counts
const studyProgress = await ctx.db
.select({
id: studies.id,
name: studies.name,
status: studies.status,
createdAt: studies.createdAt,
totalParticipants: count(participants.id),
})
.from(studies)
.innerJoin(studyMembers, eq(studies.id, studyMembers.studyId))
.leftJoin(participants, eq(studies.id, participants.studyId))
.where(whereConditions)
.groupBy(studies.id, studies.name, studies.status, studies.createdAt)
.orderBy(desc(studies.createdAt))
.limit(input.limit);
// Get trial completion counts for each study
const studyIds = studyProgress.map((s) => s.id);
const trialCounts =
studyIds.length > 0
? await ctx.db
.select({
studyId: experiments.studyId,
completedTrials: count(trials.id),
})
.from(experiments)
.innerJoin(trials, eq(experiments.id, trials.experimentId))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "completed"),
),
)
.groupBy(experiments.studyId)
: [];
const trialCountMap = new Map(
trialCounts.map((tc) => [tc.studyId, tc.completedTrials]),
);
return studyProgress.map((study) => {
const completedTrials = trialCountMap.get(study.id) ?? 0;
const totalParticipants = study.totalParticipants;
// Calculate progress based on completed trials vs participants
// If no participants, progress is 0; if trials >= participants, progress is 100%
const progress =
totalParticipants > 0
? Math.min(
100,
Math.round((completedTrials / totalParticipants) * 100),
)
: 0;
return {
id: study.id,
name: study.name,
progress,
participants: completedTrials, // Using completed trials as active participants
totalParticipants,
status: study.status,
};
});
}),
getStats: protectedProcedure
.input(
z.object({
studyId: z.string().uuid().optional(),
}),
)
.query(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
// Get studies the user has access to
const accessibleStudies = await ctx.db
.select({ studyId: studyMembers.studyId })
.from(studyMembers)
.where(eq(studyMembers.userId, userId));
let studyIds = accessibleStudies.map((s) => s.studyId);
// Filter to specific study if provided
if (input.studyId) {
// Verify user has access to the specific study
if (studyIds.includes(input.studyId)) {
studyIds = [input.studyId];
} else {
// User doesn't have access to this study
studyIds = [];
}
}
if (studyIds.length === 0) {
return {
totalStudies: 0,
totalExperiments: 0,
totalParticipants: 0,
totalTrials: 0,
activeTrials: 0,
scheduledTrials: 0,
completedToday: 0,
};
}
// Get total counts
const [studyCount] = await ctx.db
.select({ count: count() })
.from(studies)
.where(inArray(studies.id, studyIds));
const [experimentCount] = await ctx.db
.select({ count: count() })
.from(experiments)
.where(inArray(experiments.studyId, studyIds));
const [participantCount] = await ctx.db
.select({ count: count() })
.from(participants)
.where(inArray(participants.studyId, studyIds));
const [trialCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(inArray(experiments.studyId, studyIds));
// Get active trials count
const [activeTrialsCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "in_progress"),
),
);
// Get scheduled trials count
const [scheduledTrialsCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "scheduled"),
),
);
// Get today's completed trials
const today = new Date();
today.setHours(0, 0, 0, 0);
const [completedTodayCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "completed"),
gte(trials.completedAt, today),
),
);
return {
totalStudies: studyCount?.count ?? 0,
totalExperiments: experimentCount?.count ?? 0,
totalParticipants: participantCount?.count ?? 0,
totalTrials: trialCount?.count ?? 0,
activeTrials: activeTrialsCount?.count ?? 0,
scheduledTrials: scheduledTrialsCount?.count ?? 0,
completedToday: completedTodayCount?.count ?? 0,
};
}),
debug: protectedProcedure.query(async ({ ctx }) => {
const userId = ctx.session.user.id;
// Get user info
const user = await ctx.db.query.users.findFirst({
where: eq(users.id, userId),
});
// Get user system roles
const systemRoles = await ctx.db.query.userSystemRoles.findMany({
where: eq(userSystemRoles.userId, userId),
});
// Get user study memberships
const studyMemberships = await ctx.db.query.studyMembers.findMany({
where: eq(studyMembers.userId, userId),
with: {
study: {
columns: {
id: true,
name: true,
status: true,
},
},
},
});
// Get all studies (admin view)
const allStudies = await ctx.db.query.studies.findMany({
columns: {
id: true,
name: true,
status: true,
createdBy: true,
},
where: sql`deleted_at IS NULL`,
limit: 10,
});
return {
user: user
? {
id: user.id,
email: user.email,
name: user.name,
}
: null,
systemRoles: systemRoles.map((r) => r.role),
studyMemberships: studyMemberships.map((m) => ({
studyId: m.studyId,
role: m.role,
study: m.study,
})),
allStudies,
session: {
userId: ctx.session.user.id,
userEmail: ctx.session.user.email,
userRole: ctx.session.user.roles?.[0]?.role ?? null,
},
};
}),
});