Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,25 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/PageResponseDTOInterviewSlotDTO'}
/api/interviews/processes/{processId}/slots/conflict-data:
get:
tags: [interview-resource]
operationId: getConflictDataForDate
parameters:
- name: processId
in: path
required: true
schema: {type: string, format: uuid}
- name: date
in: query
required: true
schema: {type: string, format: date}
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/ConflictDataDTO'}
/api/interviews/processes/{processId}/slots/create:
post:
tags: [interview-resource]
Expand Down Expand Up @@ -2265,6 +2284,13 @@ components:
researchGroupName: {type: string}
supervisor: {$ref: '#/components/schemas/ProfessorDTO'}
userBookingInfo: {$ref: '#/components/schemas/UserBookingInfoDTO'}
ConflictDataDTO:
type: object
properties:
currentProcessId: {type: string, format: uuid}
slots:
type: array
items: {$ref: '#/components/schemas/ExistingSlotDTO'}
CreateSlotsDTO:
type: object
properties:
Expand Down Expand Up @@ -2419,6 +2445,14 @@ components:
properties:
professorName: {type: string, minLength: 1}
required: [professorName]
ExistingSlotDTO:
type: object
properties:
endDateTime: {type: string, format: date-time}
id: {type: string, format: uuid}
interviewProcessId: {type: string, format: uuid}
isBooked: {type: boolean}
startDateTime: {type: string, format: date-time}
GenderBiasAnalysisRequest:
type: object
properties:
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/de/tum/cit/aet/interview/dto/ConflictDataDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package de.tum.cit.aet.interview.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import de.tum.cit.aet.interview.domain.InterviewSlot;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

/**
* DTO containing conflict data for slot creation validation.
* Returns all slots relevant for conflict detection on a specific date in a single response.
* The client side uses currentProcessId to distinguish between:
* - SAME_PROCESS conflicts (slot belongs to current process)
* - BOOKED_OTHER_PROCESS conflicts (booked slot from another process)
*
* @param currentProcessId the current interview process ID for client-side
* filtering
* @param slots combined list of all process slots + booked slots
* from other processes
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ConflictDataDTO(UUID currentProcessId, List<ExistingSlotDTO> slots) {
/**
* Simplified slot DTO for conflict checking.
* Contains only the fields needed to detect and display conflicts.
*
* @param id the slot ID
* @param interviewProcessId the process this slot belongs to (for filtering)
* @param startDateTime start time of the slot
* @param endDateTime end time of the slot
* @param isBooked whether the slot is already booked
*/
public record ExistingSlotDTO(UUID id, UUID interviewProcessId, Instant startDateTime, Instant endDateTime, boolean isBooked) {
/**
* Creates an ExistingSlotDTO from an InterviewSlot entity.
*
* @param slot the entity to convert
* @return the DTO representation
*/
public static ExistingSlotDTO fromEntity(InterviewSlot slot) {
return new ExistingSlotDTO(
slot.getId(),
slot.getInterviewProcess().getId(),
slot.getStartDateTime(),
slot.getEndDateTime(),
slot.getIsBooked()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,29 @@ public interface InterviewSlotRepository extends JpaRepository<InterviewSlot, UU

/**
* Checks if a professor has any conflicting slots within the given time range.
* For the same process: blocks any overlapping slot.
* For other processes: only blocks BOOKED overlapping slots.
*
* @param professor the professor to check
* @param processId the current process ID (to distinguish same vs other
* process)
* @param startDateTime start of the time range
* @param endDateTime end of the time range
* @return true if at least one conflicting slot exists, false otherwise
*/
@Query(
"""

SELECT COUNT(s) > 0 FROM InterviewSlot s
JOIN s.interviewProcess ip
JOIN ip.job j
WHERE j.supervisingProfessor = :professor
AND (s.startDateTime < :endDateTime AND s.endDateTime > :startDateTime)
AND (ip.id = :processId OR s.isBooked = true)
"""
)
boolean hasConflictingSlots(
@Param("professor") User professor,
@Param("processId") UUID processId,
@Param("startDateTime") Instant startDateTime,
@Param("endDateTime") Instant endDateTime
);
Expand Down Expand Up @@ -199,4 +204,68 @@ Page<InterviewSlot> findAvailableSlotsByProcessIdAndMonth(
@Param("monthEnd") Instant monthEnd,
Pageable pageable
);

/**
* Finds all slots relevant for conflict detection on a specific date.
* Returns:
* - All slots (booked + unbooked) from the current interview process
* - All BOOKED slots from other processes of the same professor
*
* @param processId the current interview process ID
* @param professorId the supervising professor's user ID
* @param dayStart start of the day (inclusive)
* @param dayEnd end of the day (exclusive)
* @return list of slots for conflict checking, ordered by start time
*/
@Query(
"""
SELECT s FROM InterviewSlot s
JOIN s.interviewProcess ip
JOIN ip.job j
WHERE s.startDateTime >= :dayStart
AND s.startDateTime < :dayEnd
AND (
ip.id = :processId
OR (s.isBooked = true AND j.supervisingProfessor.userId = :professorId)
)
ORDER BY s.startDateTime
"""
)
List<InterviewSlot> findConflictDataByDate(
@Param("processId") UUID processId,
@Param("professorId") UUID professorId,
@Param("dayStart") Instant dayStart,
@Param("dayEnd") Instant dayEnd
);

/**
* Finds overlapping unbooked slots from other processes to auto-delete when a
* slot is booked.
* Used to prevent professors from being double-booked across different
* interview processes.
*
* @param professorId the supervising professor's user ID
* @param excludeProcessId the current process ID to exclude from results
* @param startDateTime start of the time range to check
* @param endDateTime end of the time range to check
* @return list of overlapping unbooked slots from other processes
*/
@Query(
"""
SELECT s FROM InterviewSlot s
JOIN s.interviewProcess ip
JOIN ip.job j
WHERE j.supervisingProfessor.userId = :professorId
AND ip.id != :excludeProcessId
AND s.isBooked = false
AND s.startDateTime < :endDateTime
AND s.endDateTime > :startDateTime
"""
)
List<InterviewSlot> findOverlappingUnbookedSlots(
@Param("professorId") UUID professorId,
@Param("excludeProcessId") UUID excludeProcessId,
@Param("startDateTime") Instant startDateTime,
@Param("endDateTime") Instant endDateTime
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,25 @@ public InterviewSlotDTO bookSlot(UUID processId, UUID slotId) {
slot.setIsBooked(true);
interviewee.getSlots().add(slot);

// 10. Save entities
// 10. Auto-delete overlapping unbooked slots from other processes (cleanup)
Job job = process.getJob();
UUID professorId = job.getSupervisingProfessor().getUserId();
List<InterviewSlot> overlappingSlots = interviewSlotRepository.findOverlappingUnbookedSlots(
professorId,
processId,
slot.getStartDateTime(),
slot.getEndDateTime()
);

if (!overlappingSlots.isEmpty()) {
interviewSlotRepository.deleteAll(overlappingSlots);
}

// 11. Save entities
interviewSlotRepository.save(slot);
intervieweeRepository.save(interviewee);

// 11. Send confirmation emails
Job job = process.getJob();
// 12. Send confirmation emails
sendBookingConfirmationEmails(slot, interviewee, job);

log.info("Slot {} booked by interviewee {} for process {}", slotId, interviewee.getId(), processId);
Expand Down
Loading
Loading