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
Original file line number Diff line number Diff line change
@@ -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<Flight> contextFlights = flightRepository
.findByPilotInPeriod(pilotId, monthStart, monthEnd)
.stream()
.filter(f -> !f.getId().equals(id)) // skip the finding if it is the updated flight
.toList();
List<RuleValidator.TimeRange> ranges = contextFlights.stream()
.map(f -> new RuleValidator.TimeRange(f.getFlightStart(), f.getFlightEnd()))
.toList();
List<Rule> rules = ruleRepository.findAll();
List<RuleViolation> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(",");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,10 @@ Optional<Flight> findLastByPilotBefore(@Param("pilotId") UUID pilotId,
""")
Optional<Flight> findFirstByPilotAfter(@Param("pilotId") UUID pilotId,
@Param("after") OffsetDateTime after);

@Query("""
SELECT f FROM Flight f
WHERE f.id = :id
""")
Optional<Flight> findById(@Param("id") UUID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -47,6 +50,21 @@ public ResponseEntity<FlightResponse> createFlight(@RequestBody CreateFlightRequ
return ResponseEntity.status(HttpStatus.CREATED).body(FlightResponseMapper.map(flight));
}

@PutMapping(ApiApi.PATH_UPDATE_FLIGHT)
public ResponseEntity<FlightResponse> 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<Void> deleteAircraft(@PathVariable("id") UUID id) {
deleteFlightUseCase.execute(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
56 changes: 56 additions & 0 deletions skyroster-backend/src/main/resources/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down Expand Up @@ -1116,6 +1170,8 @@ components:
format: uuid
aircraft:
$ref: '#/components/schemas/AircraftResponse'
pilot:
$ref: '#/components/schemas/PilotResponse'
flightStart:
type: string
format: date-time
Expand Down
4 changes: 4 additions & 0 deletions skyroster-frontend/src/assets/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading