Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(survey): Support multiple surveys #194

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
}
70 changes: 35 additions & 35 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
});
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ model Survey {
id String @id @default(cuid())
surveyName String @unique
questions Question[]
surveyDate DateTime @db.Date
}

model Role {
Expand Down
5 changes: 3 additions & 2 deletions prisma/seed.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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: {},
});
Expand Down
70 changes: 13 additions & 57 deletions src/app/find-the-expert/profile-page/page.tsx
Original file line number Diff line number Diff line change
@@ -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 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 }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's ask @nicojs about this:
There are two options here.
Option 1: Keep it as it is, searching by name. The url will be https://.../profile-page?name=Jelle%20Buitenhuis. However, names are not guaranteed to be unique, how would we handle that?
Option 2: Change it to userId, which is guaranteed to be unique. The url will be https://.../profile-page?id=cm893klj4h353457

Copy link
Contributor Author

@Andreas02-dev Andreas02-dev Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also use metadata to show the name of the user, social media apps also use this for link previews.
Quick code example would be:

const buildStaticMetadata = (): Metadata => {
    return {
        title: "Find the expert",
    };
};

const buildTitle = (userName: string | null): string => {
    if (userName === null) {
        return "Find the expert";
    }

    return `Find the expert - ${userName}`;
};

export async function generateMetadata({
    searchParams,
}: {
    searchParams: Promise<{ userId?: string }>;
}): Promise<Metadata> {
    const userId = (await searchParams).userId;
    if (!userId) {
        return buildStaticMetadata();
    }

    const user = await prismaClient.users.getUserById(userId);
    if (!user) {
        return buildStaticMetadata();
    }

    return {
        title: buildTitle(user.name),
        openGraph: {
            title: buildTitle(user.name),
        },
    };
}

Which would look something like:
image

const users = await prismaClient.users.getUsers();
const selectedUser = userId
? await prismaClient.users.getProfilePageUserById(userId)
: null;

return (
<>
<Suspense fallback={<ButtonSkeleton />}>
<ProfilePageSearch allUsers={users} />
<ProfilePageSearch users={users} />
</Suspense>
<Suspense fallback={<ButtonSkeleton />}>
{name ? (
{selectedUser !== null ? (
<ProfilePage user={selectedUser} />
) : (
<h3 className="text-center text-lg font-semibold">
Expand All @@ -38,57 +37,14 @@ 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<number, number> = {
0: 5,
1: 3,
2: 1,
3: 0,
};

const ProfilePage = async ({ user }: { user?: UserData }) => {
const ProfilePage = async ({ user }: { user?: ProfilePageUserData }) => {
if (!user) {
return (
<h3 className="text-center text-lg font-semibold">No user found</h3>
Expand Down Expand Up @@ -118,7 +74,7 @@ const ProfilePage = async ({ user }: { user?: UserData }) => {
[] as { role: string; sum: number }[],
);

const columns: ColumnDef<UserData["questionResults"][0]>[] = [
const columns: ColumnDef<ProfilePageUserData["questionResults"][0]>[] = [
{
accessorKey: "question.questionText",
header: "Name",
Expand Down Expand Up @@ -164,7 +120,7 @@ const ProfilePage = async ({ user }: { user?: UserData }) => {
);
};
const ProfilePageWrapper = async (context: {
searchParams: Promise<{ name: string }>;
searchParams: Promise<{ userId: string }>;
}) => {
return (
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
Expand All @@ -177,7 +133,7 @@ const ProfilePageWrapper = async (context: {
Tech Survey - Profile page
</span>
</h1>
<ContentSection name={(await context.searchParams).name} />
<ContentSection userId={(await context.searchParams).userId} />
</div>
);
};
Expand Down
27 changes: 14 additions & 13 deletions src/app/find-the-expert/tech-page/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 (
<ShowTechSearchWrapper
Expand Down Expand Up @@ -70,16 +70,17 @@ const ShowTableWrapper = async ({
role,
unit,
}: {
tech: string;
role: string;
unit: string;
tech?: string;
role?: string;
unit?: string;
}) => {
const results = await prismaClient.questionResults.getQuestionResultsByRole(
role ?? null,
tech ?? null,
unit ?? null,
);
const { dataByRoleAndQuestion, aggregatedDataByRole } =
await retrieveAnswersByRole({
role,
questionText: tech,
unit,
});
getAnswerDataByRole(results);

return (
<ShowDataTable
Expand Down
6 changes: 3 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SelectUserSurveyPreferences from "~/components/select-input";

import React, { Suspense } from "react";
import { db } from "~/server/db";
import { prismaClient } from "~/server/db";
import RoleSelectionSkeleton from "~/components/loading/role-selection-loader";
import Buttons from "~/components/additional-buttons-homepage";
import Link from "next/link";
Expand All @@ -10,8 +10,8 @@ import { auth } from "~/auth";
const Home: React.FC = async () => {
const session = await auth();
const [roles, businessUnits] = await Promise.all([
db.role.findMany(),
db.businessUnit.findMany(),
prismaClient.roles.getAll(),
prismaClient.businessUnits.getAll(),
]);

return (
Expand Down
Loading
Loading