From e736958375e6841f86b229fd76826398c767455e Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 3 May 2026 12:40:46 +0200 Subject: [PATCH 1/2] \`Development\`: Keep evaluation working after a professor leaves and add a supervisor filter Co-Authored-By: Claude Opus 4.7 (1M context) --- openapi/openapi.yaml | 6 +++ .../dto/ApplicationEvaluationDetailDTO.java | 2 +- .../service/InterviewBookingService.java | 2 +- .../aet/job/dto/ProfessorJobsFilterDTO.java | 11 ++++-- .../cit/aet/job/repository/JobRepository.java | 8 +++- .../tum/cit/aet/job/service/JobService.java | 5 ++- .../aet/usermanagement/dto/ProfessorDTO.java | 37 ++++++++++++++++--- .../app/generated/api/job-resource-api.ts | 6 ++- .../generated/api/job-resource-resources.ts | 4 ++ .../my-positions-page.component.html | 6 +++ .../my-positions-page.component.ts | 35 ++++++++++++++++++ src/main/webapp/i18n/de/job.json | 4 +- src/main/webapp/i18n/en/job.json | 4 +- 13 files changed, 112 insertions(+), 18 deletions(-) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 46dab88bef..02169e963e 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1942,6 +1942,12 @@ paths: schema: type: array items: {type: string} + - name: supervisorIds + in: query + required: false + schema: + type: array + items: {type: string, format: uuid} - name: sortBy in: query required: false diff --git a/src/main/java/de/tum/cit/aet/evaluation/dto/ApplicationEvaluationDetailDTO.java b/src/main/java/de/tum/cit/aet/evaluation/dto/ApplicationEvaluationDetailDTO.java index 5a9a801dfa..93e163ea6c 100644 --- a/src/main/java/de/tum/cit/aet/evaluation/dto/ApplicationEvaluationDetailDTO.java +++ b/src/main/java/de/tum/cit/aet/evaluation/dto/ApplicationEvaluationDetailDTO.java @@ -29,7 +29,7 @@ public static ApplicationEvaluationDetailDTO fromApplication(Application applica Job job = application.getJob(); return new ApplicationEvaluationDetailDTO( ApplicationDetailDTO.getFromEntity(application, job), - ProfessorDTO.fromEntity(job.getSupervisingProfessor()), + ProfessorDTO.fromJob(job), job.getJobId(), application.getAppliedAt(), null // averageRating will be set separately via withAverageRating diff --git a/src/main/java/de/tum/cit/aet/interview/service/InterviewBookingService.java b/src/main/java/de/tum/cit/aet/interview/service/InterviewBookingService.java index aedcd4f2b9..fec2dd8d35 100644 --- a/src/main/java/de/tum/cit/aet/interview/service/InterviewBookingService.java +++ b/src/main/java/de/tum/cit/aet/interview/service/InterviewBookingService.java @@ -93,7 +93,7 @@ public BookingDTO getBookingData(UUID processId, YearMonth yearMonth, PageDTO pa throw new InterviewProcessClosedException("This interview process is closed because the linked job has been closed."); } - ProfessorDTO supervisor = ProfessorDTO.fromEntity(job.getSupervisingProfessor()); + ProfessorDTO supervisor = ProfessorDTO.fromJob(job); // 5. Check if already booked InterviewSlot bookedSlot = interviewee.getScheduledSlot(); diff --git a/src/main/java/de/tum/cit/aet/job/dto/ProfessorJobsFilterDTO.java b/src/main/java/de/tum/cit/aet/job/dto/ProfessorJobsFilterDTO.java index d07a9eefea..a944bcf13d 100644 --- a/src/main/java/de/tum/cit/aet/job/dto/ProfessorJobsFilterDTO.java +++ b/src/main/java/de/tum/cit/aet/job/dto/ProfessorJobsFilterDTO.java @@ -1,11 +1,14 @@ package de.tum.cit.aet.job.dto; import java.util.List; +import java.util.UUID; /** - * Filter DTO for retrieving jobs created by a specific professor. - * Encapsulates optional filters for job title and state. + * Filter DTO for retrieving jobs visible to a member of a research group. * - * @param states optional filter for multiple job states + * @param states optional filter for multiple job states + * @param supervisorIds optional filter restricting jobs to a list of + * supervising-professor user ids. {@code null} or + * empty means "all supervisors in the research group". */ -public record ProfessorJobsFilterDTO(List states) {} +public record ProfessorJobsFilterDTO(List states, List supervisorIds) {} diff --git a/src/main/java/de/tum/cit/aet/job/repository/JobRepository.java b/src/main/java/de/tum/cit/aet/job/repository/JobRepository.java index 2b248c48bb..eb89662186 100644 --- a/src/main/java/de/tum/cit/aet/job/repository/JobRepository.java +++ b/src/main/java/de/tum/cit/aet/job/repository/JobRepository.java @@ -25,11 +25,13 @@ @Repository public interface JobRepository extends TumApplyJpaRepository { /** - * Finds all jobs that belong to a given research group, with optional state and title/professor search filters. - * Results are paginated. + * Finds all jobs that belong to a given research group, with optional state, + * supervisor, and title/professor search filters. Results are paginated. * * @param researchGroupId the research group ID to filter by * @param states the optional list of job states to include + * @param supervisorIds the optional list of supervising-professor user ids; + * {@code null}/empty means all supervisors * @param searchQuery the optional search string for job title or professor name * @param pageable the pagination configuration * @return a page of matching jobs @@ -49,6 +51,7 @@ public interface JobRepository extends TumApplyJpaRepository { FROM Job j WHERE j.researchGroup.researchGroupId = :researchGroupId AND (:states IS NULL OR j.state IN :states) + AND (:supervisorIds IS NULL OR j.supervisingProfessor.userId IN :supervisorIds) AND (:searchQuery IS NULL OR j.title LIKE CONCAT('%', :searchQuery, '%') OR CONCAT(j.supervisingProfessor.firstName, ' ', j.supervisingProfessor.lastName) LIKE CONCAT('%', :searchQuery, '%') @@ -58,6 +61,7 @@ j.title LIKE CONCAT('%', :searchQuery, '%') OR Page findAllJobsByResearchGroup( @Param("researchGroupId") UUID researchGroupId, @Param("states") List states, + @Param("supervisorIds") List supervisorIds, @Param("searchQuery") String searchQuery, Pageable pageable ); diff --git a/src/main/java/de/tum/cit/aet/job/service/JobService.java b/src/main/java/de/tum/cit/aet/job/service/JobService.java index d1c466181f..a56ccc3a39 100644 --- a/src/main/java/de/tum/cit/aet/job/service/JobService.java +++ b/src/main/java/de/tum/cit/aet/job/service/JobService.java @@ -364,8 +364,11 @@ public Page getJobsForCurrentResearchGroup( if (professorJobsFilterDTO.states() != null && !professorJobsFilterDTO.states().isEmpty()) { enumStates = professorJobsFilterDTO.states().stream().map(JobState::fromValue).filter(Objects::nonNull).toList(); } + List supervisorIds = professorJobsFilterDTO.supervisorIds() == null || professorJobsFilterDTO.supervisorIds().isEmpty() + ? null + : professorJobsFilterDTO.supervisorIds(); String normalizedSearchQuery = StringUtil.normalizeSearchQuery(searchQuery); - return jobRepository.findAllJobsByResearchGroup(researchGroupId, enumStates, normalizedSearchQuery, pageable); + return jobRepository.findAllJobsByResearchGroup(researchGroupId, enumStates, supervisorIds, normalizedSearchQuery, pageable); } private JobFormDTO updateJobEntity(Job job, JobFormDTO dto) { diff --git a/src/main/java/de/tum/cit/aet/usermanagement/dto/ProfessorDTO.java b/src/main/java/de/tum/cit/aet/usermanagement/dto/ProfessorDTO.java index 0ff944426e..f7e1137cfe 100644 --- a/src/main/java/de/tum/cit/aet/usermanagement/dto/ProfessorDTO.java +++ b/src/main/java/de/tum/cit/aet/usermanagement/dto/ProfessorDTO.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.usermanagement.dto; +import de.tum.cit.aet.job.domain.Job; import de.tum.cit.aet.usermanagement.domain.ResearchGroup; import de.tum.cit.aet.usermanagement.domain.User; @@ -7,21 +8,45 @@ public record ProfessorDTO(String firstName, String lastName, String email, Stri /** * Converts a {@link User} entity to a {@link ProfessorDTO}. * + *

Returns null research-group fields when the user has no research group + * (e.g. the anonymised "deleted user" sentinel after retention runs). Use + * {@link #fromJob(Job)} when the job's research group should be used as the + * authoritative source instead of the supervising professor's. + * * @param user the user entity representing a professor * @return the corresponding {@link ProfessorDTO} - * @throws IllegalStateException if the user has no associated research group */ public static ProfessorDTO fromEntity(User user) { ResearchGroup researchGroup = user.getResearchGroup(); - if (researchGroup == null) { - throw new IllegalStateException("Research group is null"); - } return new ProfessorDTO( user.getFirstName(), user.getLastName(), user.getEmail(), - researchGroup.getName(), - researchGroup.getWebsite() + researchGroup != null ? researchGroup.getName() : null, + researchGroup != null ? researchGroup.getWebsite() : null + ); + } + + /** + * Builds a {@link ProfessorDTO} from a {@link Job}, using the job's research + * group as the authoritative source. + * + *

The job retains its research group even after the supervising professor + * is anonymised, so this is the safe choice for application/evaluation flows + * where the original research-group context still matters. + * + * @param job the job whose supervising professor and research group should be used + * @return the corresponding {@link ProfessorDTO} + */ + public static ProfessorDTO fromJob(Job job) { + User professor = job.getSupervisingProfessor(); + ResearchGroup researchGroup = job.getResearchGroup(); + return new ProfessorDTO( + professor.getFirstName(), + professor.getLastName(), + professor.getEmail(), + researchGroup != null ? researchGroup.getName() : null, + researchGroup != null ? researchGroup.getWebsite() : null ); } } diff --git a/src/main/webapp/app/generated/api/job-resource-api.ts b/src/main/webapp/app/generated/api/job-resource-api.ts index 32df88fbeb..f3cd7c7148 100644 --- a/src/main/webapp/app/generated/api/job-resource-api.ts +++ b/src/main/webapp/app/generated/api/job-resource-api.ts @@ -149,11 +149,12 @@ export class JobResourceApi { * @param pageSize * @param pageNumber * @param states + * @param supervisorIds * @param sortBy * @param direction * @param searchQuery */ - getJobsForCurrentResearchGroup(pageSize?: number, pageNumber?: number, states?: Array, sortBy?: string, direction?: 'ASC' | 'DESC', searchQuery?: string): Observable { + getJobsForCurrentResearchGroup(pageSize?: number, pageNumber?: number, states?: Array, supervisorIds?: Array, sortBy?: string, direction?: 'ASC' | 'DESC', searchQuery?: string): Observable { const queryParams = new URLSearchParams(); if (pageSize !== undefined && pageSize !== null) { queryParams.set('pageSize', String(pageSize)); @@ -164,6 +165,9 @@ export class JobResourceApi { if (states !== undefined && states !== null) { states.forEach(item => queryParams.append('states', String(item))); } + if (supervisorIds !== undefined && supervisorIds !== null) { + supervisorIds.forEach(item => queryParams.append('supervisorIds', String(item))); + } if (sortBy !== undefined && sortBy !== null) { queryParams.set('sortBy', String(sortBy)); } diff --git a/src/main/webapp/app/generated/api/job-resource-resources.ts b/src/main/webapp/app/generated/api/job-resource-resources.ts index bd2325ec67..e3cafda156 100644 --- a/src/main/webapp/app/generated/api/job-resource-resources.ts +++ b/src/main/webapp/app/generated/api/job-resource-resources.ts @@ -122,6 +122,7 @@ export interface GetJobsForCurrentResearchGroupParams { pageSize?: number; pageNumber?: number; states?: Array; + supervisorIds?: Array; sortBy?: string; direction?: 'ASC' | 'DESC'; searchQuery?: string; @@ -146,6 +147,9 @@ export function getJobsForCurrentResearchGroupResource(params?: Signal searchParams.append('states', String(value))); } + if (queryParams.supervisorIds?.length) { + queryParams.supervisorIds.forEach(value => searchParams.append('supervisorIds', String(value))); + } if (queryParams.sortBy !== undefined && queryParams.sortBy !== null) { searchParams.set('sortBy', String(queryParams.sortBy)); } diff --git a/src/main/webapp/app/job/my-positions/my-positions-page.component.html b/src/main/webapp/app/job/my-positions/my-positions-page.component.html index 17fb79d2fb..cab148ca79 100644 --- a/src/main/webapp/app/job/my-positions/my-positions-page.component.html +++ b/src/main/webapp/app/job/my-positions/my-positions-page.component.html @@ -70,6 +70,12 @@

filterOptions: availableStatusLabels, shouldTranslateOptions: true, }, + { + filterId: 'supervisor', + filterLabel: 'myPositionsPage.searchFilterSortBar.filterOptions.supervisor', + filterSearchPlaceholder: 'myPositionsPage.searchFilterSortBar.filterOptions.supervisorSearchPlaceholder', + filterOptions: availableSupervisorNames(), + }, ]" [sortableFields]="sortableFields" /> diff --git a/src/main/webapp/app/job/my-positions/my-positions-page.component.ts b/src/main/webapp/app/job/my-positions/my-positions-page.component.ts index 55e52a5e69..20e87f9043 100644 --- a/src/main/webapp/app/job/my-positions/my-positions-page.component.ts +++ b/src/main/webapp/app/job/my-positions/my-positions-page.component.ts @@ -22,6 +22,8 @@ import LocalizedDatePipe from '../../shared/pipes/localized-date.pipe'; import { TagComponent } from '../../shared/components/atoms/tag/tag.component'; import { CreatedJobDTO, CreatedJobDTOStateEnum } from '../../generated/model/created-job-dto'; import { JobResourceApi } from '../../generated/api/job-resource-api'; +import { ResearchGroupResourceApi } from '../../generated/api/research-group-resource-api'; +import { UserShortDTO } from '../../generated/model/user-short-dto'; @Component({ selector: 'jhi-my-positions-page', standalone: true, @@ -90,6 +92,11 @@ export class MyPositionsPageComponent { currentJobId = signal(undefined); readonly selectedStatusFilters = signal([]); + readonly selectedSupervisorIds = signal([]); + readonly availableSupervisors = signal([]); + readonly availableSupervisorNames = computed(() => + this.availableSupervisors().map(s => `${s.firstName ?? ''} ${s.lastName ?? ''}`.trim()), + ); readonly columns = computed(() => { const tpl = this.actionTemplate(); @@ -188,6 +195,7 @@ export class MyPositionsPageComponent { }); private jobApi = inject(JobResourceApi); + private researchGroupApi = inject(ResearchGroupResourceApi); private accountService = inject(AccountService); private router = inject(Router); private toastService = inject(ToastService); @@ -195,6 +203,10 @@ export class MyPositionsPageComponent { private readonly translationKey: string = 'myPositionsPage'; + constructor() { + void this.loadSupervisors(); + } + loadOnTableEmit(event: TableLazyLoadEvent): void { const page = Math.floor((event.first ?? 0) / (event.rows ?? this.pageSize())); const size = event.rows ?? this.pageSize(); @@ -221,6 +233,11 @@ export class MyPositionsPageComponent { const enumValues = this.mapTranslationKeysToEnumValues(filterChange.selectedValues); this.selectedStatusFilters.set(enumValues); void this.loadJobs(); + } else if (filterChange.filterId === 'supervisor') { + this.page.set(0); + const ids = this.mapSupervisorNamesToIds(filterChange.selectedValues); + this.selectedSupervisorIds.set(ids); + void this.loadJobs(); } } @@ -299,6 +316,23 @@ export class MyPositionsPageComponent { return translationKeys.map(key => keyMap.get(key) ?? key); } + private mapSupervisorNamesToIds(names: string[]): string[] { + if (names.length === 0) { + return []; + } + const byName = new Map(this.availableSupervisors().map(s => [`${s.firstName ?? ''} ${s.lastName ?? ''}`.trim(), s.userId])); + return names.map(n => byName.get(n)).filter((id): id is string => id !== undefined); + } + + private async loadSupervisors(): Promise { + try { + const supervisors = await firstValueFrom(this.researchGroupApi.getResearchGroupProfessors()); + this.availableSupervisors.set(supervisors); + } catch { + this.availableSupervisors.set([]); + } + } + private async loadJobs(): Promise { this.loading.set(true); try { @@ -311,6 +345,7 @@ export class MyPositionsPageComponent { this.pageSize(), this.page(), emptyToUndef(this.selectedStatusFilters()), + emptyToUndef(this.selectedSupervisorIds()), this.sortBy(), this.sortDirection(), this.searchQuery(), diff --git a/src/main/webapp/i18n/de/job.json b/src/main/webapp/i18n/de/job.json index f6950d1698..f86c603700 100644 --- a/src/main/webapp/i18n/de/job.json +++ b/src/main/webapp/i18n/de/job.json @@ -341,7 +341,9 @@ "searchFilterSortBar": { "searchText": "Stellen suchen...", "filterOptions": { - "stateSearchPlaceholder": "Suche Status..." + "stateSearchPlaceholder": "Suche Status...", + "supervisor": "Betreuung", + "supervisorSearchPlaceholder": "Suche Betreuung..." } }, "tableColumn": { diff --git a/src/main/webapp/i18n/en/job.json b/src/main/webapp/i18n/en/job.json index 18ca2c9032..137503dd41 100644 --- a/src/main/webapp/i18n/en/job.json +++ b/src/main/webapp/i18n/en/job.json @@ -341,7 +341,9 @@ "searchFilterSortBar": { "searchText": "Search Positions...", "filterOptions": { - "stateSearchPlaceholder": "Search Status..." + "stateSearchPlaceholder": "Search Status...", + "supervisor": "Supervisor", + "supervisorSearchPlaceholder": "Search Supervisor..." } }, "tableColumn": { From ed0bbb4ac9c6f6b7ba73f1739154df3175755925 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 3 May 2026 14:13:00 +0200 Subject: [PATCH 2/2] \`Bugfix\`: Update my-positions test for supervisor filter and run prettier Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/java/de/tum/cit/aet/job/service/JobService.java | 7 ++++--- .../job/my-positions/my-positions-page.component.spec.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/job/service/JobService.java b/src/main/java/de/tum/cit/aet/job/service/JobService.java index a56ccc3a39..7cd680e565 100644 --- a/src/main/java/de/tum/cit/aet/job/service/JobService.java +++ b/src/main/java/de/tum/cit/aet/job/service/JobService.java @@ -364,9 +364,10 @@ public Page getJobsForCurrentResearchGroup( if (professorJobsFilterDTO.states() != null && !professorJobsFilterDTO.states().isEmpty()) { enumStates = professorJobsFilterDTO.states().stream().map(JobState::fromValue).filter(Objects::nonNull).toList(); } - List supervisorIds = professorJobsFilterDTO.supervisorIds() == null || professorJobsFilterDTO.supervisorIds().isEmpty() - ? null - : professorJobsFilterDTO.supervisorIds(); + List supervisorIds = + professorJobsFilterDTO.supervisorIds() == null || professorJobsFilterDTO.supervisorIds().isEmpty() + ? null + : professorJobsFilterDTO.supervisorIds(); String normalizedSearchQuery = StringUtil.normalizeSearchQuery(searchQuery); return jobRepository.findAllJobsByResearchGroup(researchGroupId, enumStates, supervisorIds, normalizedSearchQuery, pageable); } diff --git a/src/test/webapp/app/job/my-positions/my-positions-page.component.spec.ts b/src/test/webapp/app/job/my-positions/my-positions-page.component.spec.ts index 63da4b2c9a..ed8bb7bd1c 100644 --- a/src/test/webapp/app/job/my-positions/my-positions-page.component.spec.ts +++ b/src/test/webapp/app/job/my-positions/my-positions-page.component.spec.ts @@ -292,6 +292,7 @@ describe('MyPositionsPageComponent', () => { component.pageSize(), component.page(), [CreatedJobDTOStateEnum.Published], + undefined, component.sortBy(), component.sortDirection(), component.searchQuery(),