From a57a5ced94cacabfe96bc0bf8a10c34049672203 Mon Sep 17 00:00:00 2001 From: Dave Walker Date: Sun, 1 Dec 2024 10:23:08 +0000 Subject: [PATCH] Implement the 'My Flights' report --- .../Controllers/ReportsController.cs | 30 +++- src/FlightRecorder.Api/Entities/ReportType.cs | 3 +- .../Services/ReportExportService.cs | 19 ++ .../Factory/FlightRecorderFactory.cs | 5 + .../FlightRecorder.BusinessLogic.csproj | 3 + .../Sql/MyFlights.sql | 17 ++ .../FlightRecorderDbContext.cs | 2 + .../Reporting/MyFlights.cs | 36 ++++ src/FlightRecorder.Mvc/Api/ReportsClient.cs | 11 ++ .../Controllers/ModelStatisticsController.cs | 3 - .../Controllers/MyFlightsController.cs | 94 ++++++++++ src/FlightRecorder.Mvc/Entities/MyFlights.cs | 14 ++ .../Entities/ReportDefinitions.cs | 3 +- src/FlightRecorder.Mvc/Entities/ReportType.cs | 3 +- .../Models/MyFlightsViewModel.cs | 8 + .../Views/MyFlights/Index.cshtml | 169 ++++++++++++++++++ .../Views/Shared/_Layout.cshtml | 1 + src/FlightRecorder.Mvc/appsettings.json | 4 + 18 files changed, 418 insertions(+), 7 deletions(-) create mode 100644 src/FlightRecorder.BusinessLogic/Sql/MyFlights.sql create mode 100644 src/FlightRecorder.Entities/Reporting/MyFlights.cs create mode 100644 src/FlightRecorder.Mvc/Controllers/MyFlightsController.cs create mode 100644 src/FlightRecorder.Mvc/Entities/MyFlights.cs create mode 100644 src/FlightRecorder.Mvc/Models/MyFlightsViewModel.cs create mode 100644 src/FlightRecorder.Mvc/Views/MyFlights/Index.cshtml diff --git a/src/FlightRecorder.Api/Controllers/ReportsController.cs b/src/FlightRecorder.Api/Controllers/ReportsController.cs index ebc0ccb..6a6ef77 100644 --- a/src/FlightRecorder.Api/Controllers/ReportsController.cs +++ b/src/FlightRecorder.Api/Controllers/ReportsController.cs @@ -164,7 +164,7 @@ public async Task>> GetModelStatisticsAsync(s } /// - /// Generate the aircraft model statistics report + /// Generate the flights by month statistics report /// /// /// @@ -222,5 +222,33 @@ public async Task>> GetJobsAsync(string start, stri // Convert to a list and return the results return results; } + + /// + /// Generate the "My Flights" report + /// + /// + /// + /// + /// + /// + [HttpGet] + [Route("myflights/{start}/{end}/{pageNumber}/{pageSize}")] + public async Task>> GetMyFlightsAsync(string start, string end, int pageNumber, int pageSize) + { + // Decode the start and end date and convert them to dates + DateTime startDate = DateTime.ParseExact(HttpUtility.UrlDecode(start), DateTimeFormat, null); + DateTime endDate = DateTime.ParseExact(HttpUtility.UrlDecode(end), DateTimeFormat, null); + + // Get the report content + var results = await _factory.MyFlights.GenerateReportAsync(startDate, endDate, pageNumber, pageSize); + + if (!results.Any()) + { + return NoContent(); + } + + // Convert to a list and return the results + return results.ToList(); + } } } diff --git a/src/FlightRecorder.Api/Entities/ReportType.cs b/src/FlightRecorder.Api/Entities/ReportType.cs index bf31a07..4fddaca 100644 --- a/src/FlightRecorder.Api/Entities/ReportType.cs +++ b/src/FlightRecorder.Api/Entities/ReportType.cs @@ -7,6 +7,7 @@ public enum ReportType ManufacturerStatistics = 2, ModelStatistics = 3, FlightsByMonth = 4, - JobStatus = 5 + JobStatus = 5, + MyFlights = 6 } } diff --git a/src/FlightRecorder.Api/Services/ReportExportService.cs b/src/FlightRecorder.Api/Services/ReportExportService.cs index 160b9b6..949f4d7 100644 --- a/src/FlightRecorder.Api/Services/ReportExportService.cs +++ b/src/FlightRecorder.Api/Services/ReportExportService.cs @@ -58,6 +58,9 @@ protected override async Task ProcessWorkItemAsync(ReportExportWorkItem item, Fl case ReportType.JobStatus: await ExportJobStatusAsync(factory, item); break; + case ReportType.MyFlights: + await ExportMyFlightsAsync(factory, item); + break; default: break; } @@ -165,5 +168,21 @@ private async Task ExportJobStatusAsync(FlightRecorderFactory factory, ReportExp var exporter = new CsvExporter(); exporter.Export(records, filePath, ','); } + + /// + /// Export the "My Flights" report + /// + /// + /// + /// + private async Task ExportMyFlightsAsync(FlightRecorderFactory factory, ReportExportWorkItem item) + { + // The third argument to the report generation method is an arbitrarily large value intended + // to return all records + var records = await factory.MyFlights.GenerateReportAsync(item.Start, item.End, 1, int.MaxValue); + var filePath = Path.Combine(_settings.ReportsExportPath, item.FileName); + var exporter = new CsvExporter(); + exporter.Export(records, filePath, ','); + } } } \ No newline at end of file diff --git a/src/FlightRecorder.BusinessLogic/Factory/FlightRecorderFactory.cs b/src/FlightRecorder.BusinessLogic/Factory/FlightRecorderFactory.cs index a71be3e..7cc4638 100644 --- a/src/FlightRecorder.BusinessLogic/Factory/FlightRecorderFactory.cs +++ b/src/FlightRecorder.BusinessLogic/Factory/FlightRecorderFactory.cs @@ -25,6 +25,7 @@ public class FlightRecorderFactory private readonly Lazy> _manufacturerStatistics = null; private readonly Lazy> _modelStatistics = null; private readonly Lazy> _flightsByMonth = null; + private readonly Lazy> _myFlights = null; public FlightRecorderDbContext Context { get; private set; } @@ -55,6 +56,9 @@ public class FlightRecorderFactory [ExcludeFromCodeCoverage] public IDateBasedReport FlightsByMonth { get { return _flightsByMonth.Value; } } + [ExcludeFromCodeCoverage] + public IDateBasedReport MyFlights { get { return _myFlights.Value; } } + public FlightRecorderFactory(FlightRecorderDbContext context) { // Store the database context @@ -81,6 +85,7 @@ public FlightRecorderFactory(FlightRecorderDbContext context) _manufacturerStatistics = new Lazy>(() => new DateBasedReport(context)); _modelStatistics = new Lazy>(() => new DateBasedReport(context)); _flightsByMonth = new Lazy>(() => new DateBasedReport(context)); + _myFlights = new Lazy>(() => new DateBasedReport(context)); } } } diff --git a/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj b/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj index e74f600..682aa45 100644 --- a/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj +++ b/src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.csproj @@ -47,6 +47,9 @@ Never + + Never + diff --git a/src/FlightRecorder.BusinessLogic/Sql/MyFlights.sql b/src/FlightRecorder.BusinessLogic/Sql/MyFlights.sql new file mode 100644 index 0000000..4e59810 --- /dev/null +++ b/src/FlightRecorder.BusinessLogic/Sql/MyFlights.sql @@ -0,0 +1,17 @@ +SELECT s.Date, + a.Name AS "Airline", + f.Number, + f.Embarkation, + f.Destination, + ac.Registration, + m.Name AS "Model", + ma.Name AS "Manufacturer" +FROM SIGHTING s +INNER JOIN FLIGHT f on f.Id = s.Flight_Id +INNER JOIN AIRLINE a on a.Id = f.Airline_Id +INNER JOIN AIRCRAFT ac ON ac.Id = s.Aircraft_Id +INNER JOIN MODEL m ON m.Id = ac.Model_Id +INNER JOIN MANUFACTURER ma ON ma.Id = m.Manufacturer_Id +WHERE s.Is_My_Flight = 1 +AND s.Date BETWEEN '$from' AND '$to' +ORDER BY s.Date; diff --git a/src/FlightRecorder.Data/FlightRecorderDbContext.cs b/src/FlightRecorder.Data/FlightRecorderDbContext.cs index 72b1b1e..8aac459 100644 --- a/src/FlightRecorder.Data/FlightRecorderDbContext.cs +++ b/src/FlightRecorder.Data/FlightRecorderDbContext.cs @@ -27,6 +27,7 @@ public partial class FlightRecorderDbContext : DbContext public virtual DbSet ManufacturerStatistics { get; set; } public virtual DbSet ModelStatistics { get; set; } public virtual DbSet FlightsByMonth { get; set; } + public virtual DbSet MyFlights { get; set; } public FlightRecorderDbContext(DbContextOptions options) : base(options) { @@ -43,6 +44,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasNoKey(); modelBuilder.Entity().HasNoKey(); modelBuilder.Entity().HasNoKey(); + modelBuilder.Entity().HasNoKey(); modelBuilder.Entity(entity => { diff --git a/src/FlightRecorder.Entities/Reporting/MyFlights.cs b/src/FlightRecorder.Entities/Reporting/MyFlights.cs new file mode 100644 index 0000000..f80fdb0 --- /dev/null +++ b/src/FlightRecorder.Entities/Reporting/MyFlights.cs @@ -0,0 +1,36 @@ +using FlightRecorder.Entities.Attributes; +using Microsoft.EntityFrameworkCore; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace FlightRecorder.Entities.Reporting +{ + [Keyless] + [ExcludeFromCodeCoverage] + public class MyFlights + { + [Export("Date", 1)] + public DateTime Date { get; set; } + + [Export("Airline", 2)] + public string Airline { get; set; } + + [Export("Number", 3)] + public string Number { get; set; } + + [Export("Embarkation", 4)] + public string Embarkation { get; set; } + + [Export("Destination", 5)] + public string Destination { get; set; } + + [Export("Registration", 6)] + public string Registration { get; set; } + + [Export("Model", 7)] + public string Model { get; set; } + + [Export("Manufacturer", 7)] + public string Manufacturer { get; set; } + } +} diff --git a/src/FlightRecorder.Mvc/Api/ReportsClient.cs b/src/FlightRecorder.Mvc/Api/ReportsClient.cs index bbc2f5c..fa9e270 100644 --- a/src/FlightRecorder.Mvc/Api/ReportsClient.cs +++ b/src/FlightRecorder.Mvc/Api/ReportsClient.cs @@ -95,6 +95,17 @@ public async Task> FlightsByMonthAsync(DateTime? from, Date public async Task> JobStatusAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize) => await DateBasedReportAsync("JobStatus", from, to, pageNumber, pageSize); + /// + /// Return the "My Flights" report + /// + /// + /// + /// + /// + /// + public async Task> MyFlightsAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize) + => await DateBasedReportAsync("MyFlights", from, to, pageNumber, pageSize); + /// /// Return a date-based statistics report /// diff --git a/src/FlightRecorder.Mvc/Controllers/ModelStatisticsController.cs b/src/FlightRecorder.Mvc/Controllers/ModelStatisticsController.cs index d7152fd..df76148 100644 --- a/src/FlightRecorder.Mvc/Controllers/ModelStatisticsController.cs +++ b/src/FlightRecorder.Mvc/Controllers/ModelStatisticsController.cs @@ -4,9 +4,6 @@ using FlightRecorder.Mvc.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using System.Collections.Generic; -using System.Threading.Tasks; -using System; namespace FlightRecorder.Mvc.Controllers { diff --git a/src/FlightRecorder.Mvc/Controllers/MyFlightsController.cs b/src/FlightRecorder.Mvc/Controllers/MyFlightsController.cs new file mode 100644 index 0000000..59c7d27 --- /dev/null +++ b/src/FlightRecorder.Mvc/Controllers/MyFlightsController.cs @@ -0,0 +1,94 @@ +using FlightRecorder.Mvc.Api; +using FlightRecorder.Mvc.Configuration; +using FlightRecorder.Mvc.Entities; +using FlightRecorder.Mvc.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace FlightRecorder.Mvc.Controllers +{ + public class MyFlightsController : Controller + { + private readonly ReportsClient _reportsClient; + private readonly ExportClient _exportClient; + private readonly IOptions _settings; + + public MyFlightsController( + ReportsClient reportsClient, + ExportClient exportsClient, + IOptions settings) + { + _reportsClient = reportsClient; + _exportClient = exportsClient; + _settings = settings; + } + + /// + /// Serve the empty report page + /// + /// + [HttpGet] + public IActionResult Index() + { + MyFlightsViewModel model = new MyFlightsViewModel + { + PageNumber = 1 + }; + return View(model); + } + + /// + /// Respond to a POST event triggering the report generation + /// + /// + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Index(MyFlightsViewModel model) + { + if (ModelState.IsValid) + { + int page = model.PageNumber; + switch (model.Action) + { + case ControllerActions.ActionPreviousPage: + page -= 1; + break; + case ControllerActions.ActionNextPage: + page += 1; + break; + case ControllerActions.ActionSearch: + page = 1; + break; + default: + break; + } + + // Need to clear model state here or the page number that was posted + // is returned and page navigation doesn't work correctly. So, capture + // and amend the page number, above, then apply it, below + ModelState.Clear(); + + DateTime start = !string.IsNullOrEmpty(model.From) ? DateTime.Parse(model.From) : DateTime.MinValue; + DateTime end = !string.IsNullOrEmpty(model.To) ? DateTime.Parse(model.To) : DateTime.MaxValue; + + List records = await _reportsClient.MyFlightsAsync(start, end, page, _settings.Value.SearchPageSize); + model.SetRecords(records, page, _settings.Value.SearchPageSize); + } + + return View(model); + } + + /// + /// Request export of the report + /// + /// + /// + [HttpPost] + public async Task Export([FromBody] MyFlightsViewModel model) + { + await _exportClient.ExportReport(model); + return Ok(); + } + } +} diff --git a/src/FlightRecorder.Mvc/Entities/MyFlights.cs b/src/FlightRecorder.Mvc/Entities/MyFlights.cs new file mode 100644 index 0000000..27c1a8f --- /dev/null +++ b/src/FlightRecorder.Mvc/Entities/MyFlights.cs @@ -0,0 +1,14 @@ +namespace FlightRecorder.Mvc.Entities +{ + public class MyFlights + { + public DateTime Date { get; set; } + public string Airline { get; set; } + public string Number { get; set; } + public string Embarkation { get; set; } + public string Destination { get; set; } + public string Registration { get; set; } + public string Model { get; set; } + public string Manufacturer { get; set; } + } +} diff --git a/src/FlightRecorder.Mvc/Entities/ReportDefinitions.cs b/src/FlightRecorder.Mvc/Entities/ReportDefinitions.cs index 9af8c96..5c599dc 100644 --- a/src/FlightRecorder.Mvc/Entities/ReportDefinitions.cs +++ b/src/FlightRecorder.Mvc/Entities/ReportDefinitions.cs @@ -11,7 +11,8 @@ public static class ReportDefinitions new ReportDefinition(ReportType.ManufacturerStatistics, typeof(ManufacturerStatistics), "Manufacturer Statistics"), new ReportDefinition(ReportType.ModelStatistics, typeof(ModelStatistics), "Model Statistics"), new ReportDefinition(ReportType.FlightsByMonth, typeof(FlightsByMonth), "Flights By Month"), - new ReportDefinition(ReportType.JobStatus, typeof(JobStatus), "Job Status") + new ReportDefinition(ReportType.JobStatus, typeof(JobStatus), "Job Status"), + new ReportDefinition(ReportType.MyFlights, typeof(MyFlights), "My Flights") }; } } diff --git a/src/FlightRecorder.Mvc/Entities/ReportType.cs b/src/FlightRecorder.Mvc/Entities/ReportType.cs index b629ef6..1ec2756 100644 --- a/src/FlightRecorder.Mvc/Entities/ReportType.cs +++ b/src/FlightRecorder.Mvc/Entities/ReportType.cs @@ -7,6 +7,7 @@ public enum ReportType ManufacturerStatistics = 2, ModelStatistics = 3, FlightsByMonth = 4, - JobStatus = 5 + JobStatus = 5, + MyFlights = 6 } } diff --git a/src/FlightRecorder.Mvc/Models/MyFlightsViewModel.cs b/src/FlightRecorder.Mvc/Models/MyFlightsViewModel.cs new file mode 100644 index 0000000..4e5f611 --- /dev/null +++ b/src/FlightRecorder.Mvc/Models/MyFlightsViewModel.cs @@ -0,0 +1,8 @@ +using FlightRecorder.Mvc.Entities; + +namespace FlightRecorder.Mvc.Models +{ + public class MyFlightsViewModel : DateBasedReportViewModelBase + { + } +} diff --git a/src/FlightRecorder.Mvc/Views/MyFlights/Index.cshtml b/src/FlightRecorder.Mvc/Views/MyFlights/Index.cshtml new file mode 100644 index 0000000..7d771ee --- /dev/null +++ b/src/FlightRecorder.Mvc/Views/MyFlights/Index.cshtml @@ -0,0 +1,169 @@ +@using FlightRecorder.Mvc.Entities +@model FlightRecorder.Mvc.Models.MyFlightsViewModel + +@{ + ViewData["Title"] = "My Flights Report"; + string previousDisabled = (Model.PreviousEnabled) ? "" : "disabled"; + string nextDisabled = (Model.NextEnabled) ? "" : "disabled"; +} + +

