diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 2065a77..2b566a0 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,7 +1,8 @@
{
- "recommendations": [
- "esbenp.prettier-vscode",
- "streetsidesoftware.code-spell-checker",
- "ms-playwright.playwright"
- ]
+ "recommendations": [
+ "esbenp.prettier-vscode",
+ "streetsidesoftware.code-spell-checker",
+ "ms-playwright.playwright",
+ "prisma.prisma"
+ ]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d1e7269..74c68e3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -30,5 +30,9 @@
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
- }
+ },
+ "[prisma]": {
+ "editor.defaultFormatter": "Prisma.prisma"
+ },
+ "typescript.preferences.importModuleSpecifier": "non-relative"
}
diff --git a/playwright.config.ts b/playwright.config.ts
index edb93a5..b766db8 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -11,42 +11,42 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
- testDir: "./tests",
- /* Run tests in files in parallel */
- fullyParallel: true,
- /* Fail the build on CI if you accidentally left test.only in the source code. */
- forbidOnly: !!process.env.CI,
- /* Retry on CI only */
- retries: process.env.CI ? 2 : 0,
- /* Opt out of parallel tests on CI. */
- workers: 1,
- /* Reporter to use. See https://playwright.dev/docs/test-reporters */
- reporter: [["list", { printSteps: true }], ["html"]],
- /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
- use: {
- /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
- trace: "on-first-retry",
- },
-
- /* Configure projects for major browsers */
- projects: [
- {
- name: "chromium",
- grepInvert: /(Mobile)/,
- use: {
- ...devices["Desktop Chrome"],
- },
+ testDir: "./tests",
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env["CI"],
+ /* Retry on CI only */
+ retries: process.env["CI"] ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: 1,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: [["list", { printSteps: true }], ["html"]],
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry",
},
- // Mobile devices
- {
- name: "android",
- grep: /(Mobile)/,
- use: {
- ...devices["Pixel 5"],
- },
- },
- ],
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ grepInvert: /(Mobile)/,
+ use: {
+ ...devices["Desktop Chrome"],
+ },
+ },
+
+ // Mobile devices
+ {
+ name: "android",
+ grep: /(Mobile)/,
+ use: {
+ ...devices["Pixel 5"],
+ },
+ },
+ ],
- /* No webserver config, webserver is started within the tests */
+ /* No webserver config, webserver is started within the tests */
});
diff --git a/prisma/migrations/20250306125830_add_surveydate_to_survey/migration.sql b/prisma/migrations/20250306125830_add_surveydate_to_survey/migration.sql
new file mode 100644
index 0000000..ae72d30
--- /dev/null
+++ b/prisma/migrations/20250306125830_add_surveydate_to_survey/migration.sql
@@ -0,0 +1,15 @@
+/*
+ Warnings:
+
+ - Added the required column `surveyDate` to the `Survey` table without a default value. This is not possible if the table is not empty.
+
+*/
+
+-- Add the surveyDate column as nullable
+ALTER TABLE "Survey" ADD COLUMN "surveyDate" DATE NULL;
+
+-- Update the 2024 survey to have a surveyDate
+UPDATE "Survey" SET "surveyDate" = '2024-01-01' WHERE "surveyDate" IS NULL;
+
+-- Alter the surveyDate column to be non-nullable
+ALTER TABLE "Survey" ALTER COLUMN "surveyDate" SET NOT NULL;
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 079e5cb..5b4eaa7 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -27,6 +27,7 @@ model Survey {
id String @id @default(cuid())
surveyName String @unique
questions Question[]
+ surveyDate DateTime @db.Date
}
model Role {
diff --git a/prisma/seed.js b/prisma/seed.js
index 71a0740..f96697d 100644
--- a/prisma/seed.js
+++ b/prisma/seed.js
@@ -1,8 +1,8 @@
-import { PrismaClient } from "@prisma/client";
+import { PrismaDbClient } from "~/prisma";
import fs from "fs";
import csv from "csv-parser";
-const prisma = new PrismaClient();
+const prisma = new PrismaDbClient();
/**
* Parses a CSV file to extract roles, questions, and their mappings.
@@ -93,6 +93,7 @@ async function main() {
where: { surveyName: "Info Support Tech Survey - 2024" },
create: {
surveyName: "Info Support Tech Survey - 2024",
+ surveyDate: new Date(2024, 0, 1),
},
update: {},
});
diff --git a/src/app/find-the-expert/profile-page/page.tsx b/src/app/find-the-expert/profile-page/page.tsx
index 8f3d796..779b5d1 100644
--- a/src/app/find-the-expert/profile-page/page.tsx
+++ b/src/app/find-the-expert/profile-page/page.tsx
@@ -1,32 +1,31 @@
import type { Metadata } from "next";
-import type { Prisma } from "@prisma/client";
import React, { Suspense } from "react";
import ButtonSkeleton from "~/components/loading/button-loader";
import ProfilePageSearch from "~/components/ui/profile-page-search";
-import { db } from "~/server/db";
+import { prismaClient } from "~/server/db";
import { DataTable } from "~/components/data-tables/data-table";
import type { ColumnDef } from "@tanstack/react-table";
-import communicationMethodToIcon from "~/components/ui/CommunicationMethodToIcon";
+import communicationMethodToIcon from "~/components/ui/communication-method-to-icon";
import ProfileRadarChart from "~/components/profile-radar-chart";
+import type { ProfilePageUserData } from "~/server/db/prisma-client/user";
export const metadata: Metadata = {
title: "Find the expert",
};
-const ContentSection = async ({ name }: { name: string }) => {
- const users = await db.user.findMany({
- select: userSelect,
- });
-
- const selectedUser = users.find((user) => user.name === name);
+const ContentSection = async ({ userId }: { userId?: string }) => {
+ const users = await prismaClient.users.getUsers();
+ const selectedUser = userId
+ ? await prismaClient.users.getProfilePageUserById(userId)
+ : null;
return (
<>
}>
-
+
}>
- {name ? (
+ {selectedUser !== null ? (
) : (
@@ -38,49 +37,6 @@ const ContentSection = async ({ name }: { name: string }) => {
);
};
-const userSelect = {
- name: true,
- id: true,
- questionResults: {
- orderBy: {
- answer: {
- option: "asc",
- },
- },
- select: {
- answer: {
- select: {
- option: true,
- },
- },
- question: {
- select: {
- questionText: true,
- survey: {
- select: {
- surveyName: true,
- },
- },
- roles: {
- select: {
- role: true,
- },
- },
- },
- },
- },
- },
- communicationPreferences: {
- select: {
- methods: true,
- },
- },
-} satisfies Prisma.UserSelect;
-
-export type UserData = Prisma.UserGetPayload<{
- select: typeof userSelect;
-}>;
-
const optionWeights: Record = {
0: 5,
1: 3,
@@ -88,7 +44,7 @@ const optionWeights: Record = {
3: 0,
};
-const ProfilePage = async ({ user }: { user?: UserData }) => {
+const ProfilePage = async ({ user }: { user?: ProfilePageUserData }) => {
if (!user) {
return (
No user found
@@ -118,7 +74,7 @@ const ProfilePage = async ({ user }: { user?: UserData }) => {
[] as { role: string; sum: number }[],
);
- const columns: ColumnDef[] = [
+ const columns: ColumnDef[] = [
{
accessorKey: "question.questionText",
header: "Name",
@@ -164,7 +120,7 @@ const ProfilePage = async ({ user }: { user?: UserData }) => {
);
};
const ProfilePageWrapper = async (context: {
- searchParams: Promise<{ name: string }>;
+ searchParams: Promise<{ userId: string }>;
}) => {
return (
@@ -177,7 +133,7 @@ const ProfilePageWrapper = async (context: {
Tech Survey - Profile page
-
+
);
};
diff --git a/src/app/find-the-expert/tech-page/page.tsx b/src/app/find-the-expert/tech-page/page.tsx
index 9ea7990..8a1b327 100644
--- a/src/app/find-the-expert/tech-page/page.tsx
+++ b/src/app/find-the-expert/tech-page/page.tsx
@@ -2,10 +2,10 @@ import type { Metadata } from "next";
import { Suspense } from "react";
import ButtonSkeleton from "~/components/loading/button-loader";
import ShowDataTable from "~/components/data-tables/show-data-table";
-import { retrieveAnswersByRole } from "~/utils/data-manipulation";
-import { getRoles } from "~/utils/role-utils";
-import { db } from "~/server/db";
+import { sortRoles } from "~/utils/role-utils";
+import { prismaClient } from "~/server/db";
import ShowTechSearchWrapper from "~/components/ui/show-tech-search-wrapper";
+import { getAnswerDataByRole } from "~/utils/data-manipulation";
export const metadata: Metadata = {
title: "Find the expert",
@@ -31,8 +31,8 @@ const ContentSection = ({
);
const FindTheExpertSearch = async () => {
- const availableRoles = await getRoles()();
- const availableUnits = await db.businessUnit.findMany();
+ const availableRoles = sortRoles(await prismaClient.roles.getAll());
+ const availableUnits = await prismaClient.businessUnits.getAll();
return (
{
+ const results = await prismaClient.questionResults.getQuestionResultsByRole(
+ role ?? null,
+ tech ?? null,
+ unit ?? null,
+ );
const { dataByRoleAndQuestion, aggregatedDataByRole } =
- await retrieveAnswersByRole({
- role,
- questionText: tech,
- unit,
- });
+ getAnswerDataByRole(results);
return (
{
const session = await auth();
const [roles, businessUnits] = await Promise.all([
- db.role.findMany(),
- db.businessUnit.findMany(),
+ prismaClient.roles.getAll(),
+ prismaClient.businessUnits.getAll(),
]);
return (
diff --git a/src/app/result/page.tsx b/src/app/result/page.tsx
index 4c15ad1..0347abd 100644
--- a/src/app/result/page.tsx
+++ b/src/app/result/page.tsx
@@ -4,15 +4,15 @@ import {
type Role,
type TransformedData,
} from "~/models/types";
-import { db } from "~/server/db";
+import { prismaClient } from "~/server/db";
-import type { BusinessUnit, Prisma } from "@prisma/client";
+import type { BusinessUnit } from "~/prisma";
import { type Metadata } from "next";
import ButtonSkeleton from "~/components/loading/button-loader";
import LegendSkeleton from "~/components/loading/results-loader";
import SearchAnonymized from "~/components/ui/search-anonymized";
-import { getRoles } from "~/utils/role-utils";
import ShowResults from "~/components/show-results";
+import { sortRoles } from "~/utils/role-utils";
export const metadata: Metadata = {
title: "Results",
@@ -35,8 +35,8 @@ const Results = async (context: {
}>
@@ -44,8 +44,8 @@ const Results = async (context: {
};
export async function AnonymousRoleSearch() {
- const availableRoles = await getRoles()();
- const availableUnits = await db.businessUnit.findMany();
+ const availableRoles = sortRoles(await prismaClient.roles.getAll());
+ const availableUnits = await prismaClient.businessUnits.getAll();
const def: Role = {
id: "",
@@ -68,70 +68,16 @@ export async function AnonymousRoleSearch() {
);
}
-const FetchQuestionResults = async ({
- role,
- unit,
-}: {
- role: string;
- unit: string;
-}) => {
- // If both role and unit are undefined, return an empty array
- if (!role && !unit) return [];
-
- // Base include object reused in all queries
- const includeConfig = {
- question: {
- include: {
- roles: true,
- },
- },
- };
-
- // Dynamically build the where conditions
- const whereConditions: Prisma.QuestionResultWhereInput = {};
-
- if (role) {
- whereConditions.question = {
- roles: {
- some: {
- role: {
- equals: role,
- mode: "insensitive",
- },
- },
- },
- };
- }
-
- if (unit) {
- whereConditions.user = {
- businessUnit: {
- unit: {
- equals: unit,
- mode: "insensitive",
- },
- },
- };
- }
-
- return db.questionResult.findMany({
- where: whereConditions,
- include: includeConfig,
- });
-};
-
const ShowResultsWrapper = async ({
- role,
- unit,
+ roleId,
+ unitId,
}: {
- role: string;
- unit: string;
+ roleId: string | null;
+ unitId: string | null;
}) => {
- const userAnswersForRole: QuestionResult[] = await FetchQuestionResults({
- role,
- unit,
- });
- const answerOptions = await db.answerOption.findMany();
+ const userAnswersForRole: QuestionResult[] =
+ await prismaClient.questionResults.getResultPageData(roleId, unitId);
+ const answerOptions = await prismaClient.answerOptions.getAll();
const transformedData: TransformedData = {};
@@ -139,9 +85,9 @@ const ShowResultsWrapper = async ({
const questionText = question?.questionText ?? "";
const roles = question?.roles ?? [];
- if (role != undefined) {
+ if (roleId !== null) {
roles.forEach(({ role: roleName = "" }) => {
- if (roleName && questionText && roleName == role) {
+ if (roleName && questionText && roleName === roleId) {
transformedData[roleName] ??= {};
transformedData[roleName][questionText] ??= {};
@@ -154,14 +100,14 @@ const ShowResultsWrapper = async ({
] ?? 0) + 1;
}
});
- } else if (unit != undefined) {
- transformedData[unit] ??= {};
- transformedData[unit][questionText] ??= {};
+ } else if (unitId !== null) {
+ transformedData[unitId] ??= {};
+ transformedData[unitId][questionText] ??= {};
const answerString =
answerOptions.find(({ id }) => id === answerId)?.option ?? "";
- transformedData[unit][questionText][answerString] =
- (transformedData[unit][questionText][answerString] ?? 0) + 1;
+ transformedData[unitId][questionText][answerString] =
+ (transformedData[unitId][questionText][answerString] ?? 0) + 1;
}
});
diff --git a/src/app/thank-you/page.tsx b/src/app/thank-you/page.tsx
index 22598fd..20aa01f 100644
--- a/src/app/thank-you/page.tsx
+++ b/src/app/thank-you/page.tsx
@@ -1,11 +1,7 @@
-import { db } from "~/server/db";
+import { prismaClient } from "~/server/db";
import PdfDownloadButton from "~/components/download-pdf";
import React, { Suspense } from "react";
-import {
- type QuestionResult,
- type Question,
- type AnswerOption,
-} from "~/models/types";
+import { type QuestionResult, type Question } from "~/models/types";
import { type Metadata } from "next";
import ButtonSkeleton from "~/components/loading/button-loader";
@@ -18,34 +14,20 @@ export const metadata: Metadata = {
const ThankYou = async () => {
const session = (await auth())!;
const userAnswersForRole: QuestionResult[] =
- await db.questionResult.findMany({
- where: {
- userId: session.user.id,
- },
- include: {
- question: {
- include: {
- roles: true,
- },
- },
- },
- });
+ await prismaClient.questionResults.getRecentQuestionResultsWithRolesByUserId(
+ session.user.id,
+ );
- const answerOptions: AnswerOption[] = await db.answerOption.findMany();
+ const answerOptions = await prismaClient.answerOptions.getAll();
- const userSelectedRoles = await db.user.findUnique({
- where: {
- id: session.user.id,
- },
- include: {
- roles: true,
- },
- });
+ const selectedRoles = await prismaClient.users.getRolesForUser(
+ session.user.id,
+ );
// Update the userAnswersForRole object such that a question only includes the roles of the selected roles of the user.
for (const userAnswer of userAnswersForRole) {
userAnswer.question.roles = userAnswer.question.roles?.filter((role) =>
- userSelectedRoles?.roles.some(
+ selectedRoles?.roles.some(
(selectedRole) => selectedRole.id === role.id,
),
);
diff --git a/src/auth.ts b/src/auth.ts
index b0866a7..60d599c 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -1,10 +1,9 @@
-import type { Adapter } from "@auth/core/adapters";
import MicrosoftEntraID from "@auth/core/providers/microsoft-entra-id";
-import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth, { type DefaultSession } from "next-auth";
-import { db } from "~/server/db";
-import { env } from "./env";
+import { env } from "~/env";
+import { prismaClient } from "~/server/db";
+import type { IPrismaAdapterService } from "~/server/db/prisma-client";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -51,7 +50,9 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
},
- adapter: PrismaAdapter(db) as Adapter,
+ adapter: (
+ prismaClient as unknown as IPrismaAdapterService
+ ).toPrismaAdapter(),
// We trust Microsoft Entra ID to have securely verified the email address associated with the account
// so we allow linking accounts with the same email address.
// Automatic account linking on sign in is not secure between arbitrary providers, so if you are using arbitrary providers, this should be set to `false`.
diff --git a/src/components/additional-buttons-homepage.tsx b/src/components/additional-buttons-homepage.tsx
index 3a4ad26..f0bd4fc 100644
--- a/src/components/additional-buttons-homepage.tsx
+++ b/src/components/additional-buttons-homepage.tsx
@@ -1,10 +1,10 @@
"use server";
-import { ArrowRight, ArrowRightDarkModeFriendly } from "./svg";
-import { Button } from "./ui/button";
+import { HomepageFindTheExpertButton } from "~/components/homepage-find-the-expert-button";
import Link from "next/link";
-import { HomepageFindTheExpertButton } from "./homepage-find-the-expert-button";
import { auth } from "~/auth";
+import { Button } from "~/components/ui/button";
+import { ArrowRight, ArrowRightDarkModeFriendly } from "~/components/svg";
const Buttons = async () => {
const session = await auth();
diff --git a/src/components/data-tables/data-table.tsx b/src/components/data-tables/data-table.tsx
index d3a7a00..8f8eb74 100644
--- a/src/components/data-tables/data-table.tsx
+++ b/src/components/data-tables/data-table.tsx
@@ -16,9 +16,9 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
-import { idToTextMap } from "~/utils/optionMapping";
+import { idToTextMap } from "~/utils/option-mapping";
import { DataTablePagination } from "~/components/data-tables/data-table-pagination";
-import communicationMethodToIcon from "~/components/ui/CommunicationMethodToIcon";
+import communicationMethodToIcon from "~/components/ui/communication-method-to-icon";
interface DataTableProps {
columns: ColumnDef[];
diff --git a/src/components/download-pdf.tsx b/src/components/download-pdf.tsx
index 63b8ce6..78af54d 100644
--- a/src/components/download-pdf.tsx
+++ b/src/components/download-pdf.tsx
@@ -2,14 +2,14 @@
import React from "react";
import { Document, Page, Text, View, StyleSheet } from "@react-pdf/renderer";
-import { idToTextMap } from "~/utils/optionMapping";
+import { idToTextMap } from "~/utils/option-mapping";
import { type AnswerOption, type PdfTransformedData } from "~/models/types";
import dynamic from "next/dynamic";
import { Button } from "~/components/ui/button";
import Link from "next/link";
-import { ArrowLeftDarkModeFriendly, Download } from "./svg";
import type { Session } from "next-auth";
+import { ArrowLeftDarkModeFriendly, Download } from "~/components/svg";
const PDFDownloadLink = dynamic(
() => import("~/components/pdf-download-link").then((mod) => mod.default),
diff --git a/src/components/github-link.tsx b/src/components/github-link.tsx
index 10cb9ce..3ea1b33 100644
--- a/src/components/github-link.tsx
+++ b/src/components/github-link.tsx
@@ -1,7 +1,7 @@
"use client";
-import { GithubLogo } from "./svg";
-import { Button } from "./ui/button";
+import { GithubLogo } from "~/components/svg";
+import { Button } from "~/components/ui/button";
const GitHubLink = () => {
return (
diff --git a/src/components/home-link.tsx b/src/components/home-link.tsx
index 4d773af..1fef08b 100644
--- a/src/components/home-link.tsx
+++ b/src/components/home-link.tsx
@@ -1,9 +1,9 @@
"use client";
import Link from "next/link";
-import { Button } from "./ui/button";
-import { ArrowLeftDarkModeFriendly } from "./svg";
import { usePathname } from "next/navigation";
+import { ArrowLeftDarkModeFriendly } from "~/components/svg";
+import { Button } from "~/components/ui/button";
export const HomeLink = () => {
const currentPathName = usePathname();
diff --git a/src/components/homepage-find-the-expert-button.tsx b/src/components/homepage-find-the-expert-button.tsx
index 1435ef5..f669347 100644
--- a/src/components/homepage-find-the-expert-button.tsx
+++ b/src/components/homepage-find-the-expert-button.tsx
@@ -1,9 +1,9 @@
"use client";
import { api } from "~/trpc/react";
-import { ArrowRightDarkModeFriendly } from "./svg";
-import { Button } from "./ui/button";
import Link from "next/link";
+import { Button } from "~/components/ui/button";
+import { ArrowRightDarkModeFriendly } from "~/components/svg";
export const HomepageFindTheExpertButton = () => {
const { mutate: logUsageMetric } =
@@ -14,7 +14,7 @@ export const HomepageFindTheExpertButton = () => {
};
return (
-
+