Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
package com.pointtils.pointtils.src.application.controllers;

import com.pointtils.pointtils.src.application.dto.requests.ScheduleRequestDTO;
import com.pointtils.pointtils.src.application.dto.requests.SchedulePatchRequestDTO;
import com.pointtils.pointtils.src.application.dto.requests.ScheduleListRequestDTO;
import com.pointtils.pointtils.src.application.dto.requests.SchedulePatchRequestDTO;
import com.pointtils.pointtils.src.application.dto.requests.ScheduleRequestDTO;
import com.pointtils.pointtils.src.application.dto.responses.ApiResponse;
import com.pointtils.pointtils.src.application.dto.responses.ScheduleResponseDTO;
import com.pointtils.pointtils.src.application.dto.responses.AvailableTimeSlotsResponseDTO;
import com.pointtils.pointtils.src.application.dto.responses.PaginatedScheduleResponseDTO;
import com.pointtils.pointtils.src.application.dto.responses.ScheduleResponseDTO;
import com.pointtils.pointtils.src.application.services.ScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.time.LocalDate;
import java.util.List;
import java.util.UUID;

@RestController
Expand Down Expand Up @@ -75,4 +80,14 @@ public ResponseEntity<ApiResponse<Void>> deleteSchedule(@PathVariable UUID sched
ApiResponse<Void> response = new ApiResponse<>(true, "Horário excluído com sucesso", null);
return ResponseEntity.ok(response);
}