+ + My Flights + +
+ + + Report on My Flights + + +

+ + + + + +
+ @using (Html.BeginForm()) + { + @Html.AntiForgeryToken() + +
+
+ @Html.LabelFor(m => m.From) +
+ @Html.EditorFor(m => m.From, new { @class = "form-control" }) + +
+ @Html.ValidationMessageFor(m => m.From, "", new { @class = "text-danger" }) +
+
+ @Html.LabelFor(m => m.To) +
+ @Html.EditorFor(m => m.To, new { @class = "form-control" }) + +
+ @Html.ValidationMessageFor(m => m.To, "", new { @class = "text-danger" }) +
+
+ +
+ @if (Model.Records != null) + { + + + } + +
+
+
+
+ + @Html.HiddenFor(m => m.PageNumber) + + @if (Model.Records != null) + { + + + + + + + + + + + + @foreach (var record in Model.Records) + { + + + + + + + + + + + } +
DateAirlineFlightEmbarkationDestinationRegistrationModelManufacturer
@record.Date.ToShortDateString()@record.Airline@record.Number@record.Embarkation@record.Destination@record.Registration@record.Model@record.Manufacturer
+
+

+ + +

+ } + else if (Model.HasNoMatchingResults) + { +
+ @Html.Raw("There are no flights to report") + } + } +
+ + diff --git a/src/FlightRecorder.Mvc/Views/Shared/_Layout.cshtml b/src/FlightRecorder.Mvc/Views/Shared/_Layout.cshtml index d07db9c..5994f8f 100644 --- a/src/FlightRecorder.Mvc/Views/Shared/_Layout.cshtml +++ b/src/FlightRecorder.Mvc/Views/Shared/_Layout.cshtml @@ -54,6 +54,7 @@ Location Statistics Manufacturer Statistics Aircraft Model Statistics + My Flights Job Status diff --git a/src/FlightRecorder.Mvc/appsettings.json b/src/FlightRecorder.Mvc/appsettings.json index a26086b..f8356b4 100644 --- a/src/FlightRecorder.Mvc/appsettings.json +++ b/src/FlightRecorder.Mvc/appsettings.json @@ -80,6 +80,10 @@ "Name": "FlightsByMonth", "Route": "/reports/flights" }, + { + "Name": "MyFlights", + "Route": "/reports/myflights" + }, { "Name": "JobStatus", "Route": "/reports/jobs"