Skip to content

Commit 32ae8d2

Browse files
Abi107717github-actions[bot]az108
authored
Development: Add interviewee assessment page (#1700)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Aniruddh Zaveri <92953467+az108@users.noreply.github.com>
1 parent 5de9e99 commit 32ae8d2

34 files changed

Lines changed: 1478 additions & 64 deletions

File tree

openapi/openapi.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,49 @@ paths:
11231123
schema:
11241124
type: array
11251125
items: {$ref: '#/components/schemas/IntervieweeDTO'}
1126+
/api/interviews/processes/{processId}/interviewees/{intervieweeId}:
1127+
get:
1128+
tags: [interview-resource]
1129+
operationId: getIntervieweeDetails
1130+
parameters:
1131+
- name: processId
1132+
in: path
1133+
required: true
1134+
schema: {type: string, format: uuid}
1135+
- name: intervieweeId
1136+
in: path
1137+
required: true
1138+
schema: {type: string, format: uuid}
1139+
responses:
1140+
'200':
1141+
description: OK
1142+
content:
1143+
application/json:
1144+
schema: {$ref: '#/components/schemas/IntervieweeDetailDTO'}
1145+
/api/interviews/processes/{processId}/interviewees/{intervieweeId}/assessment:
1146+
put:
1147+
tags: [interview-resource]
1148+
operationId: updateAssessment
1149+
parameters:
1150+
- name: processId
1151+
in: path
1152+
required: true
1153+
schema: {type: string, format: uuid}
1154+
- name: intervieweeId
1155+
in: path
1156+
required: true
1157+
schema: {type: string, format: uuid}
1158+
requestBody:
1159+
content:
1160+
application/json:
1161+
schema: {$ref: '#/components/schemas/UpdateAssessmentDTO'}
1162+
required: true
1163+
responses:
1164+
'200':
1165+
description: OK
1166+
content:
1167+
application/json:
1168+
schema: {$ref: '#/components/schemas/IntervieweeDetailDTO'}
11261169
/api/interviews/processes/{processId}/send-invitations:
11271170
post:
11281171
tags: [interview-resource]
@@ -2431,6 +2474,21 @@ components:
24312474
type: string
24322475
enum: [UNCONTACTED, INVITED, SCHEDULED, COMPLETED]
24332476
user: {$ref: '#/components/schemas/IntervieweeUserDTO'}
2477+
IntervieweeDetailDTO:
2478+
type: object
2479+
properties:
2480+
application: {$ref: '#/components/schemas/ApplicationDetailDTO'}
2481+
applicationId: {type: string, format: uuid}
2482+
assessmentNotes: {type: string}
2483+
documents: {$ref: '#/components/schemas/ApplicationDocumentIdsDTO'}
2484+
id: {type: string, format: uuid}
2485+
lastInvited: {type: string, format: date-time}
2486+
rating: {type: integer, format: int32}
2487+
scheduledSlot: {$ref: '#/components/schemas/InterviewSlotDTO'}
2488+
state:
2489+
type: string
2490+
enum: [UNCONTACTED, INVITED, SCHEDULED, COMPLETED]
2491+
user: {$ref: '#/components/schemas/IntervieweeUserDTO'}
24342492
IntervieweeUserDTO:
24352493
type: object
24362494
properties:
@@ -2924,6 +2982,12 @@ components:
29242982
projects: {type: string}
29252983
specialSkills: {type: string}
29262984
required: [applicant, applicationId, applicationState]
2985+
UpdateAssessmentDTO:
2986+
type: object
2987+
properties:
2988+
clearRating: {type: boolean}
2989+
notes: {type: string}
2990+
rating: {type: integer, format: int32, maximum: 2, minimum: -2}
29272991
UpdatePasswordDTO:
29282992
type: object
29292993
properties:

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/java/de/tum/cit/aet/application/repository/ApplicationRepository.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import de.tum.cit.aet.application.domain.dto.ApplicationForApplicantDTO;
55
import de.tum.cit.aet.core.repository.TumApplyJpaRepository;
66
import java.util.List;
7+
import java.util.Optional;
78
import java.util.Set;
89
import java.util.UUID;
910
import org.springframework.data.jpa.repository.Modifying;
@@ -231,4 +232,20 @@ WHERE a.job IN (
231232
"""
232233
)
233234
List<Object[]> countApplicationsByJobAndStateForInterviewProcesses(@Param("professorId") UUID professorId);
235+
236+
/**
237+
* Finds an application by ID with applicant and user details fetched.
238+
*
239+
* @param id the ID of the application
240+
* @return the application with details, or empty if not found
241+
*/
242+
@Query(
243+
"""
244+
SELECT a FROM Application a
245+
LEFT JOIN FETCH a.applicant ap
246+
LEFT JOIN FETCH ap.user
247+
WHERE a.applicationId = :id
248+
"""
249+
)
250+
Optional<Application> findWithDetailsById(@Param("id") UUID id);
234251
}

src/main/java/de/tum/cit/aet/interview/domain/Interviewee.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import de.tum.cit.aet.application.domain.Application;
44
import de.tum.cit.aet.core.domain.AbstractAuditingEntity;
5+
import de.tum.cit.aet.interview.domain.enumeration.AssessmentRating;
56
import jakarta.persistence.*;
67
import java.time.Instant;
78
import java.util.ArrayList;
@@ -12,7 +13,8 @@
1213

1314
/**
1415
* Entity representing an applicant who has been added to an interview process.
15-
* Links an Application to an InterviewProcess and tracks interview related state.
16+
* Links an Application to an InterviewProcess and tracks interview related
17+
* state.
1618
*/
1719
@Entity
1820
@Table(
@@ -50,6 +52,22 @@ public class Interviewee extends AbstractAuditingEntity {
5052
@Column(name = "version")
5153
private Long version;
5254

55+
/**
56+
* Assessment rating (POOR to EXCELLENT).
57+
* Maps to Likert scale integers -2 to 2 in the database.
58+
* Null if not yet assessed.
59+
*/
60+
@Column(name = "rating")
61+
@Convert(converter = AssessmentRatingConverter.class)
62+
private AssessmentRating rating;
63+
64+
/**
65+
* Professor's evaluation notes for this interviewee.
66+
* Null if no notes have been entered.
67+
*/
68+
@Column(name = "assessment_notes", columnDefinition = "TEXT")
69+
private String assessmentNotes;
70+
5371
/**
5472
* Gets the currently scheduled interview slot.
5573
* Currently only one slot per interviewee is supported.
@@ -68,4 +86,18 @@ public InterviewSlot getScheduledSlot() {
6886
public boolean hasSlot() {
6987
return !slots.isEmpty();
7088
}
89+
90+
@Converter
91+
public static class AssessmentRatingConverter implements AttributeConverter<AssessmentRating, Integer> {
92+
93+
@Override
94+
public Integer convertToDatabaseColumn(AssessmentRating attribute) {
95+
return attribute == null ? null : attribute.getValue();
96+
}
97+
98+
@Override
99+
public AssessmentRating convertToEntityAttribute(Integer dbData) {
100+
return AssessmentRating.fromValue(dbData);
101+
}
102+
}
71103
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package de.tum.cit.aet.interview.domain.enumeration;
2+
3+
import java.util.Arrays;
4+
5+
/**
6+
* Enumeration for Interviewee Assessment Rating.
7+
* Maps Likert scale integer values (-2 to 2) to semantic names.
8+
*/
9+
public enum AssessmentRating {
10+
POOR(-2),
11+
FAIR(-1),
12+
SATISFACTORY(0),
13+
GOOD(1),
14+
EXCELLENT(2);
15+
16+
private final int value;
17+
18+
AssessmentRating(int value) {
19+
this.value = value;
20+
}
21+
22+
public int getValue() {
23+
return value;
24+
}
25+
26+
/**
27+
* Converts an integer value to the corresponding AssessmentRating.
28+
*
29+
* @param value the integer value (-2 to 2)
30+
* @return the matching AssessmentRating, or null if value is null
31+
* @throws IllegalArgumentException if the value is invalid
32+
*/
33+
public static AssessmentRating fromValue(Integer value) {
34+
if (value == null) {
35+
return null;
36+
}
37+
return Arrays.stream(values())
38+
.filter(r -> r.value == value)
39+
.findFirst()
40+
.orElseThrow(() -> new IllegalArgumentException("Invalid rating value: " + value));
41+
}
42+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package de.tum.cit.aet.interview.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import de.tum.cit.aet.application.domain.dto.ApplicationDetailDTO;
5+
import de.tum.cit.aet.application.domain.dto.ApplicationDocumentIdsDTO;
6+
import java.time.Instant;
7+
import java.util.UUID;
8+
9+
/**
10+
* Detailed DTO for a single interviewee with full application context.
11+
* Includes application details, documents, and assessment information.
12+
*/
13+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
14+
public record IntervieweeDetailDTO(
15+
UUID id,
16+
UUID applicationId,
17+
IntervieweeDTO.IntervieweeUserDTO user,
18+
Instant lastInvited,
19+
InterviewSlotDTO scheduledSlot,
20+
IntervieweeState state,
21+
Integer rating,
22+
String assessmentNotes,
23+
ApplicationDetailDTO application,
24+
ApplicationDocumentIdsDTO documents
25+
) {}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package de.tum.cit.aet.interview.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import jakarta.validation.constraints.Max;
5+
import jakarta.validation.constraints.Min;
6+
7+
/**
8+
* DTO for updating interviewee assessment (rating and notes).
9+
* At least one field must be provided.
10+
*/
11+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
12+
public record UpdateAssessmentDTO(
13+
@Min(value = -2, message = "Rating must be between -2 and 2")
14+
@Max(value = 2, message = "Rating must be between -2 and 2")
15+
Integer rating,
16+
17+
Boolean clearRating,
18+
19+
String notes
20+
) {
21+
/**
22+
* Checks if the DTO has at least one field set.
23+
*
24+
* @return true if either rating, clearRating, or notes is provided
25+
*/
26+
public boolean hasContent() {
27+
return rating != null || Boolean.TRUE.equals(clearRating) || notes != null;
28+
}
29+
}

src/main/java/de/tum/cit/aet/interview/repository/IntervieweeRepository.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,28 @@ public interface IntervieweeRepository extends TumApplyJpaRepository<Interviewee
9696
List<Interviewee> findByInterviewProcessIdInWithSlots(@Param("processIds") List<UUID> processIds);
9797

9898
/**
99-
* Finds all uninvited interviewees for a given interview process.
99+
* Finds a single interviewee by ID within a process.
100+
*
101+
* @param intervieweeId the ID of the interviewee
102+
* @param processId the ID of the interview process
103+
* @return the interviewee or empty if not found
104+
*/
105+
@Query(
106+
"""
107+
SELECT DISTINCT i FROM Interviewee i
108+
JOIN FETCH i.interviewProcess ip
109+
JOIN FETCH ip.job j
110+
LEFT JOIN FETCH i.application a
111+
LEFT JOIN FETCH a.applicant ap
112+
LEFT JOIN FETCH ap.user
113+
LEFT JOIN FETCH i.slots
114+
WHERE i.id = :intervieweeId
115+
AND ip.id = :processId
116+
"""
117+
)
118+
Optional<Interviewee> findByIdAndProcessId(@Param("intervieweeId") UUID intervieweeId, @Param("processId") UUID processId);
119+
120+
/* Finds all uninvited interviewees for a given interview process.
100121
* Fetches application and user details to avoid N+1 issues when sending emails.
101122
*
102123
* @param processId the ID of the interview process

0 commit comments

Comments
 (0)