mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
338 lines
9.8 KiB
TypeScript
Executable File
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,
|
|
},
|
|
};
|
|
}),
|
|
});
|