From 85643b64d52ea0aceb406246e4a5a01733029e08 Mon Sep 17 00:00:00 2001 From: Hsackboy Date: Tue, 17 Mar 2026 11:32:57 +0100 Subject: [PATCH 1/7] feat: add services for read committee applications and periodes and corresponding pages --- src/app/committees/[shortName]/Nav.tsx | 17 ++- src/app/committees/[shortName]/layout.tsx | 14 ++- .../[participationId]/page.module.scss | 0 .../periodes/[participationId]/page.tsx | 64 +++++++++++ .../[shortName]/periodes/page.module.scss | 0 .../committees/[shortName]/periodes/page.tsx | 44 +++++++ .../committeeParticipation/actions.ts | 7 ++ .../committeeParticipation/auth.ts | 6 + .../committeeParticipation/operations.ts | 108 ++++++++++++++++++ 9 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 src/app/committees/[shortName]/periodes/[participationId]/page.module.scss create mode 100644 src/app/committees/[shortName]/periodes/[participationId]/page.tsx create mode 100644 src/app/committees/[shortName]/periodes/page.module.scss create mode 100644 src/app/committees/[shortName]/periodes/page.tsx create mode 100644 src/services/applications/committeeParticipation/actions.ts create mode 100644 src/services/applications/committeeParticipation/auth.ts create mode 100644 src/services/applications/committeeParticipation/operations.ts diff --git a/src/app/committees/[shortName]/Nav.tsx b/src/app/committees/[shortName]/Nav.tsx index ab69c4472..899fe2ec9 100644 --- a/src/app/committees/[shortName]/Nav.tsx +++ b/src/app/committees/[shortName]/Nav.tsx @@ -3,18 +3,20 @@ import styles from './Nav.module.scss' import Link from 'next/link' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faArrowLeft, faCog, faInfo, faUsers } from '@fortawesome/free-solid-svg-icons' +import { faArrowLeft, faCog, faInfo, faScroll, faUsers } from '@fortawesome/free-solid-svg-icons' import { usePathname } from 'next/navigation' +import type { AuthResultTypeAny } from '@/auth/authorizer/AuthResult' type PropTypes = { - shortName: string + shortName: string, + canReadCommitteeApplication: AuthResultTypeAny } -export default function Nav({ shortName }: PropTypes) { +export default function Nav({ shortName, canReadCommitteeApplication }: PropTypes) { const pathname = usePathname() console.log(pathname) - const adminPath = `/committees/${shortName}/admin` + const readPeriodesPath = `/committees/${shortName}/periodes` const membersPath = `/committees/${shortName}/members` const aboutPath = `/committees/${shortName}/about` @@ -23,6 +25,13 @@ export default function Nav({ shortName }: PropTypes) { + {canReadCommitteeApplication.authorized && + + + + } diff --git a/src/app/committees/[shortName]/layout.tsx b/src/app/committees/[shortName]/layout.tsx index b7fc0ecc4..8982eea29 100644 --- a/src/app/committees/[shortName]/layout.tsx +++ b/src/app/committees/[shortName]/layout.tsx @@ -8,6 +8,8 @@ import CommitteeImage from '@/components/CommitteeImage/CommitteeImage' import { committeeAuth } from '@/services/groups/committees/auth' import { ServerSession } from '@/auth/session/ServerSession' import type { ReactNode } from 'react' +import { applicationAuth } from '@/services/applications/auth' +import { committeeParticipationAuth } from '@/services/applications/committeeParticipation/auth' export type PropTypes = { params: Promise<{ @@ -32,6 +34,14 @@ export default async function Committee({ params, children }: PropTypes) { await ServerSession.fromNextAuth() ).toJsObject() + const canReadCommitteeApplication = committeeParticipationAuth.readAll.dynamicFields( + { + groupId: committee.groupId, + }).auth( + await ServerSession.fromNextAuth() + ).toJsObject() + + return (
- { children } + {children}
diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss b/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx b/src/app/committees/[shortName]/periodes/[participationId]/page.tsx new file mode 100644 index 000000000..2dfd2b7ca --- /dev/null +++ b/src/app/committees/[shortName]/periodes/[participationId]/page.tsx @@ -0,0 +1,64 @@ +import styles from './page.module.scss' +import getCommittee from '@/app/committees/[shortName]/getCommittee' +import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { readCommitteeApplicationsInPeriodAction } from '@/services/applications/committeeParticipation/actions' +import ProfilePicture from '@/components/User/ProfilePicture' +import { readSpecialImageAction } from '@/services/images/actions' +import Link from 'next/link' +import type { Image as ImageT } from '@/prisma-generated-pn-types' +export type PropTypes = { + params: Promise<{ + shortName: string, + participationId: number, + }> +} + + +async function loadUserImage(image: ImageT | null) { + return image ? image : await readSpecialImageAction.bind( + null, { params: { special: 'DEFAULT_PROFILE_IMAGE' } } + )().then(res => { + if (!res.success) throw new Error('Kunne ikke finne standard profilbilde') + return res.data + }) +} + + +export default async function PeriodeCommitteePage({ params }: PropTypes) { + const participationId = (await params).participationId + const applications = unwrapActionReturn( + await readCommitteeApplicationsInPeriodAction({ params: { participationId } }) + ) + if (applications.length === 0) { return 'ingen søknader funnet' } + const sortedApplications = applications.sort((a, b) => a.applicationPriority - b.applicationPriority) + return ( +
+ {sortedApplications.map(async (application, index) => ( + + + + + + + + + + + +

{application.applicationPriority}.

+ + +

{application.firstname} {application.lastname}

+ +

{application.applicationText}

+ )) + } +
+ ) +} + + diff --git a/src/app/committees/[shortName]/periodes/page.module.scss b/src/app/committees/[shortName]/periodes/page.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/committees/[shortName]/periodes/page.tsx b/src/app/committees/[shortName]/periodes/page.tsx new file mode 100644 index 000000000..4cb56fc3f --- /dev/null +++ b/src/app/committees/[shortName]/periodes/page.tsx @@ -0,0 +1,44 @@ +import styles from './page.module.scss' +import getCommittee from '@/app/committees/[shortName]/getCommittee' +import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { readCommitteeParticipatingPeriodAction } from '@/services/applications/committeeParticipation/actions' +import Link from 'next/link' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faLink } from '@fortawesome/free-solid-svg-icons' +export type PropTypes = { + params: Promise<{ + shortName: string + }> +} + +export default async function PeriodeCommitteePage({ params }: PropTypes) { + const committee = await getCommittee(params) + const shortName = (await params).shortName + const committeePeriodes = unwrapActionReturn( + await readCommitteeParticipatingPeriodAction({ params: { committeeId: committee.id } }) + ) + if (committeePeriodes.length === 0) { return 'ingen søknadsperioder funnet' } + return ( + + + + + + + + {committeePeriodes.map((period, index) => ( + + + + + + + )) + } +
Start DatoSlutt DatoSøknaderSøknadstall
{period.startDate.toLocaleDateString('en-GB')}{period.endDate.toLocaleDateString('en-GB')} + + + + {period.applicationCount}
+ ) +} diff --git a/src/services/applications/committeeParticipation/actions.ts b/src/services/applications/committeeParticipation/actions.ts new file mode 100644 index 000000000..298e81ef1 --- /dev/null +++ b/src/services/applications/committeeParticipation/actions.ts @@ -0,0 +1,7 @@ +'use server' + +import { committeeParticipationOperations } from './operations' +import { makeAction } from '@/services/serverAction' + +export const readCommitteeApplicationsInPeriodAction = makeAction(committeeParticipationOperations.read) +export const readCommitteeParticipatingPeriodAction = makeAction(committeeParticipationOperations.readAll) diff --git a/src/services/applications/committeeParticipation/auth.ts b/src/services/applications/committeeParticipation/auth.ts new file mode 100644 index 000000000..64af023b2 --- /dev/null +++ b/src/services/applications/committeeParticipation/auth.ts @@ -0,0 +1,6 @@ +import { RequirePermissionOrGroupAdmin } from '@/auth/authorizer/RequirePermissionOrGroupAdmin' + +export const committeeParticipationAuth = { + read: RequirePermissionOrGroupAdmin.staticFields({ permission: 'APPLICATION_ADMIN' }), + readAll: RequirePermissionOrGroupAdmin.staticFields({ permission: 'APPLICATION_ADMIN' }), +} diff --git a/src/services/applications/committeeParticipation/operations.ts b/src/services/applications/committeeParticipation/operations.ts new file mode 100644 index 000000000..2a3dcbf34 --- /dev/null +++ b/src/services/applications/committeeParticipation/operations.ts @@ -0,0 +1,108 @@ +import '@pn-server-only' +import { committeeParticipationAuth } from './auth' +import { defineOperation } from '@/services/serviceOperation' +import { z } from 'zod' + +export const committeeParticipationOperations = { + read: defineOperation({ + paramsSchema: z.object({ + participationId: z.number(), + }), + authorizer: async ({ prisma, params }) => committeeParticipationAuth.read.dynamicFields({ + groupId: await prisma.committeeParticipationInApplicationPeriod.findUniqueOrThrow({ + where: { + id: params.participationId + }, + select: { + committee: { + select: { + group: { + select: { + id: true, + } + } + } + } + } + }).then((participation) => participation.committee.group.id) + }), + operation: async ({ prisma, params }) => ( + await prisma.committeeParticipationInApplicationPeriod.findUniqueOrThrow({ + where: { + id: params.participationId + }, + select: { + applications: { + select: { + priority: true, + text: true, + user: { + select: { + firstname: true, + lastname: true, + image: true, + email: true, + username: true, + } + } + } + } + } + }).then((applications) => applications.applications.map((application) => ( + { + applicationText: application.text, + applicationPriority: application.priority, + firstname: application.user.firstname, + lastname: application.user.lastname, + image: application.user.image, + email: application.user.email, + username: application.user.username, + } + ))) + ) + }), + readAll: defineOperation({ + paramsSchema: z.object({ + committeeId: z.number(), + }), + authorizer: async ({ prisma, params }) => committeeParticipationAuth.read.dynamicFields({ + groupId: await prisma.committee.findUniqueOrThrow({ + where: { + id: params.committeeId + }, + select: { + groupId: true + } + }).then((committee) => committee.groupId) + }), + operation: async ({ prisma, params }) => ( + await prisma.committeeParticipationInApplicationPeriod.findMany({ + where: { + committeeId: params.committeeId + }, + select: { + _count: { + select: { + applications: true, + } + }, + id: true, + applicationPeriod: { + select: { + startDate: true, + endDate: true, + } + } + } + }).then((periods) => periods.map((period) => ( + { + participationId: period.id, + applicationCount: period._count.applications, + startDate: period.applicationPeriod.startDate, + endDate: period.applicationPeriod.endDate, + }) + ) + ) + ) + }) +} From b1895c4a1e085a978203ee61c6617546a4755a65 Mon Sep 17 00:00:00 2001 From: Hsackboy Date: Mon, 23 Mar 2026 19:48:24 +0100 Subject: [PATCH 2/7] feat: Add committee aplication dev seeder --- .../seedDevApplicationsAndPeriods.ts | 75 +++++++++++++++++++ src/prisma/seeder/src/seeder.ts | 2 + 2 files changed, 77 insertions(+) create mode 100644 src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts diff --git a/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts b/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts new file mode 100644 index 000000000..c036f4086 --- /dev/null +++ b/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts @@ -0,0 +1,75 @@ + +import type { PrismaClient } from '@/prisma-generated-pn-client' + + +export default async function seedDevApplicationsAndPeriods(prisma: PrismaClient) { + const applicationText = ` + Duis dolore minim pariatur quis do ut laboris sit esse laborum quis + sint.Nisi eu consectetur officia irure proident magna culpa sunt.Lorem + reprehenderit pariatur est fugiat ea.Labore aliqua in eu veniam ex velit excepteur + sunt amet amet minim voluptate qui pariatur.Exercitation proident cupidatat adipisicing in incididunt + excepteur id aliquip sit.Dolor velit deserunt pariatur ipsum velit aute eu eiusmod esse.Voluptate veniam + esse nostrud duis elit cillum laborum mollit magna consectetur dolore sit commodo. + ` + const committees = await prisma.committee.findMany({}) + const users = await prisma.user.findMany({}) + + const applicationPeriods = await prisma.applicationPeriod.createManyAndReturn({ + data: [ + { + endDate: new Date('2100-03-25'), + endPriorityDate: new Date('2100-03-28'), + name: 'name1', + startDate: new Date('2026-01-25'), + }, + { + endDate: new Date('2025-03-25'), + endPriorityDate: new Date('2025-03-25'), + name: 'name2', + startDate: new Date('2025-03-01'), + }, + { + endDate: new Date('2024-03-25'), + endPriorityDate: new Date('2024-03-25'), + name: 'name3', + startDate: new Date('2024-03-01'), + }, + { + endDate: new Date('2023-03-25'), + endPriorityDate: new Date('2023-03-25'), + name: 'name4', + startDate: new Date('2023-03-01'), + }, + + ] + }) + const promises = applicationPeriods.map(prevApplicationPeriod => ( + committees.map(async (committee) => { + const participation = await prisma.committeeParticipationInApplicationPeriod.create({ + data: { + applicationPeriod: { + connect: { + id: prevApplicationPeriod.id + } + }, + committee: { + connect: { + id: committee.id + } + } + + } + }) + await prisma.application.createMany({ + data: users.map(user => ({ + priority: participation.committeeId, + text: applicationText, + userId: user.id, + applicationPeriodCommiteeId: participation.id, + applicationPeriodId: participation.applicationPeriodId, + })), + }) + }) + )) + await Promise.all(promises) +} diff --git a/src/prisma/seeder/src/seeder.ts b/src/prisma/seeder/src/seeder.ts index 248b13bac..c283a71a0 100644 --- a/src/prisma/seeder/src/seeder.ts +++ b/src/prisma/seeder/src/seeder.ts @@ -28,6 +28,7 @@ import seedFlairs from './seedFlairs' import { PrismaClient } from '@/prisma-generated-pn-client' import { PrismaPg } from '@prisma/adapter-pg' import seedInterestGroups from './seedInterestGroups' +import seedDevApplicationsAndPeriods from './development/seedDevApplicationsAndPeriods' export default async function seed( shouldMigrate: boolean, @@ -77,5 +78,6 @@ export default async function seed( await seedDevJobAds(prisma) await seedDevShop(prisma) await seedDevEvents(prisma) + await seedDevApplicationsAndPeriods(prisma) console.log('seed dev done') } From 3412c6d8f204a3bc5f0dda17aed2826a304231af Mon Sep 17 00:00:00 2001 From: Hsackboy Date: Mon, 23 Mar 2026 21:22:59 +0100 Subject: [PATCH 3/7] fix: number passed as string instead of number --- .../[participationId]/page.module.scss | 33 +++++++++++++++ .../periodes/[participationId]/page.tsx | 41 ++++++++----------- .../seedDevApplicationsAndPeriods.ts | 10 +++-- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss b/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss index e69de29bb..1d1a53331 100644 --- a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss +++ b/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss @@ -0,0 +1,33 @@ +@use '@/styles/ohma'; +//TODO: fix styling to be aligned to the future styling pr + +.applicationsContainer {} + +.applicationContainer { + margin-top: 1em; + display: flex; + flex-direction: column; + border: 2px solid hsla(0, 0%, 70%, 0.507); + @include ohma.round; + +} + +.headingContainer { + display: flex; + align-items: center; + + * { + display: inline-block; + margin-inline: 0.1em; + } +} + +.profilePicture {} + +.applicantName { + color: black; +} + +.applicationTextContainer {} + +.applicationText {} \ No newline at end of file diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx b/src/app/committees/[shortName]/periodes/[participationId]/page.tsx index 2dfd2b7ca..5f695fddf 100644 --- a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx +++ b/src/app/committees/[shortName]/periodes/[participationId]/page.tsx @@ -9,7 +9,7 @@ import type { Image as ImageT } from '@/prisma-generated-pn-types' export type PropTypes = { params: Promise<{ shortName: string, - participationId: number, + participationId: string, }> } @@ -25,39 +25,34 @@ async function loadUserImage(image: ImageT | null) { export default async function PeriodeCommitteePage({ params }: PropTypes) { - const participationId = (await params).participationId + const participationId = parseInt((await params).participationId, 10) const applications = unwrapActionReturn( await readCommitteeApplicationsInPeriodAction({ params: { participationId } }) ) if (applications.length === 0) { return 'ingen søknader funnet' } const sortedApplications = applications.sort((a, b) => a.applicationPriority - b.applicationPriority) return ( -
+
{sortedApplications.map(async (application, index) => ( - - - - - - - - - - - -

{application.applicationPriority}.

- - +
+
+

{application.applicationPriority}.

+ +

{application.firstname} {application.lastname}

-

{application.applicationText}

+
+
+

{application.applicationText}

+
+
)) } - + ) } diff --git a/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts b/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts index c036f4086..ace8d1d3a 100644 --- a/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts +++ b/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts @@ -43,13 +43,13 @@ export default async function seedDevApplicationsAndPeriods(prisma: PrismaClient ] }) - const promises = applicationPeriods.map(prevApplicationPeriod => ( + const promises = applicationPeriods.map(applicationPeriod => ( committees.map(async (committee) => { const participation = await prisma.committeeParticipationInApplicationPeriod.create({ data: { applicationPeriod: { connect: { - id: prevApplicationPeriod.id + id: applicationPeriod.id } }, committee: { @@ -63,7 +63,11 @@ export default async function seedDevApplicationsAndPeriods(prisma: PrismaClient await prisma.application.createMany({ data: users.map(user => ({ priority: participation.committeeId, - text: applicationText, + text: + committee.name + + applicationPeriod.startDate.toUTCString() + + applicationPeriod.endDate.toUTCString() + + applicationText, userId: user.id, applicationPeriodCommiteeId: participation.id, applicationPeriodId: participation.applicationPeriodId, From 13a2cd62f627323f1ae6da40dc990c10df7c53ca Mon Sep 17 00:00:00 2001 From: Hsackboy Date: Tue, 24 Mar 2026 15:04:00 +0100 Subject: [PATCH 4/7] feat: styling and linting --- src/app/_components/User/UserDisplayName.tsx | 2 +- src/app/committees/[shortName]/layout.tsx | 2 +- .../[participationId]/page.module.scss | 5 +- .../periodes/[participationId]/page.tsx | 2 - .../[shortName]/periodes/page.module.scss | 25 ++++++++ .../committees/[shortName]/periodes/page.tsx | 61 ++++++++++++++----- src/auth/nextAuth/authOptions.ts | 2 +- .../seeder/src/dobbelOmega/dobbelOmega.ts | 2 +- src/prisma/seeder/src/seeder.ts | 4 +- .../committeeParticipation/operations.ts | 2 + 10 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/app/_components/User/UserDisplayName.tsx b/src/app/_components/User/UserDisplayName.tsx index 9f736d1ff..1eea76a4e 100644 --- a/src/app/_components/User/UserDisplayName.tsx +++ b/src/app/_components/User/UserDisplayName.tsx @@ -1,6 +1,6 @@ +import styles from './UserDisplayName.module.scss' import Flair from '@/components/Flair/Flair' import type { UserFiltered } from '@/services/users/types' -import styles from './UserDisplayName.module.scss' // TODO: Fix flairs / badges diff --git a/src/app/committees/[shortName]/layout.tsx b/src/app/committees/[shortName]/layout.tsx index 8982eea29..e405b74b6 100644 --- a/src/app/committees/[shortName]/layout.tsx +++ b/src/app/committees/[shortName]/layout.tsx @@ -7,9 +7,9 @@ import PageWrapper from '@/components/PageWrapper/PageWrapper' import CommitteeImage from '@/components/CommitteeImage/CommitteeImage' import { committeeAuth } from '@/services/groups/committees/auth' import { ServerSession } from '@/auth/session/ServerSession' -import type { ReactNode } from 'react' import { applicationAuth } from '@/services/applications/auth' import { committeeParticipationAuth } from '@/services/applications/committeeParticipation/auth' +import type { ReactNode } from 'react' export type PropTypes = { params: Promise<{ diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss b/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss index 1d1a53331..85a115bb4 100644 --- a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss +++ b/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss @@ -1,7 +1,6 @@ @use '@/styles/ohma'; //TODO: fix styling to be aligned to the future styling pr -.applicationsContainer {} .applicationContainer { margin-top: 1em; @@ -22,12 +21,12 @@ } } -.profilePicture {} .applicantName { color: black; } +.applicationsContainer {} +.profilePicture {} .applicationTextContainer {} - .applicationText {} \ No newline at end of file diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx b/src/app/committees/[shortName]/periodes/[participationId]/page.tsx index 5f695fddf..3ccf803cf 100644 --- a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx +++ b/src/app/committees/[shortName]/periodes/[participationId]/page.tsx @@ -1,5 +1,4 @@ import styles from './page.module.scss' -import getCommittee from '@/app/committees/[shortName]/getCommittee' import { unwrapActionReturn } from '@/app/redirectToErrorPage' import { readCommitteeApplicationsInPeriodAction } from '@/services/applications/committeeParticipation/actions' import ProfilePicture from '@/components/User/ProfilePicture' @@ -56,4 +55,3 @@ export default async function PeriodeCommitteePage({ params }: PropTypes) { ) } - diff --git a/src/app/committees/[shortName]/periodes/page.module.scss b/src/app/committees/[shortName]/periodes/page.module.scss index e69de29bb..ee12b8309 100644 --- a/src/app/committees/[shortName]/periodes/page.module.scss +++ b/src/app/committees/[shortName]/periodes/page.module.scss @@ -0,0 +1,25 @@ +@use '@/styles/ohma'; +//TODO: fix styling to be aligned to the future styling pr + + + +.periodTable{ + border: 2px solid hsla(0, 0%, 70%, 0.507); + border-collapse: collapse; + text-align: center; + @include ohma.round; +} + + +.tableEntry { + border: 2px solid hsla(0, 0%, 70%, 0.507); + padding: 1em; +} + +.currentPeriodEntry{ + font-weight: bold; + background-color: hsla(120, 100%, 70%, 0.288); +} + +.periodHeading{} +.periodSection{} \ No newline at end of file diff --git a/src/app/committees/[shortName]/periodes/page.tsx b/src/app/committees/[shortName]/periodes/page.tsx index 4cb56fc3f..64ad215bb 100644 --- a/src/app/committees/[shortName]/periodes/page.tsx +++ b/src/app/committees/[shortName]/periodes/page.tsx @@ -11,6 +11,14 @@ export type PropTypes = { }> } +type periodType = { + participationId: number; + applicationCount: number; + startDate: Date; + endDate: Date; + endPriorityDate: Date; +} + export default async function PeriodeCommitteePage({ params }: PropTypes) { const committee = await getCommittee(params) const shortName = (await params).shortName @@ -19,26 +27,47 @@ export default async function PeriodeCommitteePage({ params }: PropTypes) { ) if (committeePeriodes.length === 0) { return 'ingen søknadsperioder funnet' } return ( - - - - - - +
Start DatoSlutt DatoSøknaderSøknadstall
+ + + + + + {committeePeriodes.map((period, index) => ( - - - - - - + )) }
Start datoSlutt datoOmprioritering slutt datoSøknaderSøknadstall
{period.startDate.toLocaleDateString('en-GB')}{period.endDate.toLocaleDateString('en-GB')} - - - - {period.applicationCount}
) } + +function PeriodSection({ period, shortName }: { period: periodType, shortName: string }) { + 'use client' //Use client to show user correct local time + const now = Date.now() + const isCurrentPeriod = (now > period.startDate.getTime()) && (now < period.endPriorityDate.getTime()) + const entriesClassName = `${styles.tableEntry} ${isCurrentPeriod && styles.currentPeriodEntry}` + return ( + + + {period.startDate.toLocaleDateString('en-GB')}, + kl: {period.startDate.toLocaleTimeString('en-GB')} + + + {period.endDate.toLocaleDateString('en-GB')}, + kl: {period.endDate.toLocaleTimeString('en-GB')} + + + {period.endPriorityDate.toLocaleDateString('en-GB')}, + kl: {period.endPriorityDate.toLocaleTimeString('en-GB')} + + + + + + + {period.applicationCount} + + ) +} \ No newline at end of file diff --git a/src/auth/nextAuth/authOptions.ts b/src/auth/nextAuth/authOptions.ts index 16383ed23..c7da30616 100644 --- a/src/auth/nextAuth/authOptions.ts +++ b/src/auth/nextAuth/authOptions.ts @@ -1,5 +1,6 @@ import '@pn-server-only' import VevenAdapter from './VevenAdapter' +import { compressJwt, decompressJwt } from './jwtCompression' import { decryptAndComparePassword } from '@/auth/passwordHash' import FeideProvider from '@/lib/feide/FeideProvider' import { updateUserStudyProgrammes } from '@/lib/feide/userRoutines' @@ -11,7 +12,6 @@ import { permissionOperations } from '@/services/permissions/operations' import CredentialsProvider from 'next-auth/providers/credentials' import { encode, decode } from 'next-auth/jwt' import type { AuthOptions } from 'next-auth' -import { compressJwt, decompressJwt } from './jwtCompression' export const authOptions: AuthOptions = { providers: [ diff --git a/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts b/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts index bc6ae9208..74d038237 100644 --- a/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts +++ b/src/prisma/seeder/src/dobbelOmega/dobbelOmega.ts @@ -10,9 +10,9 @@ import { UserMigrator } from './migrateUsers' import migrateCommittees from './migrateCommittees' import seedProdPermissions from './seedProdPermissions' import manifest from '@/seeder/src/logger' -import { PrismaClient as PrismaClientOw } from '@/prisma-generated-ow-basic/client' import { PrismaPg } from '@prisma/adapter-pg' import type { PrismaClient as PrismaClientPn } from '@/prisma-generated-pn-client' +import { PrismaClient as PrismaClientOw } from '@/prisma-generated-ow-basic/client' /** * !DobbelOmega! diff --git a/src/prisma/seeder/src/seeder.ts b/src/prisma/seeder/src/seeder.ts index c283a71a0..293195818 100644 --- a/src/prisma/seeder/src/seeder.ts +++ b/src/prisma/seeder/src/seeder.ts @@ -25,10 +25,10 @@ import seedEvents from './seedEvent' import seedCabin from './seedCabin' import seedPermissions from './seedPermissions' import seedFlairs from './seedFlairs' -import { PrismaClient } from '@/prisma-generated-pn-client' -import { PrismaPg } from '@prisma/adapter-pg' import seedInterestGroups from './seedInterestGroups' import seedDevApplicationsAndPeriods from './development/seedDevApplicationsAndPeriods' +import { PrismaClient } from '@/prisma-generated-pn-client' +import { PrismaPg } from '@prisma/adapter-pg' export default async function seed( shouldMigrate: boolean, diff --git a/src/services/applications/committeeParticipation/operations.ts b/src/services/applications/committeeParticipation/operations.ts index 2a3dcbf34..53b0717dc 100644 --- a/src/services/applications/committeeParticipation/operations.ts +++ b/src/services/applications/committeeParticipation/operations.ts @@ -90,6 +90,7 @@ export const committeeParticipationOperations = { applicationPeriod: { select: { startDate: true, + endPriorityDate: true, endDate: true, } } @@ -100,6 +101,7 @@ export const committeeParticipationOperations = { applicationCount: period._count.applications, startDate: period.applicationPeriod.startDate, endDate: period.applicationPeriod.endDate, + endPriorityDate: period.applicationPeriod.endPriorityDate, }) ) ) From de0484709068543ae580910dece1bd21d8a680ac Mon Sep 17 00:00:00 2001 From: Hsackboy Date: Tue, 24 Mar 2026 15:24:51 +0100 Subject: [PATCH 5/7] lint: fix linting --- src/app/committees/[shortName]/layout.tsx | 1 - .../committees/[shortName]/periodes/page.tsx | 43 ++----------------- .../periodes/periodTableSection.tsx | 42 ++++++++++++++++++ 3 files changed, 45 insertions(+), 41 deletions(-) create mode 100644 src/app/committees/[shortName]/periodes/periodTableSection.tsx diff --git a/src/app/committees/[shortName]/layout.tsx b/src/app/committees/[shortName]/layout.tsx index e405b74b6..4aedf5d2a 100644 --- a/src/app/committees/[shortName]/layout.tsx +++ b/src/app/committees/[shortName]/layout.tsx @@ -7,7 +7,6 @@ import PageWrapper from '@/components/PageWrapper/PageWrapper' import CommitteeImage from '@/components/CommitteeImage/CommitteeImage' import { committeeAuth } from '@/services/groups/committees/auth' import { ServerSession } from '@/auth/session/ServerSession' -import { applicationAuth } from '@/services/applications/auth' import { committeeParticipationAuth } from '@/services/applications/committeeParticipation/auth' import type { ReactNode } from 'react' diff --git a/src/app/committees/[shortName]/periodes/page.tsx b/src/app/committees/[shortName]/periodes/page.tsx index 64ad215bb..184cf58b5 100644 --- a/src/app/committees/[shortName]/periodes/page.tsx +++ b/src/app/committees/[shortName]/periodes/page.tsx @@ -1,30 +1,22 @@ import styles from './page.module.scss' +import { PeriodSection } from './periodTableSection' import getCommittee from '@/app/committees/[shortName]/getCommittee' import { unwrapActionReturn } from '@/app/redirectToErrorPage' import { readCommitteeParticipatingPeriodAction } from '@/services/applications/committeeParticipation/actions' -import Link from 'next/link' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faLink } from '@fortawesome/free-solid-svg-icons' + export type PropTypes = { params: Promise<{ shortName: string }> } -type periodType = { - participationId: number; - applicationCount: number; - startDate: Date; - endDate: Date; - endPriorityDate: Date; -} export default async function PeriodeCommitteePage({ params }: PropTypes) { const committee = await getCommittee(params) const shortName = (await params).shortName const committeePeriodes = unwrapActionReturn( await readCommitteeParticipatingPeriodAction({ params: { committeeId: committee.id } }) - ) + ).sort((a, b) => b.startDate.getTime() - a.startDate.getTime()) if (committeePeriodes.length === 0) { return 'ingen søknadsperioder funnet' } return ( @@ -42,32 +34,3 @@ export default async function PeriodeCommitteePage({ params }: PropTypes) {
) } - -function PeriodSection({ period, shortName }: { period: periodType, shortName: string }) { - 'use client' //Use client to show user correct local time - const now = Date.now() - const isCurrentPeriod = (now > period.startDate.getTime()) && (now < period.endPriorityDate.getTime()) - const entriesClassName = `${styles.tableEntry} ${isCurrentPeriod && styles.currentPeriodEntry}` - return ( - - - {period.startDate.toLocaleDateString('en-GB')}, - kl: {period.startDate.toLocaleTimeString('en-GB')} - - - {period.endDate.toLocaleDateString('en-GB')}, - kl: {period.endDate.toLocaleTimeString('en-GB')} - - - {period.endPriorityDate.toLocaleDateString('en-GB')}, - kl: {period.endPriorityDate.toLocaleTimeString('en-GB')} - - - - - - - {period.applicationCount} - - ) -} \ No newline at end of file diff --git a/src/app/committees/[shortName]/periodes/periodTableSection.tsx b/src/app/committees/[shortName]/periodes/periodTableSection.tsx new file mode 100644 index 000000000..7532fe2cb --- /dev/null +++ b/src/app/committees/[shortName]/periodes/periodTableSection.tsx @@ -0,0 +1,42 @@ +'use client' //Use client to show user correct local time +import styles from './page.module.scss' +import { useState } from 'react' +import Link from 'next/link' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faLink } from '@fortawesome/free-solid-svg-icons' + +type periodType = { + participationId: number; + applicationCount: number; + startDate: Date; + endDate: Date; + endPriorityDate: Date; +} + +export function PeriodSection({ period, shortName }: { period: periodType, shortName: string }) { + const [now] = useState(() => Date.now()) + const isCurrentPeriod = (now > period.startDate.getTime()) && (now < period.endPriorityDate.getTime()) + const entriesClassName = `${styles.tableEntry} ${isCurrentPeriod && styles.currentPeriodEntry}` + return ( + + + {period.startDate.toLocaleDateString('en-GB')}, + kl: {period.startDate.toLocaleTimeString('en-GB')} + + + {period.endDate.toLocaleDateString('en-GB')}, + kl: {period.endDate.toLocaleTimeString('en-GB')} + + + {period.endPriorityDate.toLocaleDateString('en-GB')}, + kl: {period.endPriorityDate.toLocaleTimeString('en-GB')} + + + + + + + {period.applicationCount} + + ) +} From 9233c20fd904318aebd42a9d47b255466c9af064 Mon Sep 17 00:00:00 2001 From: Vegard Date: Wed, 15 Apr 2026 10:28:47 +0200 Subject: [PATCH 6/7] fix: resolve merge conflicts --- package-lock.json | 39 -------------------------- src/app/committees/[shortName]/Nav.tsx | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37264cd94..4042a0664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3053,9 +3053,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3076,9 +3073,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3099,9 +3093,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3122,9 +3113,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3145,9 +3133,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5138,9 +5123,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5155,9 +5137,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5172,9 +5151,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5189,9 +5165,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5206,9 +5179,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5223,9 +5193,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5240,9 +5207,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5257,9 +5221,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/src/app/committees/[shortName]/Nav.tsx b/src/app/committees/[shortName]/Nav.tsx index 92108cb06..514541bdf 100644 --- a/src/app/committees/[shortName]/Nav.tsx +++ b/src/app/committees/[shortName]/Nav.tsx @@ -21,7 +21,7 @@ export default function Nav({ shortName, canReadCommitteeApplication }: PropType Innstillinger {canReadCommitteeApplication.authorized && - Innstillinger + Perioder } Members About From 8ec286a9ee83e481030f6de0a7a2abe3699411fb Mon Sep 17 00:00:00 2001 From: Hsackboy Date: Thu, 16 Apr 2026 11:31:57 +0200 Subject: [PATCH 7/7] refactor: unflattened return form services. Moved time check to server side fix: spelling, language error, empty code and html hydration error --- src/app/committees/[shortName]/Nav.tsx | 10 +++---- .../[participationId]/page.module.scss | 7 +---- .../[participationId]/page.tsx | 14 +++++----- .../page.module.scss | 0 .../{periodes => applicationPeriods}/page.tsx | 28 +++++++++++-------- .../periodTableSection.tsx | 15 +++++----- src/prisma/seeder/src/seeder.ts | 2 +- .../committeeParticipation/operations.ts | 27 +++++++----------- 8 files changed, 47 insertions(+), 56 deletions(-) rename src/app/committees/[shortName]/{periodes => applicationPeriods}/[participationId]/page.module.scss (81%) rename src/app/committees/[shortName]/{periodes => applicationPeriods}/[participationId]/page.tsx (80%) rename src/app/committees/[shortName]/{periodes => applicationPeriods}/page.module.scss (100%) rename src/app/committees/[shortName]/{periodes => applicationPeriods}/page.tsx (53%) rename src/app/committees/[shortName]/{periodes => applicationPeriods}/periodTableSection.tsx (71%) diff --git a/src/app/committees/[shortName]/Nav.tsx b/src/app/committees/[shortName]/Nav.tsx index 514541bdf..13ed67b43 100644 --- a/src/app/committees/[shortName]/Nav.tsx +++ b/src/app/committees/[shortName]/Nav.tsx @@ -13,7 +13,7 @@ export default function Nav({ shortName, canReadCommitteeApplication }: PropType const pathname = usePathname() const adminPath = `/committees/${shortName}/admin` - const readPeriodesPath = `/committees/${shortName}/periodes` + const readPeriodesPath = `/committees/${shortName}/applicationPeriods` const membersPath = `/committees/${shortName}/members` const aboutPath = `/committees/${shortName}/about` @@ -21,14 +21,14 @@ export default function Nav({ shortName, canReadCommitteeApplication }: PropType Innstillinger {canReadCommitteeApplication.authorized && - Perioder + Søknadsperioder } - Members - About + Medlemmer + Om - Back + Tilbake ) diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.module.scss similarity index 81% rename from src/app/committees/[shortName]/periodes/[participationId]/page.module.scss rename to src/app/committees/[shortName]/applicationPeriods/[participationId]/page.module.scss index 85a115bb4..6ef299621 100644 --- a/src/app/committees/[shortName]/periodes/[participationId]/page.module.scss +++ b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.module.scss @@ -24,9 +24,4 @@ .applicantName { color: black; -} - -.applicationsContainer {} -.profilePicture {} -.applicationTextContainer {} -.applicationText {} \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.tsx similarity index 80% rename from src/app/committees/[shortName]/periodes/[participationId]/page.tsx rename to src/app/committees/[shortName]/applicationPeriods/[participationId]/page.tsx index 3ccf803cf..c4e85faf2 100644 --- a/src/app/committees/[shortName]/periodes/[participationId]/page.tsx +++ b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.tsx @@ -13,7 +13,7 @@ export type PropTypes = { } -async function loadUserImage(image: ImageT | null) { +async function getFallbackImageIfNoImage(image: ImageT | null) { return image ? image : await readSpecialImageAction.bind( null, { params: { special: 'DEFAULT_PROFILE_IMAGE' } } )().then(res => { @@ -29,24 +29,24 @@ export default async function PeriodeCommitteePage({ params }: PropTypes) { await readCommitteeApplicationsInPeriodAction({ params: { participationId } }) ) if (applications.length === 0) { return 'ingen søknader funnet' } - const sortedApplications = applications.sort((a, b) => a.applicationPriority - b.applicationPriority) + const sortedApplications = applications.sort((a, b) => a.priority - b.priority) return (
{sortedApplications.map(async (application, index) => (
-

{application.applicationPriority}.

+

{application.priority}.

- -

{application.firstname} {application.lastname}

+ +

{application.user.firstname} {application.user.lastname}

-

{application.applicationText}

+

{application.text}

)) diff --git a/src/app/committees/[shortName]/periodes/page.module.scss b/src/app/committees/[shortName]/applicationPeriods/page.module.scss similarity index 100% rename from src/app/committees/[shortName]/periodes/page.module.scss rename to src/app/committees/[shortName]/applicationPeriods/page.module.scss diff --git a/src/app/committees/[shortName]/periodes/page.tsx b/src/app/committees/[shortName]/applicationPeriods/page.tsx similarity index 53% rename from src/app/committees/[shortName]/periodes/page.tsx rename to src/app/committees/[shortName]/applicationPeriods/page.tsx index 184cf58b5..9b0cd6a72 100644 --- a/src/app/committees/[shortName]/periodes/page.tsx +++ b/src/app/committees/[shortName]/applicationPeriods/page.tsx @@ -11,7 +11,7 @@ export type PropTypes = { } -export default async function PeriodeCommitteePage({ params }: PropTypes) { +export default async function ApplicationPeriods({ params }: PropTypes) { const committee = await getCommittee(params) const shortName = (await params).shortName const committeePeriodes = unwrapActionReturn( @@ -20,17 +20,21 @@ export default async function PeriodeCommitteePage({ params }: PropTypes) { if (committeePeriodes.length === 0) { return 'ingen søknadsperioder funnet' } return ( - - - - - - - - {committeePeriodes.map((period, index) => ( - - )) - } + + + + + + + + + + + {committeePeriodes.map((period, index) => ( + + )) + } +
Start datoSlutt datoOmprioritering slutt datoSøknaderSøknadstall
Start datoSlutt datoOmprioritering slutt datoSøknaderSøknadstall
) } diff --git a/src/app/committees/[shortName]/periodes/periodTableSection.tsx b/src/app/committees/[shortName]/applicationPeriods/periodTableSection.tsx similarity index 71% rename from src/app/committees/[shortName]/periodes/periodTableSection.tsx rename to src/app/committees/[shortName]/applicationPeriods/periodTableSection.tsx index 7532fe2cb..a0d758821 100644 --- a/src/app/committees/[shortName]/periodes/periodTableSection.tsx +++ b/src/app/committees/[shortName]/applicationPeriods/periodTableSection.tsx @@ -1,6 +1,5 @@ 'use client' //Use client to show user correct local time import styles from './page.module.scss' -import { useState } from 'react' import Link from 'next/link' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faLink } from '@fortawesome/free-solid-svg-icons' @@ -11,12 +10,11 @@ type periodType = { startDate: Date; endDate: Date; endPriorityDate: Date; + isOpen: boolean; } export function PeriodSection({ period, shortName }: { period: periodType, shortName: string }) { - const [now] = useState(() => Date.now()) - const isCurrentPeriod = (now > period.startDate.getTime()) && (now < period.endPriorityDate.getTime()) - const entriesClassName = `${styles.tableEntry} ${isCurrentPeriod && styles.currentPeriodEntry}` + const entriesClassName = `${styles.tableEntry} ${period.isOpen && styles.currentPeriodEntry}` return ( @@ -31,10 +29,11 @@ export function PeriodSection({ period, shortName }: { period: periodType, short {period.endPriorityDate.toLocaleDateString('en-GB')}, kl: {period.endPriorityDate.toLocaleTimeString('en-GB')} - - - - + + + + + {period.applicationCount} diff --git a/src/prisma/seeder/src/seeder.ts b/src/prisma/seeder/src/seeder.ts index 0754cfb03..c95169b0e 100644 --- a/src/prisma/seeder/src/seeder.ts +++ b/src/prisma/seeder/src/seeder.ts @@ -70,7 +70,7 @@ export default async function seed( await seedDevCompanies(prisma) await seedDevJobAds(prisma) await seedDevShop(prisma) - await seedDevEvents(prisma) await seedDevApplicationsAndPeriods(prisma) + await seedDevEvents(prisma) console.log('seed dev done') } diff --git a/src/services/applications/committeeParticipation/operations.ts b/src/services/applications/committeeParticipation/operations.ts index 53b0717dc..113ac888b 100644 --- a/src/services/applications/committeeParticipation/operations.ts +++ b/src/services/applications/committeeParticipation/operations.ts @@ -48,17 +48,7 @@ export const committeeParticipationOperations = { } } } - }).then((applications) => applications.applications.map((application) => ( - { - applicationText: application.text, - applicationPriority: application.priority, - firstname: application.user.firstname, - lastname: application.user.lastname, - image: application.user.image, - email: application.user.email, - username: application.user.username, - } - ))) + }).then((applications) => applications.applications) ) }), readAll: defineOperation({ @@ -95,13 +85,16 @@ export const committeeParticipationOperations = { } } } - }).then((periods) => periods.map((period) => ( + }).then((participationRows) => participationRows.map((participation) => ( { - participationId: period.id, - applicationCount: period._count.applications, - startDate: period.applicationPeriod.startDate, - endDate: period.applicationPeriod.endDate, - endPriorityDate: period.applicationPeriod.endPriorityDate, + participationId: participation.id, + applicationCount: participation._count.applications, + startDate: participation.applicationPeriod.startDate, + endDate: participation.applicationPeriod.endDate, + endPriorityDate: participation.applicationPeriod.endPriorityDate, + isOpen: (Date.now() > participation.applicationPeriod.startDate.getTime()) + && + (Date.now() < participation.applicationPeriod.endPriorityDate.getTime()) }) ) )