diff --git a/src/FlightRecorder.Api/Controllers/SightingsController.cs b/src/FlightRecorder.Api/Controllers/SightingsController.cs index 72e2fb5..7055566 100644 --- a/src/FlightRecorder.Api/Controllers/SightingsController.cs +++ b/src/FlightRecorder.Api/Controllers/SightingsController.cs @@ -56,27 +56,6 @@ public async Task>> GetSightingsByFlightAsync(string return sightings; } - [HttpGet] - [Route("flight/{date}/{number}/{pageNumber}/{pageSize}")] - public async Task>> GetSightingsByFlightAndDateAsync(string date, string number, int pageNumber, int pageSize) - { - DateTime decodedDate = DateTime.ParseExact(HttpUtility.UrlDecode(date), DateTimeFormat, null); - string decodedNumber = HttpUtility.UrlDecode(number).ToUpper(); - List sightings = await _factory.Sightings - .ListAsync(s => (s.Flight.Number == decodedNumber) && - (s.Date == decodedDate), - pageNumber, - pageSize) - .ToListAsync(); - - if (!sightings.Any()) - { - return NoContent(); - } - - return sightings; - } - [HttpGet] [Route("airline/{airlineId}/{pageNumber}/{pageSize}")] public async Task>> GetSightingsByAirlineAsync(int airlineId, int pageNumber, int pageSize) @@ -150,6 +129,37 @@ public async Task> GetSightingAsync(int id) return sighting; } + + [HttpGet] + [Route("recent/flight/{number}")] + public async Task> GetMostRecentFlightSightingAsync(string number) + { + string decodedNumber = HttpUtility.UrlDecode(number).ToUpper(); + Sighting sighting = await _factory.Sightings.GetMostRecent(x => x.Flight.Number == decodedNumber); + + if (sighting == null) + { + return NoContent(); + } + + return sighting; + } + + [HttpGet] + [Route("recent/aircraft/{registration}")] + public async Task> GetMostRecentAircraftSightingAsync(string registration) + { + string decodedRegistration = HttpUtility.UrlDecode(registration); + Sighting sighting = await _factory.Sightings.GetMostRecent(x => x.Aircraft.Registration == decodedRegistration); + + if (sighting == null) + { + return NoContent(); + } + + return sighting; + } + [HttpPut] [Route("")] diff --git a/src/FlightRecorder.Api/FlightRecorder.Api.csproj b/src/FlightRecorder.Api/FlightRecorder.Api.csproj index 423b112..169bf82 100644 --- a/src/FlightRecorder.Api/FlightRecorder.Api.csproj +++ b/src/FlightRecorder.Api/FlightRecorder.Api.csproj @@ -2,9 +2,9 @@ net9.0 - 1.12.0.0 - 1.12.0.0 - 1.12.0 + 1.13.0.0 + 1.13.0.0 + 1.13.0 enable false diff --git a/src/FlightRecorder.BusinessLogic/Database/SightingManager.cs b/src/FlightRecorder.BusinessLogic/Database/SightingManager.cs index 47760be..91e8814 100644 --- a/src/FlightRecorder.BusinessLogic/Database/SightingManager.cs +++ b/src/FlightRecorder.BusinessLogic/Database/SightingManager.cs @@ -83,6 +83,28 @@ public IAsyncEnumerable ListAsync(Expression> pre public async Task CountAsync() => await _factory.Context.Sightings.CountAsync(); + /// + /// Return the most recent sighting matching the predicate + /// + /// + /// + public async Task GetMostRecent(Expression> predicate) + { + List sightings = await _factory.Context.Sightings + .Include(s => s.Location) + .Include(s => s.Flight) + .ThenInclude(f => f.Airline) + .Include(s => s.Aircraft) + .ThenInclude(a => a.Model) + .ThenInclude(m => m.Manufacturer) + .Where(predicate) + .OrderByDescending(x => x.Date) + .Take(1) + .AsAsyncEnumerable() + .ToListAsync(); + return sightings.FirstOrDefault(); + } + /// /// Add a new sighting /// diff --git a/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj b/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj index 9c4359f..01ad9e5 100644 --- a/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj +++ b/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj @@ -3,7 +3,7 @@ net9.0 FlightRecorder.BusinessLogic - 1.8.0.0 + 1.9.0.0 Dave Walker Copyright (c) Dave Walker 2020, 2021, 2022, 2023, 2024 Dave Walker @@ -16,7 +16,7 @@ https://github.com/davewalker5/FlightRecorderDb MIT false - 1.8.0.0 + 1.9.0.0 diff --git a/src/FlightRecorder.Data/FlightRecorder.Data.csproj b/src/FlightRecorder.Data/FlightRecorder.Data.csproj index 01d15d8..d24a896 100644 --- a/src/FlightRecorder.Data/FlightRecorder.Data.csproj +++ b/src/FlightRecorder.Data/FlightRecorder.Data.csproj @@ -3,7 +3,7 @@ net9.0 FlightRecorder.Data - 1.8.0.0 + 1.9.0.0 Dave Walker Copyright (c) Dave Walker 2020, 2021, 2022, 2023, 2024 Dave Walker @@ -16,7 +16,7 @@ https://github.com/davewalker5/FlightRecorderDb MIT false - 1.8.0.0 + 1.9.0.0 diff --git a/src/FlightRecorder.DataExchange/FlightRecorder.DataExchange.csproj b/src/FlightRecorder.DataExchange/FlightRecorder.DataExchange.csproj index aaf33b9..cd60c30 100644 --- a/src/FlightRecorder.DataExchange/FlightRecorder.DataExchange.csproj +++ b/src/FlightRecorder.DataExchange/FlightRecorder.DataExchange.csproj @@ -3,7 +3,7 @@ net9.0 FlightRecorder.DataExchange - 1.8.0.0 + 1.9.0.0 Dave Walker Copyright (c) Dave Walker 2020, 2021, 2022, 2023, 2024 Dave Walker @@ -16,7 +16,7 @@ https://github.com/davewalker5/FlightRecorderDb MIT false - 1.8.0.0 + 1.9.0.0 diff --git a/src/FlightRecorder.Entities/FlightRecorder.Entities.csproj b/src/FlightRecorder.Entities/FlightRecorder.Entities.csproj index 5e1501e..5b49a55 100644 --- a/src/FlightRecorder.Entities/FlightRecorder.Entities.csproj +++ b/src/FlightRecorder.Entities/FlightRecorder.Entities.csproj @@ -3,7 +3,7 @@ net9.0 FlightRecorder.Entities - 1.8.0.0 + 1.9.0.0 Dave Walker Copyright (c) Dave Walker 2020, 2021, 2022, 2023, 2024 Dave Walker @@ -16,7 +16,7 @@ https://github.com/davewalker5/FlightRecorderDb MIT false - 1.8.0.0 + 1.9.0.0 diff --git a/src/FlightRecorder.Entities/Interfaces/ISightingManager.cs b/src/FlightRecorder.Entities/Interfaces/ISightingManager.cs index a6e36ee..31c8f69 100644 --- a/src/FlightRecorder.Entities/Interfaces/ISightingManager.cs +++ b/src/FlightRecorder.Entities/Interfaces/ISightingManager.cs @@ -12,6 +12,7 @@ public interface ISightingManager Task AddAsync(long altitude, DateTime date, long locationId, long flightId, long aircraftId); Task AddAsync(FlattenedSighting flattened); Task GetAsync(Expression> predicate); + Task GetMostRecent(Expression> predicate); IAsyncEnumerable ListAsync(Expression> predicate, int pageNumber, int pageSize); Task CountAsync(); Task> ListByAircraftAsync(string registration, int pageNumber, int pageSize); diff --git a/src/FlightRecorder.Manager/FlightRecorder.Manager.csproj b/src/FlightRecorder.Manager/FlightRecorder.Manager.csproj index 1ce0234..1d18226 100644 --- a/src/FlightRecorder.Manager/FlightRecorder.Manager.csproj +++ b/src/FlightRecorder.Manager/FlightRecorder.Manager.csproj @@ -3,9 +3,9 @@ Exe net9.0 - 1.8.0.0 - 1.8.0.0 - 1.8.0.0 + 1.9.0.0 + 1.9.0.0 + 1.9.0.0 Release;Debug diff --git a/src/FlightRecorder.Mvc/Api/SightingsSearchClient.cs b/src/FlightRecorder.Mvc/Api/SightingsSearchClient.cs index 852c2c9..02b794e 100644 --- a/src/FlightRecorder.Mvc/Api/SightingsSearchClient.cs +++ b/src/FlightRecorder.Mvc/Api/SightingsSearchClient.cs @@ -55,21 +55,31 @@ public async Task> GetSightingsByFlight(string number, int page, } /// - /// Retrieve sightings for the specified flight on the specified date + /// Get the most recent sighting of a flight /// - /// /// - /// - /// /// - public async Task> GetSightingsByFlightAndDate(DateTime date, string flightNumber, int page, int pageSize) + public async Task GetMostRecentFlightSighting(string flightNumber) { - string dateRouteSegment = date.ToString(Settings.Value.DateTimeFormat); string baseRoute = Settings.Value.ApiRoutes.First(r => r.Name == RouteKey).Route; - string route = $"{baseRoute}/flight/{dateRouteSegment}/{flightNumber}/{page}/{pageSize}"; + string route = $"{baseRoute}/recent/flight/{flightNumber}"; string json = await SendDirectAsync(route, null, HttpMethod.Get); - List sightings = JsonConvert.DeserializeObject>(json, JsonSettings); - return sightings; + Sighting sighting = JsonConvert.DeserializeObject(json, JsonSettings); + return sighting; + } + + /// + /// Get the most recent sighting of an aircraft + /// + /// + /// + public async Task GetMostRecentAircraftSighting(string registration) + { + string baseRoute = Settings.Value.ApiRoutes.First(r => r.Name == RouteKey).Route; + string route = $"{baseRoute}/recent/aircraft/{registration}"; + string json = await SendDirectAsync(route, null, HttpMethod.Get); + Sighting sighting = JsonConvert.DeserializeObject(json, JsonSettings); + return sighting; } /// diff --git a/src/FlightRecorder.Mvc/FlightRecorder.Mvc.csproj b/src/FlightRecorder.Mvc/FlightRecorder.Mvc.csproj index 0f30c30..27e012e 100644 --- a/src/FlightRecorder.Mvc/FlightRecorder.Mvc.csproj +++ b/src/FlightRecorder.Mvc/FlightRecorder.Mvc.csproj @@ -2,9 +2,9 @@ net9.0 - 1.12.0.0 - 1.12.0.0 - 1.12.0 + 1.13.0.0 + 1.13.0.0 + 1.13.0 enable false diff --git a/src/FlightRecorder.Mvc/Models/AircraftDetailsViewModel.cs b/src/FlightRecorder.Mvc/Models/AircraftDetailsViewModel.cs index b21daba..2f870b2 100644 --- a/src/FlightRecorder.Mvc/Models/AircraftDetailsViewModel.cs +++ b/src/FlightRecorder.Mvc/Models/AircraftDetailsViewModel.cs @@ -34,6 +34,7 @@ public class AircraftDetailsViewModel [DisplayName("New Model")] public string NewModel { get; set; } + public Sighting MostRecentSighting { get; set; } public string Action { get; set; } diff --git a/src/FlightRecorder.Mvc/Models/FlightDetailsViewModel.cs b/src/FlightRecorder.Mvc/Models/FlightDetailsViewModel.cs index 48b4fad..d326954 100644 --- a/src/FlightRecorder.Mvc/Models/FlightDetailsViewModel.cs +++ b/src/FlightRecorder.Mvc/Models/FlightDetailsViewModel.cs @@ -32,6 +32,8 @@ public class FlightDetailsViewModel [DisplayName("New Airline")] public string NewAirline { get; set; } public bool IsDuplicate { get; set; } + public Sighting MostRecentSighting { get; set; } + public int? SightingId { get; set;} public string Action { get; set; } public string AirlineErrorMessage { get; set; } diff --git a/src/FlightRecorder.Mvc/Views/AircraftDetails/Index.cshtml b/src/FlightRecorder.Mvc/Views/AircraftDetails/Index.cshtml index 5f6971c..f029a53 100644 --- a/src/FlightRecorder.Mvc/Views/AircraftDetails/Index.cshtml +++ b/src/FlightRecorder.Mvc/Views/AircraftDetails/Index.cshtml @@ -20,6 +20,15 @@
+ @if (Model.MostRecentSighting != null) + { +
+
+ This aircraft was last seen from @Model.MostRecentSighting.Location.Name on @Model.MostRecentSighting.Date.ToShortDateString(), flight @Model.MostRecentSighting.Flight.Number +
+
+ } +
@Html.LabelFor(m => m.Registration) diff --git a/src/FlightRecorder.Mvc/Views/FlightDetails/Index.cshtml b/src/FlightRecorder.Mvc/Views/FlightDetails/Index.cshtml index f7cc155..302fe95 100644 --- a/src/FlightRecorder.Mvc/Views/FlightDetails/Index.cshtml +++ b/src/FlightRecorder.Mvc/Views/FlightDetails/Index.cshtml @@ -26,6 +26,14 @@
} + else if (Model.MostRecentSighting != null) + { +
+
+ This flight was last seen from @Model.MostRecentSighting.Location.Name on @Model.MostRecentSighting.Date.ToShortDateString() +
+
+ }
diff --git a/src/FlightRecorder.Mvc/Wizard/AddSightingWizard.cs b/src/FlightRecorder.Mvc/Wizard/AddSightingWizard.cs index 337f49a..a7a8816 100644 --- a/src/FlightRecorder.Mvc/Wizard/AddSightingWizard.cs +++ b/src/FlightRecorder.Mvc/Wizard/AddSightingWizard.cs @@ -203,12 +203,12 @@ public async Task GetFlightDetailsModelAsync(string user model.Embarkation = flight.Embarkation; model.Destination = flight.Destination; model.AirlineId = flight.AirlineId; - } - // See if this is a potential duplicate - only need to return the first page with 1 result to do the - // duplicate check - var duplicates = await _sightingsSearch.GetSightingsByFlightAndDate((DateTime)sighting.Date, sighting.FlightNumber, 1, 1); - model.IsDuplicate = duplicates?.Count > 0; + // Retrive the most recent sighting of this flight and see if this is a duplicate. Note that duplicates + // are not reported when editing an existing sighting + model.MostRecentSighting = await _sightingsSearch.GetMostRecentFlightSighting(sighting.FlightNumber); + model.IsDuplicate = sighting.SightingId > 0 ? false : model.MostRecentSighting?.Date == sighting.Date; + } return model; } @@ -250,6 +250,9 @@ public async Task GetAircraftDetailsModelAsync(string // Load the models for the aircraft's manufacturer List models = await GetModelsAsync(model.ManufacturerId ?? 0); model.SetModels(models); + + // Retrive the most recent sighting of this aircraft + model.MostRecentSighting = await _sightingsSearch.GetMostRecentAircraftSighting(aircraft.Registration); } return model; diff --git a/src/FlightRecorder.Mvc/appsettings.json b/src/FlightRecorder.Mvc/appsettings.json index 57434b2..a26086b 100644 --- a/src/FlightRecorder.Mvc/appsettings.json +++ b/src/FlightRecorder.Mvc/appsettings.json @@ -9,7 +9,7 @@ "AllowedHosts": "*", "AppSettings": { "Secret": "e2b6e7fe16ef469d9862d43eb76d00e2802ab769b85848048cc9387743ca2cc38c0f4fd8a0de46798f347bedf676bc31", - "ApiUrl": "https://localhost:5001", + "ApiUrl": "http://localhost:5000", "ApiDateFormat": "yyyy-MM-dd H:mm:ss", "ApiRoutes": [ { diff --git a/src/FlightRecorder.Tests/FlightRecorder.Tests.csproj b/src/FlightRecorder.Tests/FlightRecorder.Tests.csproj index 82a080c..79922c0 100644 --- a/src/FlightRecorder.Tests/FlightRecorder.Tests.csproj +++ b/src/FlightRecorder.Tests/FlightRecorder.Tests.csproj @@ -4,7 +4,7 @@ net9.0 false - 1.8.0.0 + 1.9.0.0 diff --git a/src/FlightRecorder.Tests/SightingManagerTest.cs b/src/FlightRecorder.Tests/SightingManagerTest.cs index 5f25bbb..637e75c 100644 --- a/src/FlightRecorder.Tests/SightingManagerTest.cs +++ b/src/FlightRecorder.Tests/SightingManagerTest.cs @@ -202,5 +202,30 @@ public async Task ListByMissingLocation() .ListByLocationAsync("Missing", 1, 100); Assert.IsNull(sightings); } + + [TestMethod] + public async Task GetMostRecentFlightSighting() + { + var sighting = await _factory.Sightings.GetMostRecent(x => x.Flight.Number == FlightNumber); + Assert.IsNotNull(sighting); + Assert.AreEqual(FlightNumber, sighting.Flight.Number); + Assert.AreEqual(SightingDate, sighting.Date); + } + + [TestMethod] + public async Task GetMostRecentAircraftSighting() + { + var sighting = await _factory.Sightings.GetMostRecent(x => x.Aircraft.Registration == Registration); + Assert.IsNotNull(sighting); + Assert.AreEqual(Registration, sighting.Aircraft.Registration); + Assert.AreEqual(SightingDate, sighting.Date); + } + + [TestMethod] + public async Task GetMissingRecentSighting() + { + var sighting = await _factory.Sightings.GetMostRecent(x => x.Flight.Embarkation == "Missing"); + Assert.IsNull(sighting); + } } }