diff --git a/Server/Configurations/MapperInitializer.cs b/Server/Configurations/MapperInitializer.cs index 28461bf..e4bc08a 100644 --- a/Server/Configurations/MapperInitializer.cs +++ b/Server/Configurations/MapperInitializer.cs @@ -49,6 +49,14 @@ public class MapperInitializer : Profile CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); diff --git a/Server/Controllers/AutomationController.cs b/Server/Controllers/AutomationController.cs new file mode 100644 index 0000000..4b164ea --- /dev/null +++ b/Server/Controllers/AutomationController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Server.Services; + +namespace Server.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class AutomationController : ControllerBase +{ + private readonly AutomationService _automationService; + + public AutomationController(AutomationService automationService) + { + _automationService = automationService; + } + + [HttpGet] + public async Task GetRoute(int from, int to, DateTime date) + { + var result = await _automationService.GetRoute(from, to, date); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return Ok(result.result); + } +} + diff --git a/Server/Controllers/ReportController.cs b/Server/Controllers/ReportController.cs index b4dc133..9347d45 100644 --- a/Server/Controllers/ReportController.cs +++ b/Server/Controllers/ReportController.cs @@ -21,10 +21,24 @@ public class ReportController : ControllerBase if (!result.IsSucceed) { - return BadRequest(result.message); + return BadRequest(result.actionResult); } return File(result.ticketPdf, "application/pdf", $"ticket.pdf"); } + + [HttpGet("report")] + public async Task GetCompanyReport(int companyId, DateTime fromDate, DateTime toDate) + { + var result = await _reportService.GetCompanyReport(companyId, fromDate, toDate); + + if (!result.isSucceed) + { + return BadRequest(result.actionResult); + } + + return File(result.reportPdf, "application/pdf", + $"report.pdf"); + } } diff --git a/Server/Controllers/StatisticsController.cs b/Server/Controllers/StatisticsController.cs index 3ab6598..74dcfba 100644 --- a/Server/Controllers/StatisticsController.cs +++ b/Server/Controllers/StatisticsController.cs @@ -17,9 +17,18 @@ public class StatisticsController : ControllerBase } [HttpGet("routes")] - public async Task GetPopularRoutes([FromQuery] int amount = 10) + public async Task GetPopularRoutes([FromQuery] PopularRoutesParameters parameters) { - return Ok(); + var result = await _statisticsService.GetPopularRoutes(parameters); + + if (!result.IsSucceed) + { + return BadRequest(result.actionResult); + } + + Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); + + return Ok(result.route); } [HttpGet("users")] @@ -30,7 +39,7 @@ public class StatisticsController : ControllerBase if (!result.IsSucceed) { - return BadRequest(result.message); + return BadRequest(result.actionResult); } Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); @@ -45,7 +54,7 @@ public class StatisticsController : ControllerBase if (!result.IsSucceed) { - return BadRequest(result.message); + return BadRequest(result.actionResult); } Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); @@ -60,7 +69,7 @@ public class StatisticsController : ControllerBase if (!result.IsSucceed) { - return BadRequest(result.message); + return BadRequest(result.actionResult); } Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); diff --git a/Server/Controllers/TicketGroupController.cs b/Server/Controllers/TicketGroupController.cs new file mode 100644 index 0000000..2e9b8a8 --- /dev/null +++ b/Server/Controllers/TicketGroupController.cs @@ -0,0 +1,133 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Server.Services; +using SharedModels.DataTransferObjects; +using SharedModels.QueryParameters.Objects; + +namespace Server.Controllers; + +[Route("api/ticketGroups")] +[ApiController] +public class TicketGroupController : ControllerBase +{ + private readonly ITicketGroupManagementService _ticketGroupManagementService; + + public TicketGroupController(ITicketGroupManagementService ticketGroupManagementService) + { + _ticketGroupManagementService = ticketGroupManagementService; + } + + [HttpPost] + public async Task AddTicketGroup(CreateTicketGroupDto ticketGroup) + { + var result = await _ticketGroupManagementService.AddTicketGroup(ticketGroup); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return CreatedAtAction(nameof(GetTicketGroup), new {id = result.ticketGroup.Id}, result.ticketGroup); + } + + [HttpPost("withTickets")] + public async Task AddTicketGroupWithTickets(CreateTicketGroupWithTicketsDto ticketGroup) + { + var result = await _ticketGroupManagementService.AddTicketGroupWithTickets(ticketGroup); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return CreatedAtAction(nameof(GetTicketGroup), new {id = result.ticketGroup.Id}, result.ticketGroup); + } + + [HttpGet] + public async Task GetTicketGroups([FromQuery] TicketGroupParameters parameters) + { + var result = await _ticketGroupManagementService.GetTicketGroups(parameters); + + if (!result.isSucceed) + { + return result.actionResult; + } + + Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); + + return Ok(result.ticketGroups); + } + + [HttpGet("withTickets")] + public async Task GetTicketGroupsWithTickets([FromQuery] TicketGroupWithTicketsParameters parameters) + { + var result = await _ticketGroupManagementService.GetTicketGroupsWithTickets(parameters); + + if (!result.isSucceed) + { + return result.actionResult; + } + + Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); + + return Ok(result.ticketGroups); + } + + [HttpGet("{id}")] + public async Task GetTicketGroup(int id, [FromQuery] string? fields) + { + var result = await _ticketGroupManagementService.GetTicketGroup(id, fields); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return Ok(result.ticketGroup); + } + + [HttpGet("withTickets/{id}")] + public async Task GetTicketGroupWithTickets(int id, [FromQuery] string? fields) + { + var result = await _ticketGroupManagementService.GetTicketGroupWithTickets(id, fields); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return Ok(result.ticketGroup); + } + + [HttpPut("{id}")] + public async Task UpdateTicketGroup(int id, UpdateTicketGroupDto ticketGroup) + { + if (id != ticketGroup.Id) + { + return BadRequest(); + } + + var result = await _ticketGroupManagementService.UpdateTicketGroup(ticketGroup); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return Ok(result.ticketGroup); + } + + [HttpDelete("{id}")] + public async Task DeleteTicketGroup(int id) + { + var result = await _ticketGroupManagementService.DeleteTicketGroup(id); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return NoContent(); + } +} + diff --git a/Server/Program.cs b/Server/Program.cs index a370d88..326d054 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -99,6 +99,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -113,6 +114,8 @@ builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); @@ -124,6 +127,13 @@ builder.Services.AddScoped, DataShaper, Pager>(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, DataShaper>(); + // Adding DB Context with PostgreSQL var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(options => diff --git a/Server/Services/AddressManagementService.cs b/Server/Services/AddressManagementService.cs index 2102f52..05c4d0d 100644 --- a/Server/Services/AddressManagementService.cs +++ b/Server/Services/AddressManagementService.cs @@ -81,8 +81,12 @@ public class AddressManagementService : IAddressManagementService return; } - addresses = addresses.Where(a => - a.Name.ToLower().Contains(search.ToLower())); + // TODO: Remove client evaluation + + addresses = addresses.ToArray().Where(a => + a.Name.ToLower().Contains(search.ToLower()) || + a.GetFullName().ToLower().Contains(search.ToLower())) + .AsQueryable(); } void FilterByCityId(ref IQueryable
addresses, diff --git a/Server/Services/AutomationService.cs b/Server/Services/AutomationService.cs new file mode 100644 index 0000000..5193139 --- /dev/null +++ b/Server/Services/AutomationService.cs @@ -0,0 +1,315 @@ +using System.Dynamic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Server.Data; +using Server.Helpers; +using Server.Models; + +namespace Server.Services; + +public class AutomationService +{ + private readonly ApplicationDbContext _dbContext; + private readonly ISortHelper _sortHelper; + + public AutomationService(ApplicationDbContext dbContext, + ISortHelper sortHelper) + { + _dbContext = dbContext; + _sortHelper = sortHelper; + } + + public async Task<(bool isSucceed, IActionResult? actionResult, List result)> GetRoute( + int from, int to, DateTime date) + { + var dbEnrollments = await _dbContext.VehicleEnrollments + .Include(ve => ve.Tickets) + .Include(ve => ve.Vehicle) + .ThenInclude(v => v.Company) + .Include(ve => ve.RouteAddressDetails) + .Include(ve => ve.Route).ThenInclude(r => r.RouteAddresses) + .ThenInclude(ra => ra.Address).ThenInclude(a => a.City) + .ThenInclude(c => c.State).ThenInclude(s => s.Country) + .Where(ve => ve.DepartureDateTimeUtc.Date >= date.Date && + ve.DepartureDateTimeUtc.Date <= date.AddDays(3).Date) + .ToListAsync(); + + var toBeRemovedEnrollmentsIds = new List(); + + var directEnrollments = new List(); + + foreach (var e in dbEnrollments) + { + if (e.Route.RouteAddresses.Count(ra => ra.AddressId == from) == 0 || + e.Route.RouteAddresses.Count(ra => ra.AddressId == to) == 0) + { + continue; + } + + var fromOrder = e.Route.RouteAddresses.FirstOrDefault(rad => + rad.AddressId == from)?.Order; + var toOrder = e.Route.RouteAddresses.FirstOrDefault(rad => + rad.AddressId == to)?.Order; + + if (fromOrder < toOrder) + { + directEnrollments.Add(e); + toBeRemovedEnrollmentsIds.Add(e.Id); + } + } + + dbEnrollments.RemoveAll(e => toBeRemovedEnrollmentsIds.Any(id => id == e.Id)); + toBeRemovedEnrollmentsIds.Clear(); + + foreach (var de in directEnrollments) + { + var routeAddresses = de.Route.RouteAddresses; + + var fromOrder = + routeAddresses.First(ra => ra.AddressId == from).Order; + var toOrder = + routeAddresses.First(ra => ra.AddressId == to).Order; + + directEnrollments[directEnrollments.IndexOf(de)].Route.RouteAddresses = routeAddresses + .OrderBy(ra => ra.Order) + .SkipWhile(ra => ra.Order < fromOrder) + .TakeWhile(ra => ra.Order <= toOrder) + .ToList(); + + directEnrollments[directEnrollments.IndexOf(de)] + .DepartureDateTimeUtc = GetDepartureTime(de); + } + + + + var enrollmentsWithFrom = new List(); + + foreach (var e in dbEnrollments) + { + if (e.Route.RouteAddresses.Count(ra => ra.AddressId == from) == 0) + { + continue; + } + + if (e.Route.RouteAddresses.Any(ra => ra.AddressId == from)) + { + enrollmentsWithFrom.Add(e); + toBeRemovedEnrollmentsIds.Add(e.Id); + } + } + + // dbEnrollments.RemoveAll(e => ToBeRemovedEnrollmentsIds.Any(id => id == e.Id)); + toBeRemovedEnrollmentsIds.Clear(); + + foreach (var ef in enrollmentsWithFrom) + { + var routeAddresses = ef.Route.RouteAddresses; + + var fromOrder = + routeAddresses.First(ra => ra.AddressId == from).Order; + + enrollmentsWithFrom[enrollmentsWithFrom.IndexOf(ef)].Route.RouteAddresses = routeAddresses + .OrderBy(ra => ra.Order) + .SkipWhile(ra => ra.Order < fromOrder).ToList(); + } + + + var enrollmentsWithTo = new List(); + + foreach (var e in dbEnrollments) + { + if (e.Route.RouteAddresses.Count(ra => ra.AddressId == to) == 0) + { + continue; + } + + if (e.Route.RouteAddresses.Any(ra => ra.AddressId == to)) + { + enrollmentsWithTo.Add(e); + toBeRemovedEnrollmentsIds.Add(e.Id); + } + } + + // dbEnrollments.RemoveAll(e => ToBeRemovedEnrollmentsIds.Any(id => id == e.Id)); + toBeRemovedEnrollmentsIds.Clear(); + + foreach (var et in enrollmentsWithTo) + { + var routeAddresses = et.Route.RouteAddresses; + + var toOrder = + routeAddresses.First(ra => ra.AddressId == to).Order; + + enrollmentsWithTo[enrollmentsWithTo.IndexOf(et)].Route.RouteAddresses = routeAddresses + .OrderBy(ra => ra.Order) + .TakeWhile(ra => ra.Order <= toOrder).ToList(); + } + + + var oneTransferPath = new List>(); + + foreach (var ef in enrollmentsWithFrom) + { + foreach (var et in enrollmentsWithTo) + { + var efRouteAddresses = ef.Route.RouteAddresses; + var etRouteAddresses = et.Route.RouteAddresses; + + var intersectionAddressId = efRouteAddresses.IntersectBy( + etRouteAddresses.Select(x => x.AddressId), + x => x.AddressId).FirstOrDefault()?.AddressId; + + var toOrder = efRouteAddresses.First(ra => + ra.AddressId == intersectionAddressId).Order; + var fromOrder = etRouteAddresses.First(ra => + ra.AddressId == intersectionAddressId).Order; + + enrollmentsWithFrom[enrollmentsWithFrom.IndexOf(ef)].Route.RouteAddresses = + efRouteAddresses.OrderBy(ra => ra.Order) + .TakeWhile(ra => ra.Order <= toOrder).ToList(); + enrollmentsWithTo[enrollmentsWithTo.IndexOf(et)].Route.RouteAddresses = + etRouteAddresses.OrderBy(ra => ra.Order) + .SkipWhile(ra => ra.Order < fromOrder).ToList(); + + var fromArrivalTime = GetArrivalTime(ef); + var toDepartureTime = GetDepartureTime(et); + + var doesIntersect = intersectionAddressId != null; + + if (doesIntersect && (toDepartureTime - fromArrivalTime) >= TimeSpan.FromMinutes(5)) + { + oneTransferPath.Add(new List {ef, et}); + } + } + } + + foreach (var directEnrollment in directEnrollments) + { + oneTransferPath.Add(new List {directEnrollment}); + } + + var result = new List(); + + int i = 1; + foreach (var path in oneTransferPath) + { + + var shapedPath = new ExpandoObject(); + var enrollmentGroup = new ExpandoObject(); + + int j = 1; + foreach (var vehicleEnrollment in path) + { + var enrollment = new ExpandoObject(); + + enrollment.TryAdd("id", vehicleEnrollment.Id); + enrollment.TryAdd("departureTime", GetDepartureTime(vehicleEnrollment)); + enrollment.TryAdd("arrivalTime", GetArrivalTime(vehicleEnrollment)); + enrollment.TryAdd("departureAddressName", vehicleEnrollment.Route.RouteAddresses.First().Address.Name); + enrollment.TryAdd("departureAddressFullName", vehicleEnrollment.Route.RouteAddresses.First().Address.GetFullName()); + enrollment.TryAdd("departureAddressId", vehicleEnrollment.Route.RouteAddresses.First().AddressId); + enrollment.TryAdd("arrivalAddressName", vehicleEnrollment.Route.RouteAddresses.Last().Address.Name); + enrollment.TryAdd("arrivalAddressFullName", vehicleEnrollment.Route.RouteAddresses.Last().Address.GetFullName()); + enrollment.TryAdd("arrivalAddressId", vehicleEnrollment.Route.RouteAddresses.Last().AddressId); + enrollment.TryAdd("order", j); + + var vehicle = new ExpandoObject(); + + vehicle.TryAdd("type", vehicleEnrollment.Vehicle.Type); + vehicle.TryAdd("number", vehicleEnrollment.Vehicle.Number); + + enrollment.TryAdd("vehicle", vehicle); + + var company = new ExpandoObject(); + + company.TryAdd("name", vehicleEnrollment.Vehicle.Company.Name); + + enrollment.TryAdd("company", company); + + + + enrollmentGroup.TryAdd($"enrollment{j}", enrollment); + + j++; + } + + enrollmentGroup.TryAdd("totalDuration", GetTotalDuration(path)); + enrollmentGroup.TryAdd("totalCost", GetTotalCost(path)); + + shapedPath.TryAdd($"enrollmentGroup{i}", enrollmentGroup); + result.Add(shapedPath); + + i++; + } + + // result = _sortHelper.ApplySort(result[].AsQueryable(), "+cost").ToList(); + + return (true, null, result); + + DateTime GetDepartureTime(VehicleEnrollment enrollment) + { + var departureDateTimeUtc = enrollment.DepartureDateTimeUtc; + + var departureRouteAddressId = enrollment.Route.RouteAddresses.First().Id; + + foreach (var detail in enrollment.RouteAddressDetails) + { + if (detail.RouteAddressId == departureRouteAddressId) + { + departureDateTimeUtc += detail.WaitTimeSpan; + break; + } + + departureDateTimeUtc += detail.TimeSpanToNextCity + detail.WaitTimeSpan; + } + + return departureDateTimeUtc; + } + + DateTime GetArrivalTime(VehicleEnrollment enrollment) + { + var arrivalDateTimeUtc = enrollment.DepartureDateTimeUtc; + + var arrivalRouteAddressId = enrollment.Route.RouteAddresses.Last().Id; + + foreach (var detail in enrollment.RouteAddressDetails) + { + if (detail.RouteAddressId == arrivalRouteAddressId) + { + break; + } + + arrivalDateTimeUtc += detail.TimeSpanToNextCity + detail.WaitTimeSpan; + } + + return arrivalDateTimeUtc; + } + + TimeSpan GetTotalDuration(List vehicleEnrollments) + { + return GetArrivalTime(vehicleEnrollments.Last()) - + GetDepartureTime(vehicleEnrollments.First()); + } + + double GetTotalCost(List vehicleEnrollments) + { + double result = 0; + + foreach (var enrollment in vehicleEnrollments) + { + foreach (var routeAddresses in enrollment.Route.RouteAddresses) + { + if (enrollment.Route.RouteAddresses.Last().Id == routeAddresses.Id) + { + break; + } + + result += routeAddresses.RouteAddressDetails.First(rad => + rad.VehicleEnrollmentId == enrollment.Id).CostToNextCity; + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/Server/Services/IReportService.cs b/Server/Services/IReportService.cs index 50f5b6c..3bf7619 100644 --- a/Server/Services/IReportService.cs +++ b/Server/Services/IReportService.cs @@ -1,8 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + namespace Server.Services; public interface IReportService { - Task<(bool IsSucceed, string? message, Stream ticketPdf)> GetTicket(int ticketGroupId); + Task<(bool IsSucceed, IActionResult? actionResult, Stream ticketPdf)> + GetTicket(int ticketGroupId); - Task<(bool isSucceed, string? message, Stream reportPdf)> GetCompanyReport(); + Task<(bool isSucceed, IActionResult? actionResult, Stream reportPdf)> + GetCompanyReport(int companyId, DateTime fromDate, DateTime toDate); } \ No newline at end of file diff --git a/Server/Services/IStatisticsService.cs b/Server/Services/IStatisticsService.cs index da456d5..2649aeb 100644 --- a/Server/Services/IStatisticsService.cs +++ b/Server/Services/IStatisticsService.cs @@ -1,4 +1,5 @@ using System.Dynamic; +using Microsoft.AspNetCore.Mvc; using Server.Models; using SharedModels.QueryParameters; using SharedModels.QueryParameters.Statistics; @@ -7,18 +8,19 @@ namespace Server.Services; public interface IStatisticsService { - Task<(bool IsSucceed, string? message, IEnumerable route)> - GetPopularRoutes(int amount); + Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable route, + PagingMetadata pagingMetadata)> + GetPopularRoutes(PopularRoutesParameters parameters); - Task<(bool IsSucceed, string? message, IEnumerable users, + Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable users, PagingMetadata pagingMetadata)> GetEngagedUsers(EngagedUserParameters parameters); - Task<(bool IsSucceed, string? message, IEnumerable companies, + Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable companies, PagingMetadata pagingMetadata)> GetPopularCompanies(PopularCompanyParameters parameters); - Task<(bool IsSucceed, string? message, IEnumerable stations, + Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable stations, PagingMetadata pagingMetadata)> GetPopularStations(PopularAddressesParameters parameters); } \ No newline at end of file diff --git a/Server/Services/ITicketGroupManagementService.cs b/Server/Services/ITicketGroupManagementService.cs new file mode 100644 index 0000000..e495a4b --- /dev/null +++ b/Server/Services/ITicketGroupManagementService.cs @@ -0,0 +1,22 @@ +using System.Dynamic; +using Microsoft.AspNetCore.Mvc; +using SharedModels.DataTransferObjects; +using SharedModels.QueryParameters; +using SharedModels.QueryParameters.Objects; + +namespace Server.Services; + +public interface ITicketGroupManagementService +{ + Task<(bool isSucceed, IActionResult? actionResult, TicketGroupDto ticketGroup)> AddTicketGroup(CreateTicketGroupDto createTicketGroupDto); + Task<(bool isSucceed, IActionResult? actionResult, TicketGroupWithTicketsDto ticketGroup)> AddTicketGroupWithTickets(CreateTicketGroupWithTicketsDto createTicketGroupWithTicketsDto); + Task<(bool isSucceed, IActionResult? actionResult, IEnumerable ticketGroups, + PagingMetadata pagingMetadata)> GetTicketGroups(TicketGroupParameters parameters); + Task<(bool isSucceed, IActionResult? actionResult, IEnumerable ticketGroups, + PagingMetadata pagingMetadata)> GetTicketGroupsWithTickets(TicketGroupWithTicketsParameters parameters); + Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject ticketGroup)> GetTicketGroup(int id, string? fields); + Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject ticketGroup)> GetTicketGroupWithTickets(int id, string? fields); + Task<(bool isSucceed, IActionResult? actionResult, UpdateTicketGroupDto ticketGroup)> UpdateTicketGroup(UpdateTicketGroupDto updateTicketGroupDto); + Task<(bool isSucceed, IActionResult? actionResult)> DeleteTicketGroup(int id); + Task IsTicketGroupExist(int id); +} \ No newline at end of file diff --git a/Server/Services/ReportService.cs b/Server/Services/ReportService.cs index dcc354f..57b05e4 100644 --- a/Server/Services/ReportService.cs +++ b/Server/Services/ReportService.cs @@ -1,11 +1,14 @@ +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MigraDocCore.DocumentObjectModel; using MigraDocCore.DocumentObjectModel.Tables; using MigraDocCore.Rendering; +using PdfSharpCore; using PdfSharpCore.Drawing; using PdfSharpCore.Pdf; using Server.Data; using Server.Models; +using Route = Server.Models.Route; namespace Server.Services; @@ -18,8 +21,13 @@ public class ReportService : IReportService _dbContext = dbContext; } - public async Task<(bool IsSucceed, string? message, Stream ticketPdf)> GetTicket(int ticketGroupId) + public async Task<(bool IsSucceed, IActionResult? actionResult, Stream ticketPdf)> GetTicket(int ticketGroupId) { + if (!await DoesTicketGroupExist(ticketGroupId)) + { + return (false, new NotFoundResult(), null!); + } + var dbTicketGroup = await _dbContext.TicketGroups .Include(tg => tg.User) .Include(tg => tg.Tickets) @@ -42,7 +50,7 @@ public class ReportService : IReportService .ThenInclude(t => t.VehicleEnrollment) .ThenInclude(ve => ve.RouteAddressDetails) - .FirstOrDefaultAsync(tg => tg.Id == ticketGroupId); + .FirstAsync(tg => tg.Id == ticketGroupId); // Define document @@ -53,8 +61,7 @@ public class ReportService : IReportService // Craft document var pdfPage = document.AddPage(); - pdfPage.Width = XUnit.FromCentimeter(21.0); - pdfPage.Height = XUnit.FromCentimeter(29.7); + pdfPage.Size = PageSize.A4; var gfx = XGraphics.FromPdfPage(pdfPage); // HACK² @@ -248,7 +255,8 @@ public class ReportService : IReportService row.Cells[4].MergeRight = 3; row.Cells[4].MergeDown = 1; - row.Cells[4].AddParagraph($"{departureAddress}"); + row.Cells[4].Format.Font.Size = 8; + row.Cells[4].AddParagraph($"{departureAddress.GetFullName()}"); row.Cells[8].MergeRight = 1; row.Cells[8].MergeDown = 1; @@ -281,7 +289,8 @@ public class ReportService : IReportService row.Cells[4].MergeRight = 3; row.Cells[4].MergeDown = 1; - row.Cells[4].AddParagraph($"{arrivalAddress}"); + row.Cells[4].Format.Font.Size = 8; + row.Cells[4].AddParagraph($"{arrivalAddress.GetFullName()}"); row.Cells[8].MergeRight = 1; row.Cells[8].MergeDown = 1; @@ -341,14 +350,16 @@ public class ReportService : IReportService row.Cells[1].MergeRight = 3; row.Cells[1].MergeDown = 1; - row.Cells[1].AddParagraph($"{departureAddress}"); + row.Cells[1].Format.Font.Size = 8; + row.Cells[1].AddParagraph($"{departureAddress.GetFullName()}"); row.Cells[5].MergeDown = 1; row.Cells[5].AddParagraph("Куди"); row.Cells[6].MergeRight = 3; row.Cells[6].MergeDown = 1; - row.Cells[6].AddParagraph($"{arrivalAddress}"); + row.Cells[6].Format.Font.Size = 8; + row.Cells[6].AddParagraph($"{arrivalAddress.GetFullName()}"); row.Cells[10].MergeDown = 1; row.Cells[10].AddParagraph("Ціна"); @@ -386,6 +397,7 @@ public class ReportService : IReportService if (routeAddress.AddressId == ticket.FirstRouteAddressId) { + departureDateTimeUtc += details.WaitTimeSpan; break; } @@ -414,6 +426,7 @@ public class ReportService : IReportService } arrivalDateTimeUtc += details.TimeSpanToNextCity; + arrivalDateTimeUtc += details.WaitTimeSpan; } return arrivalDateTimeUtc; @@ -465,10 +478,706 @@ public class ReportService : IReportService return cost; } + + async Task DoesTicketGroupExist(int id) + { + return await _dbContext.TicketGroups.AnyAsync(tg => tg.Id == id); + } } - public async Task<(bool isSucceed, string? message, Stream reportPdf)> GetCompanyReport() + public async Task<(bool isSucceed, IActionResult? actionResult, Stream reportPdf)> + GetCompanyReport(int companyId, DateTime fromDate, DateTime toDate) { - throw new NotImplementedException(); + if (!await DoesCompanyExist(companyId)) + { + return (false, new NotFoundResult(), null!); + } + + var dbCompany = await _dbContext.Companies + .Include(c => c.Vehicles) + .ThenInclude(v => v.VehicleEnrollments) + .ThenInclude(ve => ve.Reviews) + + .Include(c => c.Vehicles) + .ThenInclude(v => v.VehicleEnrollments) + .ThenInclude(ve => ve.Tickets) + .ThenInclude(t => t.TicketGroup) + + .Include(c => c.Vehicles) + .ThenInclude(v => v.VehicleEnrollments) + .ThenInclude(ve => ve.Route) + .ThenInclude(r => r.RouteAddresses) + .ThenInclude(ra => ra.Address) + .ThenInclude(a => a.City) + .ThenInclude(c => c.State) + .ThenInclude(s => s.Country) + + .Include(c => c.Vehicles) + .ThenInclude(v => v.VehicleEnrollments) + .ThenInclude(ve => ve.RouteAddressDetails) + + .Select(c => new { + Company = c, + VehicleEnrollments = c.Vehicles + .Select(v => new { + Vehicle = v, + VehicleEnrollments = v.VehicleEnrollments.Where(ve => + ve.DepartureDateTimeUtc.Date >= fromDate && + ve.DepartureDateTimeUtc <= toDate) + }) + }) + .Select(o => o.Company) + .FirstAsync(c => c.Id == companyId); + + var routesEnrolled = new List(); + foreach (var vehicle in dbCompany.Vehicles) + { + foreach (var vehicleEnrollment in vehicle.VehicleEnrollments) + { + var route = vehicleEnrollment.Route; + if (!routesEnrolled.Contains(route)) + { + routesEnrolled.Add(route); + } + } + } + + var vehicleEnrolled = dbCompany.Vehicles; + + // Define document + + var document = new PdfDocument(); + document.Info.Title = "ticket"; + document.Info.Author = "auto.bus"; + + var doc = new Document(); + doc.DefaultPageSetup.LeftMargin = Unit.FromCentimeter(1); + doc.DefaultPageSetup.RightMargin = Unit.FromCentimeter(1); + doc.DefaultPageSetup.MirrorMargins = true; + + DefineStyles(doc); + CreatePage(doc); + FillPage(doc); + + var docRender = new DocumentRenderer(doc); + docRender.PrepareDocument(); + + var pageCount = docRender.FormattedDocument.PageCount; + for (int i = 0; i < pageCount; i++) + { + var pdfPage = document.AddPage(); + pdfPage.Size = PageSize.A4; + + using var gfx = XGraphics.FromPdfPage(pdfPage); + + // HACK² + gfx.MUH = PdfFontEncoding.Unicode; + + docRender.RenderPage(gfx, i + 1); + } + + // Save document + + var memoryStream = new MemoryStream(); + document.Save(memoryStream); + + return (true, null, memoryStream); + + void DefineStyles(Document doc) + { + var styles = doc.Styles["Normal"]; + styles.Font.Name = "Courier New Cyr"; + styles.ParagraphFormat.LineSpacingRule = LineSpacingRule.OnePtFive; + + styles = doc.Styles.AddStyle("Table", "Normal"); + styles.Font.Size = 10; + styles.ParagraphFormat.SpaceBefore = 2.5; + styles.ParagraphFormat.SpaceAfter = 2.5; + styles.ParagraphFormat.LineSpacingRule = LineSpacingRule.Single; + } + + void CreatePage(Document doc) + { + var section = doc.AddSection(); + + // Create footer + // var paragraph = section.Footers.Primary.AddParagraph(); + // paragraph.Format.Alignment = ParagraphAlignment.Center; + // paragraph.AddPageField(); + // section.Footers. + // section.PageSetup.DifferentFirstPageHeaderFooter = true; + + + var paragraph = section.AddParagraph("auto.bus"); + paragraph.Format.Font.Size = 20; + paragraph.Format.Font.Bold = true; + paragraph.Format.LineSpacingRule = LineSpacingRule.OnePtFive; + paragraph.Format.Alignment = ParagraphAlignment.Center; + paragraph.Format.SpaceBefore = Unit.FromCentimeter(5); + paragraph.Format.SpaceAfter = Unit.FromCentimeter(5); + + paragraph = section.AddParagraph("Фінансовий звіт"); + paragraph.Format.Font.Size = 20; + paragraph.Format.LineSpacingRule = LineSpacingRule.OnePtFive; + paragraph.Format.Alignment = ParagraphAlignment.Center; + + paragraph = section.AddParagraph($"{dbCompany.Name}"); + paragraph.Format.Font.Size = 20; + paragraph.Format.LineSpacingRule = LineSpacingRule.OnePtFive; + paragraph.Format.Alignment = ParagraphAlignment.Center; + paragraph.Format.SpaceAfter = Unit.FromCentimeter(5); + + paragraph = section.AddParagraph($"{fromDate:dd.MM.yyyy} – {toDate:dd.MM.yyyy}"); + paragraph.Format.Font.Size = 20; + paragraph.Format.LineSpacingRule = LineSpacingRule.OnePtFive; + paragraph.Format.Alignment = ParagraphAlignment.Center; + + Table table; + + // Section and table for each enrolled route + for (int i = 0; i < routesEnrolled.Count; i++) + { + section = doc.AddSection(); + + // Add table and define columns + table = section.AddTable(); + table.Style = "Table"; + table.Borders.Color = Colors.Black; + table.Borders.Width = 0.25; + table.Borders.Left.Width = 0.5; + table.Borders.Right.Width = 0.5; + table.Rows.LeftIndent = 0; + table.Rows.Height = Unit.FromPoint(15); + table.Rows.VerticalAlignment = VerticalAlignment.Center; + + for (int j = 0; j < 12; j++) + { + var column = table.AddColumn(Unit.FromCentimeter(1.583)); + column.Format.Alignment = ParagraphAlignment.Center; + } + } + + // Section for total + section = doc.AddSection(); + } + + void FillPage(Document doc) + { + Section section; + Paragraph paragraph; + + int i = 1; + foreach (var route in routesEnrolled) + { + section = doc.Sections[i]; + paragraph = section.Footers.Primary.AddParagraph(); + paragraph.AddPageField(); + paragraph.Format.Alignment = ParagraphAlignment.Center; + + var table = section.LastTable; + + var row = table.AddRow(); + + row.Cells[0].MergeRight = 11; + row.Cells[0].AddParagraph($"МАРШРУТ №{route.Id}"); + row.Cells[0].Format.Font.Bold = true; + + row = table.AddRow(); + + row.Cells[0].MergeRight = 1; + row.Cells[0].AddParagraph("Відправлення"); + + row.Cells[2].MergeRight = 3; + row.Cells[2].AddParagraph($"{route.RouteAddresses.First().Address.GetFullName()}"); + + row.Cells[6].MergeRight = 1; + row.Cells[6].AddParagraph("Прибуття"); + + row.Cells[8].MergeRight = 3; + row.Cells[8].AddParagraph($"{route.RouteAddresses.Last().Address.GetFullName()}"); + + row = table.AddRow(); + + row.Cells[0].MergeRight = 11; + row.Cells[0].AddParagraph("КОРОТКО"); + row.Cells[0].Format.Font.Bold = true; + + row = table.AddRow(); + + row.Cells[0].MergeRight = 1; + row.Cells[0].AddParagraph("Поїздок проведено"); + + row.Cells[2].MergeRight = 1; + row.Cells[2].AddParagraph("Поїздок скасовано"); + + row.Cells[4].MergeRight = 1; + row.Cells[4].AddParagraph("Квитків продано"); + + row.Cells[6].MergeRight = 1; + row.Cells[6].AddParagraph("Неповних квитків"); + + row.Cells[8].MergeRight = 1; + row.Cells[8].AddParagraph("Грошей зароблено"); + + row.Cells[10].MergeRight = 1; + row.Cells[10].AddParagraph("Середній рейтинг"); + + row = table.AddRow(); + row.Shading.Color = Color.FromRgbColor(25, Colors.Black); + + row.Cells[0].MergeRight = 1; + row.Cells[0].AddParagraph($"{GetRouteEnrollmentCount(route)}"); + + row.Cells[2].MergeRight = 1; + row.Cells[2].AddParagraph($"{GetRouteCanceledEnrollmentCount(route)}"); + + row.Cells[4].MergeRight = 1; + row.Cells[4].AddParagraph($"{GetRouteSelledTicketCount(route)}"); + + row.Cells[6].MergeRight = 1; + row.Cells[6].AddParagraph($"{GetRouteIndirectTicketCount(route)}"); + + row.Cells[8].MergeRight = 1; + row.Cells[8].AddParagraph($"{GetRouteTotalRevenu(route)}"); + + row.Cells[10].MergeRight = 1; + var routeAverageRating = GetRouteAvarageRating(route); + row.Cells[10].AddParagraph($"{(routeAverageRating == 0 ? "-" : routeAverageRating)}"); + + row = table.AddRow(); + + row.Cells[0].MergeRight = 11; + row.Cells[0].AddParagraph("ДОКЛАДНО"); + row.Cells[0].Format.Font.Bold = true; + + row = table.AddRow(); + + row.Cells[0].MergeRight = 2; + row.Cells[0].AddParagraph("Ідентифікатор, тип та номер транспорту"); + + row.Cells[3].MergeRight = 1; + row.Cells[3].AddParagraph("Поїздок заплановано, проведена та скасовано"); + + row.Cells[5].MergeRight = 2; + row.Cells[5].AddParagraph("Квитків продано та повернено, з яких неповні"); + + row.Cells[8].MergeRight = 1; + row.Cells[8].AddParagraph("Грошей зароблено"); + + row.Cells[10].MergeRight = 1; + row.Cells[10].AddParagraph("Середній рейтинг"); + + var isFilled = true; + foreach (var vehicle in vehicleEnrolled) + { + if (route.VehicleEnrollments.Count(ve => + ve.VehicleId == vehicle.Id) == 0) + { + continue; + } + + row = table.AddRow(); + row.Shading.Color = isFilled ? Color.FromRgbColor(25, Colors.Black) : Colors.White; + isFilled = !isFilled; + + row.Cells[0].MergeRight = 2; + row.Cells[0].AddParagraph($"{vehicle.Id}, {vehicle.Type}, {vehicle.Number}"); + + var executedEnrollmentCount = GetVehicleEnrollmentCount(vehicle, route.Id); + var canceledEnrollmentCount = GetVehicleCanceledEnrollmentCount(vehicle, route.Id); + row.Cells[3].MergeRight = 1; + row.Cells[3].AddParagraph($"{executedEnrollmentCount + canceledEnrollmentCount}, " + + $"{executedEnrollmentCount}, {canceledEnrollmentCount}"); + + row.Cells[5].MergeRight = 2; + row.Cells[5].AddParagraph($"{GetVehicleSelledTicketCount(vehicle, route.Id)}, {GetVehicleReturnedTicketCount(vehicle, route.Id)}; " + + $"{GetVehicleIndirectTicketCount(vehicle, route.Id)}, {GetVehicleReturnedIndirectTicketCount(vehicle, route.Id)}"); + + row.Cells[8].MergeRight = 1; + row.Cells[8].AddParagraph($"{GetVehicleTotalRevenue(vehicle, route.Id)}"); + + row.Cells[10].MergeRight = 1; + var vehicleAverageRating = GetVehicleAverageRating(vehicle, route.Id); + row.Cells[10].AddParagraph($"{(vehicleAverageRating == 0 ? "-" : vehicleAverageRating)}"); + } + + i++; + } + + section = doc.Sections[doc.Sections.Count - 1]; + + paragraph = section.AddParagraph("ПІДСУМОК"); + paragraph.Format.Alignment = ParagraphAlignment.Center; + paragraph.Format.Font.Size = 14; + paragraph.Format.Font.Bold = true; + section.AddParagraph(); + + var totalEnrollmentCount = GetTotalEnrollmentCount(dbCompany); + var totalCanceledEnrollmentCount = GetTotalCanceledEnrollmentCount(dbCompany); + var totalSoldTicketCount = GetTotalSoldTickets(dbCompany); + var totalReturnedTicketCount = GetTotalReturnedTicketCount(dbCompany); + var totalRevenue = GetTotalRevenu(dbCompany); + var totalAverageRating = GetTotalAverageRating(dbCompany); + + paragraph = section.AddParagraph( + $"У період з {fromDate:dd.MM.yyyy} по {toDate:dd.MM.yyyy} " + + $"({(toDate - fromDate).Days} днів) компанією {dbCompany.Name} " + + $"було заплановано {(totalEnrollmentCount)} поїздки, " + + $"з яких {totalCanceledEnrollmentCount} було скасовано, " + + $"продано {totalSoldTicketCount} квитків, " + + $"з яких {totalReturnedTicketCount} було повернено. " + + $"За цей час було зароблено {totalRevenue} гривень. " + + $"Середній рейтинг по всім поїздкам: {totalAverageRating}"); + paragraph.Format.Alignment = ParagraphAlignment.Justify; + paragraph.Format.Font.Size = 14; + + int GetRouteEnrollmentCount(Route route) + { + return route.VehicleEnrollments.Count(ve => !ve.IsCanceled); + } + + int GetVehicleEnrollmentCount(Vehicle vehicle, int routeId) + { + return vehicle.VehicleEnrollments.Count(ve => + !ve.IsCanceled && ve.RouteId == routeId); + } + + int GetTotalEnrollmentCount(Company company) + { + int result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleEnrollmentCount(vehicle, enrollment.RouteId); + } + } + + return result; + } + + int GetRouteCanceledEnrollmentCount(Route route) + { + return route.VehicleEnrollments.Count(ve => ve.IsCanceled); + } + + int GetVehicleCanceledEnrollmentCount(Vehicle vehicle, int routeId) + { + return vehicle.VehicleEnrollments.Count(ve => + ve.IsCanceled && ve.RouteId == routeId); + } + + int GetTotalCanceledEnrollmentCount(Company company) + { + int result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleCanceledEnrollmentCount(vehicle, enrollment.RouteId); + } + } + + return result; + } + + int GetRouteSelledTicketCount(Route route) + { + int result = 0; + + foreach (var enrollment in route.VehicleEnrollments) + { + result += enrollment.Tickets.Count(t => !t.IsReturned); + } + + return result; + } + + int GetVehicleSelledTicketCount(Vehicle vehicle, int routeId) + { + int result = 0; + + foreach (var enrollment in vehicle.VehicleEnrollments + .Where(ve => ve.RouteId == routeId)) + { + result += enrollment.Tickets.Count(t => !t.IsReturned); + } + + return result; + } + + int GetTotalSoldTickets(Company company) + { + int result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleSelledTicketCount(vehicle, enrollment.RouteId); + } + } + + return result; + } + + int GetVehicleReturnedTicketCount(Vehicle vehicle, int routeId) + { + int result = 0; + + foreach (var enrollment in vehicle.VehicleEnrollments + .Where(ve => ve.RouteId == routeId)) + { + result += enrollment.Tickets.Count(t => t.IsReturned); + } + + return result; + } + + int GetTotalReturnedTicketCount(Company company) + { + int result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleReturnedTicketCount(vehicle, enrollment.RouteId); + } + } + + return result; + } + + int GetRouteIndirectTicketCount(Route route) + { + int result = 0; + + int departureAddressId = route.RouteAddresses.First().AddressId; + int arrivalAddressId = route.RouteAddresses.Last().AddressId; + + foreach (var enrollment in route.VehicleEnrollments) + { + result += enrollment.Tickets.Count(t => !t.IsReturned && + t.FirstRouteAddressId != departureAddressId || + t.LastRouteAddressId != arrivalAddressId); + } + + return result; + } + + int GetVehicleIndirectTicketCount(Vehicle vehicle, int routeId) + { + int result = 0; + + foreach (var enrollment in vehicle.VehicleEnrollments + .Where(ve => ve.RouteId == routeId)) + { + var departureRouteAddressId = enrollment.Route.RouteAddresses.First().AddressId; + var arrivalRouteAddressId = enrollment.Route.RouteAddresses.Last().AddressId; + + result += enrollment.Tickets.Count(t => !t.IsReturned && + t.FirstRouteAddressId != departureRouteAddressId || + t.LastRouteAddressId != arrivalRouteAddressId); + } + + return result; + } + + int GetTotalIndirectTicketCount(Company company) + { + int result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleIndirectTicketCount(vehicle, enrollment.RouteId); + } + } + + return result; + } + + int GetVehicleReturnedIndirectTicketCount(Vehicle vehicle, int routeId) + { + int result = 0; + + foreach (var enrollment in vehicle.VehicleEnrollments + .Where(ve => ve.RouteId == routeId)) + { + var departureRouteAddressId = enrollment.Route.RouteAddresses.First().AddressId; + var arrivalRouteAddressId = enrollment.Route.RouteAddresses.Last().AddressId; + + result += enrollment.Tickets.Count(t => t.IsReturned && + (t.FirstRouteAddressId != departureRouteAddressId || + t.LastRouteAddressId != arrivalRouteAddressId)); + } + + return result; + } + + int GetTotalReturnedIndirectTicketCount(Company company) + { + int result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleReturnedIndirectTicketCount(vehicle, enrollment.RouteId); + } + } + + return result; + } + + double GetRouteTotalRevenu(Route route) + { + double result = 0; + + foreach (var enrollment in route.VehicleEnrollments) + { + foreach (var ticket in enrollment.Tickets) + { + result += GetTicketCost(ticket); + } + } + + return result; + } + + double GetVehicleTotalRevenue(Vehicle vehicle, int routeId) + { + double result = 0; + + foreach (var enrollment in vehicle.VehicleEnrollments + .Where(ve => ve.RouteId == routeId)) + { + foreach (var ticket in enrollment.Tickets) + { + result += GetTicketCost(ticket); + } + } + + return result; + } + + double GetTotalRevenu(Company company) + { + double result = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + result += GetVehicleTotalRevenue(vehicle, enrollment.RouteId); + } + } + + return result; + } + + double GetRouteAvarageRating(Route route) + { + double result = 0; + int reviewCount = 0; + + foreach (var enrollment in route.VehicleEnrollments) + { + if (enrollment.Reviews.Count == 0) + { + continue; + } + + foreach (var review in enrollment.Reviews) + { + result += review.Rating; + reviewCount += enrollment.Reviews.Count; + } + } + + result /= reviewCount; + result = !Double.IsNaN(result) ? Math.Round(result, 3) : 0; + + return result; + } + + double GetVehicleAverageRating(Vehicle vehicle, int routeId) + { + double result = 0; + int reviewCount = 0; + + foreach (var enrollment in vehicle.VehicleEnrollments + .Where(ve => ve.RouteId == routeId)) + { + reviewCount += enrollment.Reviews.Count; + + foreach (var review in enrollment.Reviews) + { + result += review.Rating; + } + } + + result /= reviewCount; + result = !Double.IsNaN(result) ? Math.Round(result, 3) : 0; + + return Math.Round(result, 3); + } + + double GetTotalAverageRating(Company company) + { + double result = 0; + int enrollmentCount = 0; + + foreach (var vehicle in company.Vehicles) + { + foreach (var enrollment in vehicle.VehicleEnrollments) + { + if (enrollment.Reviews.Count == 0) + { + continue; + } + + result += GetVehicleAverageRating(vehicle, enrollment.RouteId); + enrollmentCount++; + } + } + + result /= enrollmentCount; + result = !Double.IsNaN(result) ? Math.Round(result, 3) : 0; + + return result; + } + + // TODO: repeated function, yoinked from ticket generation + double GetTicketCost(Ticket ticket) + { + double cost = 0; + + var routeAddresses = ticket.VehicleEnrollment.Route.RouteAddresses + .OrderBy(ra => ra.Order) + .SkipWhile(ra => ra.AddressId != ticket.FirstRouteAddressId) + .TakeWhile(ra => ra.AddressId != ticket.LastRouteAddressId) + .ToArray(); + + foreach (var routeAddress in routeAddresses) + { + var details = routeAddress.RouteAddressDetails + .First(rad => rad.RouteAddressId == routeAddress.Id); + + cost += details.CostToNextCity; + } + + return cost; + } + } + + async Task DoesCompanyExist(int id) + { + return await _dbContext.Companies.AnyAsync(c => c.Id == id); + } } } \ No newline at end of file diff --git a/Server/Services/RouteManagementService.cs b/Server/Services/RouteManagementService.cs index ccbed95..b1d0796 100644 --- a/Server/Services/RouteManagementService.cs +++ b/Server/Services/RouteManagementService.cs @@ -49,8 +49,6 @@ public class RouteManagementService : IRouteManagementService { var route = _mapper.Map(createRouteWithAddressesDto); - - await _dbContext.Routes.AddAsync(route); await _dbContext.SaveChangesAsync(); @@ -78,9 +76,9 @@ public class RouteManagementService : IRouteManagementService { shapedData = _routeSortHelper.ApplySort(shapedData, parameters.Sort); } - catch (Exception e) + catch (Exception) { - return (false, new BadRequestObjectResult("Invalid sorting string"), null, null)!; + return (false, new BadRequestObjectResult("Invalid sorting string"), null!, null!); } var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber, @@ -133,7 +131,7 @@ public class RouteManagementService : IRouteManagementService { shapedData = _routeSortHelper.ApplySort(shapedData, parameters.Sort); } - catch (Exception e) + catch (Exception) { return (false, new BadRequestObjectResult("Invalid sorting string"), null, null)!; } diff --git a/Server/Services/StatisticsService.cs b/Server/Services/StatisticsService.cs index 8a3e26a..5167b86 100644 --- a/Server/Services/StatisticsService.cs +++ b/Server/Services/StatisticsService.cs @@ -1,9 +1,9 @@ using System.Dynamic; using AutoMapper; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Server.Data; using Server.Helpers; -using Server.Models; using SharedModels.DataTransferObjects; using SharedModels.QueryParameters; using SharedModels.QueryParameters.Statistics; @@ -17,11 +17,14 @@ public class StatisticsService : IStatisticsService private readonly IDataShaper _userDataShaper; private readonly IDataShaper _companyDataShaper; private readonly IDataShaper _addressDataShaper; + private readonly IDataShaper _expandoDataShaper; private readonly IPager _pager; + private readonly ISortHelper _sortHelper; public StatisticsService(ApplicationDbContext dbContext, IMapper mapper, IDataShaper userDataShaper, IDataShaper companyDataShaper, - IDataShaper addressDataShaper, IPager pager) + IDataShaper addressDataShaper, IPager pager, + IDataShaper expandoDataShaper, ISortHelper sortHelper) { _dbContext = dbContext; _mapper = mapper; @@ -29,25 +32,131 @@ public class StatisticsService : IStatisticsService _companyDataShaper = companyDataShaper; _addressDataShaper = addressDataShaper; _pager = pager; + _expandoDataShaper = expandoDataShaper; + _sortHelper = sortHelper; } // Popularity is measured in number of purchased tickets - public async Task<(bool IsSucceed, string? message, IEnumerable route)> - GetPopularRoutes(int amount) + public async Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable route, + PagingMetadata pagingMetadata)> + GetPopularRoutes(PopularRoutesParameters parameters) { - throw new NotImplementedException(); + parameters.Days ??= parameters.DefaultDays; + var fromDateUtc = DateTime.UtcNow.Date - TimeSpan.FromDays((double) parameters.Days); + + var dbTicketGroupsArray = await _dbContext.TicketGroups + .Include(tg => tg.Tickets) + .Where(tg => tg.Tickets.First().PurchaseDateTimeUtc >= fromDateUtc) + .ToArrayAsync(); + + var depArrCombCountDict = new Dictionary<(int, int), int>(); + + foreach (var tg in dbTicketGroupsArray) + { + // TODO: implement ticket ordering + var departureAddress = tg.Tickets.OrderBy(t => t.Id).First().FirstRouteAddressId; + var arrivalAddress = tg.Tickets.OrderBy(t => t.Id).Last().LastRouteAddressId; + + if (!depArrCombCountDict.ContainsKey((departureAddress, arrivalAddress))) + { + depArrCombCountDict.Add((departureAddress, arrivalAddress), 1); + } + else + { + depArrCombCountDict[(departureAddress, arrivalAddress)] += 1; + } + } + + depArrCombCountDict = depArrCombCountDict + .OrderByDescending(a => a.Value) + .Take(parameters.Amount) + .ToDictionary(x => x.Key, x => x.Value); + + var addressIds = new List(); + foreach (var depArrAddressIds in depArrCombCountDict.Keys) + { + var (departureAddressId, arrivalAddressId) = depArrAddressIds; + + if (!addressIds.Contains(departureAddressId)) + { + addressIds.Add(departureAddressId); + } + + if (!addressIds.Contains(arrivalAddressId)) + { + addressIds.Add(arrivalAddressId); + } + } + + var dbAddressArray = await _dbContext.Addresses + .Include(a => a.City) + .ThenInclude(c => c.State) + .ThenInclude(s => s.Country) + .Where(a => addressIds.Contains(a.Id)) + .ToArrayAsync(); + + var derArrCount = new List(); + + foreach (var depArrCount in depArrCombCountDict) + { + var obj = new ExpandoObject(); + + var departureAddressId = depArrCount.Key.Item1; + var departureAddress = dbAddressArray + .First(a => a.Id == departureAddressId); + var arrivalAddressId = depArrCount.Key.Item2; + var arrivalAddress = dbAddressArray + .First(a => a.Id == arrivalAddressId); + var count = depArrCount.Value; + + var fields = parameters.Fields!.Split(','); + + if (fields.Any(f => f.ToLower() == "departureAddressId".ToLower())) + obj.TryAdd("departureAddressId", departureAddressId); + if (fields.Any(f => f.ToLower() == "departureAddress".ToLower())) + obj.TryAdd("departureAddress", departureAddress.GetFullName()); + if (fields.Any(f => f.ToLower() == "arrivalAddressId".ToLower())) + obj.TryAdd("arrivalAddressId", arrivalAddressId); + if (fields.Any(f => f.ToLower() == "arrivalAddress".ToLower())) + obj.TryAdd("arrivalAddress", arrivalAddress.GetFullName()); + if (fields.Any(f => f.ToLower() == "count".ToLower())) + obj.TryAdd("count", count); + + derArrCount.Add(obj); + } + + var n = derArrCount.Count; + for (int i = 0; i < n - 1; i++) + { + for (int j = 0; j < n - i - 1; j++) + { + if ((int) (derArrCount[j] as IDictionary)["count"] > (int) (derArrCount[j + 1] as IDictionary)["count"]) + { + // swap temp and arr[i] + (derArrCount[j], derArrCount[j + 1]) = (derArrCount[j + 1], derArrCount[j]); + (derArrCount[j], derArrCount[j + 1]) = (derArrCount[j + 1], derArrCount[j]); + } + } + } + + var result = derArrCount.AsQueryable(); + + var pagingMetadata = _pager.ApplyPaging(ref result, + parameters.PageNumber, parameters.PageSize); + + return (true, null, result.AsEnumerable(), pagingMetadata); } // Engagement is measured in number of purchases made in past N days // One purchase contains one (direct route) or more (route with transfers) tickets - public async Task<(bool IsSucceed, string? message, IEnumerable users, + public async Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable users, PagingMetadata pagingMetadata)> GetEngagedUsers(EngagedUserParameters parameters) { - var fromDateUtc = - DateTime.UtcNow - TimeSpan.FromDays(parameters.Days ?? parameters.DefaultDays); + parameters.Days ??= parameters.DefaultDays; + var fromDateUtc = DateTime.UtcNow.Date - TimeSpan.FromDays((double) parameters.Days); - var resultObjects = _dbContext.Users + var dbUsers = _dbContext.Users .Include(u => u.TicketGroups) .ThenInclude(tg => tg.Tickets) .Select(u => new @@ -57,10 +166,9 @@ public class StatisticsService : IStatisticsService tg.Tickets.First().PurchaseDateTimeUtc >= fromDateUtc) }) .OrderByDescending(o => o.TicketGroups.Count()) - .Take(parameters.Amount); - + .Take(parameters.Amount) + .Select(i => i.User); - var dbUsers = resultObjects.Select(i => i.User); var userDtos = _mapper.ProjectTo(dbUsers).ToArray(); var shapedDataArray = _userDataShaper .ShapeData(userDtos, parameters.Fields ?? parameters.DefaultFields) @@ -86,7 +194,7 @@ public class StatisticsService : IStatisticsService } // Popularity is measured in average rating of all VehicleEnrollments of a company - public async Task<(bool IsSucceed, string? message, IEnumerable companies, + public async Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable companies, PagingMetadata pagingMetadata)> GetPopularCompanies(PopularCompanyParameters parameters) { @@ -105,28 +213,22 @@ public class StatisticsService : IStatisticsService for (int i = 0; i < dbCompaniesArray.Length; i++) { - double tempC = 0; + double result = 0; + int reviewCount = 0; foreach (var v in dbCompaniesArray[i].Vehicles) { - double tempV = 0; - foreach (var ve in v.VehicleEnrollments) { - double tempVE = 0; - foreach (var r in ve.Reviews) { - tempVE += r.Rating; + result += r.Rating; + reviewCount++; } - - tempV += tempVE / ve.Reviews.Count; } - - tempC += tempV / v.VehicleEnrollments.Count; } - companiesAvgRatings[i] = tempC / dbCompaniesArray[i].Vehicles.Count; + companiesAvgRatings[i] = reviewCount != 0 ? Math.Round(result / reviewCount, 5) : 0; } // Sort companiesAvgRatings and apply the same sorting to dbCompaniesArray @@ -176,14 +278,12 @@ public class StatisticsService : IStatisticsService } // Popularity is measured in number tickets in which the address is the first or last station - public async Task<(bool IsSucceed, string? message, IEnumerable stations, + public async Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable stations, PagingMetadata pagingMetadata)> GetPopularStations(PopularAddressesParameters parameters) { - // throw new NotImplementedException(); - - var fromDateUtc = - DateTime.UtcNow - TimeSpan.FromDays(parameters.Days ?? parameters.DefaultDays); + parameters.Days ??= parameters.DefaultDays; + var fromDateUtc = DateTime.UtcNow.Date - TimeSpan.FromDays((double) parameters.Days); var dbTicketGroupsArray = await _dbContext.TicketGroups .Include(tg => tg.Tickets) @@ -195,22 +295,25 @@ public class StatisticsService : IStatisticsService foreach (var tg in dbTicketGroupsArray) { - if (!addressCountDict.ContainsKey(tg.Tickets.First().FirstRouteAddressId)) + // TODO: implement ticket ordering + var tickets = tg.Tickets.OrderBy(t => t.Id).ToArray(); + + if (!addressCountDict.ContainsKey(tickets.First().FirstRouteAddressId)) { - addressCountDict.Add(tg.Tickets.First().FirstRouteAddressId, 1); + addressCountDict.Add(tickets.First().FirstRouteAddressId, 1); } else { - addressCountDict[tg.Tickets.First().FirstRouteAddressId] += 1; + addressCountDict[tickets.First().FirstRouteAddressId] += 1; } - if (!addressCountDict.ContainsKey(tg.Tickets.Last().LastRouteAddressId)) + if (!addressCountDict.ContainsKey(tickets.Last().LastRouteAddressId)) { - addressCountDict.Add(tg.Tickets.Last().LastRouteAddressId, 1); + addressCountDict.Add(tickets.Last().LastRouteAddressId, 1); } else { - addressCountDict[tg.Tickets.Last().LastRouteAddressId] += 1; + addressCountDict[tickets.Last().LastRouteAddressId] += 1; } } @@ -228,6 +331,9 @@ public class StatisticsService : IStatisticsService // Get top addresses from database ordered by Id (same as // addressIds addressCountDict and ) var dbAddressesArray = await _dbContext.Addresses + .Include(a => a.City) + .ThenInclude(c => c.State) + .ThenInclude(s => s.Country) .Where(a => addressIds.Any(id => a.Id == id)) .OrderBy(a => a.Id).ToArrayAsync(); @@ -238,11 +344,12 @@ public class StatisticsService : IStatisticsService { for (int j = 0; j < n - i - 1; j++) { - if (addressCountArray[j] > addressCountArray[j + 1]) + if (addressCountArray[j] > addressCountArray[j + 1]) { - // swap temp and arr[i] - (addressCountArray[j], addressCountArray[j + 1]) = (addressCountArray[j + 1], addressCountArray[j]); - (dbAddressesArray[j], dbAddressesArray[j + 1]) = (dbAddressesArray[j + 1], dbAddressesArray[j]); + (addressCountArray[j], addressCountArray[j + 1]) = + (addressCountArray[j + 1], addressCountArray[j]); + (dbAddressesArray[j], dbAddressesArray[j + 1]) = + (dbAddressesArray[j + 1], dbAddressesArray[j]); } } } diff --git a/Server/Services/TicketGroupManagementService.cs b/Server/Services/TicketGroupManagementService.cs new file mode 100644 index 0000000..dfb7f77 --- /dev/null +++ b/Server/Services/TicketGroupManagementService.cs @@ -0,0 +1,220 @@ +using System.Dynamic; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Server.Data; +using Server.Helpers; +using Server.Models; +using SharedModels.DataTransferObjects; +using SharedModels.QueryParameters; +using SharedModels.QueryParameters.Objects; + +namespace Server.Services; + +public class TicketGroupManagementService : ITicketGroupManagementService +{ + private readonly ApplicationDbContext _dbContext; + private readonly IMapper _mapper; + private readonly ISortHelper _ticketGroupSortHelper; + private readonly IDataShaper _ticketGroupDataShaper; + private readonly IDataShaper _ticketGroupWithTicketsDataShaper; + private readonly IPager _pager; + + public TicketGroupManagementService(ApplicationDbContext dbContext, IMapper mapper, + ISortHelper ticketGroupSortHelper, IDataShaper ticketGroupDataShaper, + IDataShaper ticketGroupWithTicketsDataShaper, IPager pager) + { + _dbContext = dbContext; + _mapper = mapper; + _ticketGroupSortHelper = ticketGroupSortHelper; + _ticketGroupDataShaper = ticketGroupDataShaper; + _ticketGroupWithTicketsDataShaper = ticketGroupWithTicketsDataShaper; + _pager = pager; + } + + public async Task<(bool isSucceed, IActionResult? actionResult, TicketGroupDto ticketGroup)> AddTicketGroup(CreateTicketGroupDto createTicketGroupDto) + { + var ticketGroup = _mapper.Map(createTicketGroupDto); + + await _dbContext.TicketGroups.AddAsync(ticketGroup); + await _dbContext.SaveChangesAsync(); + + return (true, null, _mapper.Map(ticketGroup)); + } + + public async Task<(bool isSucceed, IActionResult? actionResult, TicketGroupWithTicketsDto ticketGroup)> AddTicketGroupWithTickets( + CreateTicketGroupWithTicketsDto createTicketGroupWithTicketsDto) + { + var ticketGroup = _mapper.Map(createTicketGroupWithTicketsDto); + + await _dbContext.TicketGroups.AddAsync(ticketGroup); + await _dbContext.SaveChangesAsync(); + + ticketGroup = await _dbContext.TicketGroups + .Include(tg => tg.Tickets) + .FirstAsync(tg => tg.Id == ticketGroup.Id); + + return (true, null, _mapper.Map(ticketGroup)); + } + + public async Task<(bool isSucceed, IActionResult? actionResult, IEnumerable ticketGroups, + PagingMetadata pagingMetadata)> + GetTicketGroups(TicketGroupParameters parameters) + { + var dbTicketGroups = _dbContext.TicketGroups.AsQueryable(); + + FilterTicketGroupsByUserId(ref dbTicketGroups, parameters.UserId); + + var ticketGroupDtos = _mapper.ProjectTo(dbTicketGroups); + var shapedData = _ticketGroupDataShaper.ShapeData(ticketGroupDtos, parameters.Fields).AsQueryable(); + + try + { + shapedData = _ticketGroupSortHelper.ApplySort(shapedData, parameters.Sort); + } + catch (Exception) + { + return (false, new BadRequestObjectResult("Invalid sorting string"), null!, null!); + } + + var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber, + parameters.PageSize); + + return (true, null, shapedData, pagingMetadata); + + void FilterTicketGroupsByUserId(ref IQueryable ticketGroups, + string? userId) + { + if (!ticketGroups.Any() || String.IsNullOrWhiteSpace(userId)) + { + return; + } + + ticketGroups = ticketGroups.Where(tg => tg.UserId == userId); + } + } + + public async Task<(bool isSucceed, IActionResult? actionResult, IEnumerable ticketGroups, + PagingMetadata pagingMetadata)> + GetTicketGroupsWithTickets(TicketGroupWithTicketsParameters parameters) + { + var dbTicketGroups = _dbContext.TicketGroups + .Include(tg => tg.Tickets) + .AsQueryable(); + + FilterTicketGroupsByUserId(ref dbTicketGroups, parameters.UserId); + + var ticketGroupDtos = _mapper.ProjectTo(dbTicketGroups); + var shapedData = _ticketGroupWithTicketsDataShaper.ShapeData(ticketGroupDtos, parameters.Fields).AsQueryable(); + + try + { + shapedData = _ticketGroupSortHelper.ApplySort(shapedData, parameters.Sort); + } + catch (Exception) + { + return (false, new BadRequestObjectResult("Invalid sorting string"), null, null)!; + } + + var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber, + parameters.PageSize); + + return (true, null, shapedData, pagingMetadata); + + void FilterTicketGroupsByUserId(ref IQueryable ticketGroups, + string? userId) + { + if (!ticketGroups.Any() || String.IsNullOrWhiteSpace(userId)) + { + return; + } + + ticketGroups = ticketGroups.Where(tg => tg.UserId == userId); + } + } + + public async Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject ticketGroup)> GetTicketGroup(int id, string? fields) + { + if (!await IsTicketGroupExist(id)) + { + return (false, new NotFoundResult(), null)!; + } + + var dbTicketGroup = await _dbContext.TicketGroups.Where(tg => tg.Id == id) + .FirstAsync(); + + if (String.IsNullOrWhiteSpace(fields)) + { + fields = RouteParameters.DefaultFields; + } + + var ticketGroupDto = _mapper.Map(dbTicketGroup); + var shapedRouteData = _ticketGroupDataShaper.ShapeData(ticketGroupDto, fields); + + return (true, null, shapedRouteData); + } + + public async Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject ticketGroup)> GetTicketGroupWithTickets(int id, string? fields) + { + if (!await IsTicketGroupExist(id)) + { + return (false, new NotFoundResult(), null)!; + } + + var dbTicketGroup = await _dbContext.TicketGroups.Where(tg => tg.Id == id) + .Include(tg => tg.Tickets) + .FirstAsync(); + + if (String.IsNullOrWhiteSpace(fields)) + { + fields = RouteParameters.DefaultFields; + } + + var ticketGroupDto = _mapper.Map(dbTicketGroup); + var shapedRouteData = _ticketGroupDataShaper.ShapeData(ticketGroupDto, fields); + + return (true, null, shapedRouteData); + } + + public async Task<(bool isSucceed, IActionResult? actionResult, UpdateTicketGroupDto ticketGroup)> UpdateTicketGroup(UpdateTicketGroupDto updateTicketGroupDto) + { + var ticketGroup = _mapper.Map(updateTicketGroupDto); + _dbContext.Entry(ticketGroup).State = EntityState.Modified; + + try + { + await _dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await IsTicketGroupExist(updateTicketGroupDto.Id)) + { + return (false, new NotFoundResult(), null)!; + } + } + + var dbTicketGroup = await _dbContext.TicketGroups.FirstAsync(r => r.Id == ticketGroup.Id); + + return (true, null, _mapper.Map(dbTicketGroup)); + } + + public async Task<(bool isSucceed, IActionResult? actionResult)> DeleteTicketGroup(int id) + { + var dbTicketGroup = await _dbContext.TicketGroups.FirstOrDefaultAsync(tg => tg.Id == id); + + if (dbTicketGroup == null) + { + return (false, new NotFoundResult()); + } + + _dbContext.TicketGroups.Remove(dbTicketGroup); + await _dbContext.SaveChangesAsync(); + + return (true, null); + } + + public async Task IsTicketGroupExist(int id) + { + return await _dbContext.TicketGroups.AnyAsync(tg => tg.Id == id); + } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/AddressDto.cs b/SharedModels/DataTransferObjects/AddressDto.cs index 52206fc..be976e5 100644 --- a/SharedModels/DataTransferObjects/AddressDto.cs +++ b/SharedModels/DataTransferObjects/AddressDto.cs @@ -5,7 +5,7 @@ namespace SharedModels.DataTransferObjects; public class AddressDto : CreateAddressDto { public int Id { get; set; } - public string FullName = null!; + public string FullName { get; set; } = null!; } public class CreateAddressDto diff --git a/SharedModels/DataTransferObjects/TicketDto.cs b/SharedModels/DataTransferObjects/TicketDto.cs index db5056a..de345b7 100644 --- a/SharedModels/DataTransferObjects/TicketDto.cs +++ b/SharedModels/DataTransferObjects/TicketDto.cs @@ -15,10 +15,16 @@ public class TicketDto : CreateTicketDto public class CreateTicketDto { [Required] - public string UserId { get; set; } = null!; + public int TicketGroupId { get; set; } [Required] public int VehicleEnrollmentId { get; set; } + + [Required] + public int FirstRouteAddressId { get; set; } + + [Required] + public int LastRouteAddressId { get; set; } } public class UpdateTicketDto @@ -26,11 +32,36 @@ public class UpdateTicketDto [Required] public int Id { get; set; } - public string? UserId { get; set; } = null!; - public int? VehicleEnrollmentId { get; set; } + [Required] + public int TicketGroupId { get; set; } + [Required] + public int VehicleEnrollmentId { get; set; } + + [Required] [DataType(DataType.DateTime)] public DateTime PurchaseDateTimeUtc { get; set; } + + [Required] public bool IsReturned { get; set; } = false; + + [Required] public bool IsMissed { get; set; } = false; +} + +public class CreateInTicketGroupTicketDto +{ + [Required] + public int VehicleEnrollmentId { get; set; } + + [Required] + public int FirstRouteAddressId { get; set; } + + [Required] + public int LastRouteAddressId { get; set; } +} + +public class InTicketGroupTicketDto : CreateInTicketGroupTicketDto +{ + public int Id { get; set; } } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/TicketGroupDto.cs b/SharedModels/DataTransferObjects/TicketGroupDto.cs index e166997..4948fd8 100644 --- a/SharedModels/DataTransferObjects/TicketGroupDto.cs +++ b/SharedModels/DataTransferObjects/TicketGroupDto.cs @@ -1,6 +1,35 @@ +using System.ComponentModel.DataAnnotations; + namespace SharedModels.DataTransferObjects; -public class TicketGroupDto +public class TicketGroupDto : CreateTicketGroupDto { + public int Id { get; set; } +} + +public class CreateTicketGroupDto +{ + [Required] + public string UserId { get; set; } = null!; +} + +public class UpdateTicketGroupDto +{ + [Required] + public int Id { get; set; } + [Required] + public string UserId { get; set; } = null!; +} + +public class TicketGroupWithTicketsDto +{ + public int Id { get; set; } + public IList Tickets { get; set; } = null!; +} + +public class CreateTicketGroupWithTicketsDto : CreateTicketGroupDto +{ + [Required] + public IList Tickets { get; set; } = null!; } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/VehicleEnrollmentDto.cs b/SharedModels/DataTransferObjects/VehicleEnrollmentDto.cs index 9020513..963402f 100644 --- a/SharedModels/DataTransferObjects/VehicleEnrollmentDto.cs +++ b/SharedModels/DataTransferObjects/VehicleEnrollmentDto.cs @@ -36,9 +36,7 @@ public class CreateVehicleEnrollmentWithDetailsDto : CreateVehicleEnrollmentDto public IList RouteAddressDetails { get; set; } = null!; } -public class VehicleEnrollmentWithDetailsDto +public class VehicleEnrollmentWithDetailsDto : VehicleEnrollmentDto { - public int Id { get; set; } - public IList RouteAddressDetails { get; set; } = null!; } \ No newline at end of file diff --git a/SharedModels/QueryParameters/Objects/TicketGroupParameters.cs b/SharedModels/QueryParameters/Objects/TicketGroupParameters.cs new file mode 100644 index 0000000..6aeb624 --- /dev/null +++ b/SharedModels/QueryParameters/Objects/TicketGroupParameters.cs @@ -0,0 +1,13 @@ +namespace SharedModels.QueryParameters.Objects; + +public class TicketGroupParameters : ParametersBase +{ + public const string DefaultFields = "id,userId"; + + public TicketGroupParameters() + { + Fields = DefaultFields; + } + + public string? UserId { get; set; } +} \ No newline at end of file diff --git a/SharedModels/QueryParameters/Objects/TicketGroupWithTicketsParameters.cs b/SharedModels/QueryParameters/Objects/TicketGroupWithTicketsParameters.cs new file mode 100644 index 0000000..d2a89f5 --- /dev/null +++ b/SharedModels/QueryParameters/Objects/TicketGroupWithTicketsParameters.cs @@ -0,0 +1,13 @@ +namespace SharedModels.QueryParameters.Objects; + +public class TicketGroupWithTicketsParameters : ParametersBase +{ + public const string DefaultFields = "id,userId,tickets"; + + public TicketGroupWithTicketsParameters() + { + Fields = DefaultFields; + } + + public string? UserId { get; set; } +} \ No newline at end of file diff --git a/SharedModels/QueryParameters/Statistics/PopularAddressesParameters.cs b/SharedModels/QueryParameters/Statistics/PopularAddressesParameters.cs index 7fb684d..5125b8d 100644 --- a/SharedModels/QueryParameters/Statistics/PopularAddressesParameters.cs +++ b/SharedModels/QueryParameters/Statistics/PopularAddressesParameters.cs @@ -2,7 +2,7 @@ namespace SharedModels.QueryParameters.Statistics; public class PopularAddressesParameters : ParametersBase { - public readonly string DefaultFields = "id,name,purchaseCount"; + public readonly string DefaultFields = "id,name,fullName,purchaseCount"; public readonly int DefaultDays = 60; public PopularAddressesParameters() diff --git a/SharedModels/QueryParameters/Statistics/PopularRoutesParameters.cs b/SharedModels/QueryParameters/Statistics/PopularRoutesParameters.cs new file mode 100644 index 0000000..fde7bb4 --- /dev/null +++ b/SharedModels/QueryParameters/Statistics/PopularRoutesParameters.cs @@ -0,0 +1,16 @@ +namespace SharedModels.QueryParameters.Statistics; + +public class PopularRoutesParameters : ParametersBase +{ + public readonly string DefaultFields = "departureAddressId,departureAddress," + + "arrivalAddressId,arrivalAddress," + + "count"; + public readonly int DefaultDays = 60; + + public PopularRoutesParameters() + { + Fields = DefaultFields; + } + + public int? Days { get; set; } +} \ No newline at end of file