diff --git a/src/app/committees/[shortName]/Nav.tsx b/src/app/committees/[shortName]/Nav.tsx
index 83c2173a6..13ed67b43 100644
--- a/src/app/committees/[shortName]/Nav.tsx
+++ b/src/app/committees/[shortName]/Nav.tsx
@@ -1,28 +1,34 @@
'use client'
import { SubPageNavBar, SubPageNavBarItem } from '@/components/NavBar/SubPageNavBar/SubPageNavBar'
-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()
- const settingsPath = `/committees/${shortName}/admin`
+ const adminPath = `/committees/${shortName}/admin`
+ const readPeriodesPath = `/committees/${shortName}/applicationPeriods`
const membersPath = `/committees/${shortName}/members`
const aboutPath = `/committees/${shortName}/about`
return (
- Innstillinger
- Members
- About
+ Innstillinger
+ {canReadCommitteeApplication.authorized &&
+ Søknadsperioder
+ }
+ Medlemmer
+ Om
- Back
+ Tilbake
)
diff --git a/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.module.scss b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.module.scss
new file mode 100644
index 000000000..6ef299621
--- /dev/null
+++ b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.module.scss
@@ -0,0 +1,27 @@
+@use '@/styles/ohma';
+//TODO: fix styling to be aligned to the future styling pr
+
+
+.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;
+ }
+}
+
+
+.applicantName {
+ color: black;
+}
\ No newline at end of file
diff --git a/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.tsx b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.tsx
new file mode 100644
index 000000000..c4e85faf2
--- /dev/null
+++ b/src/app/committees/[shortName]/applicationPeriods/[participationId]/page.tsx
@@ -0,0 +1,57 @@
+import styles from './page.module.scss'
+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: string,
+ }>
+}
+
+
+async function getFallbackImageIfNoImage(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 = 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.priority - b.priority)
+ return (
+
+ {sortedApplications.map(async (application, index) => (
+
+
+
{application.priority}.
+
+
+
{application.user.firstname} {application.user.lastname}
+
+
+
+
+ ))
+ }
+
+ )
+}
+
diff --git a/src/app/committees/[shortName]/applicationPeriods/page.module.scss b/src/app/committees/[shortName]/applicationPeriods/page.module.scss
new file mode 100644
index 000000000..ee12b8309
--- /dev/null
+++ b/src/app/committees/[shortName]/applicationPeriods/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]/applicationPeriods/page.tsx b/src/app/committees/[shortName]/applicationPeriods/page.tsx
new file mode 100644
index 000000000..9b0cd6a72
--- /dev/null
+++ b/src/app/committees/[shortName]/applicationPeriods/page.tsx
@@ -0,0 +1,40 @@
+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'
+
+export type PropTypes = {
+ params: Promise<{
+ shortName: string
+ }>
+}
+
+
+export default async function ApplicationPeriods({ 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 (
+
+
+
+ | Start dato |
+ Slutt dato |
+ Omprioritering slutt dato |
+ Søknader |
+ Søknadstall |
+
+
+
+ {committeePeriodes.map((period, index) => (
+
+ ))
+ }
+
+
+ )
+}
diff --git a/src/app/committees/[shortName]/applicationPeriods/periodTableSection.tsx b/src/app/committees/[shortName]/applicationPeriods/periodTableSection.tsx
new file mode 100644
index 000000000..2a0791b8d
--- /dev/null
+++ b/src/app/committees/[shortName]/applicationPeriods/periodTableSection.tsx
@@ -0,0 +1,37 @@
+'use client' //Use client to show user correct local time
+import styles from './page.module.scss'
+import Link from 'next/link'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faLink } from '@fortawesome/free-solid-svg-icons'
+import type { readCommitteeParticipatingPeriodAction } from '@/services/applications/committeeParticipation/actions'
+
+export type CommitteeParticipationPeriodType =
+ Pick> & { success: true }, 'data'>['data'][number]
+
+
+export function PeriodSection({ period, shortName }: { period: CommitteeParticipationPeriodType, shortName: string }) {
+ const entriesClassName = `${styles.tableEntry} ${period.isOpen && 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} |
+
+ )
+}
diff --git a/src/app/committees/[shortName]/layout.tsx b/src/app/committees/[shortName]/layout.tsx
index 80ff69593..7369ea461 100644
--- a/src/app/committees/[shortName]/layout.tsx
+++ b/src/app/committees/[shortName]/layout.tsx
@@ -7,6 +7,7 @@ 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 { committeeParticipationAuth } from '@/services/applications/committeeParticipation/auth'
import type { ReactNode } from 'react'
export type PropTypes = {
@@ -32,6 +33,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 (
<>
@@ -49,7 +58,10 @@ export default async function Committee({ params, children }: PropTypes) {
-
+
>
)
}
diff --git a/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts b/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts
new file mode 100644
index 000000000..ace8d1d3a
--- /dev/null
+++ b/src/prisma/seeder/src/development/seedDevApplicationsAndPeriods.ts
@@ -0,0 +1,79 @@
+
+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(applicationPeriod => (
+ committees.map(async (committee) => {
+ const participation = await prisma.committeeParticipationInApplicationPeriod.create({
+ data: {
+ applicationPeriod: {
+ connect: {
+ id: applicationPeriod.id
+ }
+ },
+ committee: {
+ connect: {
+ id: committee.id
+ }
+ }
+
+ }
+ })
+ await prisma.application.createMany({
+ data: users.map(user => ({
+ priority: participation.committeeId,
+ text:
+ committee.name
+ + applicationPeriod.startDate.toUTCString()
+ + applicationPeriod.endDate.toUTCString()
+ + 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 56942ece7..c95169b0e 100644
--- a/src/prisma/seeder/src/seeder.ts
+++ b/src/prisma/seeder/src/seeder.ts
@@ -26,6 +26,7 @@ import seedCabin from './seedCabin'
import seedPermissions from './seedPermissions'
import seedFlairs from './seedFlairs'
import seedInterestGroups from './seedInterestGroups'
+import seedDevApplicationsAndPeriods from './development/seedDevApplicationsAndPeriods'
import { prisma } from '@/prisma/client'
@@ -69,6 +70,7 @@ export default async function seed(
await seedDevCompanies(prisma)
await seedDevJobAds(prisma)
await seedDevShop(prisma)
+ await seedDevApplicationsAndPeriods(prisma)
await seedDevEvents(prisma)
console.log('seed dev done')
}
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..113ac888b
--- /dev/null
+++ b/src/services/applications/committeeParticipation/operations.ts
@@ -0,0 +1,103 @@
+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)
+ )
+ }),
+ 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,
+ endPriorityDate: true,
+ endDate: true,
+ }
+ }
+ }
+ }).then((participationRows) => participationRows.map((participation) => (
+ {
+ 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())
+ })
+ )
+ )
+ )
+ })
+}