From 4ff610816da44769d37daff791321836998698eb Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sat, 12 Oct 2024 01:22:34 +0900 Subject: [PATCH] feat: enhance admin pages --- .../admin/problems/[problemId]/page.tsx | 118 ++++++++++++++++++ src/app/(withAuth)/admin/statistics/page.tsx | 17 ++- 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/app/(withAuth)/admin/problems/[problemId]/page.tsx diff --git a/src/app/(withAuth)/admin/problems/[problemId]/page.tsx b/src/app/(withAuth)/admin/problems/[problemId]/page.tsx new file mode 100644 index 00000000..de23afcd --- /dev/null +++ b/src/app/(withAuth)/admin/problems/[problemId]/page.tsx @@ -0,0 +1,118 @@ +import type { NextPage } from 'next'; + +import { logger } from '../../../../../infrastructures/pino'; +import { prisma } from '../../../../../infrastructures/prisma'; +import { Box, Heading, Table, Tbody, Td, Th, Thead, Tr, VStack } from '../../../../../infrastructures/useClient/chakra'; +import type { CourseId, ProblemId } from '../../../../../problems/problemData'; +import { courseIdToLectureIds } from '../../../../../problems/problemData'; +import { getEmailFromSession } from '../../../../../utils/session'; +import { dayjs } from '../../../../utils/dayjs'; + +interface UserProblemInfo { + userId: string; + email?: string; + courseId: string; + lectureIndex: number; + problemId: string; + completedAt: Date | null; + elapsedMilliseconds: number; + incorrectSubmissionCount: number; +} + +type Props = { + params: { problemId: ProblemId }; +}; + +const StatisticsPage: NextPage = async (props) => { + const userInfos = await fetchUserProblemInfo(props.params.problemId); + + return ( + + 問題: {props.params.problemId} + + + + + + + + + + + + + + + {userInfos.map((info) => ( + + + + + + + + + + ))} + +
ユーザーIDEmailコース講義完了日時所要時間不正解回数
{info.userId}{info.email || 'N/A'}{info.courseId}{info.lectureIndex + 1}{info.completedAt ? dayjs(info.completedAt).format('YYYY-MM-DD HH:mm:ss') : '未完了'}{info.elapsedMilliseconds ? dayjs.duration(info.elapsedMilliseconds).humanize() : 'N/A'}{info.incorrectSubmissionCount}
+
+
+ ); +}; + +async function fetchUserProblemInfo(problemId: ProblemId): Promise { + try { + const sessions = await prisma.problemSession.findMany({ + where: { + problemId, + // eslint-disable-next-line unicorn/no-null + completedAt: { not: null }, + submissions: { + some: { isCorrect: true }, + }, + }, + orderBy: { + completedAt: 'asc', + }, + distinct: ['userId'], + include: { + submissions: { where: { isCorrect: false }, select: { id: true } }, + }, + }); + console.log('sessions:', sessions); + + return await Promise.all( + sessions.map(async (session) => ({ + userId: session.userId, + email: await getEmailFromSession(session.userId), + courseId: session.courseId, + lectureIndex: getLectureIndex(session.courseId, session.lectureId), + problemId: session.problemId, + completedAt: session.completedAt, + elapsedMilliseconds: session.elapsedMilliseconds, + incorrectSubmissionCount: session.submissions.length, + })) + ); + } catch (error) { + logger.error(`Failed to fetch user problem info for ${problemId}: %o`, error); + return []; + } +} + +function getLectureIndex(courseId: string, lectureId: string): number { + const lectureIds = courseIdToLectureIds[courseId as CourseId]; + if (!lectureIds) { + logger.warn(`Course (${courseId}) not found.`); + return -1; + } + for (const [index, lectureId_] of lectureIds.entries()) { + if (lectureId_.includes(lectureId)) { + return index; + } + } + logger.warn(`Lecture (${lectureId}) not found in course (${courseId}).`); + return -1; +} + +export default StatisticsPage; diff --git a/src/app/(withAuth)/admin/statistics/page.tsx b/src/app/(withAuth)/admin/statistics/page.tsx index a7dea115..23fecc98 100644 --- a/src/app/(withAuth)/admin/statistics/page.tsx +++ b/src/app/(withAuth)/admin/statistics/page.tsx @@ -2,7 +2,18 @@ import type { NextPage } from 'next'; import { logger } from '../../../../infrastructures/pino'; import { prisma } from '../../../../infrastructures/prisma'; -import { Box, Heading, Table, Tbody, Td, Th, Thead, Tr, VStack } from '../../../../infrastructures/useClient/chakra'; +import { + Box, + Heading, + Link, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + VStack, +} from '../../../../infrastructures/useClient/chakra'; import type { CourseId } from '../../../../problems/problemData'; import { courseIdToLectureIndexToProblemIds } from '../../../../problems/problemData'; import { dayjs } from '../../../utils/dayjs'; @@ -41,7 +52,9 @@ const StatisticsPage: NextPage = async () => { {stat.courseId} {stat.lectureIndex + 1} - {stat.problemId} + + {stat.problemId} + {stat.userCount} {stat.completedUserCount} {dayjs.duration(stat.avgElapsedMilliseconds).humanize()}