diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/ScheduleController.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/ScheduleController.java index 0b4bebe..b6cd4a0 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/ScheduleController.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/ScheduleController.java @@ -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 @@ -75,4 +80,14 @@ public ResponseEntity> deleteSchedule(@PathVariable UUID sched ApiResponse 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>> listAvailableSchedules(@RequestParam UUID interpreterId, + @RequestParam LocalDate dateFrom, + @RequestParam LocalDate dateTo) { + List availableTimeSlots = service.findAvailableSchedules(interpreterId, dateFrom, dateTo); + ApiResponse> response = new ApiResponse<>(true, "Horários disponíveis obtidos com sucesso", availableTimeSlots); + return ResponseEntity.ok(response); + } } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TimeSlotDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TimeSlotDTO.java new file mode 100644 index 0000000..335f605 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TimeSlotDTO.java @@ -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; + private UUID interpreterId; + private LocalTime startTime; + private LocalTime endTime; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/responses/AvailableTimeSlotsResponseDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/responses/AvailableTimeSlotsResponseDTO.java new file mode 100644 index 0000000..e4e6eb5 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/responses/AvailableTimeSlotsResponseDTO.java @@ -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; + @JsonProperty("interpreter_id") + private UUID interpreterId; + @JsonProperty("time_slots") + private List timeSlots; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/responses/TimeSlotResponseDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/responses/TimeSlotResponseDTO.java new file mode 100644 index 0000000..9319099 --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/responses/TimeSlotResponseDTO.java @@ -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; +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/mapper/TimeSlotMapper.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/mapper/TimeSlotMapper.java new file mode 100644 index 0000000..2e86b2f --- /dev/null +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/mapper/TimeSlotMapper.java @@ -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 toAvailableTimeSlotsResponse(List timeSlots) { + Map availableTimeSlots = new HashMap<>(); + timeSlots.forEach(timeSlot -> { + String key = timeSlot.getDate().toString() + timeSlot.getInterpreterId().toString(); + 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(); + } +} diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/ScheduleService.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/ScheduleService.java index 0de61be..ec961ba 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/ScheduleService.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/ScheduleService.java @@ -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; @@ -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 foundInterpreter = interpreterRepository.findById(dto.getInterpreterId()); @@ -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) { @@ -74,37 +82,51 @@ public ScheduleResponseDTO findById(UUID scheduleId) { public PaginatedScheduleResponseDTO findAll(ScheduleListRequestDTO query, Pageable pageable) { Page schedules = scheduleRepository.findAllWithFilters( - pageable, - query.getInterpreterId(), - query.getDay(), - query.getDateFrom(), - query.getDateTo() + pageable, + query.getInterpreterId(), + query.getDay(), + query.getDateFrom(), + query.getDateTo() ); - + List 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 findAvailableSchedules(UUID interpreterId, LocalDate dateFrom, LocalDate dateTo) { + List timeSlots = scheduleRepository.findAvailableTimeSlots(interpreterId, dateFrom, dateTo); + + List foundTimeSlots = timeSlots.stream() + .map(timeSlot -> new TimeSlotDTO( + (Date) timeSlot[1], + (UUID) timeSlot[0], + ((Time) timeSlot[2]).toLocalTime(), + ((Time) timeSlot[3]).toLocalTime() + )) + .toList(); + return timeSlotMapper.toAvailableTimeSlotsResponse(foundTimeSlots); } 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()); } @@ -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) { @@ -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) { diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/ScheduleRepository.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/ScheduleRepository.java index d93bc01..b0f2a1c 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/ScheduleRepository.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/ScheduleRepository.java @@ -1,18 +1,19 @@ package com.pointtils.pointtils.src.infrastructure.repositories; -import java.util.UUID; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import com.pointtils.pointtils.src.core.domain.entities.Schedule; -import java.time.LocalTime; import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.List; - -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.UUID; public interface ScheduleRepository extends JpaRepository, JpaSpecificationExecutor { List findByInterpreterIdAndDay(UUID interpreterId, DayOfWeek day); @@ -26,4 +27,93 @@ default Page findAllWithFilters(Pageable pageable, UUID interpreterId, Specification spec = ScheduleSpecifications.withFilters(interpreterId, day, dateFrom, dateTo); return findAll(spec, pageable); } + + @Query(value = """ + WITH date_range AS ( + SELECT + i AS selected_date, + EXTRACT(DOW FROM i)::int AS dow + FROM generate_series(CAST(:dateFrom AS DATE), CAST(:dateTo AS DATE), '1 day'::interval) AS i + ), + schedule_days AS ( + SELECT + s.id AS schedule_id, + s.interpreter_id, + s.day, + s.start_time, + s.end_time, + n.selected_date + FROM schedule s + JOIN date_range n ON + ( + CASE s.day + WHEN 'SUN' THEN 0 + WHEN 'MON' THEN 1 + WHEN 'TUE' THEN 2 + WHEN 'WED' THEN 3 + WHEN 'THU' THEN 4 + WHEN 'FRI' THEN 5 + WHEN 'SAT' THEN 6 + END + ) = n.dow + WHERE interpreter_id = :interpreterId + ), + time_slots AS ( + -- Slots iniciando em hora cheia + SELECT + sd.schedule_id, + sd.interpreter_id, + sd.day, + sd.selected_date, + generate_series( + '2000-01-01'::timestamp + sd.start_time, + '2000-01-01'::timestamp + sd.end_time - interval '1 hour', + interval '1 hour' + ) AS slot_start + FROM schedule_days sd + + UNION ALL + + -- Slots iniciando em meia hora + SELECT + sd.schedule_id, + sd.interpreter_id, + sd.day, + sd.selected_date, + generate_series( + '2000-01-01'::timestamp + sd.start_time + interval '30 minutes', + '2000-01-01'::timestamp + sd.end_time - interval '30 minutes' - interval '1 hour', + interval '1 hour' + ) AS slot_start + FROM schedule_days sd + ), + slots_with_end AS ( + SELECT + *, + slot_start + interval '1 hour' AS slot_end + FROM time_slots + ), + filtered_slots AS ( + SELECT sw.* + FROM slots_with_end sw + WHERE NOT EXISTS ( + SELECT 1 + FROM appointment a + WHERE a.interpreter_id = sw.interpreter_id + AND a.date = sw.selected_date + AND ('2000-01-01'::timestamp + a.start_time) < sw.slot_end + AND ('2000-01-01'::timestamp + a.end_time) > sw.slot_start + ) + ) + SELECT + interpreter_id AS interpreterId, + selected_date::date, + slot_start::time AS startTime, + slot_end::time AS endTime + FROM filtered_slots + ORDER BY interpreter_id, selected_date, slot_start + """, nativeQuery = true) + List findAvailableTimeSlots(@Param("interpreterId") UUID interpreterId, + @Param("dateFrom") LocalDate dateFrom, + @Param("dateTo") LocalDate dateTo); }