diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java new file mode 100644 index 0000000..2234ded --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/AddPilotUseCase.java @@ -0,0 +1,24 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.PilotAlreadyExistsException; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +@Service +@RequiredArgsConstructor +public class AddPilotUseCase { + private final PilotRepository pilotRepository; + + @Transactional + public PilotResponse addPilot(PilotRequest request) { + if (pilotRepository.existsByLicence(request.getLicence())){ + throw new PilotAlreadyExistsException("Pilot already exists"); + } + var pilot = PilotMapper.toEntity(request); + return PilotMapper.toResponse(pilotRepository.save(pilot)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java new file mode 100644 index 0000000..6acb599 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/DeletePilotUseCase.java @@ -0,0 +1,35 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.exception.PilotNotFoundException; +import pl.skyroster.skyroster_backend.domain.port.FlightRepository; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class DeletePilotUseCase { + + private final PilotRepository pilotRepository; + private final FlightRepository flightRepository; + + @Transactional + public void deletePilotById(UUID pilotId) { + if (!pilotRepository.existsById(pilotId)) { + throw new PilotNotFoundException(pilotId); + } + + if(pilotHasScheduledFlights(pilotId)) + throw new IllegalStateException("Pilot has ongoing or incoming flights"); + + pilotRepository.deleteById(pilotId); + } + + private boolean pilotHasScheduledFlights(UUID pilotId){ + return flightRepository.existsByPilotIdAndFlightEndAfter(pilotId, OffsetDateTime.now()); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java new file mode 100644 index 0000000..a42dc85 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/GetPilotUseCase.java @@ -0,0 +1,59 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; + +@Service +@RequiredArgsConstructor +public class GetPilotUseCase { + + private final PilotRepository pilotRepository; + + public PagedPilotResponse getPilots(Integer page, Integer size, String sort) { + Pageable pageable = createPageable(page, size, sort); + + Page pilotsPage = pilotRepository.findAllWithRelations(pageable); + + return new PagedPilotResponse() + .content( + pilotsPage.getContent() + .stream() + .map(PilotMapper::toResponse) + .toList() + ) + .page(pilotsPage.getNumber()) + .size(pilotsPage.getSize()) + .totalElements(pilotsPage.getTotalElements()) + .totalPages(pilotsPage.getTotalPages()) + .first(pilotsPage.isFirst()) + .last(pilotsPage.isLast()); + } + + private Pageable createPageable(Integer page, Integer size, String sort) { + int resolvedPage = page == null ? 0 : page; + int resolvedSize = size == null ? 20 : Math.min(size, 100); + + Sort resolvedSort = Sort.by(Sort.Direction.ASC, "lastName"); + + if (sort != null && !sort.isBlank()) { + String[] parts = sort.split(","); + String property = parts[0]; + + Sort.Direction direction = parts.length > 1 + ? Sort.Direction.fromOptionalString(parts[1]).orElse(Sort.Direction.ASC) + : Sort.Direction.ASC; + + resolvedSort = Sort.by(direction, property); + } + + return PageRequest.of(resolvedPage, resolvedSize, resolvedSort); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java new file mode 100644 index 0000000..5e8b88c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java @@ -0,0 +1,55 @@ +package pl.skyroster.skyroster_backend.application.pilot; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; +import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.infrastructure.mappers.AircraftTypeInfoMapper; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotMapper; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.PilotQualificationMapper; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PatchPilotUseCase { + private final PilotRepository pilotRepository; + private final OperationalBaseRepository operationalBaseRepository; + + @Transactional + public PilotResponse patchPilot(UUID id, PilotPatchRequest request) { + Pilot pilot = pilotRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Pilot not found: " + id)); + + String name = request.getName(); + if (name != null) { + pilot.setName(name); + } + String surname = request.getSurname(); + if (surname != null) { + pilot.setSurname(surname); + } + String icaoCode = request.getOperationalBaseIcaoCode(); + if (icaoCode != null) { + pilot.setOperationalBase( + operationalBaseRepository.findByIcaoCode(icaoCode) + .orElseThrow(() -> new IllegalArgumentException("Operational base not found: " + icaoCode)) + ); + } + Set qualifications = request.getQualifications().stream().map(PilotQualificationMapper::fromPilotQualificationInfo).collect(Collectors.toSet()); + pilot.setQualifications(qualifications); + + Set aircraftTypes = request.getAircraftTypes().stream().map(AircraftTypeInfoMapper::toAircraftTypeInfo).collect(Collectors.toSet()); + pilot.setAircraftTypes(aircraftTypes); + + return PilotMapper.toResponse(pilotRepository.save(pilot)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java new file mode 100644 index 0000000..1703721 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotAlreadyExistsException.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +public class PilotAlreadyExistsException extends RuntimeException { + public PilotAlreadyExistsException(String message) { + super(message); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java new file mode 100644 index 0000000..fa5720e --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/PilotNotFoundException.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +import java.util.UUID; + +public class PilotNotFoundException extends RuntimeException { + public PilotNotFoundException(UUID pilotId) { + super("Pilot with id " + pilotId + " not found"); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java index efe885a..c186fe0 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Pilot.java @@ -1,11 +1,19 @@ package pl.skyroster.skyroster_backend.domain.model; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.util.Set; import java.util.UUID; -import jakarta.persistence.*; - @Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor public class Pilot { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java index 750a24a..7cdb3d9 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Qualification.java @@ -3,10 +3,16 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.UUID; @Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor public class Qualification { @Id diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java index 555e749..e335ab3 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/FlightRepository.java @@ -3,10 +3,15 @@ import org.springframework.data.jpa.repository.JpaRepository; import pl.skyroster.skyroster_backend.domain.model.Flight; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; public interface FlightRepository extends JpaRepository { List findAll(); boolean existsByAircraftId(UUID aircraftId); + boolean existsByPilotIdAndFlightEndAfter( + UUID pilotId, + OffsetDateTime now + ); } \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java new file mode 100644 index 0000000..2213bb6 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/PilotRepository.java @@ -0,0 +1,25 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import pl.skyroster.skyroster_backend.domain.model.Pilot; + +import java.util.Optional; +import java.util.UUID; + +public interface PilotRepository { + @EntityGraph(attributePaths = { + "qualifications", + "aircraftTypes", + "operationalBase" + }) + @Query("SELECT p FROM Pilot p") + Page findAllWithRelations(Pageable pageable); + Optional findById(UUID id); + boolean existsById(UUID id); + boolean existsByLicence(String licence); + void deleteById(UUID id); + Pilot save(Pilot pilot); +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java new file mode 100644 index 0000000..9384efd --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/PilotController.java @@ -0,0 +1,48 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import pl.skyroster.skyroster_backend.application.pilot.AddPilotUseCase; +import pl.skyroster.skyroster_backend.application.pilot.DeletePilotUseCase; +import pl.skyroster.skyroster_backend.application.pilot.GetPilotUseCase; +import pl.skyroster.skyroster_backend.application.pilot.PatchPilotUseCase; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.PagedPilotResponse; +import pl.skyroster.skyroster_backend.generated.model.PilotPatchRequest; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; + +import java.util.UUID; + +@RequiredArgsConstructor +@RestController +public class PilotController { + + private final GetPilotUseCase getPilotUseCase; + private final DeletePilotUseCase deletePilotUseCase; + private final PatchPilotUseCase patchPilotUseCase; + private final AddPilotUseCase addPilotUseCase; + + @GetMapping(ApiApi.PATH_GET_PILOTS) + public ResponseEntity getPilots(@RequestParam Integer page, @RequestParam Integer size, @RequestParam @Nullable String sort) { + return ResponseEntity.ok(getPilotUseCase.getPilots(page, size, sort)); + } + + @DeleteMapping(ApiApi.PATH_DELETE_PILOT_BY_ID) + public ResponseEntity deletePilotById(@PathVariable UUID pilotId) { + deletePilotUseCase.deletePilotById(pilotId); + return ResponseEntity.noContent().build(); + } + + @PatchMapping(ApiApi.PATH_PATCH_PILOT) + public ResponseEntity patchPilot(@PathVariable UUID pilotId, @RequestBody PilotPatchRequest request) { + return ResponseEntity.ok(patchPilotUseCase.patchPilot(pilotId, request)); + } + + @PostMapping(ApiApi.PATH_CREATE_PILOT) + public ResponseEntity addPilot(@RequestBody PilotRequest pilot){ + return ResponseEntity.ok(addPilotUseCase.addPilot(pilot)); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java new file mode 100644 index 0000000..e659210 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaPilotRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.port.PilotRepository; + +import java.util.UUID; + +public interface JpaPilotRepository extends JpaRepository, PilotRepository { +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java index 6123af3..8347c88 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -6,11 +6,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import pl.skyroster.skyroster_backend.domain.exception.AircraftAlreadyExistsException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftHasAssignedFlightsException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; -import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.*; import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; import java.time.OffsetDateTime; @@ -50,6 +46,16 @@ public ResponseEntity handleAircraftTypeNotFound(AircraftTypeNotF return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); } + @ExceptionHandler(PilotNotFoundException.class) + public ResponseEntity handlePilotNotFound(PilotNotFoundException ex,HttpServletRequest request) { + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), request); + } + + @ExceptionHandler(PilotAlreadyExistsException.class) + public ResponseEntity handlePilotAlreadyExists(PilotAlreadyExistsException ex,HttpServletRequest request) { + return buildResponse(HttpStatus.CONFLICT, ex.getMessage(), request); + } + @ExceptionHandler(OperationalBaseNotFoundException.class) public ResponseEntity handleOperationalBaseNotFound(OperationalBaseNotFoundException ex, HttpServletRequest request) { @@ -62,6 +68,11 @@ public ResponseEntity handleIllegalArgument(IllegalArgumentExcept return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); } + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalState(Exception ex, HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); + } + private ResponseEntity buildResponse(HttpStatus status, String message, HttpServletRequest request) { ErrorResponse error = new ErrorResponse() diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index 2df005b..89a4e0a 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -48,6 +48,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/api/planning/**").hasRole("schedule_planner") .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() + .requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.POST, "/api/pilots/**").hasRole("operations_administrator") .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) @@ -64,7 +67,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { public CorsConfigurationSource corsConfigurationSource() { var config = new CorsConfiguration(); config.setAllowedOrigins(allowedOrigins); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); var source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java index d5ae2e8..601e18b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftResponseMapper.java @@ -8,7 +8,7 @@ public static AircraftResponse map(Aircraft aircraft) { return new AircraftResponse( aircraft.getId(), aircraft.getRegistrationNumber(), - AircraftTypeInfoMapper.map(aircraft.getAircraftType()), + AircraftTypeInfoMapper.toAircraftTypeInfo(aircraft.getAircraftType()), OperationalBaseInfoMapper.map(aircraft.getOperationalBase()) ); } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java index 05ba98c..4f5eef1 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/AircraftTypeInfoMapper.java @@ -4,11 +4,18 @@ import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; public class AircraftTypeInfoMapper { - public static AircraftTypeInfo map(AircraftType aircraftType) { + public static AircraftTypeInfo toAircraftTypeInfo(AircraftType aircraftType) { return new AircraftTypeInfo( aircraftType.getId(), aircraftType.getIcaoCode(), aircraftType.getName() ); } + public static AircraftType toAircraftTypeInfo(AircraftTypeInfo aircraftType) { + return new AircraftType( + aircraftType.getId(), + aircraftType.getIcaoCode(), + aircraftType.getName() + ); + } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java index 7370ed7..654f49b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/OperationalBaseInfoMapper.java @@ -11,4 +11,12 @@ public static OperationalBaseInfo map(OperationalBase operationalBase) { operationalBase.getName() ); } + + public static OperationalBase fromInfo(OperationalBaseInfo operationalBase) { + return new OperationalBase( + operationalBase.getId(), + operationalBase.getIcaoCode(), + operationalBase.getName() + ); + } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java new file mode 100644 index 0000000..4b08478 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotMapper.java @@ -0,0 +1,46 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import org.springframework.stereotype.Component; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.Pilot; +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.generated.model.PilotResponse; +import pl.skyroster.skyroster_backend.generated.model.PilotRequest; + +import java.util.Comparator; +import java.util.UUID; +import java.util.stream.Collectors; + +@Component +public class PilotMapper { + public static PilotResponse toResponse(Pilot pilot) { + return new PilotResponse() + .id(pilot.getId()) + .firstName(pilot.getName()) + .surname(pilot.getSurname()) + .licence(pilot.getLicence()) + .homeBase(OperationalBaseInfoMapper.map(pilot.getOperationalBase())) + .qualifications( + pilot.getQualifications() + .stream() + .sorted(Comparator.comparing(Qualification::getLabel)) + .map(PilotQualificationMapper::toPilotQualificationInfo) + .toList()) + .aircraftTypes( + pilot.getAircraftTypes() + .stream() + .sorted(Comparator.comparing(AircraftType::getName)) + .map(AircraftTypeInfoMapper::toAircraftTypeInfo) + .toList()); + } + public static Pilot toEntity(PilotRequest request){ + return new Pilot( + UUID.randomUUID(), + request.getFirstName(), + request.getLastName(), + request.getLicence(), + OperationalBaseInfoMapper.fromInfo(request.getHomeBase()), + request.getQualifications().stream().map(PilotQualificationMapper::fromPilotQualificationInfo).collect(Collectors.toSet()), + request.getAircraftTypes().stream().map(AircraftTypeInfoMapper::toAircraftTypeInfo).collect(Collectors.toSet())); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java new file mode 100644 index 0000000..9baaa66 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/PilotQualificationMapper.java @@ -0,0 +1,17 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Qualification; +import pl.skyroster.skyroster_backend.generated.model.PilotQualificationInfo; + +public class PilotQualificationMapper { + public static PilotQualificationInfo toPilotQualificationInfo(Qualification qualification) { + return new PilotQualificationInfo() + .id(qualification.getId()) + .values(qualification.getValue()) + .label(qualification.getLabel()); + } + + public static Qualification fromPilotQualificationInfo(PilotQualificationInfo qualification) { + return new Qualification(qualification.getId(), qualification.getValues(), qualification.getLabel()); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index facf0e1..cb1c775 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -5,15 +5,15 @@ info: version: 0.1.0 security: - - bearerAuth: [] + - bearerAuth: [ ] paths: /api/health: get: operationId: getHealth - tags: [Health] + tags: [ Health ] summary: Health check - security: [] + security: [ ] responses: '200': description: Service is healthy @@ -25,7 +25,7 @@ paths: /api/me: get: operationId: getCurrentUser - tags: [User] + tags: [ User ] summary: Get current authenticated user info responses: '200': @@ -44,7 +44,7 @@ paths: /api/compliance/reports: get: operationId: getComplianceReports - tags: [Compliance] + tags: [ Compliance ] summary: List compliance reports responses: '200': @@ -71,7 +71,7 @@ paths: /api/planning/schedules: get: operationId: getPlanningSchedules - tags: [Planning] + tags: [ Planning ] summary: List schedules responses: '200': @@ -98,7 +98,7 @@ paths: /api/aircraft: post: operationId: addAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Add a new aircraft to the fleet requestBody: required: true @@ -139,7 +139,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' get: operationId: getAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: List all aircraft in the fleet responses: '200': @@ -156,6 +156,97 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /api/pilots: + get: + tags: + - Pilots + operationId: getPilots + summary: Get paginated list of pilots + parameters: + - name: page + in: query + required: false + schema: + type: integer + minimum: 0 + default: 0 + - name: size + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + required: false + schema: + type: string + example: surname,asc + responses: + '200': + description: Paginated list of pilots + content: + application/json: + schema: + $ref: '#/components/schemas/PagedPilotResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + post: + tags: + - Pilots + summary: Add a new pilot + description: Creates a new pilot in the system. + operationId: createPilot + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PilotRequest' + responses: + '201': + description: Pilot created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PilotResponse' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Pilot already exists, for example duplicate license number or email + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/flights: get: @@ -187,7 +278,7 @@ paths: /api/aircraft/{id}: put: operationId: updateAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Update an aircraft in the fleet parameters: - in: path @@ -241,7 +332,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' delete: operationId: deleteAircraft - tags: [Aircraft] + tags: [ Aircraft ] summary: Delete an aircraft from the fleet parameters: - in: path @@ -278,6 +369,80 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/pilots/{pilotId}: + delete: + tags: + - AdminPilots + operationId: deletePilotById + summary: Delete pilot by id + parameters: + - name: pilotId + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Pilot deleted successfully + '404': + description: Pilot not found + + patch: + tags: + - Pilots + summary: Partially update a pilot + description: Updates selected pilot fields. Fields omitted from the request body remain unchanged. + operationId: patchPilot + parameters: + - name: pilotId + in: path + required: true + description: Pilot ID + schema: + type: string + format: uuid + example: 0b7a9c1d-86c2-49dc-8ef8-d7f6c7e6e6f1 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PilotPatchRequest' + examples: + updateBasicData: + summary: Update pilot basic data + value: + name: Jan + surname: Kowalski + licence: ATPL-123456 + updateOperationalBase: + summary: Update pilot operational base + value: + operationalBaseId: 2a9dfd20-90fc-4b7e-a996-358d4b5f79c7 + updateQualificationsAndAircraftTypes: + summary: Update qualifications and aircraft types + value: + qualifications: + - PPL + - CPL + - ATPL + aircraftTypes: + - A320 + - B737 + responses: + '200': + description: Pilot updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PilotResponse' + '400': + description: Invalid request body + '404': + description: Pilot not found + + components: securitySchemes: bearerAuth: @@ -336,7 +501,7 @@ components: description: List of realm roles assigned to the user items: type: string - example: ["operations_administrator"] + example: [ "operations_administrator" ] HealthStatus: type: object @@ -388,6 +553,153 @@ components: format: date-time example: "2026-03-20T11:00:00Z" + PagedPilotResponse: + type: object + required: + - content + - page + - size + - totalElements + - totalPages + - first + - last + properties: + content: + type: array + items: + $ref: '#/components/schemas/PilotResponse' + page: + type: integer + example: 0 + size: + type: integer + example: 20 + totalElements: + type: integer + format: int64 + example: 42 + totalPages: + type: integer + example: 3 + first: + type: boolean + example: true + last: + type: boolean + example: false + + PilotResponse: + type: object + required: + - id + - firstName + - surname + - homeBase + - qualifications + properties: + id: + type: string + format: uuid + licence: + type: string + example: PIL-001 + firstName: + type: string + example: Jan + surname: + type: string + example: Kowalski + homeBase: + $ref: '#/components/schemas/OperationalBaseInfo' + qualifications: + type: array + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + items: + $ref: '#/components/schemas/AircraftTypeInfo' + + PilotRequest: + type: object + required: + - firstName + - lastName + - homeBase + - qualifications + properties: + licence: + type: string + example: PIL-001 + firstName: + type: string + example: Jan + lastName: + type: string + example: Kowalski + homeBase: + $ref: '#/components/schemas/OperationalBaseInfo' + qualifications: + type: array + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + items: + $ref: '#/components/schemas/AircraftTypeInfo' + + PilotPatchRequest: + type: object + description: Request body for partially updating a pilot. Null or omitted fields are not changed. + additionalProperties: false + properties: + name: + type: string + description: Pilot name + example: Jan + surname: + type: string + description: Pilot surname + example: Kowalski + licence: + type: string + description: Pilot licence number + example: ATPL-123456 + operationalBaseIcaoCode: + type: string + description: ICAO operational base code + example: "EPWA" + qualifications: + type: array + description: Pilot qualifications. If provided, replaces the current qualification set. + uniqueItems: true + items: + $ref: '#/components/schemas/PilotQualificationInfo' + aircraftTypes: + type: array + description: Aircraft types assigned to the pilot. If provided, replaces the current aircraft type set. + uniqueItems: true + items: + $ref: '#/components/schemas/AircraftTypeInfo' + + + PilotQualificationInfo: + type: object + required: + - id + - aircraftType + - licenseValidUntil + properties: + id: + type: string + format: uuid + values: + type: string + label: + type: string + + + AddAircraftRequest: type: object required: