diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 289864b..758b819 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["native", "debian-openssl-3.0.x"] + binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] } datasource db { diff --git a/src/app/find-the-expert/[role]/page.tsx b/src/app/find-the-expert/[role]/page.tsx index 0905b4a..66ef65d 100644 --- a/src/app/find-the-expert/[role]/page.tsx +++ b/src/app/find-the-expert/[role]/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import type { Session } from "next-auth"; + import { Suspense } from "react"; import { ShowRolesWrapper } from "~/app/result/[role]/page"; import ButtonSkeleton from "~/components/loading/button-loader"; @@ -7,13 +8,9 @@ import { Login } from "~/components/login"; import ShowDataTable from "~/components/show-data-table"; import { getServerAuthSession } from "~/server/auth"; import { - aggregateDataByRole, - createUserAndAnswerMaps, extractUniqueIds, fetchUserAnswersForRole, fetchUsersAndAnswerOptions, - groupDataByRoleAndQuestion, - sortResults, } from "~/utils/data-manipulation"; export const metadata: Metadata = { @@ -63,26 +60,12 @@ const ShowTableWrapper = async () => { userIds, answerIds, ); - const { userMap, answerOptionMap } = createUserAndAnswerMaps( - users, - answerOptions, - ); - const dataByRoleAndQuestion = groupDataByRoleAndQuestion( - userAnswersForRole, - userMap, - answerOptionMap, - ); - const aggregatedDataByRole = aggregateDataByRole( - userAnswersForRole, - userMap, - answerOptionMap, - ); - sortResults(aggregatedDataByRole); return ( ); }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ef6894d..b631533 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,7 +16,6 @@ import { Button } from "~/components/ui/button"; import { ArrowLeftDarkModeFriendly } from "~/components/svg"; import Link from "next/link"; import { headers } from "next/headers"; -import { Github } from "lucide-react"; import GithubLink from "~/components/github-link"; const inter = Inter({ diff --git a/src/components/show-data-table.tsx b/src/components/show-data-table.tsx index 2ae611b..340bc3a 100644 --- a/src/components/show-data-table.tsx +++ b/src/components/show-data-table.tsx @@ -1,5 +1,5 @@ "use client"; - +import type { $Enums } from "@prisma/client"; import React from "react"; import { slugify } from "~/utils/slugify"; import { @@ -10,32 +10,63 @@ import { import type { ColumnDef } from "@tanstack/react-table"; import { DataTable } from "./data-table"; import { usePathname } from "next/navigation"; +import { + aggregateDataByRole, + createUserAndAnswerMaps, + groupDataByRoleAndQuestion, + sortResults, +} from "~/utils/client-data-manipulation"; +import type { UserAnswersForRoleArray } from "~/models/types"; -type ShowDataTableProps = { - dataByRoleAndQuestion: Record< - string, - Record - >; - aggregatedDataByRole: Record< - string, - Record< - string, - { - name: string; - communicationPreferences: string[]; - counts: number[]; - } - > - >; -}; - -const ShowDataTable: React.FC = ({ - dataByRoleAndQuestion, - aggregatedDataByRole, +const ShowDataTable = ({ + userAnswersForRole, + users, + answerOptions, +}: { + userAnswersForRole: UserAnswersForRoleArray; + users: { + name: string | null; + id: string; + roles: { + id: string; + role: string; + default: boolean; + }[]; + email: string | null; + communicationPreferences: { + id: string; + userId: string; + methods: $Enums.CommunicationMethod[]; + }[]; + }[]; + answerOptions: { id: string; option: number }[]; }) => { const pathname = usePathname(); const currentRole = pathname.split("/").pop(); + + const usersForRole: typeof users = users.filter((user) => + user.roles.some((role) => slugify(role.role) === currentRole), + ); + + const { userMap, answerOptionMap } = createUserAndAnswerMaps( + usersForRole, + answerOptions, + ); + + const dataByRoleAndQuestion = groupDataByRoleAndQuestion( + userAnswersForRole, + userMap, + answerOptionMap, + ); + + const aggregatedDataByRole = aggregateDataByRole( + userAnswersForRole, + userMap, + answerOptionMap, + ); + sortResults(aggregatedDataByRole); + return ( <> {Object.keys(dataByRoleAndQuestion).length === 0 ? ( diff --git a/src/models/types.ts b/src/models/types.ts index 2d6c310..f7c22be 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -126,6 +126,7 @@ export type DataByRoleAndQuestion = Record< email: string; communicationPreferences: string[] | undefined; answer: string; + roles: string[]; }[] > >; @@ -136,6 +137,7 @@ export type UserMap = Record< name: string; email: string; communicationPreferences: string[]; + roles: string[]; } >; diff --git a/src/utils/client-data-manipulation.ts b/src/utils/client-data-manipulation.ts new file mode 100644 index 0000000..06a93d4 --- /dev/null +++ b/src/utils/client-data-manipulation.ts @@ -0,0 +1,249 @@ +"use client"; + +import type { $Enums } from "@prisma/client"; +import type { + AggregatedDataByRole, + AnswerOptionMap, + DataByRoleAndQuestion, + Entry, + Role, + UserAnswersForRoleArray, + UserMap, +} from "~/models/types"; + +export const createUserAndAnswerMaps = ( + users: { + id: string; + name: string | null; + email: string | null; + communicationPreferences: { + id: string; + userId: string; + methods: $Enums.CommunicationMethod[]; + }[]; + roles: { id: string; role: string }[]; + }[], + answerOptions: { id: string; option: number }[], +) => { + const userMap: UserMap = {}; + for (const user of users) { + userMap[user.id] = { + name: user.name ?? "Unknown User", + email: user.email ?? "Unknown Email", + communicationPreferences: user.communicationPreferences.map((method) => + method.methods.toString(), + ), + roles: user.roles.map((role) => role.role), + }; + } + + const answerOptionMap: AnswerOptionMap = {}; + for (const answerOption of answerOptions) { + answerOptionMap[answerOption.id] = + answerOption.option.toString() ?? "Unknown Answer"; + } + + return { userMap, answerOptionMap }; +}; + +// Helper function to initialize role and question in the data structure +const initializeRoleAndQuestion = ( + dataByRoleAndQuestion: DataByRoleAndQuestion, + roleName: string, + questionText: string, +): void => { + if (!dataByRoleAndQuestion[roleName]) { + dataByRoleAndQuestion[roleName] = {}; + } + if (!dataByRoleAndQuestion[roleName]![questionText]) { + dataByRoleAndQuestion[roleName]![questionText] = []; + } +}; + +// Helper function to push user data into the data structure +const pushUserData = ( + dataByRoleAndQuestion: DataByRoleAndQuestion, + roleName: string, + questionText: string, + entry: Entry, + userMap: UserMap, + answerOptionMap: AnswerOptionMap, +): void => { + // do nothing if there is no user data + if (!userMap[entry.userId]) { + return; + } + + dataByRoleAndQuestion[roleName]![questionText]!.push({ + name: userMap[entry.userId]?.name ?? "Unknown User", + email: userMap[entry.userId]?.email ?? "Unknown Email", + communicationPreferences: + userMap[entry.userId]!.communicationPreferences?.length > 0 + ? userMap[entry.userId]?.communicationPreferences + : ["Do not contact"], + answer: answerOptionMap[entry.answerId] ?? "Unknown Answer", + roles: userMap[entry.userId]?.roles ?? [], + }); +}; + +const sortUserData = ( + dataByRoleAndQuestion: DataByRoleAndQuestion, + roleName: string, + questionText: string, +): void => { + dataByRoleAndQuestion[roleName]![questionText]!.sort((a, b) => { + const answerValueA = parseInt(a.answer); + const answerValueB = parseInt(b.answer); + + // We don't want the same person be appear on top of the table all the time. So we shuffle the order of the users with the same answer value. + if (answerValueA === answerValueB) { + return Math.random() - 0.5; + } else { + return answerValueA - answerValueB; + } + }); +}; + +export const groupDataByRoleAndQuestion = ( + userAnswersForRole: UserAnswersForRoleArray, + userMap: UserMap, + answerOptionMap: AnswerOptionMap, +) => { + const dataByRoleAndQuestion: DataByRoleAndQuestion = {}; + + for (const entry of userAnswersForRole) { + for (const role of entry.question.roles ?? []) { + const roleName = role.role || "Unknown Role"; + const questionText = entry.question.questionText || "Unknown Question"; + + initializeRoleAndQuestion(dataByRoleAndQuestion, roleName, questionText); + pushUserData( + dataByRoleAndQuestion, + roleName, + questionText, + entry, + userMap, + answerOptionMap, + ); + sortUserData(dataByRoleAndQuestion, roleName, questionText); + } + } + + return dataByRoleAndQuestion; +}; + +const getRoleName = (role: Role) => role.role || "Unknown Role"; + +const initializeRole = ( + aggregatedDataByRole: AggregatedDataByRole, + roleName: string, +) => { + if (!aggregatedDataByRole[roleName]) { + aggregatedDataByRole[roleName] = {}; + } +}; + +const getUserDetails = (userMap: UserMap, entry: Entry) => { + const userName = userMap[entry.userId]?.name ?? "Unknown User"; + const userEmail = userMap[entry.userId]?.email ?? "Unknown Email"; + let userCommunicationPreferences = + userMap[entry.userId]?.communicationPreferences; + userCommunicationPreferences = + userCommunicationPreferences?.length ?? 0 > 0 + ? userCommunicationPreferences + : ["Do not contact"] ?? []; + return { userName, userEmail, userCommunicationPreferences }; +}; + +const initializeUser = ( + aggregatedDataByRole: AggregatedDataByRole, + roleName: string, + userEmail: string, + userName: string, + userCommunicationPreferences: string[], +) => { + if (!aggregatedDataByRole[roleName]![userEmail]) { + aggregatedDataByRole[roleName]![userEmail] = { + name: userName, + communicationPreferences: userCommunicationPreferences ?? [], + counts: [0, 0, 0, 0], + }; + } +}; + +const updateCounts = ( + aggregatedDataByRole: AggregatedDataByRole, + roleName: string, + userEmail: string, + answerValue: number, +) => { + if (!aggregatedDataByRole[roleName]![userEmail]?.counts[answerValue]) { + aggregatedDataByRole[roleName]![userEmail]!.counts[answerValue] = 0; + } + aggregatedDataByRole[roleName]![userEmail]!.counts[answerValue] = + (aggregatedDataByRole[roleName]![userEmail]?.counts[answerValue] ?? 0) + 1; +}; + +export const aggregateDataByRole = ( + userAnswersForRole: UserAnswersForRoleArray, + userMap: UserMap, + answerOptionMap: AnswerOptionMap, +) => { + const aggregatedDataByRole: AggregatedDataByRole = {}; + + for (const entry of userAnswersForRole) { + for (const role of entry.question.roles ?? []) { + const roleName = getRoleName(role); + initializeRole(aggregatedDataByRole, roleName); + + if (userMap[entry.userId]) { + const answerValue = parseInt(answerOptionMap[entry.answerId] ?? "", 10); + const { userName, userEmail, userCommunicationPreferences } = + getUserDetails(userMap, entry); + initializeUser( + aggregatedDataByRole, + roleName, + userEmail, + userName, + userCommunicationPreferences!, + ); + + if (!isNaN(answerValue)) { + updateCounts(aggregatedDataByRole, roleName, userEmail, answerValue); + } + } + } + } + + return aggregatedDataByRole; +}; + +export const sortResults = (aggregatedDataByRole: AggregatedDataByRole) => { + // Sorting the results based on the counts of each answer + for (const role in aggregatedDataByRole) { + for (const user in aggregatedDataByRole[role]) { + const counts = aggregatedDataByRole[role]![user]?.counts ?? [0, 0, 0, 0]; + aggregatedDataByRole[role]![user]!.counts = counts; + } + } + + // Sorting users based on the total count of 0 answers, then 1, 2, and 3 + for (const role in aggregatedDataByRole) { + const sortedEntries = Object.entries(aggregatedDataByRole[role] ?? {}).sort( + (a, b) => { + const countsA = a[1].counts; + const countsB = b[1].counts; + for (let i = 0; i < countsA.length; i++) { + const diff = (countsB[i] ?? 0) - (countsA[i] ?? 0); + if (diff !== 0) { + return diff; + } + } + return 0; + }, + ); + aggregatedDataByRole[role] = Object.fromEntries(sortedEntries); + } + + return aggregatedDataByRole; +}; diff --git a/src/utils/data-manipulation.ts b/src/utils/data-manipulation.ts index 163e9fe..51a64ee 100644 --- a/src/utils/data-manipulation.ts +++ b/src/utils/data-manipulation.ts @@ -1,14 +1,4 @@ -import type { $Enums } from "@prisma/client"; -import type { - AggregatedDataByRole, - AnswerOptionMap, - DataByRoleAndQuestion, - Entry, - Role, - UserAnswersForRoleArray, - UserIdAndAnswerId, - UserMap, -} from "~/models/types"; +import type { UserIdAndAnswerId } from "~/models/types"; import { db } from "~/server/db"; export const fetchUserAnswersForRole = async () => { @@ -35,6 +25,7 @@ export const fetchUsersAndAnswerOptions = async ( name: true, email: true, communicationPreferences: true, + roles: true, }, }), db.answerOption.findMany({ @@ -58,232 +49,3 @@ export function extractUniqueIds(userAnswersForRole: UserIdAndAnswerId[]): { ); return { userIds, answerIds }; } - -export const createUserAndAnswerMaps = ( - users: { - name: string | null; - email: string | null; - communicationPreferences: { - id: string; - userId: string; - methods: $Enums.CommunicationMethod[]; - }[]; - id: string; - }[], - answerOptions: { id: string; option: number }[], -) => { - const userMap: UserMap = {}; - for (const user of users) { - userMap[user.id] = { - name: user.name ?? "Unknown User", - email: user.email ?? "Unknown Email", - communicationPreferences: user.communicationPreferences.map((method) => - method.methods.toString(), - ), - }; - } - - const answerOptionMap: AnswerOptionMap = {}; - for (const answerOption of answerOptions) { - answerOptionMap[answerOption.id] = - answerOption.option.toString() ?? "Unknown Answer"; - } - - return { userMap, answerOptionMap }; -}; - -// Helper function to initialize role and question in the data structure -const initializeRoleAndQuestion = ( - dataByRoleAndQuestion: DataByRoleAndQuestion, - roleName: string, - questionText: string, -): void => { - if (!dataByRoleAndQuestion[roleName]) { - dataByRoleAndQuestion[roleName] = {}; - } - if (!dataByRoleAndQuestion[roleName]![questionText]) { - dataByRoleAndQuestion[roleName]![questionText] = []; - } -}; - -// Helper function to push user data into the data structure -const pushUserData = ( - dataByRoleAndQuestion: DataByRoleAndQuestion, - roleName: string, - questionText: string, - entry: Entry, - userMap: UserMap, - answerOptionMap: AnswerOptionMap, -): void => { - dataByRoleAndQuestion[roleName]![questionText]!.push({ - name: userMap[entry.userId]?.name ?? "Unknown User", - email: userMap[entry.userId]?.email ?? "Unknown Email", - communicationPreferences: - userMap[entry.userId]!.communicationPreferences?.length > 0 - ? userMap[entry.userId]?.communicationPreferences - : ["Do not contact"], - answer: answerOptionMap[entry.answerId] ?? "Unknown Answer", - }); -}; - -const sortUserData = ( - dataByRoleAndQuestion: DataByRoleAndQuestion, - roleName: string, - questionText: string, -): void => { - dataByRoleAndQuestion[roleName]![questionText]!.sort((a, b) => { - const answerValueA = parseInt(a.answer); - const answerValueB = parseInt(b.answer); - - // We don't want the same person be appear on top of the table all the time. So we shuffle the order of the users with the same answer value. - if (answerValueA === answerValueB) { - return Math.random() - 0.5; - } else { - return answerValueA - answerValueB; - } - }); -}; - -export const groupDataByRoleAndQuestion = ( - userAnswersForRole: UserAnswersForRoleArray, - userMap: UserMap, - answerOptionMap: AnswerOptionMap, -) => { - const dataByRoleAndQuestion: DataByRoleAndQuestion = {}; - - for (const entry of userAnswersForRole) { - for (const role of entry.question.roles ?? []) { - const roleName = role.role || "Unknown Role"; - const questionText = entry.question.questionText || "Unknown Question"; - - initializeRoleAndQuestion(dataByRoleAndQuestion, roleName, questionText); - pushUserData( - dataByRoleAndQuestion, - roleName, - questionText, - entry, - userMap, - answerOptionMap, - ); - sortUserData(dataByRoleAndQuestion, roleName, questionText); - } - } - - return dataByRoleAndQuestion; -}; - -const getRoleName = (role: Role) => role.role || "Unknown Role"; - -const initializeRole = ( - aggregatedDataByRole: AggregatedDataByRole, - roleName: string, -) => { - if (!aggregatedDataByRole[roleName]) { - aggregatedDataByRole[roleName] = {}; - } -}; - -const getUserDetails = (userMap: UserMap, entry: Entry) => { - const userName = userMap[entry.userId]?.name ?? "Unknown User"; - const userEmail = userMap[entry.userId]?.email ?? "Unknown Email"; - let userCommunicationPreferences = - userMap[entry.userId]?.communicationPreferences; - userCommunicationPreferences = - userCommunicationPreferences?.length ?? 0 > 0 - ? userCommunicationPreferences - : ["Do not contact"] ?? []; - return { userName, userEmail, userCommunicationPreferences }; -}; - -const initializeUser = ( - aggregatedDataByRole: AggregatedDataByRole, - roleName: string, - userEmail: string, - userName: string, - userCommunicationPreferences: string[], -) => { - if (!aggregatedDataByRole[roleName]![userEmail]) { - aggregatedDataByRole[roleName]![userEmail] = { - name: userName, - communicationPreferences: userCommunicationPreferences ?? [], - counts: [0, 0, 0, 0], - }; - } -}; - -const updateCounts = ( - aggregatedDataByRole: AggregatedDataByRole, - roleName: string, - userEmail: string, - answerValue: number, -) => { - if (!aggregatedDataByRole[roleName]![userEmail]?.counts[answerValue]) { - aggregatedDataByRole[roleName]![userEmail]!.counts[answerValue] = 0; - } - aggregatedDataByRole[roleName]![userEmail]!.counts[answerValue] = - (aggregatedDataByRole[roleName]![userEmail]?.counts[answerValue] ?? 0) + 1; -}; - -export const aggregateDataByRole = ( - userAnswersForRole: UserAnswersForRoleArray, - userMap: UserMap, - answerOptionMap: AnswerOptionMap, -) => { - const aggregatedDataByRole: AggregatedDataByRole = {}; - - for (const entry of userAnswersForRole) { - for (const role of entry.question.roles ?? []) { - const roleName = getRoleName(role); - initializeRole(aggregatedDataByRole, roleName); - - if (userMap[entry.userId]) { - const answerValue = parseInt(answerOptionMap[entry.answerId] ?? "", 10); - const { userName, userEmail, userCommunicationPreferences } = - getUserDetails(userMap, entry); - initializeUser( - aggregatedDataByRole, - roleName, - userEmail, - userName, - userCommunicationPreferences!, - ); - - if (!isNaN(answerValue)) { - updateCounts(aggregatedDataByRole, roleName, userEmail, answerValue); - } - } - } - } - - return aggregatedDataByRole; -}; - -export const sortResults = (aggregatedDataByRole: AggregatedDataByRole) => { - // Sorting the results based on the counts of each answer - for (const role in aggregatedDataByRole) { - for (const user in aggregatedDataByRole[role]) { - const counts = aggregatedDataByRole[role]![user]?.counts ?? [0, 0, 0, 0]; - aggregatedDataByRole[role]![user]!.counts = counts; - } - } - - // Sorting users based on the total count of 0 answers, then 1, 2, and 3 - for (const role in aggregatedDataByRole) { - const sortedEntries = Object.entries(aggregatedDataByRole[role] ?? {}).sort( - (a, b) => { - const countsA = a[1].counts; - const countsB = b[1].counts; - for (let i = 0; i < countsA.length; i++) { - const diff = (countsB[i] ?? 0) - (countsA[i] ?? 0); - if (diff !== 0) { - return diff; - } - } - return 0; - }, - ); - aggregatedDataByRole[role] = Object.fromEntries(sortedEntries); - } - - return aggregatedDataByRole; -};