diff --git a/package-lock.json b/package-lock.json index ca39d8a..92384aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@next/font": "13.0.1", "@prisma/client": "4.5.0", "@uzh-bf/design-system": "0.0.66", + "date-fns": "2.29.3", "formik": "2.2.9", "graphql": "16.6.0", "graphql-yoga": "3.0.0-next.8", @@ -4427,6 +4428,18 @@ "integrity": "sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==", "dev": true }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -13107,6 +13120,11 @@ "integrity": "sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==", "dev": true }, + "date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + }, "debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", diff --git a/package.json b/package.json index 6a856db..f8018e5 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@next/font": "13.0.1", "@prisma/client": "4.5.0", "@uzh-bf/design-system": "0.0.66", + "date-fns": "2.29.3", "formik": "2.2.9", "graphql": "16.6.0", "graphql-yoga": "3.0.0-next.8", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 06208fe..277ef93 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,10 +52,13 @@ model Account { model User { id String @id @default(uuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? + name String? + email String? @unique + emailVerified DateTime? + image String? + matriculationNumber String? + personalCV String? + transcriptOfRecords String? role String @default("STUDENT") @@ -64,6 +67,7 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + ownedProposals UserProposalOwnership[] assignedProposals UserProposalAssignment[] supervisedProposals UserProposalSupervision[] appliedFor UserProposalApplication[] @@ -98,11 +102,10 @@ model ProposalAttachment { model Proposal { id String @id @unique @default(uuid()) - title String - description String - + title String + description String + language String? plannedStartAt DateTime? - plannedEndAt DateTime? attachments ProposalAttachment[] @@ -115,12 +118,12 @@ model Proposal { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + ownedBy UserProposalOwnership[] assignedTo UserProposalAssignment[] supervisedBy UserProposalSupervision[] applications UserProposalApplication[] receivedFeedbacks UserProposalFeedback[] - // TODO: student selects one or multiple topic areas? // TODO: BW can reassign topic areas? topicAreas TopicArea[] } @@ -158,6 +161,16 @@ model UserProposalApplication { @@unique([proposalId, userId]) } +model UserProposalOwnership { + proposal Proposal @relation(fields: [proposalId], references: [id]) + proposalId String + + user User @relation(fields: [userId], references: [id]) + userId String + + @@unique([proposalId, userId]) +} + model UserProposalFeedback { proposal Proposal @relation(fields: [proposalId], references: [id]) proposalId String diff --git a/src/graphql/generated/nexus-typegen.ts b/src/graphql/generated/nexus-typegen.ts index ff3e6bb..d14ca2d 100644 --- a/src/graphql/generated/nexus-typegen.ts +++ b/src/graphql/generated/nexus-typegen.ts @@ -34,11 +34,23 @@ export interface NexusGenObjects { Proposal: { // root type description: string; // String! id: string; // String! + ownedBy: NexusGenRootTypes['User'][]; // [User!]! statusKey: NexusGenEnums['ProposalStatus']; // ProposalStatus! title: string; // String! + topicAreas: NexusGenRootTypes['TopicArea'][]; // [TopicArea!]! typeKey: NexusGenEnums['ProposalType']; // ProposalType! } Query: {}; + TopicArea: { // root type + id?: string | null; // String + name?: string | null; // String + } + User: { // root type + email: string; // String! + id: number; // Int! + name: string; // String! + role: string; // String! + } } export interface NexusGenInterfaces { @@ -55,26 +67,50 @@ export interface NexusGenFieldTypes { Proposal: { // field return type description: string; // String! id: string; // String! + ownedBy: NexusGenRootTypes['User'][]; // [User!]! statusKey: NexusGenEnums['ProposalStatus']; // ProposalStatus! title: string; // String! + topicAreas: NexusGenRootTypes['TopicArea'][]; // [TopicArea!]! typeKey: NexusGenEnums['ProposalType']; // ProposalType! } Query: { // field return type proposals: NexusGenRootTypes['Proposal'][]; // [Proposal!]! } + TopicArea: { // field return type + id: string | null; // String + name: string | null; // String + } + User: { // field return type + email: string; // String! + id: number; // Int! + name: string; // String! + role: string; // String! + } } export interface NexusGenFieldTypeNames { Proposal: { // field return type name description: 'String' id: 'String' + ownedBy: 'User' statusKey: 'ProposalStatus' title: 'String' + topicAreas: 'TopicArea' typeKey: 'ProposalType' } Query: { // field return type name proposals: 'Proposal' } + TopicArea: { // field return type name + id: 'String' + name: 'String' + } + User: { // field return type name + email: 'String' + id: 'Int' + name: 'String' + role: 'String' + } } export interface NexusGenArgTypes { diff --git a/src/graphql/generated/ops.ts b/src/graphql/generated/ops.ts index 5887b64..d04882a 100644 --- a/src/graphql/generated/ops.ts +++ b/src/graphql/generated/ops.ts @@ -19,8 +19,10 @@ export type Proposal = { __typename?: 'Proposal'; description: Scalars['String']; id: Scalars['String']; + ownedBy: Array; statusKey: ProposalStatus; title: Scalars['String']; + topicAreas: Array; typeKey: ProposalType; }; @@ -44,10 +46,24 @@ export type Query = { proposals: Array; }; +export type TopicArea = { + __typename?: 'TopicArea'; + id?: Maybe; + name?: Maybe; +}; + +export type User = { + __typename?: 'User'; + email: Scalars['String']; + id: Scalars['Int']; + name: Scalars['String']; + role: Scalars['String']; +}; + export type ProposalsQueryVariables = Exact<{ [key: string]: never; }>; -export type ProposalsQuery = { __typename?: 'Query', proposals: Array<{ __typename?: 'Proposal', id: string, title: string, description: string, typeKey: ProposalType, statusKey: ProposalStatus }> }; +export type ProposalsQuery = { __typename?: 'Query', proposals: Array<{ __typename?: 'Proposal', id: string, title: string, description: string, typeKey: ProposalType, statusKey: ProposalStatus, topicAreas: Array<{ __typename?: 'TopicArea', id?: string | null, name?: string | null }>, ownedBy: Array<{ __typename?: 'User', id: number, name: string, role: string }> }> }; @@ -120,20 +136,26 @@ export type DirectiveResolverFn; File: ResolverTypeWrapper; + Int: ResolverTypeWrapper; Proposal: ResolverTypeWrapper; ProposalStatus: ProposalStatus; ProposalType: ProposalType; Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; + TopicArea: ResolverTypeWrapper; + User: ResolverTypeWrapper; }; /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = { Boolean: Scalars['Boolean']; File: Scalars['File']; + Int: Scalars['Int']; Proposal: Proposal; Query: {}; String: Scalars['String']; + TopicArea: TopicArea; + User: User; }; export interface FileScalarConfig extends GraphQLScalarTypeConfig { @@ -143,8 +165,10 @@ export interface FileScalarConfig extends GraphQLScalarTypeConfig = { description?: Resolver; id?: Resolver; + ownedBy?: Resolver, ParentType, ContextType>; statusKey?: Resolver; title?: Resolver; + topicAreas?: Resolver, ParentType, ContextType>; typeKey?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -153,15 +177,31 @@ export type QueryResolvers, ParentType, ContextType>; }; +export type TopicAreaResolvers = { + id?: Resolver, ParentType, ContextType>; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UserResolvers = { + email?: Resolver; + id?: Resolver; + name?: Resolver; + role?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { File?: GraphQLScalarType; Proposal?: ProposalResolvers; Query?: QueryResolvers; + TopicArea?: TopicAreaResolvers; + User?: UserResolvers; }; -export const ProposalsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Proposals"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"proposals"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"typeKey"}},{"kind":"Field","name":{"kind":"Name","value":"statusKey"}}]}}]}}]} as unknown as DocumentNode; +export const ProposalsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Proposals"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"proposals"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"typeKey"}},{"kind":"Field","name":{"kind":"Name","value":"statusKey"}},{"kind":"Field","name":{"kind":"Name","value":"topicAreas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"ownedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; export interface PossibleTypesResultData { possibleTypes: { diff --git a/src/graphql/generated/schema.graphql b/src/graphql/generated/schema.graphql index d967df3..82a3682 100644 --- a/src/graphql/generated/schema.graphql +++ b/src/graphql/generated/schema.graphql @@ -7,8 +7,10 @@ scalar File type Proposal { description: String! id: String! + ownedBy: [User!]! statusKey: ProposalStatus! title: String! + topicAreas: [TopicArea!]! typeKey: ProposalType! } @@ -29,4 +31,16 @@ enum ProposalType { type Query { proposals: [Proposal!]! +} + +type TopicArea { + id: String + name: String +} + +type User { + email: String! + id: Int! + name: String! + role: String! } \ No newline at end of file diff --git a/src/graphql/generated/schema.json b/src/graphql/generated/schema.json index 605cc0e..e2005d3 100644 --- a/src/graphql/generated/schema.json +++ b/src/graphql/generated/schema.json @@ -26,6 +26,16 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Proposal", @@ -63,6 +73,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "ownedBy", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "statusKey", "description": null, @@ -95,6 +129,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "topicAreas", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TopicArea", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "typeKey", "description": null, @@ -238,6 +296,116 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "TopicArea", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": null, + "fields": [ + { + "name": "email", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "role", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "__Directive", diff --git a/src/graphql/ops/QProposals.graphql b/src/graphql/ops/QProposals.graphql index ff7813b..830a023 100644 --- a/src/graphql/ops/QProposals.graphql +++ b/src/graphql/ops/QProposals.graphql @@ -7,5 +7,18 @@ query Proposals { typeKey statusKey + + topicAreas { + id + + name + } + + ownedBy { + id + + name + role + } } } diff --git a/src/graphql/types/proposals.ts b/src/graphql/types/proposals.ts index d5b0873..418fece 100644 --- a/src/graphql/types/proposals.ts +++ b/src/graphql/types/proposals.ts @@ -11,6 +11,26 @@ export const EnumProposalStatus = enumType({ members: ProposalStatus, }) +export const User = objectType({ + name: 'User', + definition(t) { + t.nonNull.int('id') + + t.nonNull.string('name') + t.nonNull.string('email') + t.nonNull.string('role') + }, +}) + +export const TopicArea = objectType({ + name: 'TopicArea', + definition(t) { + t.string('id') + + t.string('name') + }, +}) + export const Proposal = objectType({ name: 'Proposal', definition(t) { @@ -26,5 +46,13 @@ export const Proposal = objectType({ t.nonNull.field('statusKey', { type: EnumProposalStatus, }) + + t.nonNull.list.nonNull.field('topicAreas', { + type: TopicArea, + }) + + t.nonNull.list.nonNull.field('ownedBy', { + type: User, + }) }, }) diff --git a/src/graphql/types/query.ts b/src/graphql/types/query.ts index f1bef5a..75d686e 100644 --- a/src/graphql/types/query.ts +++ b/src/graphql/types/query.ts @@ -18,9 +18,23 @@ export const Query = objectType({ : ['SUPERVISOR'], }, }, + include: { + attachments: true, + topicAreas: true, + ownedBy: { + include: { user: true }, + }, + supervisedBy: { + include: { user: true }, + }, + }, }) - return proposals + return proposals.map((p) => ({ + ...p, + ownedBy: p.ownedBy[0].user, + supervisedBy: p.supervisedBy?.[0].user ?? [], + })) }, }) }, diff --git a/src/lib/authOptions.ts b/src/lib/authOptions.ts index 5b75408..a33aa2b 100644 --- a/src/lib/authOptions.ts +++ b/src/lib/authOptions.ts @@ -2,6 +2,7 @@ import { PrismaAdapter } from '@next-auth/prisma-adapter' import type { NextAuthOptions } from 'next-auth' import { decode, encode } from 'next-auth/jwt' import GithubProvider from 'next-auth/providers/github' +// import AzureADProvider from 'next-auth/providers/azure-ad' import prisma from './prisma' @@ -12,6 +13,11 @@ export const authOptions: NextAuthOptions = { clientId: process.env.GITHUB_ID as string, clientSecret: process.env.GITHUB_SECRET as string, }), + // AzureADProvider({ + // clientId: process.env.AZURE_AD_CLIENT_ID as string, + // clientSecret: process.env.AZURE_AD_CLIENT_SECRET as string, + // tenantId: process.env.AZURE_AD_TENANT_ID as string, + // }), ], session: { strategy: 'jwt', diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 49d4452..c15b87b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -5,6 +5,7 @@ import { FormikTextField, H1, } from '@uzh-bf/design-system' +import { add, format } from 'date-fns' import { Field, Form, Formik, FormikHelpers } from 'formik' import { signIn, signOut, useSession } from 'next-auth/react' import { useMemo, useState } from 'react' @@ -13,7 +14,7 @@ import { useQuery } from 'urql' interface ApplicationValues { matriculationNumber: string fullName: string - plannedStartingDate?: Date + plannedStartingDate: string motivation: string personalCV?: File transcriptOfRecords?: File @@ -22,7 +23,7 @@ interface ApplicationValues { const ApplicationInitialValues: ApplicationValues = { matriculationNumber: '', fullName: '', - plannedStartingDate: undefined, + plannedStartingDate: format(add(new Date(), { months: 6 }), 'yyyy-MM-dd'), motivation: '', personalCV: undefined, transcriptOfRecords: undefined, @@ -47,80 +48,88 @@ function Index() { if (session?.user) { return (
-
-
Signed in as {session.user.email}
+
+
+ Signed in as {session.user.email} ({session.user.role}) +
-
- {data?.proposals?.map((proposal) => ( +
+
+

Available Theses

+
+ {data?.proposals?.map((proposal) => ( + + ))} +
+
+ +
- ))} - - +
- {(displayMode === 'details' || displayMode === 'application') && - proposalDetails && ( -
-

{proposalDetails.title}

-

{proposalDetails.description}

-
{proposalDetails.status}
-
{proposalDetails.type}
- + {proposalDetails && ( +
+
+

Proposal {proposalDetails.title}

+
{proposalDetails.statusKey}
- )} +

{proposalDetails.description}

+
{proposalDetails.typeKey}
+
+ )} - {displayMode === 'application' && proposalDetails && ( -
+ {proposalDetails && ( +
, ) => { - console.log(values) - const formData = new FormData() formData.append( 'matriculationNumber', values.matriculationNumber, ) - formData.append('cv', values.personalCV!) + formData.append('fullName', values.fullName) + formData.append( + 'plannedStartingDate', + values.plannedStartingDate, + ) + formData.append('motivation', values.motivation) + formData.append('personalCV', values.personalCV!) + formData.append( + 'transcriptOfRecords', + values.transcriptOfRecords!, + ) fetch( 'https://prod-119.westeurope.logic.azure.com:443/workflows/8a7c3785ade64d168a78cc9e21ed7a1c/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=yykbjdA-5KZju5qiBWHw5Gt5WsBa_t1tgBTlqTk7_WU', @@ -131,7 +140,7 @@ function Index() { ) }} > -
+ - +
+ Starting Date + +
- - +
+ Personal CV (PDF){' '} + +
+
+ Transcript of Records (PDF){' '} + +
+