From 280d88321399c85b2d3d972511466940e6aa8f9e Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 30 Nov 2022 11:06:57 +0200 Subject: [PATCH] feat: add complex route addition and retrieval --- Server/Configurations/MapperInitializer.cs | 4 + Server/Controllers/RouteController.cs | 46 + Server/Data/ApplicationDbContext.cs | 2 + Server/Helpers/IPager.cs | 8 + Server/Helpers/Pager.cs | 19 + Server/Helpers/SortHelper.cs | 16 +- ...ddress_and_RouteAddressDetails.Designer.cs | 915 ++++++++++++++++++ ...le_RouteAddress_and_RouteAddressDetails.cs | 114 +++ .../ApplicationDbContextModelSnapshot.cs | 73 +- Server/Models/Address.cs | 15 +- Server/Models/City.cs | 17 +- Server/Models/Country.cs | 6 + Server/Models/RouteAddress.cs | 7 +- Server/Models/RouteAddressDetails.cs | 22 + Server/Models/State.cs | 14 +- Server/Models/VehicleEnrollment.cs | 4 + Server/Program.cs | 9 +- Server/Server.csproj | 4 + Server/Services/IRouteManagementService.cs | 12 +- Server/Services/ReportService.cs | 24 +- Server/Services/RouteManagementService.cs | 188 +++- Server/Services/StateManagementService.cs | 2 + Server/Services/StatisticsService.cs | 29 +- .../DataTransferObjects/AddressDto.cs | 24 +- SharedModels/DataTransferObjects/CityDto.cs | 2 +- .../DataTransferObjects/RouteAddressDto.cs | 36 +- SharedModels/DataTransferObjects/RouteDto.cs | 5 + SharedModels/DataTransferObjects/StateDto.cs | 3 +- .../Objects/RouteWithAddressesParameters.cs | 15 + 29 files changed, 1523 insertions(+), 112 deletions(-) create mode 100644 Server/Helpers/IPager.cs create mode 100644 Server/Helpers/Pager.cs create mode 100644 Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.Designer.cs create mode 100644 Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.cs create mode 100644 Server/Models/RouteAddressDetails.cs create mode 100644 SharedModels/QueryParameters/Objects/RouteWithAddressesParameters.cs diff --git a/Server/Configurations/MapperInitializer.cs b/Server/Configurations/MapperInitializer.cs index faec9dd..e4950be 100644 --- a/Server/Configurations/MapperInitializer.cs +++ b/Server/Configurations/MapperInitializer.cs @@ -31,16 +31,20 @@ 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(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); diff --git a/Server/Controllers/RouteController.cs b/Server/Controllers/RouteController.cs index 93746e0..c8db57e 100644 --- a/Server/Controllers/RouteController.cs +++ b/Server/Controllers/RouteController.cs @@ -30,6 +30,19 @@ public class RouteController : ControllerBase return CreatedAtAction(nameof(GetRoute), new {id = result.route.Id}, result.route); } + [HttpPost("withAddresses")] + public async Task AddRouteWithAddresses(CreateRouteWithAddressesDto route) + { + var result = await _routeManagementService.AddRouteWithAddresses(route); + + if (!result.isSucceed) + { + return result.actionResult; + } + + return CreatedAtAction(nameof(GetRoute), new {id = result.route.Id}, result.route); + } + [HttpGet] public async Task GetRoutes([FromQuery] RouteParameters parameters) { @@ -45,6 +58,21 @@ public class RouteController : ControllerBase return Ok(result.routes); } + [HttpGet("withAddresses")] + public async Task GetRouteWithAddresses([FromQuery] RouteWithAddressesParameters parameters) + { + var result = await _routeManagementService.GetRoutesWithAddresses(parameters); + + if (!result.isSucceed) + { + return result.actionResult; + } + + Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); + + return Ok(result.routes); + } + [HttpGet("{id}")] public async Task GetRoute(int id, [FromQuery] string? fields) { @@ -62,6 +90,24 @@ public class RouteController : ControllerBase return Ok(result.route); } + + [HttpGet("withAddresses/{id}")] + public async Task GetRouteWithAddresses(int id, [FromQuery] string? fields) + { + if (!await _routeManagementService.IsRouteExists(id)) + { + return NotFound(); + } + + var result = await _routeManagementService.GetRouteWithAddresses(id, fields); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return Ok(result.route); + } [HttpPut("{id}")] public async Task UpdateRoute(int id, UpdateRouteDto route) diff --git a/Server/Data/ApplicationDbContext.cs b/Server/Data/ApplicationDbContext.cs index 2dfc8f0..003c09b 100644 --- a/Server/Data/ApplicationDbContext.cs +++ b/Server/Data/ApplicationDbContext.cs @@ -18,6 +18,8 @@ public class ApplicationDbContext : IdentityDbContext public DbSet Routes { get; set; } = null!; public DbSet RouteAddresses { get; set; } = null!; public DbSet
Addresses { get; set; } = null!; + + public DbSet RouteAddressDetails { get; set; } = null!; public DbSet Cities { get; set; } = null!; public DbSet States { get; set; } = null!; public DbSet Countries { get; set; } = null!; diff --git a/Server/Helpers/IPager.cs b/Server/Helpers/IPager.cs new file mode 100644 index 0000000..5838b22 --- /dev/null +++ b/Server/Helpers/IPager.cs @@ -0,0 +1,8 @@ +using SharedModels.QueryParameters; + +namespace Server.Helpers; + +public interface IPager +{ + PagingMetadata ApplyPaging(ref IQueryable obj, int pageNumber, int pageSize); +} \ No newline at end of file diff --git a/Server/Helpers/Pager.cs b/Server/Helpers/Pager.cs new file mode 100644 index 0000000..834a096 --- /dev/null +++ b/Server/Helpers/Pager.cs @@ -0,0 +1,19 @@ +using SharedModels.QueryParameters; + +namespace Server.Helpers; + +public class Pager : IPager +{ + public PagingMetadata ApplyPaging(ref IQueryable obj, + int pageNumber, int pageSize) + { + var metadata = new PagingMetadata(obj, + pageNumber, pageSize); + + obj = obj + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); + + return metadata; + } +} \ No newline at end of file diff --git a/Server/Helpers/SortHelper.cs b/Server/Helpers/SortHelper.cs index 79dad5d..0fb4b02 100644 --- a/Server/Helpers/SortHelper.cs +++ b/Server/Helpers/SortHelper.cs @@ -1,3 +1,4 @@ +using System.Dynamic; using System.Linq.Dynamic.Core; using System.Reflection; using System.Text; @@ -14,8 +15,9 @@ public class SortHelper : ISortHelper } var orderParams = orderByQueryString.Trim().Split(","); - var propertyInfos = - typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); + var propertyStrings = typeof(T) == typeof(ExpandoObject) ? + (entities.First() as ExpandoObject).ToDictionary(o => o.Key, o => o.Value).Keys.ToList() : + typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList().ConvertAll(o => o.GetType().ToString()); var orderQueryBuilder = new StringBuilder(); foreach (var param in orderParams) @@ -26,8 +28,8 @@ public class SortHelper : ISortHelper } var propertyFromQueryName = param[0] == '-' || param[0] == '+' ? param.Substring(1) : param; - var objectProperty = propertyInfos.FirstOrDefault(pi => - pi.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase)); + var objectProperty = propertyStrings.FirstOrDefault(ps => + ps.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase)); if (objectProperty == null) { @@ -36,11 +38,13 @@ public class SortHelper : ISortHelper var sortingOrder = param[0] == '-' ? "descending" : "ascending"; - orderQueryBuilder.Append($"{objectProperty.Name} {sortingOrder}, "); + orderQueryBuilder.Append($"{objectProperty} {sortingOrder}, "); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - return entities.OrderBy(orderQuery); + return typeof(T) == typeof(ExpandoObject) ? + entities.Cast().OrderBy(orderQuery).Cast() : + entities.OrderBy(orderQuery); } } \ No newline at end of file diff --git a/Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.Designer.cs b/Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.Designer.cs new file mode 100644 index 0000000..3274a1c --- /dev/null +++ b/Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.Designer.cs @@ -0,0 +1,915 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20221129160345_Decouple_RouteAddress_and_RouteAddressDetails")] + partial class Decouple_RouteAddress_and_RouteAddressDetails + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("StateId"); + + b.ToTable("Cities"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("Server.Models.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Countries"); + }); + + modelBuilder.Entity("Server.Models.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("PostDateTimeUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("VehicleEnrollmentId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("Server.Models.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RouteAddressDetailsId") + .HasColumnType("integer"); + + b.Property("RouteId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("RouteId"); + + b.ToTable("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.RouteAddressDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CostToNextCity") + .HasColumnType("double precision"); + + b.Property("RouteAddressId") + .HasColumnType("integer"); + + b.Property("TimeSpanToNextCity") + .HasColumnType("interval"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("integer"); + + b.Property("WaitTimeSpan") + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("RouteAddressId"); + + b.HasIndex("VehicleEnrollmentId"); + + b.ToTable("RouteAddressDetails"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.ToTable("States"); + }); + + modelBuilder.Entity("Server.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FirstRouteAddressId") + .HasColumnType("integer"); + + b.Property("IsMissed") + .HasColumnType("boolean"); + + b.Property("IsReturned") + .HasColumnType("boolean"); + + b.Property("LastRouteAddressId") + .HasColumnType("integer"); + + b.Property("PurchaseDateTimeUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("TicketGroupId") + .HasColumnType("integer"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TicketGroupId"); + + b.HasIndex("VehicleEnrollmentId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Server.Models.TicketGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("TicketGroups"); + }); + + modelBuilder.Entity("Server.Models.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CompanyId") + .HasColumnType("integer"); + + b.Property("HasBelts") + .HasColumnType("boolean"); + + b.Property("HasClimateControl") + .HasColumnType("boolean"); + + b.Property("HasOutlet") + .HasColumnType("boolean"); + + b.Property("HasStewardess") + .HasColumnType("boolean"); + + b.Property("HasTV") + .HasColumnType("boolean"); + + b.Property("HasWC") + .HasColumnType("boolean"); + + b.Property("HasWiFi") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Vehicles"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelationComment") + .HasColumnType("text"); + + b.Property("DelayTimeSpan") + .HasColumnType("interval"); + + b.Property("DepartureDateTimeUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCanceled") + .HasColumnType("boolean"); + + b.Property("RouteAddressDetailsId") + .HasColumnType("integer"); + + b.Property("RouteId") + .HasColumnType("integer"); + + b.Property("VehicleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RouteId"); + + b.HasIndex("VehicleId"); + + b.ToTable("VehicleEnrollments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Server.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.HasOne("Server.Models.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.HasOne("Server.Models.State", "State") + .WithMany("Cities") + .HasForeignKey("StateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.HasOne("Server.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Server.Models.Review", b => + { + b.HasOne("Server.Models.User", "User") + .WithMany("Reviews") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Reviews") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.HasOne("Server.Models.Address", "Address") + .WithMany("RouteAddresses") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Server.Models.RouteAddressDetails", b => + { + b.HasOne("Server.Models.RouteAddress", "RouteAddress") + .WithMany("RouteAddressDetails") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.HasOne("Server.Models.Country", "Country") + .WithMany("States") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Server.Models.Ticket", b => + { + b.HasOne("Server.Models.TicketGroup", "TicketGroup") + .WithMany("Tickets") + .HasForeignKey("TicketGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment") + .WithMany("Tickets") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TicketGroup"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("Server.Models.TicketGroup", b => + { + b.HasOne("Server.Models.User", "User") + .WithMany("TicketGroups") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Server.Models.User", b => + { + b.OwnsMany("Server.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("UserId") + .HasColumnType("text"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreationDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("ExpiryDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("UserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.HasOne("Server.Models.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.HasOne("Server.Models.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.Vehicle", "Vehicle") + .WithMany("VehicleEnrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Server.Models.Address", b => + { + b.Navigation("RouteAddresses"); + }); + + modelBuilder.Entity("Server.Models.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("Server.Models.Company", b => + { + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("Server.Models.Country", b => + { + b.Navigation("States"); + }); + + modelBuilder.Entity("Server.Models.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.Navigation("RouteAddressDetails"); + }); + + modelBuilder.Entity("Server.Models.State", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("Server.Models.TicketGroup", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("Server.Models.User", b => + { + b.Navigation("Reviews"); + + b.Navigation("TicketGroups"); + }); + + modelBuilder.Entity("Server.Models.Vehicle", b => + { + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("Server.Models.VehicleEnrollment", b => + { + b.Navigation("Reviews"); + + b.Navigation("RouteAddressDetails"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.cs b/Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.cs new file mode 100644 index 0000000..38630ca --- /dev/null +++ b/Server/Migrations/20221129160345_Decouple_RouteAddress_and_RouteAddressDetails.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Server.Migrations +{ + public partial class Decouple_RouteAddress_and_RouteAddressDetails : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CostToNextCity", + table: "RouteAddresses"); + + migrationBuilder.DropColumn( + name: "TimeSpanToNextCity", + table: "RouteAddresses"); + + migrationBuilder.DropColumn( + name: "WaitTimeSpan", + table: "RouteAddresses"); + + migrationBuilder.AddColumn( + name: "RouteAddressDetailsId", + table: "VehicleEnrollments", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "RouteAddressDetailsId", + table: "RouteAddresses", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "RouteAddressDetails", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + VehicleEnrollmentId = table.Column(type: "integer", nullable: false), + RouteAddressId = table.Column(type: "integer", nullable: false), + TimeSpanToNextCity = table.Column(type: "interval", nullable: false), + WaitTimeSpan = table.Column(type: "interval", nullable: false), + CostToNextCity = table.Column(type: "double precision", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RouteAddressDetails", x => x.Id); + table.ForeignKey( + name: "FK_RouteAddressDetails_RouteAddresses_RouteAddressId", + column: x => x.RouteAddressId, + principalTable: "RouteAddresses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RouteAddressDetails_VehicleEnrollments_VehicleEnrollmentId", + column: x => x.VehicleEnrollmentId, + principalTable: "VehicleEnrollments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RouteAddressDetails_RouteAddressId", + table: "RouteAddressDetails", + column: "RouteAddressId"); + + migrationBuilder.CreateIndex( + name: "IX_RouteAddressDetails_VehicleEnrollmentId", + table: "RouteAddressDetails", + column: "VehicleEnrollmentId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RouteAddressDetails"); + + migrationBuilder.DropColumn( + name: "RouteAddressDetailsId", + table: "VehicleEnrollments"); + + migrationBuilder.DropColumn( + name: "RouteAddressDetailsId", + table: "RouteAddresses"); + + migrationBuilder.AddColumn( + name: "CostToNextCity", + table: "RouteAddresses", + type: "double precision", + nullable: false, + defaultValue: 0.0); + + migrationBuilder.AddColumn( + name: "TimeSpanToNextCity", + table: "RouteAddresses", + type: "interval", + nullable: false, + defaultValue: new TimeSpan(0, 0, 0, 0, 0)); + + migrationBuilder.AddColumn( + name: "WaitTimeSpan", + table: "RouteAddresses", + type: "interval", + nullable: false, + defaultValue: new TimeSpan(0, 0, 0, 0, 0)); + } + } +} diff --git a/Server/Migrations/ApplicationDbContextModelSnapshot.cs b/Server/Migrations/ApplicationDbContextModelSnapshot.cs index 8d956f8..daf3e39 100644 --- a/Server/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Server/Migrations/ApplicationDbContextModelSnapshot.cs @@ -309,21 +309,15 @@ namespace Server.Migrations b.Property("AddressId") .HasColumnType("integer"); - b.Property("CostToNextCity") - .HasColumnType("double precision"); - b.Property("Order") .HasColumnType("integer"); + b.Property("RouteAddressDetailsId") + .HasColumnType("integer"); + b.Property("RouteId") .HasColumnType("integer"); - b.Property("TimeSpanToNextCity") - .HasColumnType("interval"); - - b.Property("WaitTimeSpan") - .HasColumnType("interval"); - b.HasKey("Id"); b.HasIndex("AddressId"); @@ -333,6 +327,38 @@ namespace Server.Migrations b.ToTable("RouteAddresses"); }); + modelBuilder.Entity("Server.Models.RouteAddressDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CostToNextCity") + .HasColumnType("double precision"); + + b.Property("RouteAddressId") + .HasColumnType("integer"); + + b.Property("TimeSpanToNextCity") + .HasColumnType("interval"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("integer"); + + b.Property("WaitTimeSpan") + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("RouteAddressId"); + + b.HasIndex("VehicleEnrollmentId"); + + b.ToTable("RouteAddressDetails"); + }); + modelBuilder.Entity("Server.Models.State", b => { b.Property("Id") @@ -552,6 +578,9 @@ namespace Server.Migrations b.Property("IsCanceled") .HasColumnType("boolean"); + b.Property("RouteAddressDetailsId") + .HasColumnType("integer"); + b.Property("RouteId") .HasColumnType("integer"); @@ -689,6 +718,25 @@ namespace Server.Migrations b.Navigation("Route"); }); + modelBuilder.Entity("Server.Models.RouteAddressDetails", b => + { + b.HasOne("Server.Models.RouteAddress", "RouteAddress") + .WithMany("RouteAddressDetails") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + modelBuilder.Entity("Server.Models.State", b => { b.HasOne("Server.Models.Country", "Country") @@ -824,6 +872,11 @@ namespace Server.Migrations b.Navigation("VehicleEnrollments"); }); + modelBuilder.Entity("Server.Models.RouteAddress", b => + { + b.Navigation("RouteAddressDetails"); + }); + modelBuilder.Entity("Server.Models.State", b => { b.Navigation("Cities"); @@ -850,6 +903,8 @@ namespace Server.Migrations { b.Navigation("Reviews"); + b.Navigation("RouteAddressDetails"); + b.Navigation("Tickets"); }); #pragma warning restore 612, 618 diff --git a/Server/Models/Address.cs b/Server/Models/Address.cs index 7f003a5..a6c6135 100644 --- a/Server/Models/Address.cs +++ b/Server/Models/Address.cs @@ -10,18 +10,25 @@ public class Address public int Id { get; set; } public string Name { get; set; } = null!; + public double Latitude { get; set; } public double Longitude { get; set; } [ForeignKey("CityId")] public int CityId { get; set; } - public City? City { get; set; } + public City City { get; set; } = null!; public virtual IList RouteAddresses { get; set; } = null!; - public override string ToString() + public string GetFullName() { - return $"{City.State.Country.Name}, {City.State.Name}, " + - $"{City.Name}, {this.Name}"; + if (City == null || City.State == null || City.State.Country == null) + { + throw new NullReferenceException( + $"Properties {nameof(City)}, {nameof(City.State)}, " + + $"{nameof(City.State.Country)} must not be null"); + } + + return $"{City.GetFullName()}, {Name}"; } } \ No newline at end of file diff --git a/Server/Models/City.cs b/Server/Models/City.cs index 1927105..72cd52a 100644 --- a/Server/Models/City.cs +++ b/Server/Models/City.cs @@ -10,10 +10,23 @@ public class City public int Id { get; set; } public string Name { get; set; } = null!; - + public virtual IList
? Addresses { get; set; } [ForeignKey("StateId")] public int StateId { get; set; } - public State? State { get; set; } + + public State State { get; set; } = null!; + + public string GetFullName() + { + if (State == null || State.Country == null) + { + throw new NullReferenceException( + $"Properties {nameof(State)}, " + + $"{nameof(State.Country)} must not be null"); + } + + return $"{State.GetFullName()}, {Name}"; + } } \ No newline at end of file diff --git a/Server/Models/Country.cs b/Server/Models/Country.cs index 77b533b..6f1f93e 100644 --- a/Server/Models/Country.cs +++ b/Server/Models/Country.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using SharedModels.DataTransferObjects; namespace Server.Models; @@ -12,4 +13,9 @@ public class Country public string Name { get; set; } = null!; public virtual IList States { get; set; } = null!; + + public string GetFullName() + { + return $"{Name}"; + } } \ No newline at end of file diff --git a/Server/Models/RouteAddress.cs b/Server/Models/RouteAddress.cs index a97874b..ff0262d 100644 --- a/Server/Models/RouteAddress.cs +++ b/Server/Models/RouteAddress.cs @@ -16,8 +16,9 @@ public class RouteAddress public int AddressId { get; set; } public Address Address { get; set; } = null!; + [ForeignKey("RouteAddressDetailsId")] + public int RouteAddressDetailsId { get; set; } + public virtual IList RouteAddressDetails { get; set; } = null!; + public int Order { get; set; } - public TimeSpan TimeSpanToNextCity { get; set; } - public TimeSpan WaitTimeSpan { get; set; } - public double CostToNextCity { get; set; } } \ No newline at end of file diff --git a/Server/Models/RouteAddressDetails.cs b/Server/Models/RouteAddressDetails.cs new file mode 100644 index 0000000..a63f6b5 --- /dev/null +++ b/Server/Models/RouteAddressDetails.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Server.Models; + +public class RouteAddressDetails +{ + [Key] + public int Id { get; set; } + + [ForeignKey("VehicleEnrollmentId")] + public int VehicleEnrollmentId { get; set; } + public VehicleEnrollment VehicleEnrollment { get; set; } = null!; + + [ForeignKey("RouteAddressId")] + public int RouteAddressId { get; set; } + public RouteAddress RouteAddress { get; set; } = null!; + + public TimeSpan TimeSpanToNextCity { get; set; } + public TimeSpan WaitTimeSpan { get; set; } + public double CostToNextCity { get; set; } +} \ No newline at end of file diff --git a/Server/Models/State.cs b/Server/Models/State.cs index 2db6547..44f6408 100644 --- a/Server/Models/State.cs +++ b/Server/Models/State.cs @@ -10,10 +10,20 @@ public class State public int Id { get; set; } public string Name { get; set; } = null!; - + public virtual IList Cities { get; set; } = null!; [ForeignKey("CountryId")] public int CountryId { get; set; } - public Country? Country { get; set; } = null!; + public Country Country { get; set; } = null!; + + public string GetFullName() + { + if (Country == null) + { + throw new NullReferenceException($"Property {nameof(Country)} must not be null"); + } + + return $"{Country.GetFullName()}, {Name}"; + } } \ No newline at end of file diff --git a/Server/Models/VehicleEnrollment.cs b/Server/Models/VehicleEnrollment.cs index 7271d85..a4fe704 100644 --- a/Server/Models/VehicleEnrollment.cs +++ b/Server/Models/VehicleEnrollment.cs @@ -16,6 +16,10 @@ public class VehicleEnrollment public int RouteId { get; set; } public Route Route { get; set; } = null!; + [ForeignKey("RouteAddressDetailsId")] + public int RouteAddressDetailsId { get; set; } + public virtual IList RouteAddressDetails { get; set; } = null!; + public DateTime DepartureDateTimeUtc { get; set; } public TimeSpan? DelayTimeSpan { get; set; } diff --git a/Server/Program.cs b/Server/Program.cs index 7ed9313..a76ac41 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,3 +1,4 @@ +using System.Dynamic; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; @@ -105,7 +106,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - +builder.Services.AddScoped(); builder.Services.AddScoped, SortHelper>(); builder.Services.AddScoped, SortHelper>(); @@ -132,9 +133,15 @@ builder.Services.AddScoped, DataShaper, DataShaper>(); builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, SortHelper>(); + builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, DataShaper>(); + +builder.Services.AddScoped, Pager>(); // Adding DB Context with PostgreSQL var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); diff --git a/Server/Server.csproj b/Server/Server.csproj index f6c6bbf..8da995d 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -22,8 +22,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Server/Services/IRouteManagementService.cs b/Server/Services/IRouteManagementService.cs index 6f26e54..0c888c3 100644 --- a/Server/Services/IRouteManagementService.cs +++ b/Server/Services/IRouteManagementService.cs @@ -1,3 +1,5 @@ +using System.Dynamic; +using Microsoft.AspNetCore.Mvc; using SharedModels.DataTransferObjects; using SharedModels.QueryParameters; using SharedModels.QueryParameters.Objects; @@ -8,9 +10,13 @@ namespace Server.Services; public interface IRouteManagementService { Task<(bool isSucceed, string message, RouteDto route)> AddRoute(CreateRouteDto createRouteDto); - Task<(bool isSucceed, string message, IEnumerable routes, - PagingMetadata pagingMetadata)> GetRoutes(RouteParameters parameters); - Task<(bool isSucceed, string message, RouteDto route)> GetRoute(int id, string? fields); + Task<(bool isSucceed, IActionResult? actionResult, RouteWithAddressesDto route)> AddRouteWithAddresses(CreateRouteWithAddressesDto createRouteWithAddressesDto); + Task<(bool isSucceed, string message, IEnumerable routes, + PagingMetadata pagingMetadata)> GetRoutes(RouteParameters parameters); + Task<(bool isSucceed, IActionResult? actionResult, IEnumerable routes, + PagingMetadata pagingMetadata)> GetRoutesWithAddresses(RouteWithAddressesParameters parameters); + Task<(bool isSucceed, string message, ExpandoObject route)> GetRoute(int id, string? fields); + Task<(bool isSucceed, string message, ExpandoObject route)> GetRouteWithAddresses(int id, string? fields); Task<(bool isSucceed, string message, UpdateRouteDto route)> UpdateRoute(UpdateRouteDto updateRouteDto); Task<(bool isSucceed, string message)> DeleteRoute(int id); Task IsRouteExists(int id); diff --git a/Server/Services/ReportService.cs b/Server/Services/ReportService.cs index 9d50661..dcc354f 100644 --- a/Server/Services/ReportService.cs +++ b/Server/Services/ReportService.cs @@ -26,6 +26,7 @@ public class ReportService : IReportService .ThenInclude(t => t.VehicleEnrollment) .ThenInclude(ve => ve.Vehicle) .ThenInclude(v => v.Company) + .Include(tg => tg.User) .Include(tg => tg.Tickets) .ThenInclude(t => t.VehicleEnrollment) @@ -35,6 +36,12 @@ public class ReportService : IReportService .ThenInclude(a => a.City) .ThenInclude(c => c.State) .ThenInclude(s => s.Country) + + .Include(tg => tg.User) + .Include(tg => tg.Tickets) + .ThenInclude(t => t.VehicleEnrollment) + .ThenInclude(ve => ve.RouteAddressDetails) + .FirstOrDefaultAsync(tg => tg.Id == ticketGroupId); // Define document @@ -374,13 +381,16 @@ public class ReportService : IReportService foreach (var routeAddress in routeAddresses) { + var details = routeAddress.RouteAddressDetails + .First(rad => rad.RouteAddressId == routeAddress.Id); + if (routeAddress.AddressId == ticket.FirstRouteAddressId) { break; } - departureDateTimeUtc += routeAddress.TimeSpanToNextCity; - departureDateTimeUtc += routeAddress.WaitTimeSpan; + departureDateTimeUtc += details.TimeSpanToNextCity; + departureDateTimeUtc += details.WaitTimeSpan; } return departureDateTimeUtc; @@ -395,12 +405,15 @@ public class ReportService : IReportService foreach (var routeAddress in routeAddresses) { + var details = routeAddress.RouteAddressDetails + .First(rad => rad.RouteAddressId == routeAddress.Id); + if (routeAddress.AddressId == ticket.LastRouteAddressId) { break; } - arrivalDateTimeUtc += routeAddress.TimeSpanToNextCity; + arrivalDateTimeUtc += details.TimeSpanToNextCity; } return arrivalDateTimeUtc; @@ -432,7 +445,10 @@ public class ReportService : IReportService foreach (var routeAddress in routeAddresses) { - cost += routeAddress.CostToNextCity; + var details = routeAddress.RouteAddressDetails + .First(rad => rad.RouteAddressId == routeAddress.Id); + + cost += details.CostToNextCity; } return cost; diff --git a/Server/Services/RouteManagementService.cs b/Server/Services/RouteManagementService.cs index b6e2f92..0f41a6d 100644 --- a/Server/Services/RouteManagementService.cs +++ b/Server/Services/RouteManagementService.cs @@ -1,4 +1,7 @@ +using System.Dynamic; using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Server.Data; using Server.Helpers; @@ -13,17 +16,23 @@ public class RouteManagementService : IRouteManagementService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; - private readonly ISortHelper _routeSortHelper; - private readonly IDataShaper _routeDataShaper; + private readonly ISortHelper _routeSortHelper; + private readonly IDataShaper _routeDataShaper; + private readonly IDataShaper _routeWithAddressesDataShaper; + private readonly IPager _pager; public RouteManagementService(ApplicationDbContext dbContext, - IMapper mapper, ISortHelper routeSortHelper, - IDataShaper routeDataShaper) + IMapper mapper, ISortHelper routeSortHelper, + IDataShaper routeDataShaper, + IDataShaper routeWithAddressesDataShaper, + IPager pager) { _dbContext = dbContext; _mapper = mapper; _routeSortHelper = routeSortHelper; _routeDataShaper = routeDataShaper; + _routeWithAddressesDataShaper = routeWithAddressesDataShaper; + _pager = pager; } public async Task<(bool isSucceed, string message, RouteDto route)> AddRoute(CreateRouteDto createRouteDto) @@ -36,8 +45,31 @@ public class RouteManagementService : IRouteManagementService return (true, String.Empty, _mapper.Map(route)); } - public async Task<(bool isSucceed, string message, IEnumerable routes, - PagingMetadata pagingMetadata)> GetRoutes(RouteParameters parameters) + public async Task<(bool isSucceed, IActionResult? actionResult, RouteWithAddressesDto route)> AddRouteWithAddresses(CreateRouteWithAddressesDto createRouteWithAddressesDto) + { + var route = _mapper.Map(createRouteWithAddressesDto); + + foreach (var routeAddress in route.RouteAddresses) + { + var dbAddress = await _dbContext.Addresses + .FirstOrDefaultAsync(a => a.Id == routeAddress.AddressId); + + if (dbAddress == null) + { + return (false, new BadRequestObjectResult($"Address with Id = {routeAddress.AddressId} doesn't exist"), null!); + } + + routeAddress.Address = dbAddress; + } + + await _dbContext.Routes.AddAsync(route); + await _dbContext.SaveChangesAsync(); + + return (true, null, _mapper.Map(route)); + } + + public async Task<(bool isSucceed, string message, IEnumerable routes, + PagingMetadata pagingMetadata)> GetRoutes(RouteParameters parameters) { var dbRoutes = _dbContext.Routes .AsQueryable(); @@ -45,9 +77,12 @@ public class RouteManagementService : IRouteManagementService SearchByAllRouteFields(ref dbRoutes, parameters.Search); FilterByRouteType(ref dbRoutes, parameters.Type); + var routeDtos = _mapper.ProjectTo(dbRoutes); + var shapedData = _routeDataShaper.ShapeData(routeDtos, parameters.Fields).AsQueryable(); + try { - dbRoutes = _routeSortHelper.ApplySort(dbRoutes, parameters.Sort); + shapedData = _routeSortHelper.ApplySort(shapedData, parameters.Sort); // By calling Any() we will check if LINQ to Entities Query will be // executed. If not it will throw an InvalidOperationException exception @@ -57,14 +92,11 @@ public class RouteManagementService : IRouteManagementService { return (false, "Invalid sorting string", null, null)!; } - - var pagingMetadata = ApplyPaging(ref dbRoutes, parameters.PageNumber, - parameters.PageSize); - - var shapedRoutesData = _routeDataShaper.ShapeData(dbRoutes, parameters.Fields); - var routeDtos = shapedRoutesData.ToList().ConvertAll(r => _mapper.Map(r)); - return (true, "", routeDtos, pagingMetadata); + var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber, + parameters.PageSize); + + return (true, "", shapedData, pagingMetadata); void SearchByAllRouteFields(ref IQueryable route, string? search) @@ -88,23 +120,100 @@ public class RouteManagementService : IRouteManagementService routes = routes.Where(r => r.Type == type); } + } + + public async Task<(bool isSucceed, IActionResult? actionResult, IEnumerable routes, + PagingMetadata pagingMetadata)> GetRoutesWithAddresses(RouteWithAddressesParameters parameters) + { + var dbRoutes = _dbContext.Routes + .Include(r => r.RouteAddresses.OrderBy(ra => ra.Order)) + .ThenInclude(ra => ra.Address).ThenInclude(a => a.City) + .ThenInclude(c => c.State).ThenInclude(s => s.Country) + .AsQueryable(); + + SearchByAllRouteFields(ref dbRoutes, parameters.Search); + FilterByRouteType(ref dbRoutes, parameters.Type); + FilterByFromAddressName(ref dbRoutes, parameters.FromAddressName); + FilterByToAddressName(ref dbRoutes, parameters.ToAddressName); + + var routeDtos = _mapper.ProjectTo(dbRoutes); + var shapedData = _routeWithAddressesDataShaper.ShapeData(routeDtos, parameters.Fields).AsQueryable(); - - PagingMetadata ApplyPaging(ref IQueryable routes, - int pageNumber, int pageSize) + try { - var metadata = new PagingMetadata(routes, - pageNumber, pageSize); - - routes = routes - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize); + shapedData = _routeSortHelper.ApplySort(shapedData, parameters.Sort); + } + catch (Exception e) + { + return (false, new BadRequestObjectResult("Invalid sorting string"), null, null)!; + } + + var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber, + parameters.PageSize); - return metadata; + return (true, null, shapedData, pagingMetadata); + + void SearchByAllRouteFields(ref IQueryable route, + string? search) + { + if (!route.Any() || String.IsNullOrWhiteSpace(search)) + { + return; + } + + // TODO Optimize (remove client evaluation) + route = route.ToArray().Where(r => + r.Type.ToLower().Contains(search.ToLower()) || + r.RouteAddresses.OrderBy(ra => ra.Order).First().Address + .GetFullName().ToLower().Contains(search.ToLower()) || + r.RouteAddresses.OrderBy(ra => ra.Order).Last().Address + .GetFullName().ToLower().Contains(search.ToLower())) + .AsQueryable(); + } + + void FilterByRouteType(ref IQueryable routes, + string? type) + { + if (!routes.Any() || String.IsNullOrWhiteSpace(type)) + { + return; + } + + routes = routes.Where(r => r.Type.ToLower().Contains(type.ToLower())); + } + + void FilterByFromAddressName(ref IQueryable routes, + string? addressName) + { + if (!routes.Any() || String.IsNullOrWhiteSpace(addressName)) + { + return; + } + + // TODO Optimize (remove client evaluation) + routes = routes.ToArray().Where(r => + r.RouteAddresses.First().Address + .GetFullName().ToLower().Contains(addressName.ToLower())) + .AsQueryable(); + } + + void FilterByToAddressName(ref IQueryable routes, + string? addressName) + { + if (!routes.Any() || String.IsNullOrWhiteSpace(addressName)) + { + return; + } + + // TODO Optimize (remove client evaluation) + routes = routes.ToArray().Where(r => + r.RouteAddresses.Last().Address. + GetFullName().ToLower().Contains(addressName.ToLower())) + .AsQueryable(); } } - public async Task<(bool isSucceed, string message, RouteDto route)> GetRoute(int id, string? fields) + public async Task<(bool isSucceed, string message, ExpandoObject route)> GetRoute(int id, string? fields) { var dbRoute = await _dbContext.Routes.Where(r => r.Id == id) .FirstOrDefaultAsync(); @@ -119,10 +228,33 @@ public class RouteManagementService : IRouteManagementService fields = RouteParameters.DefaultFields; } - var shapedRouteData = _routeDataShaper.ShapeData(dbRoute, fields); - var routeDto = _mapper.Map(shapedRouteData); + var routeDto = _mapper.Map(dbRoute); + var shapedRouteData = _routeDataShaper.ShapeData(routeDto, fields); - return (true, "", routeDto); + return (true, "", shapedRouteData); + } + + public async Task<(bool isSucceed, string message, ExpandoObject route)> GetRouteWithAddresses(int id, string? fields) + { + var dbRoute = await _dbContext.Routes.Where(r => r.Id == id) + .Include(r => r.RouteAddresses).ThenInclude(ra => ra.Address) + .ThenInclude(a => a.City).ThenInclude(c => c.State) + .ThenInclude(s => s.Country).FirstOrDefaultAsync(); + + if (dbRoute == null) + { + return (false, $"Route doesn't exist", null)!; + } + + if (String.IsNullOrWhiteSpace(fields)) + { + fields = RouteWithAddressesParameters.DefaultFields; + } + + var routeDto = _mapper.Map(dbRoute); + var shapedRouteData = _routeDataShaper.ShapeData(routeDto, fields); + + return (true, "", shapedRouteData); } public async Task<(bool isSucceed, string message, UpdateRouteDto route)> UpdateRoute(UpdateRouteDto updateRouteDto) diff --git a/Server/Services/StateManagementService.cs b/Server/Services/StateManagementService.cs index c0bbcfb..2ce3c34 100644 --- a/Server/Services/StateManagementService.cs +++ b/Server/Services/StateManagementService.cs @@ -44,6 +44,8 @@ public class StateManagementService : IStateManagementService .Include(s => s.Cities) .ThenInclude(c => c.Addresses).AsQueryable(); + var s = dbStates.ToList().ConvertAll(s => _mapper.Map(s)); + SearchByAllStateFields(ref dbStates, parameters.Search); FilterByStateName(ref dbStates, parameters.Name); FilterByCountryId(ref dbStates, parameters.CountryId); diff --git a/Server/Services/StatisticsService.cs b/Server/Services/StatisticsService.cs index 3857560..8a3e26a 100644 --- a/Server/Services/StatisticsService.cs +++ b/Server/Services/StatisticsService.cs @@ -17,16 +17,18 @@ public class StatisticsService : IStatisticsService private readonly IDataShaper _userDataShaper; private readonly IDataShaper _companyDataShaper; private readonly IDataShaper _addressDataShaper; + private readonly IPager _pager; public StatisticsService(ApplicationDbContext dbContext, IMapper mapper, IDataShaper userDataShaper, IDataShaper companyDataShaper, - IDataShaper addressDataShaper) + IDataShaper addressDataShaper, IPager pager) { _dbContext = dbContext; _mapper = mapper; _userDataShaper = userDataShaper; _companyDataShaper = companyDataShaper; _addressDataShaper = addressDataShaper; + _pager = pager; } // Popularity is measured in number of purchased tickets @@ -76,8 +78,8 @@ public class StatisticsService : IStatisticsService } var shapedData = shapedDataArray.AsQueryable(); - var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber, - parameters.PageSize); + var pagingMetadata = _pager.ApplyPaging(ref shapedData, + parameters.PageNumber, parameters.PageSize); shapedDataArray = shapedData.ToArray(); return (true, null, shapedDataArray, pagingMetadata); @@ -166,8 +168,8 @@ public class StatisticsService : IStatisticsService } var shapedData = shapedDataArray.AsQueryable(); - var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber, - parameters.PageSize); + var pagingMetadata = _pager.ApplyPaging(ref shapedData, + parameters.PageNumber, parameters.PageSize); shapedDataArray = shapedData.ToArray(); return (true, null, shapedDataArray, pagingMetadata); @@ -265,23 +267,10 @@ public class StatisticsService : IStatisticsService } var shapedData = shapedDataArray.AsQueryable(); - var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber, - parameters.PageSize); + var pagingMetadata = _pager.ApplyPaging(ref shapedData, + parameters.PageNumber, parameters.PageSize); shapedDataArray = shapedData.ToArray(); return (true, null, shapedDataArray, pagingMetadata); } - - PagingMetadata ApplyPaging(ref IQueryable obj, - int pageNumber, int pageSize) - { - var metadata = new PagingMetadata(obj, - pageNumber, pageSize); - - obj = obj - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize); - - return metadata; - } } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/AddressDto.cs b/SharedModels/DataTransferObjects/AddressDto.cs index 67d53f6..29eb500 100644 --- a/SharedModels/DataTransferObjects/AddressDto.cs +++ b/SharedModels/DataTransferObjects/AddressDto.cs @@ -5,8 +5,7 @@ namespace SharedModels.DataTransferObjects; public class AddressDto : CreateAddressDto { public int Id { get; set; } - - public InAddressCityDto City { get; set; } = null!; + public string FullName = null!; } public class CreateAddressDto @@ -37,4 +36,25 @@ public class InCityAddressDto public string Name { get; set; } = null!; public double Latitude { get; set; } public double Longitude { get; set; } +} + +public class CreateAddressInRouteAddress +{ + public int? Id { get; set; } + + [StringLength(maximumLength: 250, ErrorMessage = "Address name is too long")] + public string? Name { get; set; } = null!; + + [Range(-90, 90, ErrorMessage = "Latitude must be in range(-90, 90)")] + public double? Latitude { get; set; } + + [Range(-180, 180, ErrorMessage = "Longitude must be in range(-180, 180)")] + public double? Longitude { get; set; } + + public int? CityId { get; set; } +} + +public class AddressInRouteAddress : CreateAddressInRouteAddress +{ + public string FullName = null!; } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/CityDto.cs b/SharedModels/DataTransferObjects/CityDto.cs index 824a272..6cd11c9 100644 --- a/SharedModels/DataTransferObjects/CityDto.cs +++ b/SharedModels/DataTransferObjects/CityDto.cs @@ -6,7 +6,7 @@ public class CityDto : CreateCityDto { public int Id { get; set; } - public InCityStateDto State { get; set; } = null!; + public string FullName = null!; public virtual IList? Addresses { get; set; } } diff --git a/SharedModels/DataTransferObjects/RouteAddressDto.cs b/SharedModels/DataTransferObjects/RouteAddressDto.cs index 94afdd3..5e7b98d 100644 --- a/SharedModels/DataTransferObjects/RouteAddressDto.cs +++ b/SharedModels/DataTransferObjects/RouteAddressDto.cs @@ -16,19 +16,8 @@ public class CreateRouteAddressDto public int AddressId { get; set; } [Required] + [Range(0, Int32.MaxValue)] public int Order { get; set; } - - [Required] - [DataType(DataType.Duration)] - public TimeSpan TimeSpanToNextCity { get; set; } - - [Required] - [DataType(DataType.Duration)] - public TimeSpan WaitTimeSpan { get; set; } - - [Required] - [DataType(DataType.Currency)] - public double CostToNextCity { get; set; } } public class UpdateRouteAddressDto : CreateRouteAddressDto @@ -39,21 +28,16 @@ public class UpdateRouteAddressDto : CreateRouteAddressDto public class CreateRouteAddressWithAddressDto { + [Range(0, Int32.MaxValue)] + public int Order { get; set; } + [Required] + public CreateAddressInRouteAddress Address { get; set; } = null!; +} + +public class RouteAddressWithAddressDto +{ public int Order { get; set; } - [Required] - [DataType(DataType.Duration)] - public TimeSpan TimeSpanToNextCity { get; set; } - - [Required] - [DataType(DataType.Duration)] - public TimeSpan WaitTimeSpan { get; set; } - - [Required] - [DataType(DataType.Currency)] - public double CostToNextCity { get; set; } - - [Required] - public CreateAddressDto Address { get; set; } = null!; + public AddressInRouteAddress Address { get; set; } = null!; } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/RouteDto.cs b/SharedModels/DataTransferObjects/RouteDto.cs index 5d4e34a..5784aff 100644 --- a/SharedModels/DataTransferObjects/RouteDto.cs +++ b/SharedModels/DataTransferObjects/RouteDto.cs @@ -24,4 +24,9 @@ public class CreateRouteWithAddressesDto : CreateRouteDto [Required] [MinLength(2)] public IList RouteAddresses { get; set; } = null!; +} + +public class RouteWithAddressesDto : RouteDto +{ + public IList RouteAddresses { get; set; } = null!; } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/StateDto.cs b/SharedModels/DataTransferObjects/StateDto.cs index 49f2e56..98e3a4b 100644 --- a/SharedModels/DataTransferObjects/StateDto.cs +++ b/SharedModels/DataTransferObjects/StateDto.cs @@ -5,7 +5,8 @@ namespace SharedModels.DataTransferObjects; public class StateDto : CreateStateDto { public int Id { get; set; } - public InStateCountryDto Country { get; set; } = null!; + + public string FullName = null!; public virtual IList Cities { get; set; } = null!; } diff --git a/SharedModels/QueryParameters/Objects/RouteWithAddressesParameters.cs b/SharedModels/QueryParameters/Objects/RouteWithAddressesParameters.cs new file mode 100644 index 0000000..2ece76a --- /dev/null +++ b/SharedModels/QueryParameters/Objects/RouteWithAddressesParameters.cs @@ -0,0 +1,15 @@ +namespace SharedModels.QueryParameters.Objects; + +public class RouteWithAddressesParameters : ParametersBase +{ + public const string DefaultFields = "id,type,routeAddresses"; + + public RouteWithAddressesParameters() + { + Fields = DefaultFields; + } + + public string? Type { get; set; } + public string? FromAddressName { get; set; } + public string? ToAddressName { get; set; } +} \ No newline at end of file