Skip to content
6 changes: 6 additions & 0 deletions openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2197,6 +2197,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> states) {}
public record ProfessorJobsFilterDTO(List<String> states, List<UUID> supervisorIds) {}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
@Repository
public interface JobRepository extends DocApplyJpaRepository<Job, UUID> {
/**
* 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
Expand All @@ -50,6 +52,7 @@ public interface JobRepository extends DocApplyJpaRepository<Job, UUID> {
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, '%')
Expand All @@ -59,6 +62,7 @@ j.title LIKE CONCAT('%', :searchQuery, '%') OR
Page<CreatedJobDTO> findAllJobsByResearchGroup(
@Param("researchGroupId") UUID researchGroupId,
@Param("states") List<JobState> states,
@Param("supervisorIds") List<UUID> supervisorIds,
@Param("searchQuery") String searchQuery,
Pageable pageable
);
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/de/tum/cit/aet/job/service/JobService.java
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,12 @@ public Page<CreatedJobDTO> getJobsForCurrentResearchGroup(
if (professorJobsFilterDTO.states() != null && !professorJobsFilterDTO.states().isEmpty()) {
enumStates = professorJobsFilterDTO.states().stream().map(JobState::fromValue).filter(Objects::nonNull).toList();
}
List<UUID> 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);
}

/**
Expand Down
26 changes: 13 additions & 13 deletions src/main/java/de/tum/cit/aet/usermanagement/dto/ProfessorDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,29 @@

public record ProfessorDTO(String firstName, String lastName, String email, String researchGroupName, String researchGroupWebsite) {
/**
* Converts a {@link Job}'s supervising professor to a {@link ProfessorDTO},
* pulling the research group fields from the job itself rather than from the
* professor's membership list (a professor may belong to multiple groups).
* Converts a {@link Job}'s supervising professor to a {@link ProfessorDTO}, pulling the research group
* fields from the job itself rather than from the professor's membership list (a professor may belong to
* multiple groups).
*
* @param job the job whose supervising professor is being represented
* <p>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. Research-group
* fields are left null when the job has no research group (e.g. the
* anonymised "deleted user" sentinel after retention runs), so evaluation
* keeps working after a professor leaves.
*
* @param job the job whose supervising professor and research group should be used
* @return the corresponding {@link ProfessorDTO}
* @throws IllegalStateException if the job has no supervising professor or no research group
*/
public static ProfessorDTO fromJob(Job job) {
User professor = job.getSupervisingProfessor();
if (professor == null) {
throw new IllegalStateException("Job has no supervising professor");
}
ResearchGroup researchGroup = job.getResearchGroup();
if (researchGroup == null) {
throw new IllegalStateException("Research group is null");
}
return new ProfessorDTO(
professor.getFirstName(),
professor.getLastName(),
professor.getEmail(),
researchGroup.getName(),
researchGroup.getWebsite()
researchGroup != null ? researchGroup.getName() : null,
researchGroup != null ? researchGroup.getWebsite() : null
);
}
}
6 changes: 5 additions & 1 deletion src/main/webapp/app/generated/api/job-resource-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,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<string>, sortBy?: string, direction?: 'ASC' | 'DESC', searchQuery?: string): Observable<PageCreatedJobDTO> {
getJobsForCurrentResearchGroup(pageSize?: number, pageNumber?: number, states?: Array<string>, supervisorIds?: Array<string>, sortBy?: string, direction?: 'ASC' | 'DESC', searchQuery?: string): Observable<PageCreatedJobDTO> {
const queryParams = new URLSearchParams();
if (pageSize !== undefined && pageSize !== null) {
queryParams.set('pageSize', String(pageSize));
Expand All @@ -208,6 +209,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));
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/webapp/app/generated/api/job-resource-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export interface GetJobsForCurrentResearchGroupParams {
pageSize?: number;
pageNumber?: number;
states?: Array<string>;
supervisorIds?: Array<string>;
sortBy?: string;
direction?: 'ASC' | 'DESC';
searchQuery?: string;
Expand All @@ -200,6 +201,9 @@ export function getJobsForCurrentResearchGroupResource(params?: Signal<GetJobsFo
if (queryParams.states?.length) {
queryParams.states.forEach(value => 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ <h1>
filterOptions: availableStatusLabels,
shouldTranslateOptions: true,
},
{
filterId: 'supervisor',
filterLabel: 'myPositionsPage.searchFilterSortBar.filterOptions.supervisor',
filterSearchPlaceholder: 'myPositionsPage.searchFilterSortBar.filterOptions.supervisorSearchPlaceholder',
filterOptions: availableSupervisorNames(),
},
]"
[sortableFields]="sortableFields"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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,
Expand Down Expand Up @@ -89,6 +91,11 @@ export class MyPositionsPageComponent {
currentJobId = signal<string | undefined>(undefined);

readonly selectedStatusFilters = signal<string[]>([]);
readonly selectedSupervisorIds = signal<string[]>([]);
readonly availableSupervisors = signal<UserShortDTO[]>([]);
readonly availableSupervisorNames = computed<string[]>(() =>
this.availableSupervisors().map(s => `${s.firstName ?? ''} ${s.lastName ?? ''}`.trim()),
);

readonly columns = computed<DynamicTableColumn[]>(() => {
const tpl = this.actionTemplate();
Expand Down Expand Up @@ -187,6 +194,7 @@ export class MyPositionsPageComponent {
});

private jobApi = inject(JobResourceApi);
private researchGroupApi = inject(ResearchGroupResourceApi);
private accountService = inject(AccountService);
private router = inject(Router);
private toastService = inject(ToastService);
Expand All @@ -213,6 +221,10 @@ export class MyPositionsPageComponent {
void this.loadJobs();
});

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();
Expand All @@ -239,6 +251,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();
}
}

Expand Down Expand Up @@ -317,6 +334,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<void> {
try {
const supervisors = await firstValueFrom(this.researchGroupApi.getResearchGroupProfessors());
this.availableSupervisors.set(supervisors);
} catch {
this.availableSupervisors.set([]);
}
}

private async loadJobs(): Promise<void> {
this.loading.set(true);
try {
Expand All @@ -329,6 +363,7 @@ export class MyPositionsPageComponent {
this.pageSize(),
this.page(),
emptyToUndef(this.selectedStatusFilters()),
emptyToUndef(this.selectedSupervisorIds()),
this.sortBy(),
this.sortDirection(),
this.searchQuery(),
Expand Down
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/de/job.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@
"searchFilterSortBar": {
"searchText": "Stellen suchen...",
"filterOptions": {
"stateSearchPlaceholder": "Suche Status..."
"stateSearchPlaceholder": "Suche Status...",
"supervisor": "Betreuung",
"supervisorSearchPlaceholder": "Suche Betreuung..."
}
},
"tableColumn": {
Expand Down
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/en/job.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@
"searchFilterSortBar": {
"searchText": "Search Positions...",
"filterOptions": {
"stateSearchPlaceholder": "Search Status..."
"stateSearchPlaceholder": "Search Status...",
"supervisor": "Supervisor",
"supervisorSearchPlaceholder": "Search Supervisor..."
}
},
"tableColumn": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ describe('MyPositionsPageComponent', () => {
component.pageSize(),
component.page(),
[CreatedJobDTOStateEnum.Published],
undefined,
component.sortBy(),
component.sortDirection(),
component.searchQuery(),
Expand Down
Loading