diff --git a/.gitignore b/.gitignore index 16e8f8a2..97aaa2d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,7 @@ db_backups # User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf +.idea # AWS User-specific .idea/**/aws.xml diff --git a/client/src/management/components/ThesisApplicationsDatatable.tsx b/client/src/management/components/ThesisApplicationsDatatable.tsx index 1e1ad95c..5f286dba 100644 --- a/client/src/management/components/ThesisApplicationsDatatable.tsx +++ b/client/src/management/components/ThesisApplicationsDatatable.tsx @@ -1,129 +1,71 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { ActionIcon, Badge, Group, Modal, MultiSelect, Stack, TextInput } from '@mantine/core' import { DataTable, type DataTableSortStatus } from 'mantine-datatable' import { useAutoAnimate } from '@formkit/auto-animate/react' import { IconExternalLink, IconEyeEdit, IconSearch } from '@tabler/icons-react' import moment from 'moment' -import { Link, useNavigate, useParams } from 'react-router-dom' +import { Link } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { Query } from '../../state/query' import { getThesisApplications } from '../../network/thesisApplication' import { ThesisApplication } from '../../interface/thesisApplication' import { ApplicationStatus } from '../../interface/application' -import { Gender } from '../../interface/student' import { ApplicationFormAccessMode, ThesisApplicationForm, } from '../../student/form/ThesisApplicationForm' import { Pageable } from '../../interface/pageable' -interface Filters { - male: boolean - female: boolean - status: string[] -} - export const ThesisApplicationsDatatable = (): JSX.Element => { - const { applicationId } = useParams() - const navigate = useNavigate() const [bodyRef] = useAutoAnimate() + + const [page, setPage] = useState(1) + const [limit, setLimit] = useState(20) + + const [selectedApplications, setSelectedApplications] = useState([]) + const [openedApplication, setOpenedApplication] = useState() + const [searchQuery, setSearchQuery] = useState('') - const [tablePage, setTablePage] = useState(1) - const [tablePageSize, setTablePageSize] = useState(20) - const [tableRecords, setTableRecords] = useState([]) - const [selectedTableRecords, setSelectedTableRecords] = useState([]) - const [selectedApplicationToView, setSelectedApplicationToView] = useState< - ThesisApplication | undefined - >(undefined) - const [sortStatus, setSortStatus] = useState>({ + const [sort, setSort] = useState>({ columnAccessor: 'createdAt', direction: 'desc', }) - const [filters, setFilters] = useState({ - male: false, - female: false, - status: [], - }) + const [filteredStates, setFilteredStates] = useState(['NOT_ASSESSED']) - const { data: fetchedThesisApplications, isLoading } = useQuery>({ + const { data: applications, isLoading } = useQuery>({ queryKey: [ Query.THESIS_APPLICATION, - tablePage, - tablePageSize, + page, + limit, searchQuery, - sortStatus.columnAccessor, - sortStatus.direction, + filteredStates?.join(','), + sort.columnAccessor, + sort.direction, ], queryFn: () => getThesisApplications( - tablePage - 1, - tablePageSize, + page - 1, + limit, + filteredStates, searchQuery, - sortStatus.columnAccessor, - sortStatus.direction, + sort.columnAccessor, + sort.direction, ), }) - useEffect(() => { - if (applicationId) { - setSelectedApplicationToView( - fetchedThesisApplications?.content.find((a) => a.id === applicationId), - ) - } else { - setSelectedApplicationToView(undefined) - } - }, [fetchedThesisApplications, applicationId]) - - useEffect(() => { - const filteredSortedData = fetchedThesisApplications?.content - .filter( - (application) => - filters.status.length === 0 || filters.status.includes(application.applicationStatus), - ) - .filter((application) => - filters.female && application.student.gender - ? Gender[application.student.gender] === Gender.FEMALE - : true, - ) - .filter((application) => - filters.male && application.student.gender - ? Gender[application.student.gender] === Gender.MALE - : true, - ) - - setTableRecords(filteredSortedData ?? []) - - if (selectedApplicationToView) { - setSelectedApplicationToView( - fetchedThesisApplications?.content - .filter((ca) => ca.id === selectedApplicationToView.id) - .at(0), - ) - } - }, [ - fetchedThesisApplications, - tablePageSize, - tablePage, - searchQuery, - filters, - sortStatus, - selectedApplicationToView, - ]) - return ( - {selectedApplicationToView && ( + {openedApplication && ( { - navigate('/management/thesis-applications') - setSelectedApplicationToView(undefined) + setOpenedApplication(undefined) }} > @@ -146,25 +88,25 @@ export const ThesisApplicationsDatatable = (): JSX.Element => { verticalSpacing='md' striped highlightOnHover - totalRecords={fetchedThesisApplications?.totalElements ?? 0} - recordsPerPage={tablePageSize} - page={tablePage} - onPageChange={(page) => { - setTablePage(page) + totalRecords={applications?.totalElements ?? 0} + recordsPerPage={limit} + page={page} + onPageChange={(x) => { + setPage(x) }} recordsPerPageOptions={[5, 10, 15, 20, 25, 30, 35, 40, 50, 100, 200, 300]} onRecordsPerPageChange={(pageSize) => { - setTablePageSize(pageSize) + setLimit(pageSize) }} - sortStatus={sortStatus} + sortStatus={sort} onSortStatusChange={(status) => { - setTablePage(1) - setSortStatus(status) + setPage(1) + setSort(status) }} bodyRef={bodyRef} - records={tableRecords} - selectedRecords={selectedTableRecords} - onSelectedRecordsChange={setSelectedTableRecords} + records={applications?.content} + selectedRecords={selectedApplications} + onSelectedRecordsChange={setSelectedApplications} columns={[ { accessor: 'application_status', @@ -181,20 +123,17 @@ export const ThesisApplicationsDatatable = (): JSX.Element => { value: key, } })} - value={filters.status} + value={filteredStates} placeholder='Search status...' onChange={(value) => { - setFilters({ - ...filters, - status: value, - }) + setFilteredStates(value.length > 0 ? value : undefined) }} leftSection={} clearable searchable /> ), - filtering: filters.status.length > 0, + filtering: (filteredStates?.length ?? 0) > 0, render: (application) => { let color: string = 'gray' switch (application.applicationStatus) { @@ -251,24 +190,28 @@ export const ThesisApplicationsDatatable = (): JSX.Element => { { + onClick={(e) => { e.stopPropagation() - navigate(`/management/thesis-applications/${application.id}`) + + setOpenedApplication(application) }} > { + e.stopPropagation() + + setOpenedApplication(application) + }} target='_blank' rel='noopener noreferrer' > { - e.stopPropagation() - }} + onClick={(e) => e.stopPropagation()} > @@ -277,9 +220,7 @@ export const ThesisApplicationsDatatable = (): JSX.Element => { ), }, ]} - onRowClick={({ record: application }) => { - navigate(`/management/thesis-applications/${application.id}`) - }} + onRowClick={({ record: application }) => setOpenedApplication(application)} /> ) diff --git a/client/src/network/thesisApplication.ts b/client/src/network/thesisApplication.ts index 646d7aba..53061bc0 100644 --- a/client/src/network/thesisApplication.ts +++ b/client/src/network/thesisApplication.ts @@ -8,6 +8,7 @@ import { Pageable } from '../interface/pageable' export const getThesisApplications = async ( page: number, limit: number, + states?: string[], searchQuery?: string, sortBy?: string, sortOrder?: 'asc' | 'desc', @@ -18,6 +19,7 @@ export const getThesisApplications = async ( params: { page, limit, + states: states?.join(',') ?? Object.keys(ApplicationStatus).join(','), searchQuery, sortBy, sortOrder, diff --git a/docker-compose.yml b/docker-compose.yml index ac902a41..9d33b0f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: - SPRING_DATASOURCE_USERNAME=thesis-track-postgres - SPRING_DATASOURCE_PASSWORD=thesis-track-postgres - SPRING_JPA_HIBERNATE_DDL_AUTO=update + - KEYCLOAK_ISSUER_URI=http://keycloak:8080/realms/thesis-track + - KEYCLOAK_JWK_SET_URI=http://keycloak:8080/realms/thesis-track/protocol/openid-connect/certs ports: - "8080:8080" diff --git a/server/src/main/java/thesistrack/ls1/controller/ThesisApplicationController.java b/server/src/main/java/thesistrack/ls1/controller/ThesisApplicationController.java index 6e35705e..da1e1398 100644 --- a/server/src/main/java/thesistrack/ls1/controller/ThesisApplicationController.java +++ b/server/src/main/java/thesistrack/ls1/controller/ThesisApplicationController.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -52,16 +53,14 @@ public ThesisApplicationController(final ThesisApplicationService thesisApplicat @PreAuthorize("hasRole('chair-member') || hasRole('thesis-track-admin')") public ResponseEntity> getAll(@RequestParam Integer page, @RequestParam final Integer limit, + @RequestParam(required = false) final String states, @RequestParam(required = false) final String searchQuery, @RequestParam(defaultValue = "createdAt", required = false) final String sortBy, @RequestParam(defaultValue = "desc", required = false) final String sortOrder) { - return ResponseEntity.ok(thesisApplicationService.getAll(page, limit, searchQuery, sortBy, sortOrder)); - } + List filteredStates = Arrays.asList(states.split(",")); + filteredStates.removeIf(String::isEmpty); - @GetMapping("/not-assessed") - @PreAuthorize("hasRole('chair-member') || hasRole('thesis-track-admin')") - public ResponseEntity> getAllNotAssessed() { - return ResponseEntity.ok(thesisApplicationService.getAllNotAssessed()); + return ResponseEntity.ok(thesisApplicationService.getAll(page, limit, filteredStates, searchQuery, sortBy, sortOrder)); } @GetMapping("/{thesisApplicationId}/examination-report") diff --git a/server/src/main/java/thesistrack/ls1/model/ThesisApplication.java b/server/src/main/java/thesistrack/ls1/model/ThesisApplication.java index f1db494b..975b996d 100644 --- a/server/src/main/java/thesistrack/ls1/model/ThesisApplication.java +++ b/server/src/main/java/thesistrack/ls1/model/ThesisApplication.java @@ -21,13 +21,6 @@ @Data @Entity @Table -@org.hibernate.annotations.NamedQueries({ - @org.hibernate.annotations.NamedQuery( - name = "ThesisApplication.findAllNotAssessed", - query = "SELECT ta FROM ThesisApplication ta " + - "WHERE ta.applicationStatus = thesistrack.ls1.model.enums.ApplicationStatus.NOT_ASSESSED" - ) -}) public class ThesisApplication implements Serializable { @Id @GeneratedValue(strategy = GenerationType.UUID) diff --git a/server/src/main/java/thesistrack/ls1/repository/ThesisApplicationRepository.java b/server/src/main/java/thesistrack/ls1/repository/ThesisApplicationRepository.java index 535e8d7a..a19995f5 100644 --- a/server/src/main/java/thesistrack/ls1/repository/ThesisApplicationRepository.java +++ b/server/src/main/java/thesistrack/ls1/repository/ThesisApplicationRepository.java @@ -4,23 +4,23 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import thesistrack.ls1.model.ThesisApplication; import java.util.List; +import java.util.Set; import java.util.UUID; @Repository public interface ThesisApplicationRepository extends JpaRepository { - @Transactional - List findAllNotAssessed(); - @Query("SELECT ta FROM ThesisApplication ta WHERE " + - "LOWER(ta.student.firstName) LIKE %?1% OR " + - "LOWER(ta.student.lastName) LIKE %?1% OR " + - "LOWER(ta.student.email) LIKE %?1% OR " + - "LOWER(ta.student.matriculationNumber) LIKE %?1% OR " + - "LOWER(ta.student.tumId) LIKE %?1%") - Page searchThesisApplications(final String searchQuery, final Pageable page); + "(ta.applicationStatus IN(:states)) AND " + + "(LOWER(ta.student.firstName) LIKE %:searchQuery% OR " + + "LOWER(ta.student.lastName) LIKE %:searchQuery% OR " + + "LOWER(ta.student.email) LIKE %:searchQuery% OR " + + "LOWER(ta.student.matriculationNumber) LIKE %:searchQuery% OR " + + "LOWER(ta.student.tumId) LIKE %:searchQuery%)") + Page searchThesisApplications(@Param("states") Set states, @Param("searchQuery") String searchQuery, final Pageable page); } diff --git a/server/src/main/java/thesistrack/ls1/service/ThesisApplicationService.java b/server/src/main/java/thesistrack/ls1/service/ThesisApplicationService.java index 44a59d79..52647469 100644 --- a/server/src/main/java/thesistrack/ls1/service/ThesisApplicationService.java +++ b/server/src/main/java/thesistrack/ls1/service/ThesisApplicationService.java @@ -22,10 +22,7 @@ import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; @Service public class ThesisApplicationService { @@ -51,14 +48,10 @@ public ThesisApplicationService(final ThesisApplicationRepository thesisApplicat this.rootLocation = Paths.get(thesesApplicationsUploadsLocation); } - public Page getAll(final int page, final int limit, final String searchString, final String sortBy, final String sortOrder) { + public Page getAll(final int page, final int limit, final List states, final String searchString, final String sortBy, final String sortOrder) { final Sort.Order order = new Sort.Order(sortOrder.equals("asc") ? Sort.Direction.ASC : Sort.Direction.DESC, sortBy); return thesisApplicationRepository - .searchThesisApplications(searchString.toLowerCase(), PageRequest.of(page, limit, Sort.by(order))); - } - - public List getAllNotAssessed() { - return thesisApplicationRepository.findAllNotAssessed(); + .searchThesisApplications(new HashSet<>(states), searchString.toLowerCase(), PageRequest.of(page, limit, Sort.by(order))); } public Resource getExaminationReport(final UUID thesisApplicationId) {