diff --git a/prisma/seed.ts b/prisma/seed.ts index 2701302..c28f4d2 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -73,7 +73,7 @@ async function seed(prisma: PrismaClient) { ) await new Promise((resolve) => - rl.question('Please sign-in with Azure AD before continuing...', (ans) => { + rl.question('Please sign-in before continuing...', (ans) => { rl.close() resolve(ans) }) @@ -87,177 +87,177 @@ async function seed(prisma: PrismaClient) { }, }) - await prisma.proposal.upsert({ - where: { id: '3ef84a3b-cff0-4350-b760-4c5bb3b3c98f' }, - create: { - id: '3ef84a3b-cff0-4350-b760-4c5bb3b3c98f', - title: 'Effects of Interest Rate on Happiness', - description: 'This is a very interesting topic', - language: '["English"]', - studyLevel: 'Master Thesis (30 ECTS)', - topicArea: { - connect: { - slug: 'sustainable_finance', - }, - }, - status: { - connect: { key: ProposalStatus.OPEN }, - }, - type: { - connect: { key: ProposalType.STUDENT }, - }, - applications: { - create: { - email: 'roland.ferdinand@uzh.ch', - plannedStartAt: new Date(), - fullName: 'Roland Ferdinand', - matriculationNumber: '12-345-678', - motivation: 'I want to', - }, - }, - ownedByStudent: 'roland.ferdinand@uzh.ch', - ownedByUser: { - connect: { email: user.email }, - }, - receivedFeedbacks: { - create: { - comment: 'Rejected because', - type: { - connect: { key: ProposalFeedbackType.REJECTED_NOT_SCIENTIFIC }, - }, - reason: ProposalFeedbackType.REJECTED_NOT_SCIENTIFIC, - user: { - connect: { email: user.email }, - }, - }, - }, - }, - update: {}, - }) + // await prisma.proposal.upsert({ + // where: { id: '3ef84a3b-cff0-4350-b760-4c5bb3b3c98f' }, + // create: { + // id: '3ef84a3b-cff0-4350-b760-4c5bb3b3c98f', + // title: 'Effects of Interest Rate on Happiness', + // description: 'This is a very interesting topic', + // language: '["English"]', + // studyLevel: 'Master Thesis (30 ECTS)', + // topicArea: { + // connect: { + // slug: 'sustainable_finance', + // }, + // }, + // status: { + // connect: { key: ProposalStatus.OPEN }, + // }, + // type: { + // connect: { key: ProposalType.STUDENT }, + // }, + // applications: { + // create: { + // email: 'roland.ferdinand@uzh.ch', + // plannedStartAt: new Date(), + // fullName: 'Roland Ferdinand', + // matriculationNumber: '12-345-678', + // motivation: 'I want to', + // }, + // }, + // ownedByStudent: 'roland.ferdinand@uzh.ch', + // ownedByUser: { + // connect: { email: user.email }, + // }, + // receivedFeedbacks: { + // create: { + // comment: 'Rejected because', + // type: { + // connect: { key: ProposalFeedbackType.REJECTED_NOT_SCIENTIFIC }, + // }, + // reason: ProposalFeedbackType.REJECTED_NOT_SCIENTIFIC, + // user: { + // connect: { email: user.email }, + // }, + // }, + // }, + // }, + // update: {}, + // }) - await prisma.proposal.upsert({ - where: { id: '33a9a1b7-cad7-46e7-8b72-cfcbdbaa60d6' }, - create: { - id: '33a9a1b7-cad7-46e7-8b72-cfcbdbaa60d6', - title: - 'The role of interest rate expectations for the choice between Fixed-Rate Mortgages and Adjustable-Rate Mortgages', - description: - 'The role of interest rate expectations for the choice between Fixed-Rate Mortgages and Adjustable-Rate Mortgages', - language: '["English", "German"]', - studyLevel: 'Master Thesis (30 ECTS)', - timeFrame: 'Sometime next year.', - topicArea: { - connect: { - slug: 'banking_and_insurance', - }, - }, - status: { - connect: { key: ProposalStatus.OPEN }, - }, - type: { - connect: { key: ProposalType.SUPERVISOR }, - }, - supervisedBy: { - create: { - supervisor: { - connect: { email: user.email }, - }, - }, - }, - ownedByUser: { - connect: { email: user.email }, - }, - applications: { - create: { - plannedStartAt: new Date(), - email: 'roland.ferdinand@uzh.ch', - fullName: 'Roland Ferdinand', - matriculationNumber: '12-345-678', - motivation: - 'I want to do this topic as I think it is very interesting.', - attachments: { - createMany: { - data: [ - { - name: 'CV.pdf', - href: 'https://example.com/cv.pdf', - type: 'application/pdf', - }, - { - name: 'Transcript.pdf', - href: 'https://example.com/cv.pdf', - type: 'application/pdf', - }, - ], - }, - }, - }, - }, - }, - update: {}, - }) + // await prisma.proposal.upsert({ + // where: { id: '33a9a1b7-cad7-46e7-8b72-cfcbdbaa60d6' }, + // create: { + // id: '33a9a1b7-cad7-46e7-8b72-cfcbdbaa60d6', + // title: + // 'The role of interest rate expectations for the choice between Fixed-Rate Mortgages and Adjustable-Rate Mortgages', + // description: + // 'The role of interest rate expectations for the choice between Fixed-Rate Mortgages and Adjustable-Rate Mortgages', + // language: '["English", "German"]', + // studyLevel: 'Master Thesis (30 ECTS)', + // timeFrame: 'Sometime next year.', + // topicArea: { + // connect: { + // slug: 'banking_and_insurance', + // }, + // }, + // status: { + // connect: { key: ProposalStatus.OPEN }, + // }, + // type: { + // connect: { key: ProposalType.SUPERVISOR }, + // }, + // supervisedBy: { + // create: { + // supervisor: { + // connect: { email: user.email }, + // }, + // }, + // }, + // ownedByUser: { + // connect: { email: user.email }, + // }, + // applications: { + // create: { + // plannedStartAt: new Date(), + // email: 'roland.ferdinand@uzh.ch', + // fullName: 'Roland Ferdinand', + // matriculationNumber: '12-345-678', + // motivation: + // 'I want to do this topic as I think it is very interesting.', + // attachments: { + // createMany: { + // data: [ + // { + // name: 'CV.pdf', + // href: 'https://example.com/cv.pdf', + // type: 'application/pdf', + // }, + // { + // name: 'Transcript.pdf', + // href: 'https://example.com/cv.pdf', + // type: 'application/pdf', + // }, + // ], + // }, + // }, + // }, + // }, + // }, + // update: {}, + // }) - await prisma.proposal.upsert({ - where: { id: '21140e2e-e630-494a-ab18-374fd11c62c0' }, - create: { - id: '21140e2e-e630-494a-ab18-374fd11c62c0', - title: - 'Treiber der Inflation in der Schweiz / Inflationsmessung in der Schweiz', - description: - 'Treiber der Inflation in der Schweiz / Inflationsmessung in der Schweiz', - language: '["English"]', - studyLevel: 'Master Thesis (30 ECTS)', - timeFrame: '2023', - topicArea: { - connect: { - slug: 'financial_economics', - }, - }, - status: { - connect: { key: ProposalStatus.OPEN }, - }, - type: { - connect: { key: ProposalType.SUPERVISOR }, - }, - supervisedBy: { - create: { - supervisor: { - connect: { email: user.email }, - }, - }, - }, - ownedByUser: { - connect: { email: user.email }, - }, - applications: { - create: { - plannedStartAt: new Date(), - email: 'roland.ferdinand@uzh.ch', - fullName: 'Roland Ferdinand', - matriculationNumber: '12-345-678', - motivation: - 'I want to do this topic as I think it is very interesting.', - attachments: { - createMany: { - data: [ - { - name: 'CV.pdf', - href: 'https://example.com/cv.pdf', - type: 'application/pdf', - }, - { - name: 'Transcript.pdf', - href: 'https://example.com/cv.pdf', - type: 'application/pdf', - }, - ], - }, - }, - }, - }, - }, - update: {}, - }) + // await prisma.proposal.upsert({ + // where: { id: '21140e2e-e630-494a-ab18-374fd11c62c0' }, + // create: { + // id: '21140e2e-e630-494a-ab18-374fd11c62c0', + // title: + // 'Treiber der Inflation in der Schweiz / Inflationsmessung in der Schweiz', + // description: + // 'Treiber der Inflation in der Schweiz / Inflationsmessung in der Schweiz', + // language: '["English"]', + // studyLevel: 'Master Thesis (30 ECTS)', + // timeFrame: '2023', + // topicArea: { + // connect: { + // slug: 'financial_economics', + // }, + // }, + // status: { + // connect: { key: ProposalStatus.OPEN }, + // }, + // type: { + // connect: { key: ProposalType.SUPERVISOR }, + // }, + // supervisedBy: { + // create: { + // supervisor: { + // connect: { email: user.email }, + // }, + // }, + // }, + // ownedByUser: { + // connect: { email: user.email }, + // }, + // applications: { + // create: { + // plannedStartAt: new Date(), + // email: 'roland.ferdinand@uzh.ch', + // fullName: 'Roland Ferdinand', + // matriculationNumber: '12-345-678', + // motivation: + // 'I want to do this topic as I think it is very interesting.', + // attachments: { + // createMany: { + // data: [ + // { + // name: 'CV.pdf', + // href: 'https://example.com/cv.pdf', + // type: 'application/pdf', + // }, + // { + // name: 'Transcript.pdf', + // href: 'https://example.com/cv.pdf', + // type: 'application/pdf', + // }, + // ], + // }, + // }, + // }, + // }, + // }, + // update: {}, + // }) } seed(prismaClient) diff --git a/src/components/ApplicationDetailsModal.tsx b/src/components/ApplicationDetailsModal.tsx index 90dbd92..66f0887 100644 --- a/src/components/ApplicationDetailsModal.tsx +++ b/src/components/ApplicationDetailsModal.tsx @@ -5,7 +5,15 @@ import { add, format, parseISO } from 'date-fns' import { useState } from 'react' import { ApplicationDetails } from 'src/types/app' -function ApplicationDetailsModal({ row }: { row: ApplicationDetails }) { +function ApplicationDetailsModal({ + row, + isModalOpen, + setIsModalOpen, +}: { + row: ApplicationDetails + isModalOpen: boolean + setIsModalOpen: (isOpen: boolean) => void +}) { const FileTypeIconMap: Record = { 'application/pdf': faFilePdf, } diff --git a/src/components/ConfirmationModal.tsx b/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..0e4d14a --- /dev/null +++ b/src/components/ConfirmationModal.tsx @@ -0,0 +1,105 @@ +import { + faCheckCircle, + faCircleXmark, +} from '@fortawesome/free-regular-svg-icons' +import { faSpinner } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Button, Modal, Prose } from '@uzh-bf/design-system' +import { useState } from 'react' +import { ProposalStatusFilter } from 'src/types/app' +import { twMerge } from 'tailwind-merge' + +export default function ConfirmationModal({ + row, + isConfirmationModalOpen, + setIsConfirmationModalOpen, + acceptApplication, + proposalDetails, + refetch, + setFilters, +}: { + row: any + isConfirmationModalOpen: boolean + setIsConfirmationModalOpen: (isOpen: boolean) => void + acceptApplication: any + proposalDetails: any + refetch: () => void + setFilters: (filters: { status: string }) => void +}) { + const [isModalOpen, setIsModalOpen] = useState(false) + + return ( + setIsModalOpen(true)} + > + + {acceptApplication.isLoading + ? 'Loading...' + : row.statusKey === 'OPEN' + ? 'Accept' + : row.statusKey === 'ACCEPTED' + ? 'Accepted' + : 'Declined'} + + } + onClose={() => setIsModalOpen(false)} + onPrimaryAction={ + + } + onSecondaryAction={ + + } + hideCloseButton={true} + className={{ + content: 'w-max h-max self-center p-8 pt-4 text-sm', + }} + > +
+ + + This action cannot be undone. Once confirmed, the accepted student + will receive an acceptance notification, while the other students will + receive a notification indicating their application has been declined. + +
+
+ ) +} diff --git a/src/components/ProposalApplication.tsx b/src/components/ProposalApplication.tsx index 221ba00..d3cf638 100644 --- a/src/components/ProposalApplication.tsx +++ b/src/components/ProposalApplication.tsx @@ -1,20 +1,33 @@ import { H2, Table } from '@uzh-bf/design-system' import { add, format, parseISO } from 'date-fns' +import { useState } from 'react' +import { trpc } from 'src/lib/trpc' import { ApplicationDetails, ProposalDetails } from 'src/types/app' import ApplicationDetailsModal from './ApplicationDetailsModal' import ApplicationForm from './ApplicationForm' +import ConfirmationModal from './ConfirmationModal' interface ProposalApplicationProps { proposalDetails: ProposalDetails isStudent: boolean isSupervisor: boolean + refetch: () => void + setFilters: (filters: { status: string }) => void } export default function ProposalApplication({ proposalDetails, isStudent, isSupervisor, + refetch, + setFilters, }: ProposalApplicationProps) { + const [isModalOpen, setIsModalOpen] = useState(true) + const [isConfirmationModalOpen, setIsConfirmationModalOpen] = + useState(false) + + const acceptApplication = trpc.acceptProposalApplication.useMutation() + if (proposalDetails?.typeKey === 'SUPERVISOR') { return (
@@ -66,11 +79,30 @@ export default function ProposalApplication({ label: 'Details', accessor: 'details', transformer: ({ row }) => ( - + + ), + }, + { + label: 'Action', + accessor: 'action', + transformer: ({ row }) => ( + ), }, ]} - data={proposalDetails.applications} + data={proposalDetails?.applications} /> )}
diff --git a/src/components/ProposalCard.tsx b/src/components/ProposalCard.tsx index 222475c..bbc2e70 100644 --- a/src/components/ProposalCard.tsx +++ b/src/components/ProposalCard.tsx @@ -1,3 +1,8 @@ +import { + faCircleCheck, + faHourglassHalf, +} from '@fortawesome/free-regular-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { inferProcedureOutput } from '@trpc/server' import { Button } from '@uzh-bf/design-system' import { useSession } from 'next-auth/react' @@ -36,6 +41,13 @@ export default function ProposalCard({ active={isActive} onClick={onClick} > + {proposal.statusKey === 'MATCHED_TENTATIVE' && + proposal.supervisedBy[0].supervisorEmail === session?.user?.email ? ( + + ) : proposal.statusKey === 'MATCHED' && + proposal.supervisedBy[0].supervisorEmail === session?.user?.email ? ( + + ) : null}
{proposal.title}
{proposal.studyLevel}
diff --git a/src/components/ProposalStatusForm.tsx b/src/components/ProposalStatusForm.tsx index 26ceeef..838cee5 100644 --- a/src/components/ProposalStatusForm.tsx +++ b/src/components/ProposalStatusForm.tsx @@ -21,7 +21,9 @@ export default function ProposalStatusForm({ if ( (proposalDetails?.typeKey === 'STUDENT' && - proposalDetails?.statusKey === 'MATCHED_TENTATIVE') || + proposalDetails?.statusKey === 'MATCHED_TENTATIVE' && + proposalDetails.supervisedBy[0].supervisorEmail === + session?.user?.email) || providedFeedback === 'ACCEPT_TENTATIVE' ) { return ( diff --git a/src/pages/[[...proposalId]].tsx b/src/pages/[[...proposalId]].tsx index 16424d0..c957946 100644 --- a/src/pages/[[...proposalId]].tsx +++ b/src/pages/[[...proposalId]].tsx @@ -29,9 +29,10 @@ export default function Index() { const { isAdmin, isStudent, isSupervisor } = useUserRole() - const { data, isLoading, isError, isFetching } = trpc.proposals.useQuery({ - filters, - }) + const { data, isLoading, isError, isFetching, refetch } = + trpc.proposals.useQuery({ + filters, + }) useEffect(() => { if (!router.query.proposalId && data?.[0]?.id) { @@ -95,6 +96,8 @@ export default function Index() { proposalDetails={proposalDetails} isStudent={isStudent} isSupervisor={isSupervisor} + refetch={refetch} + setFilters={setFilters} /> { const res = await axios.post(process.env.APPLICATION_URL as string, input) }), + + acceptProposalApplication: publicProcedure + .input( + z.object({ + proposalId: z.string(), + proposalApplicationId: z.string(), + applicantEmail: z.string().email(), + }) + ) + .mutation(async ({ ctx, input }) => { + const res = await axios.post( + process.env.APPLICATION_ACCEPTANCE_URL as string, + input, + { + headers: { + 'Content-Type': 'application/json', + secretkey: process.env.FLOW_SECRET as string, + }, + } + ) + return res.data + }), }) export type AppRouter = typeof appRouter