@GetMapping("/available")
@Operation(summary = "Lista todos os intervalos de horários disponíveis cadastrados")
public ResponseEntity<ApiResponse<List<AvailableTimeSlotsResponseDTO>>> listAvailableSchedules(@RequestParam UUID interpreterId,
@RequestParam LocalDate dateFrom,
@RequestParam LocalDate dateTo) {
Comment on lines +86 to +88
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation annotations on request parameters. Add @NotNull validation to ensure required parameters are provided, preventing NullPointerException in the service layer.

Copilot uses AI. Check for mistakes.

List<AvailableTimeSlotsResponseDTO> availableTimeSlots = service.findAvailableSchedules(interpreterId, dateFrom, dateTo);
ApiResponse<List<AvailableTimeSlotsResponseDTO>> response = new ApiResponse<>(true, "Horários disponíveis obtidos com sucesso", availableTimeSlots);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.pointtils.pointtils.src.application.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalTime;
import java.util.Date;
import java.util.UUID;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TimeSlotDTO {

private Date date;
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using legacy java.util.Date instead of modern java.time.LocalDate. Since other fields use LocalTime, consider using LocalDate for consistency and better type safety.

Copilot uses AI. Check for mistakes.

private UUID interpreterId;
private LocalTime startTime;
private LocalTime endTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.pointtils.pointtils.src.application.dto.responses;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Date;
import java.util.List;
import java.util.UUID;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AvailableTimeSlotsResponseDTO {

private Date date;
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using legacy java.util.Date instead of modern java.time.LocalDate. For consistency with the application's use of LocalDate in controller parameters, consider using LocalDate here as well.

Copilot uses AI. Check for mistakes.

@JsonProperty("interpreter_id")
private UUID interpreterId;
@JsonProperty("time_slots")
private List<TimeSlotResponseDTO> timeSlots;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.pointtils.pointtils.src.application.dto.responses;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TimeSlotResponseDTO {

@JsonProperty("start_time")
private LocalTime startTime;
@JsonProperty("end_time")
private LocalTime endTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.pointtils.pointtils.src.application.mapper;

import com.pointtils.pointtils.src.application.dto.TimeSlotDTO;
import com.pointtils.pointtils.src.application.dto.responses.AvailableTimeSlotsResponseDTO;
import com.pointtils.pointtils.src.application.dto.responses.TimeSlotResponseDTO;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class TimeSlotMapper {

public List<AvailableTimeSlotsResponseDTO> toAvailableTimeSlotsResponse(List<TimeSlotDTO> timeSlots) {
Map<String, AvailableTimeSlotsResponseDTO> availableTimeSlots = new HashMap<>();
timeSlots.forEach(timeSlot -> {
String key = timeSlot.getDate().toString() + timeSlot.getInterpreterId().toString();
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String concatenation for map key creation is fragile and could lead to collisions. Use a proper composite key or delimiter, such as timeSlot.getDate().toString() + \"|\" + timeSlot.getInterpreterId().toString() to avoid potential key conflicts.

Suggested change
String key = timeSlot.getDate().toString() + timeSlot.getInterpreterId().toString();
String key = timeSlot.getDate().toString() + "|" + timeSlot.getInterpreterId().toString();

Copilot uses AI. Check for mistakes.

if (availableTimeSlots.containsKey(key)) {
availableTimeSlots.get(key).getTimeSlots()
.add(new TimeSlotResponseDTO(timeSlot.getStartTime(), timeSlot.getEndTime()));
} else {
var response = new AvailableTimeSlotsResponseDTO(
timeSlot.getDate(),
timeSlot.getInterpreterId(),
new ArrayList<>(List.of(new TimeSlotResponseDTO(timeSlot.getStartTime(), timeSlot.getEndTime())))
);
availableTimeSlots.put(key, response);
}
});
return availableTimeSlots.values().stream()
.sorted(Comparator.comparing(AvailableTimeSlotsResponseDTO::getDate))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.pointtils.pointtils.src.application.services;

import com.pointtils.pointtils.src.application.dto.requests.ScheduleRequestDTO;
import com.pointtils.pointtils.src.application.dto.TimeSlotDTO;
import com.pointtils.pointtils.src.application.dto.requests.ScheduleListRequestDTO;
import com.pointtils.pointtils.src.application.dto.requests.SchedulePatchRequestDTO;
import com.pointtils.pointtils.src.application.dto.responses.ScheduleResponseDTO;
import com.pointtils.pointtils.src.application.dto.requests.ScheduleRequestDTO;
import com.pointtils.pointtils.src.application.dto.responses.AvailableTimeSlotsResponseDTO;
import com.pointtils.pointtils.src.application.dto.responses.PaginatedScheduleResponseDTO;
import com.pointtils.pointtils.src.application.dto.responses.ScheduleResponseDTO;
import com.pointtils.pointtils.src.application.mapper.TimeSlotMapper;
import com.pointtils.pointtils.src.core.domain.entities.Interpreter;
import com.pointtils.pointtils.src.core.domain.entities.Schedule;
import com.pointtils.pointtils.src.infrastructure.repositories.ScheduleRepository;
import com.pointtils.pointtils.src.infrastructure.repositories.InterpreterRepository;
import com.pointtils.pointtils.src.infrastructure.repositories.ScheduleRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.sql.Time;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -23,6 +30,7 @@
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
private final InterpreterRepository interpreterRepository;
private final TimeSlotMapper timeSlotMapper;

public ScheduleResponseDTO registerSchedule(ScheduleRequestDTO dto) {
Optional<Interpreter> foundInterpreter = interpreterRepository.findById(dto.getInterpreterId());
Expand All @@ -31,10 +39,10 @@ public ScheduleResponseDTO registerSchedule(ScheduleRequestDTO dto) {
}

boolean hasConflict = scheduleRepository.existsByInterpreterIdAndDayAndStartTimeLessThanAndEndTimeGreaterThan(
dto.getInterpreterId(),
dto.getDay(),
dto.getEndTime(),
dto.getStartTime()
dto.getInterpreterId(),
dto.getDay(),
dto.getEndTime(),
dto.getStartTime()
);

if (hasConflict) {
Expand Down Expand Up @@ -74,37 +82,51 @@ public ScheduleResponseDTO findById(UUID scheduleId) {

public PaginatedScheduleResponseDTO findAll(ScheduleListRequestDTO query, Pageable pageable) {
Page<Schedule> schedules = scheduleRepository.findAllWithFilters(
pageable,
query.getInterpreterId(),
query.getDay(),
query.getDateFrom(),
query.getDateTo()
pageable,
query.getInterpreterId(),
query.getDay(),
query.getDateFrom(),
query.getDateTo()
);

List<ScheduleResponseDTO> items = schedules.map(s -> ScheduleResponseDTO.builder()
.id(s.getId())
.interpreterId(s.getInterpreter().getId())
.day(s.getDay())
.startTime(s.getStartTime())
.endTime(s.getEndTime())
.build()).toList();
.id(s.getId())
.interpreterId(s.getInterpreter().getId())
.day(s.getDay())
.startTime(s.getStartTime())
.endTime(s.getEndTime())
.build()).toList();

return PaginatedScheduleResponseDTO.builder()
.page(schedules.getNumber())
.size(schedules.getSize())
.total(schedules.getTotalElements())
.items(items)
.build();
.page(schedules.getNumber())
.size(schedules.getSize())
.total(schedules.getTotalElements())
.items(items)
.build();
}

public List<AvailableTimeSlotsResponseDTO> findAvailableSchedules(UUID interpreterId, LocalDate dateFrom, LocalDate dateTo) {
List<Object[]> timeSlots = scheduleRepository.findAvailableTimeSlots(interpreterId, dateFrom, dateTo);

List<TimeSlotDTO> foundTimeSlots = timeSlots.stream()
.map(timeSlot -> new TimeSlotDTO(
(Date) timeSlot[1],
(UUID) timeSlot[0],
((Time) timeSlot[2]).toLocalTime(),
((Time) timeSlot[3]).toLocalTime()
))
Comment on lines +112 to +117
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe casting of Object[] elements without null checks. If the query returns null values, these casts will throw ClassCastException. Add null checks and handle potential casting errors.

Suggested change
.map(timeSlot -> new TimeSlotDTO(
(Date) timeSlot[1],
(UUID) timeSlot[0],
((Time) timeSlot[2]).toLocalTime(),
((Time) timeSlot[3]).toLocalTime()
))
.map(timeSlot -> {
// Defensive: check for nulls and types before casting
if (timeSlot == null || timeSlot.length < 4) {
return null;
}
Object dateObj = timeSlot[1];
Object uuidObj = timeSlot[0];
Object startTimeObj = timeSlot[2];
Object endTimeObj = timeSlot[3];
if (dateObj == null || uuidObj == null || startTimeObj == null || endTimeObj == null) {
return null;
}
try {
Date date = (Date) dateObj;
UUID uuid = (UUID) uuidObj;
Time startTime = (Time) startTimeObj;
Time endTime = (Time) endTimeObj;
return new TimeSlotDTO(
date,
uuid,
startTime.toLocalTime(),
endTime.toLocalTime()
);
} catch (ClassCastException e) {
// Optionally log the error here
return null;
}
})
.filter(java.util.Objects::nonNull)

Copilot uses AI. Check for mistakes.

.toList();
return timeSlotMapper.toAvailableTimeSlotsResponse(foundTimeSlots);
}
Comment on lines +108 to 120
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for the method parameters. Add validation to ensure interpreterId is not null, and dateFrom/dateTo are not null and dateFrom is not after dateTo.

Copilot uses AI. Check for mistakes.


public ScheduleResponseDTO updateSchedule(UUID scheduleId, SchedulePatchRequestDTO dto) {
Schedule schedule = scheduleRepository.findById(scheduleId)
.orElseThrow(() -> new EntityNotFoundException("Horário não encontrado"));
.orElseThrow(() -> new EntityNotFoundException("Horário não encontrado"));

if (dto.getDay() != null) {
schedule.setDay(dto.getDay());
}

if (dto.getStartTime() != null) {
schedule.setStartTime(dto.getStartTime());
}
Expand All @@ -118,11 +140,11 @@ public ScheduleResponseDTO updateSchedule(UUID scheduleId, SchedulePatchRequestD
}

boolean hasConflict = scheduleRepository.existsConflictForUpdate(
schedule.getId(),
schedule.getInterpreter().getId(),
schedule.getDay(),
schedule.getStartTime(),
schedule.getEndTime()
schedule.getId(),
schedule.getInterpreter().getId(),
schedule.getDay(),
schedule.getStartTime(),
schedule.getEndTime()
);

if (hasConflict) {
Expand All @@ -132,12 +154,12 @@ public ScheduleResponseDTO updateSchedule(UUID scheduleId, SchedulePatchRequestD
Schedule saved = scheduleRepository.save(schedule);

return ScheduleResponseDTO.builder()
.id(saved.getId())
.interpreterId(saved.getInterpreter().getId())
.day(saved.getDay())
.startTime(saved.getStartTime())
.endTime(saved.getEndTime())
.build();
.id(saved.getId())
.interpreterId(saved.getInterpreter().getId())
.day(saved.getDay())
.startTime(saved.getStartTime())
.endTime(saved.getEndTime())
.build();
}

public void deleteById(UUID scheduleId) {
Expand Down
Loading