diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/UpdateFlightUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/UpdateFlightUseCase.java new file mode 100644 index 0000000..71a5906 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/flight/UpdateFlightUseCase.java @@ -0,0 +1,112 @@ +package pl.skyroster.skyroster_backend.application.flight; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pl.skyroster.skyroster_backend.domain.exception.*; +import pl.skyroster.skyroster_backend.domain.model.*; +import pl.skyroster.skyroster_backend.domain.port.*; +import pl.skyroster.skyroster_backend.domain.service.RuleValidator; + +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.List; +import java.util.UUID; + +@Service +public class UpdateFlightUseCase { + + private final AircraftRepository aircraftRepository; + private final PilotRepository pilotRepository; + private final OperationalBaseRepository operationalBaseRepository; + private final FlightRepository flightRepository; + private final RuleRepository ruleRepository; + private final RuleValidator ruleValidator = new RuleValidator(); + + public UpdateFlightUseCase(AircraftRepository aircraftRepository, + PilotRepository pilotRepository, + OperationalBaseRepository operationalBaseRepository, + FlightRepository flightRepository, + RuleRepository ruleRepository) { + this.aircraftRepository = aircraftRepository; + this.pilotRepository = pilotRepository; + this.operationalBaseRepository = operationalBaseRepository; + this.flightRepository = flightRepository; + this.ruleRepository = ruleRepository; + } + + @Transactional + public Flight execute(UUID id, UUID aircraftId, UUID pilotId, + OffsetDateTime startDateTime, OffsetDateTime endDateTime, + UUID startAirportId, UUID endAirportIdOrNull, + String descriptionOrNull) { + if (startDateTime == null || endDateTime == null || !startDateTime.isBefore(endDateTime)) { + throw new IllegalArgumentException("startDateTime must be before endDateTime"); + } + + Flight oldFlight = flightRepository.findById(id) + .orElseThrow(FlightNotFoundException::new); + Aircraft aircraft = aircraftRepository.findById(aircraftId) + .orElseThrow(AircraftNotFoundException::new); + Pilot pilot = pilotRepository.findById(pilotId) + .orElseThrow(() -> new PilotNotFoundException(pilotId)); + OperationalBase startAirport = operationalBaseRepository.findById(startAirportId) + .orElseThrow(() -> new OperationalBaseNotFoundException(startAirportId.toString())); + OperationalBase endAirport = null; + if (endAirportIdOrNull != null) { + endAirport = operationalBaseRepository.findById(endAirportIdOrNull) + .orElseThrow(() -> new OperationalBaseNotFoundException(endAirportIdOrNull.toString())); + } + + boolean aircraftConflict = flightRepository + .findOverlappingByAircraft(aircraftId, startDateTime, endDateTime) + .stream() + .anyMatch(f -> !f.getId().equals(id)); // skip the finding if it is the updated flight + + if (aircraftConflict) { + throw new FlightTimeConflictException( + "Aircraft already has a flight in this time window"); + } + + boolean pilotConflict = flightRepository + .findOverlappingByPilot(pilotId, startDateTime, endDateTime) + .stream() + .anyMatch(f -> !f.getId().equals(id)); // skip the finding if it is the updated flight + + if (pilotConflict) { + throw new FlightTimeConflictException( + "Pilot already has a flight in this time window"); + } + + OffsetDateTime monthStart = startDateTime + .with(TemporalAdjusters.firstDayOfMonth()) + .toLocalDate() + .atStartOfDay() + .atOffset(startDateTime.getOffset()); + OffsetDateTime monthEnd = monthStart.plusMonths(1); + List contextFlights = flightRepository + .findByPilotInPeriod(pilotId, monthStart, monthEnd) + .stream() + .filter(f -> !f.getId().equals(id)) // skip the finding if it is the updated flight + .toList(); + List ranges = contextFlights.stream() + .map(f -> new RuleValidator.TimeRange(f.getFlightStart(), f.getFlightEnd())) + .toList(); + List rules = ruleRepository.findAll(); + List violations = ruleValidator.validate(pilotId, startDateTime, endDateTime, rules, ranges); + if (!violations.isEmpty()) { + throw new RuleViolationException(violations); + } + + Flight flight = new Flight( + id, + aircraft, + pilot, + startDateTime, + endDateTime, + startAirport, + endAirport, + descriptionOrNull + ); + return flightRepository.save(flight); + } +} 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 index a42dc85..aeda022 100644 --- 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 @@ -41,7 +41,7 @@ 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"); + Sort resolvedSort = Sort.by(Sort.Direction.ASC, "surname"); if (sort != null && !sort.isBlank()) { String[] parts = sort.split(","); 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 0bc384f..ebb154d 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 @@ -68,4 +68,10 @@ Optional findLastByPilotBefore(@Param("pilotId") UUID pilotId, """) Optional findFirstByPilotAfter(@Param("pilotId") UUID pilotId, @Param("after") OffsetDateTime after); + + @Query(""" + SELECT f FROM Flight f + WHERE f.id = :id + """) + Optional findById(@Param("id") UUID id); } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java index da3cdb9..6877b09 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/FlightController.java @@ -6,6 +6,7 @@ import pl.skyroster.skyroster_backend.application.flight.CreateFlightUseCase; import pl.skyroster.skyroster_backend.application.flight.DeleteFlightUseCase; import pl.skyroster.skyroster_backend.application.flight.GetFlightsUseCase; +import pl.skyroster.skyroster_backend.application.flight.UpdateFlightUseCase; import pl.skyroster.skyroster_backend.domain.model.Flight; import pl.skyroster.skyroster_backend.generated.api.ApiApi; import pl.skyroster.skyroster_backend.generated.model.CreateFlightRequest; @@ -18,14 +19,16 @@ @RestController public class FlightController { - private final GetFlightsUseCase getFlightsUseCase; private final CreateFlightUseCase createFlightUseCase; + private final GetFlightsUseCase getFlightsUseCase; + private final UpdateFlightUseCase updateFlightUseCase; private final DeleteFlightUseCase deleteFlightUseCase; - public FlightController(GetFlightsUseCase getFlightsUseCase, CreateFlightUseCase createFlightUseCase, DeleteFlightUseCase deleteFlightUseCase) { + public FlightController(GetFlightsUseCase getFlightsUseCase, CreateFlightUseCase createFlightUseCase, DeleteFlightUseCase deleteFlightUseCase, UpdateFlightUseCase updateFlightUseCase) { this.getFlightsUseCase = getFlightsUseCase; this.createFlightUseCase = createFlightUseCase; this.deleteFlightUseCase = deleteFlightUseCase; + this.updateFlightUseCase = updateFlightUseCase; } @GetMapping(ApiApi.PATH_DISPLAY_FLIGHTS) @@ -47,6 +50,21 @@ public ResponseEntity createFlight(@RequestBody CreateFlightRequ return ResponseEntity.status(HttpStatus.CREATED).body(FlightResponseMapper.map(flight)); } + @PutMapping(ApiApi.PATH_UPDATE_FLIGHT) + public ResponseEntity updateFlight(@PathVariable("id") UUID id, @RequestBody CreateFlightRequest request) { + Flight flight = updateFlightUseCase.execute( + id, + request.getAircraftId(), + request.getPilotId(), + request.getStartDateTime(), + request.getEndDateTime(), + request.getStartAirportId(), + request.getEndAirportId(), + request.getDescription() + ); + return ResponseEntity.ok(FlightResponseMapper.map(flight)); + } + @DeleteMapping(ApiApi.PATH_DELETE_FLIGHT) public ResponseEntity deleteAircraft(@PathVariable("id") UUID id) { deleteFlightUseCase.execute(id); 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 fa9299e..8927dec 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 @@ -1,5 +1,6 @@ package pl.skyroster.skyroster_backend.infrastructure.config.security; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -12,8 +13,6 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.beans.factory.annotation.Value; - import java.util.List; @Configuration @@ -47,11 +46,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/api/compliance/**").hasRole("compliance_officer") .requestMatchers("/api/planning/**").hasRole("schedule_planner") .requestMatchers(HttpMethod.DELETE,"/api/flights/**").hasRole("schedule_planner") + .requestMatchers(HttpMethod.PUT, "/api/flights/**").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(HttpMethod.GET, "/api/pilots/**").authenticated() .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/FlightResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/FlightResponseMapper.java index 65ead1a..66f91f5 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/FlightResponseMapper.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/FlightResponseMapper.java @@ -8,6 +8,7 @@ public static FlightResponse map(Flight flight) { return new FlightResponse( flight.getId(), AircraftResponseMapper.map(flight.getAircraft()), + PilotMapper.toResponse(flight.getPilot()), flight.getFlightStart(), flight.getFlightEnd(), OperationalBaseInfoMapper.map(flight.getStartAirport()), diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index c613fb6..864852d 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -476,6 +476,60 @@ paths: $ref: '#/components/schemas/RuleViolationErrorResponse' /api/flights/{id}: + put: + operationId: updateFlight + tags: [ Flights ] + summary: Update an existing flight (with rule validation) + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFlightRequest' + responses: + '200': + description: Flight updated + content: + application/json: + schema: + $ref: '#/components/schemas/FlightResponse' + '400': + description: Bad request (invalid payload or start >= end) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Aircraft/Pilot/Airport not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Time conflict for aircraft or pilot + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + description: Rule violation + content: + application/json: + schema: + $ref: '#/components/schemas/RuleViolationErrorResponse' delete: operationId: deleteFlight tags: [ Flights ] @@ -1116,6 +1170,8 @@ components: format: uuid aircraft: $ref: '#/components/schemas/AircraftResponse' + pilot: + $ref: '#/components/schemas/PilotResponse' flightStart: type: string format: date-time diff --git a/skyroster-frontend/src/assets/global.css b/skyroster-frontend/src/assets/global.css index d99eeab..7d05db9 100644 --- a/skyroster-frontend/src/assets/global.css +++ b/skyroster-frontend/src/assets/global.css @@ -28,6 +28,10 @@ border-top-right-radius: 12px; } +div, h1, span, p, label, td, .p-inputtext { + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + .p-dialog .p-dialog-footer { border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; diff --git a/skyroster-frontend/src/components/scheduler/FlightWizardDialog.vue b/skyroster-frontend/src/components/scheduler/FlightWizardDialog.vue index b1dc2b0..95956cb 100644 --- a/skyroster-frontend/src/components/scheduler/FlightWizardDialog.vue +++ b/skyroster-frontend/src/components/scheduler/FlightWizardDialog.vue @@ -1,5 +1,5 @@