diff --git a/.gitignore b/.gitignore index 6c814ded..b3ad9276 100644 --- a/.gitignore +++ b/.gitignore @@ -271,3 +271,4 @@ Web/Resgrid.WebCore/wwwroot/js/ng/styles.css Web/Resgrid.WebCore/wwwroot/js/ng/* Web/Resgrid.WebCore/wwwroot/lib/* /Web/Resgrid.Web/wwwroot/lib +.dual-graph/ diff --git a/Core/Resgrid.Model/AuditLogTypes.cs b/Core/Resgrid.Model/AuditLogTypes.cs index f745c436..7c93dc27 100644 --- a/Core/Resgrid.Model/AuditLogTypes.cs +++ b/Core/Resgrid.Model/AuditLogTypes.cs @@ -110,6 +110,20 @@ public enum AuditLogTypes UdfFieldAdded, UdfFieldUpdated, UdfFieldRemoved, - UdfFieldValueSaved + UdfFieldValueSaved, + // Route Planning + RouteCreated, + RouteUpdated, + RouteDeleted, + RouteStarted, + RouteCompleted, + RouteCancelled, + RoutePaused, + RouteResumed, + RouteStopCheckedIn, + RouteStopCheckedOut, + RouteStopSkipped, + RouteDeviationDetected, + RouteDeviationAcknowledged } } diff --git a/Core/Resgrid.Model/Call.cs b/Core/Resgrid.Model/Call.cs index 7c60ae6e..b55e8b5e 100644 --- a/Core/Resgrid.Model/Call.cs +++ b/Core/Resgrid.Model/Call.cs @@ -175,6 +175,10 @@ public class Call : IEntity public DateTime? DeletedOn { get; set; } + public string IndoorMapZoneId { get; set; } + + public string IndoorMapFloorId { get; set; } + [NotMapped] [JsonIgnore] public object IdValue diff --git a/Core/Resgrid.Model/CustomMapImport.cs b/Core/Resgrid.Model/CustomMapImport.cs new file mode 100644 index 00000000..7b9291a5 --- /dev/null +++ b/Core/Resgrid.Model/CustomMapImport.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class CustomMapImport : IEntity + { + public string CustomMapImportId { get; set; } + + public string CustomMapId { get; set; } + + public string CustomMapLayerId { get; set; } + + public string SourceFileName { get; set; } + + public int SourceFileType { get; set; } + + public int Status { get; set; } + + public string ErrorMessage { get; set; } + + public string ImportedById { get; set; } + + public DateTime ImportedOn { get; set; } + + [NotMapped] + public string TableName => "CustomMapImports"; + + [NotMapped] + public string IdName => "CustomMapImportId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return CustomMapImportId; } + set { CustomMapImportId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/CustomMapImportFileType.cs b/Core/Resgrid.Model/CustomMapImportFileType.cs new file mode 100644 index 00000000..d5e518e0 --- /dev/null +++ b/Core/Resgrid.Model/CustomMapImportFileType.cs @@ -0,0 +1,12 @@ +namespace Resgrid.Model +{ + public enum CustomMapImportFileType + { + GeoJSON = 0, + Shapefile = 1, + KML = 2, + KMZ = 3, + WMS = 4, + WFS = 5 + } +} diff --git a/Core/Resgrid.Model/CustomMapImportStatus.cs b/Core/Resgrid.Model/CustomMapImportStatus.cs new file mode 100644 index 00000000..5df1a604 --- /dev/null +++ b/Core/Resgrid.Model/CustomMapImportStatus.cs @@ -0,0 +1,10 @@ +namespace Resgrid.Model +{ + public enum CustomMapImportStatus + { + Pending = 0, + Processing = 1, + Complete = 2, + Failed = 3 + } +} diff --git a/Core/Resgrid.Model/CustomMapLayerType.cs b/Core/Resgrid.Model/CustomMapLayerType.cs new file mode 100644 index 00000000..c9e65fdf --- /dev/null +++ b/Core/Resgrid.Model/CustomMapLayerType.cs @@ -0,0 +1,10 @@ +namespace Resgrid.Model +{ + public enum CustomMapLayerType + { + FloorPlan = 0, + Overlay = 1, + DataLayer = 2, + Infrastructure = 3 + } +} diff --git a/Core/Resgrid.Model/CustomMapTile.cs b/Core/Resgrid.Model/CustomMapTile.cs new file mode 100644 index 00000000..1eae77a9 --- /dev/null +++ b/Core/Resgrid.Model/CustomMapTile.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class CustomMapTile : IEntity + { + public string CustomMapTileId { get; set; } + + public string CustomMapLayerId { get; set; } + + public int ZoomLevel { get; set; } + + public int TileX { get; set; } + + public int TileY { get; set; } + + public byte[] TileData { get; set; } + + public string TileContentType { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "CustomMapTiles"; + + [NotMapped] + public string IdName => "CustomMapTileId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return CustomMapTileId; } + set { CustomMapTileId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/CustomMapType.cs b/Core/Resgrid.Model/CustomMapType.cs new file mode 100644 index 00000000..961e3db3 --- /dev/null +++ b/Core/Resgrid.Model/CustomMapType.cs @@ -0,0 +1,10 @@ +namespace Resgrid.Model +{ + public enum CustomMapType + { + Indoor = 0, + Outdoor = 1, + Event = 2, + Custom = 3 + } +} diff --git a/Core/Resgrid.Model/DepartmentNotification.cs b/Core/Resgrid.Model/DepartmentNotification.cs index 359cd0cc..c872e867 100644 --- a/Core/Resgrid.Model/DepartmentNotification.cs +++ b/Core/Resgrid.Model/DepartmentNotification.cs @@ -147,7 +147,7 @@ public string TranslateBefore(List customStates = null) break; case EventTypes.PersonnelStatusChanged: if (string.IsNullOrWhiteSpace(BeforeData)) - return "None"; + return "Any"; if (BeforeData == "-1") return "Any"; else @@ -209,7 +209,7 @@ public string TranslateCurrent(List customStates = null) { case EventTypes.UnitStatusChanged: if (string.IsNullOrWhiteSpace(CurrentData)) - return "None"; + return "Any"; if (CurrentData == "-1") return "Any"; else @@ -238,7 +238,7 @@ public string TranslateCurrent(List customStates = null) break; case EventTypes.PersonnelStaffingChanged: if (string.IsNullOrWhiteSpace(CurrentData)) - return "None"; + return "Any"; if (CurrentData == "-1") return "Any"; else @@ -267,7 +267,7 @@ public string TranslateCurrent(List customStates = null) break; case EventTypes.PersonnelStatusChanged: if (string.IsNullOrWhiteSpace(CurrentData)) - return "None"; + return "Any"; if (CurrentData == "-1") return "Any"; else diff --git a/Core/Resgrid.Model/IndoorMap.cs b/Core/Resgrid.Model/IndoorMap.cs new file mode 100644 index 00000000..6e3172f0 --- /dev/null +++ b/Core/Resgrid.Model/IndoorMap.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class IndoorMap : IEntity + { + public string IndoorMapId { get; set; } + + public int DepartmentId { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public decimal CenterLatitude { get; set; } + + public decimal CenterLongitude { get; set; } + + public decimal BoundsNELat { get; set; } + + public decimal BoundsNELon { get; set; } + + public decimal BoundsSWLat { get; set; } + + public decimal BoundsSWLon { get; set; } + + public string DefaultFloorId { get; set; } + + public bool IsDeleted { get; set; } + + public string AddedById { get; set; } + + public DateTime AddedOn { get; set; } + + public string UpdatedById { get; set; } + + public DateTime? UpdatedOn { get; set; } + + public int MapType { get; set; } + + public string BoundsGeoJson { get; set; } + + public byte[] ThumbnailData { get; set; } + + public string ThumbnailContentType { get; set; } + + [NotMapped] + public string TableName => "IndoorMaps"; + + [NotMapped] + public string IdName => "IndoorMapId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return IndoorMapId; } + set { IndoorMapId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/IndoorMapFloor.cs b/Core/Resgrid.Model/IndoorMapFloor.cs new file mode 100644 index 00000000..1da3a9f9 --- /dev/null +++ b/Core/Resgrid.Model/IndoorMapFloor.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class IndoorMapFloor : IEntity + { + public string IndoorMapFloorId { get; set; } + + public string IndoorMapId { get; set; } + + public string Name { get; set; } + + public int FloorOrder { get; set; } + + public byte[] ImageData { get; set; } + + public string ImageContentType { get; set; } + + public decimal? BoundsNELat { get; set; } + + public decimal? BoundsNELon { get; set; } + + public decimal? BoundsSWLat { get; set; } + + public decimal? BoundsSWLon { get; set; } + + public decimal Opacity { get; set; } + + public int LayerType { get; set; } + + public bool IsTiled { get; set; } + + public int? TileMinZoom { get; set; } + + public int? TileMaxZoom { get; set; } + + public long? SourceFileSize { get; set; } + + public string GeoJsonData { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "IndoorMapFloors"; + + [NotMapped] + public string IdName => "IndoorMapFloorId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return IndoorMapFloorId; } + set { IndoorMapFloorId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/IndoorMapZone.cs b/Core/Resgrid.Model/IndoorMapZone.cs new file mode 100644 index 00000000..4be0ac62 --- /dev/null +++ b/Core/Resgrid.Model/IndoorMapZone.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class IndoorMapZone : IEntity + { + public string IndoorMapZoneId { get; set; } + + public string IndoorMapFloorId { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public int ZoneType { get; set; } + + public string PixelGeometry { get; set; } + + public string GeoGeometry { get; set; } + + public double CenterPixelX { get; set; } + + public double CenterPixelY { get; set; } + + public decimal CenterLatitude { get; set; } + + public decimal CenterLongitude { get; set; } + + public string Color { get; set; } + + public string Metadata { get; set; } + + public bool IsSearchable { get; set; } + + public bool IsDispatchable { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "IndoorMapZones"; + + [NotMapped] + public string IdName => "IndoorMapZoneId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return IndoorMapZoneId; } + set { IndoorMapZoneId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/IndoorMapZoneType.cs b/Core/Resgrid.Model/IndoorMapZoneType.cs new file mode 100644 index 00000000..f4d04951 --- /dev/null +++ b/Core/Resgrid.Model/IndoorMapZoneType.cs @@ -0,0 +1,25 @@ +namespace Resgrid.Model +{ + public enum IndoorMapZoneType + { + Room = 0, + Wing = 1, + Corridor = 2, + Stairwell = 3, + Elevator = 4, + HazardZone = 5, + AssemblyPoint = 6, + StagingArea = 7, + AccessPoint = 8, + Utility = 9, + SearchGrid = 10, + Stage = 11, + Gate = 12, + Entrance = 13, + ParkingArea = 14, + VendorArea = 15, + EmergencyLane = 16, + District = 17, + Custom = 99 + } +} diff --git a/Core/Resgrid.Model/PermissionTypes.cs b/Core/Resgrid.Model/PermissionTypes.cs index d18a3ae0..bdc7d23f 100644 --- a/Core/Resgrid.Model/PermissionTypes.cs +++ b/Core/Resgrid.Model/PermissionTypes.cs @@ -27,7 +27,9 @@ public enum PermissionTypes CreateWorkflow = 22, ManageWorkflowCredentials = 23, ViewWorkflowRuns = 24, - ViewUdfFields = 25 + ViewUdfFields = 25, + CreateRoute = 26, + ManageRoutes = 27 } } diff --git a/Core/Resgrid.Model/Repositories/ICustomMapImportsRepository.cs b/Core/Resgrid.Model/Repositories/ICustomMapImportsRepository.cs new file mode 100644 index 00000000..39de6245 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/ICustomMapImportsRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface ICustomMapImportsRepository : IRepository + { + Task> GetImportsForMapAsync(string mapId); + Task> GetPendingImportsAsync(); + } +} diff --git a/Core/Resgrid.Model/Repositories/ICustomMapTilesRepository.cs b/Core/Resgrid.Model/Repositories/ICustomMapTilesRepository.cs new file mode 100644 index 00000000..b327ed74 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/ICustomMapTilesRepository.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface ICustomMapTilesRepository : IRepository + { + Task GetTileAsync(string layerId, int zoomLevel, int tileX, int tileY); + Task> GetTilesForLayerAsync(string layerId); + Task DeleteTilesForLayerAsync(string layerId, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/Core/Resgrid.Model/Repositories/IIndoorMapFloorsRepository.cs b/Core/Resgrid.Model/Repositories/IIndoorMapFloorsRepository.cs new file mode 100644 index 00000000..ec80a15c --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IIndoorMapFloorsRepository.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IIndoorMapFloorsRepository : IRepository + { + Task> GetFloorsByIndoorMapIdAsync(string indoorMapId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IIndoorMapZonesRepository.cs b/Core/Resgrid.Model/Repositories/IIndoorMapZonesRepository.cs new file mode 100644 index 00000000..e43104a0 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IIndoorMapZonesRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IIndoorMapZonesRepository : IRepository + { + Task> GetZonesByFloorIdAsync(string indoorMapFloorId); + Task> SearchZonesAsync(int departmentId, string searchTerm); + } +} diff --git a/Core/Resgrid.Model/Repositories/IIndoorMapsRepository.cs b/Core/Resgrid.Model/Repositories/IIndoorMapsRepository.cs new file mode 100644 index 00000000..ac98892f --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IIndoorMapsRepository.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IIndoorMapsRepository : IRepository + { + Task> GetIndoorMapsByDepartmentIdAsync(int departmentId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IRouteDeviationsRepository.cs b/Core/Resgrid.Model/Repositories/IRouteDeviationsRepository.cs new file mode 100644 index 00000000..15bace5f --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRouteDeviationsRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IRouteDeviationsRepository : IRepository + { + Task> GetDeviationsByRouteInstanceIdAsync(string routeInstanceId); + Task> GetUnacknowledgedDeviationsByDepartmentAsync(int departmentId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IRouteInstanceStopsRepository.cs b/Core/Resgrid.Model/Repositories/IRouteInstanceStopsRepository.cs new file mode 100644 index 00000000..21763fc6 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRouteInstanceStopsRepository.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IRouteInstanceStopsRepository : IRepository + { + Task> GetStopsByRouteInstanceIdAsync(string routeInstanceId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IRouteInstancesRepository.cs b/Core/Resgrid.Model/Repositories/IRouteInstancesRepository.cs new file mode 100644 index 00000000..5fd5be13 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRouteInstancesRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IRouteInstancesRepository : IRepository + { + Task> GetInstancesByDepartmentIdAsync(int departmentId); + Task> GetActiveInstancesByUnitIdAsync(int unitId); + Task> GetInstancesByRoutePlanIdAsync(string routePlanId); + Task> GetInstancesByDateRangeAsync(int departmentId, DateTime startDate, DateTime endDate); + } +} diff --git a/Core/Resgrid.Model/Repositories/IRoutePlansRepository.cs b/Core/Resgrid.Model/Repositories/IRoutePlansRepository.cs new file mode 100644 index 00000000..9c1b58e0 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRoutePlansRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IRoutePlansRepository : IRepository + { + Task> GetRoutePlansByDepartmentIdAsync(int departmentId); + Task> GetRoutePlansByUnitIdAsync(int unitId); + Task> GetActiveRoutePlansByDepartmentIdAsync(int departmentId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IRouteSchedulesRepository.cs b/Core/Resgrid.Model/Repositories/IRouteSchedulesRepository.cs new file mode 100644 index 00000000..6bf849d5 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRouteSchedulesRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IRouteSchedulesRepository : IRepository + { + Task> GetSchedulesByRoutePlanIdAsync(string routePlanId); + Task> GetActiveSchedulesDueAsync(DateTime asOfDate); + } +} diff --git a/Core/Resgrid.Model/Repositories/IRouteStopsRepository.cs b/Core/Resgrid.Model/Repositories/IRouteStopsRepository.cs new file mode 100644 index 00000000..36d87557 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRouteStopsRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface IRouteStopsRepository : IRepository + { + Task> GetStopsByRoutePlanIdAsync(string routePlanId); + Task> GetStopsByCallIdAsync(int callId); + } +} diff --git a/Core/Resgrid.Model/RouteDeviation.cs b/Core/Resgrid.Model/RouteDeviation.cs new file mode 100644 index 00000000..08f90e3c --- /dev/null +++ b/Core/Resgrid.Model/RouteDeviation.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class RouteDeviation : IEntity + { + public string RouteDeviationId { get; set; } + + public string RouteInstanceId { get; set; } + + public DateTime DetectedOn { get; set; } + + public decimal Latitude { get; set; } + + public decimal Longitude { get; set; } + + public double DeviationDistanceMeters { get; set; } + + public int DeviationType { get; set; } + + public bool IsAcknowledged { get; set; } + + public string AcknowledgedByUserId { get; set; } + + public DateTime? AcknowledgedOn { get; set; } + + public string Notes { get; set; } + + [NotMapped] + public string TableName => "RouteDeviations"; + + [NotMapped] + public string IdName => "RouteDeviationId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return RouteDeviationId; } + set { RouteDeviationId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/RouteInstance.cs b/Core/Resgrid.Model/RouteInstance.cs new file mode 100644 index 00000000..cf2bb69b --- /dev/null +++ b/Core/Resgrid.Model/RouteInstance.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class RouteInstance : IEntity + { + public string RouteInstanceId { get; set; } + + public string RoutePlanId { get; set; } + + public string RouteScheduleId { get; set; } + + public int UnitId { get; set; } + + public int DepartmentId { get; set; } + + public int Status { get; set; } + + public string StartedByUserId { get; set; } + + public DateTime? ScheduledStartOn { get; set; } + + public DateTime? ActualStartOn { get; set; } + + public DateTime? ActualEndOn { get; set; } + + public string EndedByUserId { get; set; } + + public double? TotalDistanceMeters { get; set; } + + public double? TotalDurationSeconds { get; set; } + + public string ActualRouteGeometry { get; set; } + + public int StopsCompleted { get; set; } + + public int StopsTotal { get; set; } + + public string Notes { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "RouteInstances"; + + [NotMapped] + public string IdName => "RouteInstanceId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return RouteInstanceId; } + set { RouteInstanceId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/RouteInstanceStatus.cs b/Core/Resgrid.Model/RouteInstanceStatus.cs new file mode 100644 index 00000000..95621a89 --- /dev/null +++ b/Core/Resgrid.Model/RouteInstanceStatus.cs @@ -0,0 +1,12 @@ +namespace Resgrid.Model +{ + public enum RouteInstanceStatus + { + Scheduled = 0, + InProgress = 1, + Completed = 2, + Cancelled = 3, + Abandoned = 4, + Paused = 5 + } +} diff --git a/Core/Resgrid.Model/RouteInstanceStop.cs b/Core/Resgrid.Model/RouteInstanceStop.cs new file mode 100644 index 00000000..86a831d8 --- /dev/null +++ b/Core/Resgrid.Model/RouteInstanceStop.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class RouteInstanceStop : IEntity + { + public string RouteInstanceStopId { get; set; } + + public string RouteInstanceId { get; set; } + + public string RouteStopId { get; set; } + + public int StopOrder { get; set; } + + public int Status { get; set; } + + public DateTime? CheckInOn { get; set; } + + public int? CheckInType { get; set; } + + public decimal? CheckInLatitude { get; set; } + + public decimal? CheckInLongitude { get; set; } + + public DateTime? CheckOutOn { get; set; } + + public decimal? CheckOutLatitude { get; set; } + + public decimal? CheckOutLongitude { get; set; } + + public int? DwellSeconds { get; set; } + + public string SkipReason { get; set; } + + public string Notes { get; set; } + + public DateTime? EstimatedArrivalOn { get; set; } + + public int? ActualArrivalDeviation { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "RouteInstanceStops"; + + [NotMapped] + public string IdName => "RouteInstanceStopId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return RouteInstanceStopId; } + set { RouteInstanceStopId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/RoutePlan.cs b/Core/Resgrid.Model/RoutePlan.cs new file mode 100644 index 00000000..117e7199 --- /dev/null +++ b/Core/Resgrid.Model/RoutePlan.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class RoutePlan : IEntity + { + public string RoutePlanId { get; set; } + + public int DepartmentId { get; set; } + + public int? UnitId { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public int RouteStatus { get; set; } + + public string RouteColor { get; set; } + + public decimal? StartLatitude { get; set; } + + public decimal? StartLongitude { get; set; } + + public decimal? EndLatitude { get; set; } + + public decimal? EndLongitude { get; set; } + + public bool UseStationAsStart { get; set; } + + public bool UseStationAsEnd { get; set; } + + public bool OptimizeStopOrder { get; set; } + + public string MapboxRouteProfile { get; set; } + + public string MapboxRouteGeometry { get; set; } + + public double? EstimatedDistanceMeters { get; set; } + + public double? EstimatedDurationSeconds { get; set; } + + public int GeofenceRadiusMeters { get; set; } + + public bool IsDeleted { get; set; } + + public string AddedById { get; set; } + + public DateTime AddedOn { get; set; } + + public string UpdatedById { get; set; } + + public DateTime? UpdatedOn { get; set; } + + [NotMapped] + public List Stops { get; set; } + + [NotMapped] + public List Schedules { get; set; } + + [NotMapped] + public string TableName => "RoutePlans"; + + [NotMapped] + public string IdName => "RoutePlanId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return RoutePlanId; } + set { RoutePlanId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Stops", "Schedules" }; + } +} diff --git a/Core/Resgrid.Model/RouteRecurrenceType.cs b/Core/Resgrid.Model/RouteRecurrenceType.cs new file mode 100644 index 00000000..c5adc54e --- /dev/null +++ b/Core/Resgrid.Model/RouteRecurrenceType.cs @@ -0,0 +1,12 @@ +namespace Resgrid.Model +{ + public enum RouteRecurrenceType + { + None = 0, + Daily = 1, + Weekly = 2, + BiWeekly = 3, + Monthly = 4, + Custom = 5 + } +} diff --git a/Core/Resgrid.Model/RouteSchedule.cs b/Core/Resgrid.Model/RouteSchedule.cs new file mode 100644 index 00000000..3c9a58f2 --- /dev/null +++ b/Core/Resgrid.Model/RouteSchedule.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class RouteSchedule : IEntity + { + public string RouteScheduleId { get; set; } + + public string RoutePlanId { get; set; } + + public int RecurrenceType { get; set; } + + public string RecurrenceCron { get; set; } + + public string DaysOfWeek { get; set; } + + public int? DayOfMonth { get; set; } + + public string ScheduledStartTime { get; set; } + + public DateTime EffectiveFrom { get; set; } + + public DateTime? EffectiveTo { get; set; } + + public bool IsActive { get; set; } + + public DateTime? LastInstanceCreatedOn { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "RouteSchedules"; + + [NotMapped] + public string IdName => "RouteScheduleId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return RouteScheduleId; } + set { RouteScheduleId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/RouteStatus.cs b/Core/Resgrid.Model/RouteStatus.cs new file mode 100644 index 00000000..52a5c1f7 --- /dev/null +++ b/Core/Resgrid.Model/RouteStatus.cs @@ -0,0 +1,10 @@ +namespace Resgrid.Model +{ + public enum RouteStatus + { + Draft = 0, + Active = 1, + Paused = 2, + Archived = 3 + } +} diff --git a/Core/Resgrid.Model/RouteStop.cs b/Core/Resgrid.Model/RouteStop.cs new file mode 100644 index 00000000..22a893c1 --- /dev/null +++ b/Core/Resgrid.Model/RouteStop.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace Resgrid.Model +{ + public class RouteStop : IEntity + { + public string RouteStopId { get; set; } + + public string RoutePlanId { get; set; } + + public int StopOrder { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public int StopType { get; set; } + + public int? CallId { get; set; } + + public decimal Latitude { get; set; } + + public decimal Longitude { get; set; } + + public string Address { get; set; } + + public int? GeofenceRadiusMeters { get; set; } + + public int Priority { get; set; } + + public DateTime? PlannedArrivalTime { get; set; } + + public DateTime? PlannedDepartureTime { get; set; } + + public int? EstimatedDwellMinutes { get; set; } + + public string ContactName { get; set; } + + public string ContactNumber { get; set; } + + public string Notes { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime AddedOn { get; set; } + + [NotMapped] + public string TableName => "RouteStops"; + + [NotMapped] + public string IdName => "RouteStopId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return RouteStopId; } + set { RouteStopId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/RouteStopCheckInType.cs b/Core/Resgrid.Model/RouteStopCheckInType.cs new file mode 100644 index 00000000..04a9d48c --- /dev/null +++ b/Core/Resgrid.Model/RouteStopCheckInType.cs @@ -0,0 +1,9 @@ +namespace Resgrid.Model +{ + public enum RouteStopCheckInType + { + Manual = 0, + Geofence = 1, + QrCode = 2 + } +} diff --git a/Core/Resgrid.Model/RouteStopPriority.cs b/Core/Resgrid.Model/RouteStopPriority.cs new file mode 100644 index 00000000..49059b12 --- /dev/null +++ b/Core/Resgrid.Model/RouteStopPriority.cs @@ -0,0 +1,10 @@ +namespace Resgrid.Model +{ + public enum RouteStopPriority + { + Normal = 0, + High = 1, + Critical = 2, + Optional = 3 + } +} diff --git a/Core/Resgrid.Model/RouteStopType.cs b/Core/Resgrid.Model/RouteStopType.cs new file mode 100644 index 00000000..1ba0621d --- /dev/null +++ b/Core/Resgrid.Model/RouteStopType.cs @@ -0,0 +1,10 @@ +namespace Resgrid.Model +{ + public enum RouteStopType + { + Manual = 0, + ScheduledCall = 1, + Station = 2, + Waypoint = 3 + } +} diff --git a/Core/Resgrid.Model/Services/ICustomMapService.cs b/Core/Resgrid.Model/Services/ICustomMapService.cs new file mode 100644 index 00000000..623dae8c --- /dev/null +++ b/Core/Resgrid.Model/Services/ICustomMapService.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Resgrid.Model.Services +{ + public interface ICustomMapService + { + // Maps CRUD + Task SaveCustomMapAsync(IndoorMap map, CancellationToken cancellationToken = default(CancellationToken)); + Task GetCustomMapByIdAsync(string mapId); + Task> GetCustomMapsForDepartmentAsync(int departmentId, CustomMapType? filterType = null); + Task DeleteCustomMapAsync(string mapId, CancellationToken cancellationToken = default(CancellationToken)); + + // Layers CRUD + Task SaveLayerAsync(IndoorMapFloor layer, CancellationToken cancellationToken = default(CancellationToken)); + Task GetLayerByIdAsync(string layerId); + Task> GetLayersForMapAsync(string mapId); + Task DeleteLayerAsync(string layerId, CancellationToken cancellationToken = default(CancellationToken)); + + // Regions CRUD + Task SaveRegionAsync(IndoorMapZone region, CancellationToken cancellationToken = default(CancellationToken)); + Task GetRegionByIdAsync(string regionId); + Task> GetRegionsForLayerAsync(string layerId); + Task DeleteRegionAsync(string regionId, CancellationToken cancellationToken = default(CancellationToken)); + + // Dispatch integration + Task> SearchRegionsAsync(int departmentId, string searchTerm); + Task GetRegionDisplayNameAsync(string regionId); + + // Tile processing + Task ProcessAndStoreTilesAsync(string layerId, byte[] imageData, CancellationToken cancellationToken = default(CancellationToken)); + Task DeleteTilesForLayerAsync(string layerId, CancellationToken cancellationToken = default(CancellationToken)); + Task GetTileAsync(string layerId, int z, int x, int y); + + // Geo imports + Task ImportGeoJsonAsync(string mapId, string layerId, string geoJsonString, string userId, CancellationToken cancellationToken = default(CancellationToken)); + Task ImportKmlAsync(string mapId, string layerId, Stream kmlStream, bool isKmz, string userId, CancellationToken cancellationToken = default(CancellationToken)); + Task> GetImportsForMapAsync(string mapId); + } +} diff --git a/Core/Resgrid.Model/Services/IIndoorMapService.cs b/Core/Resgrid.Model/Services/IIndoorMapService.cs new file mode 100644 index 00000000..31a6bd45 --- /dev/null +++ b/Core/Resgrid.Model/Services/IIndoorMapService.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Resgrid.Model.Services +{ + public interface IIndoorMapService + { + // Indoor Maps CRUD + Task SaveIndoorMapAsync(IndoorMap indoorMap, CancellationToken cancellationToken = default(CancellationToken)); + Task GetIndoorMapByIdAsync(string indoorMapId); + Task> GetIndoorMapsForDepartmentAsync(int departmentId); + Task DeleteIndoorMapAsync(string indoorMapId, CancellationToken cancellationToken = default(CancellationToken)); + + // Floors CRUD + Task SaveFloorAsync(IndoorMapFloor floor, CancellationToken cancellationToken = default(CancellationToken)); + Task GetFloorByIdAsync(string floorId); + Task> GetFloorsForMapAsync(string indoorMapId); + Task DeleteFloorAsync(string floorId, CancellationToken cancellationToken = default(CancellationToken)); + + // Zones CRUD + Task SaveZoneAsync(IndoorMapZone zone, CancellationToken cancellationToken = default(CancellationToken)); + Task GetZoneByIdAsync(string zoneId); + Task> GetZonesForFloorAsync(string floorId); + Task DeleteZoneAsync(string zoneId, CancellationToken cancellationToken = default(CancellationToken)); + + // Dispatch integration + Task> SearchZonesAsync(int departmentId, string searchTerm); + Task GetZoneDisplayNameAsync(string zoneId); + } +} diff --git a/Core/Resgrid.Model/Services/IRouteService.cs b/Core/Resgrid.Model/Services/IRouteService.cs new file mode 100644 index 00000000..54b66339 --- /dev/null +++ b/Core/Resgrid.Model/Services/IRouteService.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Resgrid.Model.Services +{ + public interface IRouteService + { + // Route Plan CRUD + Task SaveRoutePlanAsync(RoutePlan routePlan, CancellationToken cancellationToken = default); + Task GetRoutePlanByIdAsync(string routePlanId); + Task> GetRoutePlansForDepartmentAsync(int departmentId); + Task> GetRoutePlansForUnitAsync(int unitId); + Task DeleteRoutePlanAsync(string routePlanId, CancellationToken cancellationToken = default); + + // Route Stop CRUD + Task SaveRouteStopAsync(RouteStop routeStop, CancellationToken cancellationToken = default); + Task> GetRouteStopsForPlanAsync(string routePlanId); + Task ReorderRouteStopsAsync(string routePlanId, List orderedStopIds, CancellationToken cancellationToken = default); + Task DeleteRouteStopAsync(string routeStopId, CancellationToken cancellationToken = default); + + // Route Schedule CRUD + Task SaveRouteScheduleAsync(RouteSchedule schedule, CancellationToken cancellationToken = default); + Task> GetSchedulesForPlanAsync(string routePlanId); + Task> GetDueSchedulesAsync(DateTime asOfDate); + Task DeleteRouteScheduleAsync(string routeScheduleId, CancellationToken cancellationToken = default); + + // Route Instance Lifecycle + Task StartRouteAsync(string routePlanId, int unitId, string startedByUserId, CancellationToken cancellationToken = default); + Task EndRouteAsync(string routeInstanceId, string endedByUserId, CancellationToken cancellationToken = default); + Task CancelRouteAsync(string routeInstanceId, string userId, string reason, CancellationToken cancellationToken = default); + Task PauseRouteAsync(string routeInstanceId, string userId, CancellationToken cancellationToken = default); + Task ResumeRouteAsync(string routeInstanceId, string userId, CancellationToken cancellationToken = default); + Task GetActiveInstanceForUnitAsync(int unitId); + Task GetInstanceByIdAsync(string routeInstanceId); + Task> GetInstancesForDepartmentAsync(int departmentId); + Task> GetInstancesByDateRangeAsync(int departmentId, DateTime startDate, DateTime endDate); + + // Stop Check-in/Check-out + Task CheckInAtStopAsync(string routeInstanceStopId, decimal latitude, decimal longitude, RouteStopCheckInType checkInType, CancellationToken cancellationToken = default); + Task CheckOutFromStopAsync(string routeInstanceStopId, decimal latitude, decimal longitude, CancellationToken cancellationToken = default); + Task SkipStopAsync(string routeInstanceStopId, string reason, CancellationToken cancellationToken = default); + Task> GetInstanceStopsAsync(string routeInstanceId); + + // Deviation Tracking + Task RecordDeviationAsync(RouteDeviation deviation, CancellationToken cancellationToken = default); + Task AcknowledgeDeviationAsync(string routeDeviationId, string userId, CancellationToken cancellationToken = default); + Task> GetUnacknowledgedDeviationsAsync(int departmentId); + + // Mapbox Integration + Task UpdateRouteGeometryAsync(string routePlanId, string geometry, double distance, double duration, CancellationToken cancellationToken = default); + + // Geofence + Task CheckGeofenceProximityAsync(int unitId, decimal latitude, decimal longitude); + } +} diff --git a/Core/Resgrid.Services/CallsService.cs b/Core/Resgrid.Services/CallsService.cs index 6f4a0fa5..44649901 100644 --- a/Core/Resgrid.Services/CallsService.cs +++ b/Core/Resgrid.Services/CallsService.cs @@ -41,6 +41,7 @@ public class CallsService : ICallsService private readonly IDepartmentsService _departmentsService; private readonly ICallReferencesRepository _callReferencesRepository; private readonly ICallContactsRepository _callContactsRepository; + private readonly IIndoorMapService _indoorMapService; public CallsService(ICallsRepository callsRepository, ICommunicationService communicationService, ICallDispatchesRepository callDispatchesRepository, ICallTypesRepository callTypesRepository, ICallEmailFactory callEmailFactory, @@ -49,7 +50,8 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm ICallDispatchUnitRepository callDispatchUnitRepository, ICallDispatchRoleRepository callDispatchRoleRepository, IDepartmentCallPriorityRepository departmentCallPriorityRepository, IShortenUrlProvider shortenUrlProvider, ICallProtocolsRepository callProtocolsRepository, IGeoLocationProvider geoLocationProvider, IDepartmentsService departmentsService, - ICallReferencesRepository callReferencesRepository, ICallContactsRepository callContactsRepository) + ICallReferencesRepository callReferencesRepository, ICallContactsRepository callContactsRepository, + IIndoorMapService indoorMapService) { _callsRepository = callsRepository; _communicationService = communicationService; @@ -69,6 +71,7 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm _departmentsService = departmentsService; _callReferencesRepository = callReferencesRepository; _callContactsRepository = callContactsRepository; + _indoorMapService = indoorMapService; } public async Task SaveCallAsync(Call call, CancellationToken cancellationToken = default(CancellationToken)) @@ -83,6 +86,23 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm if (!String.IsNullOrWhiteSpace(call.GeoLocationData) && call.GeoLocationData.Length == 1) call.GeoLocationData = ""; + // Enrich call with indoor zone data if an indoor zone is selected + if (!String.IsNullOrWhiteSpace(call.IndoorMapZoneId)) + { + var zone = await _indoorMapService.GetZoneByIdAsync(call.IndoorMapZoneId); + if (zone != null) + { + if (String.IsNullOrWhiteSpace(call.IndoorMapFloorId)) + call.IndoorMapFloorId = zone.IndoorMapFloorId; + + if (String.IsNullOrWhiteSpace(call.Address)) + call.Address = await _indoorMapService.GetZoneDisplayNameAsync(call.IndoorMapZoneId); + + if (String.IsNullOrWhiteSpace(call.GeoLocationData) && zone.CenterLatitude != 0 && zone.CenterLongitude != 0) + call.GeoLocationData = $"{zone.CenterLatitude},{zone.CenterLongitude}"; + } + } + if (call.Dispatches != null && call.Dispatches.Any()) { foreach (var dispatch in call.Dispatches) diff --git a/Core/Resgrid.Services/CustomMapService.cs b/Core/Resgrid.Services/CustomMapService.cs new file mode 100644 index 00000000..e0d024f1 --- /dev/null +++ b/Core/Resgrid.Services/CustomMapService.cs @@ -0,0 +1,541 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GeoJSON.Net.Feature; +using GeoJSON.Net.Geometry; +using Newtonsoft.Json; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using SharpKml.Engine; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Png; + +namespace Resgrid.Services +{ + public class CustomMapService : ICustomMapService + { + private const int TileSize = 256; + private const int TilingThreshold = 2048; + + private readonly IIndoorMapsRepository _indoorMapsRepository; + private readonly IIndoorMapFloorsRepository _indoorMapFloorsRepository; + private readonly IIndoorMapZonesRepository _indoorMapZonesRepository; + private readonly ICustomMapTilesRepository _customMapTilesRepository; + private readonly ICustomMapImportsRepository _customMapImportsRepository; + + public CustomMapService( + IIndoorMapsRepository indoorMapsRepository, + IIndoorMapFloorsRepository indoorMapFloorsRepository, + IIndoorMapZonesRepository indoorMapZonesRepository, + ICustomMapTilesRepository customMapTilesRepository, + ICustomMapImportsRepository customMapImportsRepository) + { + _indoorMapsRepository = indoorMapsRepository; + _indoorMapFloorsRepository = indoorMapFloorsRepository; + _indoorMapZonesRepository = indoorMapZonesRepository; + _customMapTilesRepository = customMapTilesRepository; + _customMapImportsRepository = customMapImportsRepository; + } + + #region Maps CRUD + + public async Task SaveCustomMapAsync(IndoorMap map, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _indoorMapsRepository.SaveOrUpdateAsync(map, cancellationToken); + } + + public async Task GetCustomMapByIdAsync(string mapId) + { + return await _indoorMapsRepository.GetByIdAsync(mapId); + } + + public async Task> GetCustomMapsForDepartmentAsync(int departmentId, CustomMapType? filterType = null) + { + var maps = await _indoorMapsRepository.GetIndoorMapsByDepartmentIdAsync(departmentId); + + if (maps == null) + return new List(); + + var result = maps.Where(x => !x.IsDeleted); + + if (filterType.HasValue) + result = result.Where(x => x.MapType == (int)filterType.Value); + + return result.ToList(); + } + + public async Task DeleteCustomMapAsync(string mapId, CancellationToken cancellationToken = default(CancellationToken)) + { + var map = await _indoorMapsRepository.GetByIdAsync(mapId); + + if (map == null) + return false; + + map.IsDeleted = true; + await _indoorMapsRepository.SaveOrUpdateAsync(map, cancellationToken); + + return true; + } + + #endregion Maps CRUD + + #region Layers CRUD + + public async Task SaveLayerAsync(IndoorMapFloor layer, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _indoorMapFloorsRepository.SaveOrUpdateAsync(layer, cancellationToken); + } + + public async Task GetLayerByIdAsync(string layerId) + { + return await _indoorMapFloorsRepository.GetByIdAsync(layerId); + } + + public async Task> GetLayersForMapAsync(string mapId) + { + var layers = await _indoorMapFloorsRepository.GetFloorsByIndoorMapIdAsync(mapId); + + if (layers != null) + return layers.Where(x => !x.IsDeleted).OrderBy(x => x.FloorOrder).ToList(); + + return new List(); + } + + public async Task DeleteLayerAsync(string layerId, CancellationToken cancellationToken = default(CancellationToken)) + { + var layer = await _indoorMapFloorsRepository.GetByIdAsync(layerId); + + if (layer == null) + return false; + + layer.IsDeleted = true; + await _indoorMapFloorsRepository.SaveOrUpdateAsync(layer, cancellationToken); + + return true; + } + + #endregion Layers CRUD + + #region Regions CRUD + + public async Task SaveRegionAsync(IndoorMapZone region, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _indoorMapZonesRepository.SaveOrUpdateAsync(region, cancellationToken); + } + + public async Task GetRegionByIdAsync(string regionId) + { + return await _indoorMapZonesRepository.GetByIdAsync(regionId); + } + + public async Task> GetRegionsForLayerAsync(string layerId) + { + var regions = await _indoorMapZonesRepository.GetZonesByFloorIdAsync(layerId); + + if (regions != null) + return regions.Where(x => !x.IsDeleted).ToList(); + + return new List(); + } + + public async Task DeleteRegionAsync(string regionId, CancellationToken cancellationToken = default(CancellationToken)) + { + var region = await _indoorMapZonesRepository.GetByIdAsync(regionId); + + if (region == null) + return false; + + region.IsDeleted = true; + await _indoorMapZonesRepository.SaveOrUpdateAsync(region, cancellationToken); + + return true; + } + + #endregion Regions CRUD + + #region Dispatch Integration + + public async Task> SearchRegionsAsync(int departmentId, string searchTerm) + { + var zones = await _indoorMapZonesRepository.SearchZonesAsync(departmentId, searchTerm); + + if (zones != null) + return zones.ToList(); + + return new List(); + } + + public async Task GetRegionDisplayNameAsync(string regionId) + { + var region = await _indoorMapZonesRepository.GetByIdAsync(regionId); + + if (region == null) + return null; + + var layer = await _indoorMapFloorsRepository.GetByIdAsync(region.IndoorMapFloorId); + + if (layer == null) + return region.Name; + + var map = await _indoorMapsRepository.GetByIdAsync(layer.IndoorMapId); + + if (map == null) + return $"{layer.Name} > {region.Name}"; + + return $"{map.Name} > {layer.Name} > {region.Name}"; + } + + #endregion Dispatch Integration + + #region Tile Processing + + public async Task ProcessAndStoreTilesAsync(string layerId, byte[] imageData, CancellationToken cancellationToken = default(CancellationToken)) + { + var layer = await _indoorMapFloorsRepository.GetByIdAsync(layerId); + if (layer == null) + return; + + using (var image = Image.Load(imageData)) + { + int maxDimension = Math.Max(image.Width, image.Height); + + if (maxDimension <= TilingThreshold) + { + // Small image — store directly, no tiling + layer.ImageData = imageData; + layer.IsTiled = false; + layer.TileMinZoom = null; + layer.TileMaxZoom = null; + layer.SourceFileSize = imageData.Length; + await _indoorMapFloorsRepository.SaveOrUpdateAsync(layer, cancellationToken); + return; + } + + // Large image — generate tiles + layer.ImageData = null; + layer.IsTiled = true; + layer.SourceFileSize = imageData.Length; + + // Calculate zoom levels + int minZoom = 0; + int maxZoom = (int)Math.Ceiling(Math.Log2(Math.Max(image.Width, image.Height) / (double)TileSize)); + if (maxZoom < 1) maxZoom = 1; + + layer.TileMinZoom = minZoom; + layer.TileMaxZoom = maxZoom; + await _indoorMapFloorsRepository.SaveOrUpdateAsync(layer, cancellationToken); + + // Delete existing tiles + await _customMapTilesRepository.DeleteTilesForLayerAsync(layerId, cancellationToken); + + // Generate tiles at each zoom level + for (int z = minZoom; z <= maxZoom; z++) + { + double scale = Math.Pow(2, z); + int scaledWidth = (int)(image.Width * scale / Math.Pow(2, maxZoom)); + int scaledHeight = (int)(image.Height * scale / Math.Pow(2, maxZoom)); + + if (scaledWidth < 1) scaledWidth = 1; + if (scaledHeight < 1) scaledHeight = 1; + + using (var resized = image.Clone(ctx => ctx.Resize(scaledWidth, scaledHeight))) + { + int tilesX = (int)Math.Ceiling((double)scaledWidth / TileSize); + int tilesY = (int)Math.Ceiling((double)scaledHeight / TileSize); + + for (int tx = 0; tx < tilesX; tx++) + { + for (int ty = 0; ty < tilesY; ty++) + { + int cropX = tx * TileSize; + int cropY = ty * TileSize; + int cropW = Math.Min(TileSize, scaledWidth - cropX); + int cropH = Math.Min(TileSize, scaledHeight - cropY); + + using (var tile = resized.Clone(ctx => ctx.Crop(new Rectangle(cropX, cropY, cropW, cropH)))) + { + // Pad to 256x256 if needed + if (cropW < TileSize || cropH < TileSize) + { + tile.Mutate(ctx => ctx.Resize(new ResizeOptions + { + Size = new Size(TileSize, TileSize), + Mode = ResizeMode.BoxPad, + PadColor = Color.Transparent + })); + } + + using (var ms = new MemoryStream()) + { + await tile.SaveAsPngAsync(ms, cancellationToken); + + var tileEntity = new CustomMapTile + { + CustomMapLayerId = layerId, + ZoomLevel = z, + TileX = tx, + TileY = ty, + TileData = ms.ToArray(), + TileContentType = "image/png", + AddedOn = DateTime.UtcNow + }; + + await _customMapTilesRepository.SaveOrUpdateAsync(tileEntity, cancellationToken); + } + } + } + } + } + } + } + } + + public async Task DeleteTilesForLayerAsync(string layerId, CancellationToken cancellationToken = default(CancellationToken)) + { + await _customMapTilesRepository.DeleteTilesForLayerAsync(layerId, cancellationToken); + } + + public async Task GetTileAsync(string layerId, int z, int x, int y) + { + return await _customMapTilesRepository.GetTileAsync(layerId, z, x, y); + } + + #endregion Tile Processing + + #region Geo Imports + + public async Task ImportGeoJsonAsync(string mapId, string layerId, string geoJsonString, string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + var import = new CustomMapImport + { + CustomMapId = mapId, + CustomMapLayerId = layerId, + SourceFileName = "geojson-import.json", + SourceFileType = (int)CustomMapImportFileType.GeoJSON, + Status = (int)CustomMapImportStatus.Processing, + ImportedById = userId, + ImportedOn = DateTime.UtcNow + }; + + await _customMapImportsRepository.SaveOrUpdateAsync(import, cancellationToken); + + try + { + var featureCollection = JsonConvert.DeserializeObject(geoJsonString); + + if (featureCollection?.Features != null) + { + foreach (var feature in featureCollection.Features) + { + var zone = CreateZoneFromGeoJsonFeature(feature, layerId); + if (zone != null) + await _indoorMapZonesRepository.SaveOrUpdateAsync(zone, cancellationToken); + } + } + + import.Status = (int)CustomMapImportStatus.Complete; + await _customMapImportsRepository.SaveOrUpdateAsync(import, cancellationToken); + } + catch (Exception ex) + { + import.Status = (int)CustomMapImportStatus.Failed; + import.ErrorMessage = ex.Message; + await _customMapImportsRepository.SaveOrUpdateAsync(import, cancellationToken); + } + + return import; + } + + public async Task ImportKmlAsync(string mapId, string layerId, Stream kmlStream, bool isKmz, string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + var import = new CustomMapImport + { + CustomMapId = mapId, + CustomMapLayerId = layerId, + SourceFileName = isKmz ? "import.kmz" : "import.kml", + SourceFileType = isKmz ? (int)CustomMapImportFileType.KMZ : (int)CustomMapImportFileType.KML, + Status = (int)CustomMapImportStatus.Processing, + ImportedById = userId, + ImportedOn = DateTime.UtcNow + }; + + await _customMapImportsRepository.SaveOrUpdateAsync(import, cancellationToken); + + try + { + SharpKml.Engine.KmlFile kmlFile; + + if (isKmz) + { + using (var kmz = SharpKml.Engine.KmzFile.Open(kmlStream)) + { + kmlFile = kmz.GetDefaultKmlFile(); + } + } + else + { + kmlFile = SharpKml.Engine.KmlFile.Load(kmlStream); + } + + if (kmlFile?.Root != null) + { + var placemarks = kmlFile.Root.Flatten().OfType(); + + foreach (var placemark in placemarks) + { + var zone = CreateZoneFromKmlPlacemark(placemark, layerId); + if (zone != null) + await _indoorMapZonesRepository.SaveOrUpdateAsync(zone, cancellationToken); + } + } + + import.Status = (int)CustomMapImportStatus.Complete; + await _customMapImportsRepository.SaveOrUpdateAsync(import, cancellationToken); + } + catch (Exception ex) + { + import.Status = (int)CustomMapImportStatus.Failed; + import.ErrorMessage = ex.Message; + await _customMapImportsRepository.SaveOrUpdateAsync(import, cancellationToken); + } + + return import; + } + + public async Task> GetImportsForMapAsync(string mapId) + { + var imports = await _customMapImportsRepository.GetImportsForMapAsync(mapId); + + if (imports != null) + return imports.ToList(); + + return new List(); + } + + #endregion Geo Imports + + #region Private Helpers + + private IndoorMapZone CreateZoneFromGeoJsonFeature(Feature feature, string layerId) + { + if (feature?.Geometry == null) + return null; + + string name = "Imported Region"; + if (feature.Properties != null && feature.Properties.ContainsKey("name")) + name = feature.Properties["name"]?.ToString() ?? name; + else if (feature.Properties != null && feature.Properties.ContainsKey("Name")) + name = feature.Properties["Name"]?.ToString() ?? name; + + string geoJson = JsonConvert.SerializeObject(feature.Geometry); + + decimal centerLat = 0; + decimal centerLon = 0; + + if (feature.Geometry is GeoJSON.Net.Geometry.Point point) + { + centerLat = (decimal)point.Coordinates.Latitude; + centerLon = (decimal)point.Coordinates.Longitude; + } + else if (feature.Geometry is Polygon polygon && polygon.Coordinates.Any()) + { + var ring = polygon.Coordinates.First().Coordinates; + if (ring.Any()) + { + centerLat = (decimal)ring.Average(c => c.Latitude); + centerLon = (decimal)ring.Average(c => c.Longitude); + } + } + else if (feature.Geometry is LineString line && line.Coordinates.Any()) + { + centerLat = (decimal)line.Coordinates.Average(c => c.Latitude); + centerLon = (decimal)line.Coordinates.Average(c => c.Longitude); + } + + return new IndoorMapZone + { + IndoorMapFloorId = layerId, + Name = name, + Description = feature.Properties?.ContainsKey("description") == true + ? feature.Properties["description"]?.ToString() + : null, + ZoneType = (int)IndoorMapZoneType.Custom, + GeoGeometry = geoJson, + CenterLatitude = centerLat, + CenterLongitude = centerLon, + IsSearchable = true, + IsDispatchable = true, + IsDeleted = false, + AddedOn = DateTime.UtcNow + }; + } + + private IndoorMapZone CreateZoneFromKmlPlacemark(SharpKml.Dom.Placemark placemark, string layerId) + { + if (placemark?.Geometry == null) + return null; + + decimal centerLat = 0; + decimal centerLon = 0; + string geoJson = null; + + if (placemark.Geometry is SharpKml.Dom.Point kmlPoint) + { + centerLat = (decimal)kmlPoint.Coordinate.Latitude; + centerLon = (decimal)kmlPoint.Coordinate.Longitude; + geoJson = JsonConvert.SerializeObject(new GeoJSON.Net.Geometry.Point( + new Position(kmlPoint.Coordinate.Latitude, kmlPoint.Coordinate.Longitude))); + } + else if (placemark.Geometry is SharpKml.Dom.Polygon kmlPolygon) + { + var coords = kmlPolygon.OuterBoundary?.LinearRing?.Coordinates; + if (coords != null && coords.Any()) + { + centerLat = (decimal)coords.Average(c => c.Latitude); + centerLon = (decimal)coords.Average(c => c.Longitude); + + var positions = coords.Select(c => new Position(c.Latitude, c.Longitude)).ToList(); + geoJson = JsonConvert.SerializeObject(new Polygon( + new List { new LineString(positions) })); + } + } + else if (placemark.Geometry is SharpKml.Dom.LineString kmlLine) + { + var coords = kmlLine.Coordinates; + if (coords != null && coords.Any()) + { + centerLat = (decimal)coords.Average(c => c.Latitude); + centerLon = (decimal)coords.Average(c => c.Longitude); + + var positions = coords.Select(c => new Position(c.Latitude, c.Longitude)).ToList(); + geoJson = JsonConvert.SerializeObject(new LineString(positions)); + } + } + + if (geoJson == null) + return null; + + return new IndoorMapZone + { + IndoorMapFloorId = layerId, + Name = placemark.Name ?? "Imported Region", + Description = placemark.Description?.Text, + ZoneType = (int)IndoorMapZoneType.Custom, + GeoGeometry = geoJson, + CenterLatitude = centerLat, + CenterLongitude = centerLon, + IsSearchable = true, + IsDispatchable = true, + IsDeleted = false, + AddedOn = DateTime.UtcNow + }; + } + + #endregion Private Helpers + } +} diff --git a/Core/Resgrid.Services/IndoorMapService.cs b/Core/Resgrid.Services/IndoorMapService.cs new file mode 100644 index 00000000..9aeaa461 --- /dev/null +++ b/Core/Resgrid.Services/IndoorMapService.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; + +namespace Resgrid.Services +{ + public class IndoorMapService : IIndoorMapService + { + private readonly IIndoorMapsRepository _indoorMapsRepository; + private readonly IIndoorMapFloorsRepository _indoorMapFloorsRepository; + private readonly IIndoorMapZonesRepository _indoorMapZonesRepository; + + public IndoorMapService(IIndoorMapsRepository indoorMapsRepository, + IIndoorMapFloorsRepository indoorMapFloorsRepository, + IIndoorMapZonesRepository indoorMapZonesRepository) + { + _indoorMapsRepository = indoorMapsRepository; + _indoorMapFloorsRepository = indoorMapFloorsRepository; + _indoorMapZonesRepository = indoorMapZonesRepository; + } + + #region Indoor Maps CRUD + + public async Task SaveIndoorMapAsync(IndoorMap indoorMap, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _indoorMapsRepository.SaveOrUpdateAsync(indoorMap, cancellationToken); + } + + public async Task GetIndoorMapByIdAsync(string indoorMapId) + { + return await _indoorMapsRepository.GetByIdAsync(indoorMapId); + } + + public async Task> GetIndoorMapsForDepartmentAsync(int departmentId) + { + var maps = await _indoorMapsRepository.GetIndoorMapsByDepartmentIdAsync(departmentId); + + if (maps != null) + return maps.Where(x => !x.IsDeleted).ToList(); + + return new List(); + } + + public async Task DeleteIndoorMapAsync(string indoorMapId, CancellationToken cancellationToken = default(CancellationToken)) + { + var map = await _indoorMapsRepository.GetByIdAsync(indoorMapId); + + if (map == null) + return false; + + map.IsDeleted = true; + await _indoorMapsRepository.SaveOrUpdateAsync(map, cancellationToken); + + return true; + } + + #endregion Indoor Maps CRUD + + #region Floors CRUD + + public async Task SaveFloorAsync(IndoorMapFloor floor, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _indoorMapFloorsRepository.SaveOrUpdateAsync(floor, cancellationToken); + } + + public async Task GetFloorByIdAsync(string floorId) + { + return await _indoorMapFloorsRepository.GetByIdAsync(floorId); + } + + public async Task> GetFloorsForMapAsync(string indoorMapId) + { + var floors = await _indoorMapFloorsRepository.GetFloorsByIndoorMapIdAsync(indoorMapId); + + if (floors != null) + return floors.Where(x => !x.IsDeleted).OrderBy(x => x.FloorOrder).ToList(); + + return new List(); + } + + public async Task DeleteFloorAsync(string floorId, CancellationToken cancellationToken = default(CancellationToken)) + { + var floor = await _indoorMapFloorsRepository.GetByIdAsync(floorId); + + if (floor == null) + return false; + + floor.IsDeleted = true; + await _indoorMapFloorsRepository.SaveOrUpdateAsync(floor, cancellationToken); + + return true; + } + + #endregion Floors CRUD + + #region Zones CRUD + + public async Task SaveZoneAsync(IndoorMapZone zone, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _indoorMapZonesRepository.SaveOrUpdateAsync(zone, cancellationToken); + } + + public async Task GetZoneByIdAsync(string zoneId) + { + return await _indoorMapZonesRepository.GetByIdAsync(zoneId); + } + + public async Task> GetZonesForFloorAsync(string floorId) + { + var zones = await _indoorMapZonesRepository.GetZonesByFloorIdAsync(floorId); + + if (zones != null) + return zones.Where(x => !x.IsDeleted).ToList(); + + return new List(); + } + + public async Task DeleteZoneAsync(string zoneId, CancellationToken cancellationToken = default(CancellationToken)) + { + var zone = await _indoorMapZonesRepository.GetByIdAsync(zoneId); + + if (zone == null) + return false; + + zone.IsDeleted = true; + await _indoorMapZonesRepository.SaveOrUpdateAsync(zone, cancellationToken); + + return true; + } + + #endregion Zones CRUD + + #region Dispatch Integration + + public async Task> SearchZonesAsync(int departmentId, string searchTerm) + { + var zones = await _indoorMapZonesRepository.SearchZonesAsync(departmentId, searchTerm); + + if (zones != null) + return zones.ToList(); + + return new List(); + } + + public async Task GetZoneDisplayNameAsync(string zoneId) + { + var zone = await _indoorMapZonesRepository.GetByIdAsync(zoneId); + + if (zone == null) + return null; + + var floor = await _indoorMapFloorsRepository.GetByIdAsync(zone.IndoorMapFloorId); + + if (floor == null) + return zone.Name; + + var map = await _indoorMapsRepository.GetByIdAsync(floor.IndoorMapId); + + if (map == null) + return $"{floor.Name} > {zone.Name}"; + + return $"{map.Name} > {floor.Name} > {zone.Name}"; + } + + #endregion Dispatch Integration + } +} diff --git a/Core/Resgrid.Services/Resgrid.Services.csproj b/Core/Resgrid.Services/Resgrid.Services.csproj index e08f74a5..1644cdac 100644 --- a/Core/Resgrid.Services/Resgrid.Services.csproj +++ b/Core/Resgrid.Services/Resgrid.Services.csproj @@ -14,6 +14,7 @@ + diff --git a/Core/Resgrid.Services/RouteService.cs b/Core/Resgrid.Services/RouteService.cs new file mode 100644 index 00000000..01c31063 --- /dev/null +++ b/Core/Resgrid.Services/RouteService.cs @@ -0,0 +1,452 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; + +namespace Resgrid.Services +{ + public class RouteService : IRouteService + { + private readonly IRoutePlansRepository _routePlansRepository; + private readonly IRouteStopsRepository _routeStopsRepository; + private readonly IRouteSchedulesRepository _routeSchedulesRepository; + private readonly IRouteInstancesRepository _routeInstancesRepository; + private readonly IRouteInstanceStopsRepository _routeInstanceStopsRepository; + private readonly IRouteDeviationsRepository _routeDeviationsRepository; + + public RouteService( + IRoutePlansRepository routePlansRepository, + IRouteStopsRepository routeStopsRepository, + IRouteSchedulesRepository routeSchedulesRepository, + IRouteInstancesRepository routeInstancesRepository, + IRouteInstanceStopsRepository routeInstanceStopsRepository, + IRouteDeviationsRepository routeDeviationsRepository) + { + _routePlansRepository = routePlansRepository; + _routeStopsRepository = routeStopsRepository; + _routeSchedulesRepository = routeSchedulesRepository; + _routeInstancesRepository = routeInstancesRepository; + _routeInstanceStopsRepository = routeInstanceStopsRepository; + _routeDeviationsRepository = routeDeviationsRepository; + } + + #region Route Plan CRUD + + public async Task SaveRoutePlanAsync(RoutePlan routePlan, CancellationToken cancellationToken = default) + { + return await _routePlansRepository.SaveOrUpdateAsync(routePlan, cancellationToken); + } + + public async Task GetRoutePlanByIdAsync(string routePlanId) + { + return await _routePlansRepository.GetByIdAsync(routePlanId); + } + + public async Task> GetRoutePlansForDepartmentAsync(int departmentId) + { + var plans = await _routePlansRepository.GetRoutePlansByDepartmentIdAsync(departmentId); + return plans?.Where(x => !x.IsDeleted).ToList() ?? new List(); + } + + public async Task> GetRoutePlansForUnitAsync(int unitId) + { + var plans = await _routePlansRepository.GetRoutePlansByUnitIdAsync(unitId); + return plans?.Where(x => !x.IsDeleted).ToList() ?? new List(); + } + + public async Task DeleteRoutePlanAsync(string routePlanId, CancellationToken cancellationToken = default) + { + var plan = await _routePlansRepository.GetByIdAsync(routePlanId); + if (plan == null) + return false; + + plan.IsDeleted = true; + await _routePlansRepository.SaveOrUpdateAsync(plan, cancellationToken); + return true; + } + + #endregion Route Plan CRUD + + #region Route Stop CRUD + + public async Task SaveRouteStopAsync(RouteStop routeStop, CancellationToken cancellationToken = default) + { + return await _routeStopsRepository.SaveOrUpdateAsync(routeStop, cancellationToken); + } + + public async Task> GetRouteStopsForPlanAsync(string routePlanId) + { + var stops = await _routeStopsRepository.GetStopsByRoutePlanIdAsync(routePlanId); + return stops?.Where(x => !x.IsDeleted).OrderBy(x => x.StopOrder).ToList() ?? new List(); + } + + public async Task ReorderRouteStopsAsync(string routePlanId, List orderedStopIds, CancellationToken cancellationToken = default) + { + var stops = await _routeStopsRepository.GetStopsByRoutePlanIdAsync(routePlanId); + if (stops == null) + return false; + + var stopDict = stops.ToDictionary(s => s.RouteStopId); + for (int i = 0; i < orderedStopIds.Count; i++) + { + if (stopDict.TryGetValue(orderedStopIds[i], out var stop)) + { + stop.StopOrder = i; + await _routeStopsRepository.SaveOrUpdateAsync(stop, cancellationToken); + } + } + + return true; + } + + public async Task DeleteRouteStopAsync(string routeStopId, CancellationToken cancellationToken = default) + { + var stop = await _routeStopsRepository.GetByIdAsync(routeStopId); + if (stop == null) + return false; + + stop.IsDeleted = true; + await _routeStopsRepository.SaveOrUpdateAsync(stop, cancellationToken); + return true; + } + + #endregion Route Stop CRUD + + #region Route Schedule CRUD + + public async Task SaveRouteScheduleAsync(RouteSchedule schedule, CancellationToken cancellationToken = default) + { + return await _routeSchedulesRepository.SaveOrUpdateAsync(schedule, cancellationToken); + } + + public async Task> GetSchedulesForPlanAsync(string routePlanId) + { + var schedules = await _routeSchedulesRepository.GetSchedulesByRoutePlanIdAsync(routePlanId); + return schedules?.ToList() ?? new List(); + } + + public async Task> GetDueSchedulesAsync(DateTime asOfDate) + { + var schedules = await _routeSchedulesRepository.GetActiveSchedulesDueAsync(asOfDate); + return schedules?.ToList() ?? new List(); + } + + public async Task DeleteRouteScheduleAsync(string routeScheduleId, CancellationToken cancellationToken = default) + { + var schedule = await _routeSchedulesRepository.GetByIdAsync(routeScheduleId); + if (schedule == null) + return false; + + return await _routeSchedulesRepository.DeleteAsync(schedule, cancellationToken); + } + + #endregion Route Schedule CRUD + + #region Route Instance Lifecycle + + public async Task StartRouteAsync(string routePlanId, int unitId, string startedByUserId, CancellationToken cancellationToken = default) + { + var existingActive = await GetActiveInstanceForUnitAsync(unitId); + if (existingActive != null) + throw new InvalidOperationException("Unit already has an active route instance."); + + var plan = await _routePlansRepository.GetByIdAsync(routePlanId); + if (plan == null) + throw new ArgumentException("Route plan not found.", nameof(routePlanId)); + + var stops = await GetRouteStopsForPlanAsync(routePlanId); + + var instance = new RouteInstance + { + RoutePlanId = routePlanId, + UnitId = unitId, + DepartmentId = plan.DepartmentId, + Status = (int)RouteInstanceStatus.InProgress, + StartedByUserId = startedByUserId, + ActualStartOn = DateTime.UtcNow, + StopsCompleted = 0, + StopsTotal = stops.Count, + AddedOn = DateTime.UtcNow + }; + + await _routeInstancesRepository.SaveOrUpdateAsync(instance, cancellationToken); + + foreach (var stop in stops) + { + var instanceStop = new RouteInstanceStop + { + RouteInstanceId = instance.RouteInstanceId, + RouteStopId = stop.RouteStopId, + StopOrder = stop.StopOrder, + Status = 0, // Pending + AddedOn = DateTime.UtcNow + }; + + await _routeInstanceStopsRepository.SaveOrUpdateAsync(instanceStop, cancellationToken); + } + + return instance; + } + + public async Task EndRouteAsync(string routeInstanceId, string endedByUserId, CancellationToken cancellationToken = default) + { + var instance = await _routeInstancesRepository.GetByIdAsync(routeInstanceId); + if (instance == null) + throw new ArgumentException("Route instance not found.", nameof(routeInstanceId)); + + if (instance.Status != (int)RouteInstanceStatus.InProgress) + throw new InvalidOperationException("Route instance is not in progress."); + + instance.Status = (int)RouteInstanceStatus.Completed; + instance.ActualEndOn = DateTime.UtcNow; + instance.EndedByUserId = endedByUserId; + + if (instance.ActualStartOn.HasValue) + instance.TotalDurationSeconds = (DateTime.UtcNow - instance.ActualStartOn.Value).TotalSeconds; + + await _routeInstancesRepository.SaveOrUpdateAsync(instance, cancellationToken); + return instance; + } + + public async Task CancelRouteAsync(string routeInstanceId, string userId, string reason, CancellationToken cancellationToken = default) + { + var instance = await _routeInstancesRepository.GetByIdAsync(routeInstanceId); + if (instance == null) + throw new ArgumentException("Route instance not found.", nameof(routeInstanceId)); + + instance.Status = (int)RouteInstanceStatus.Cancelled; + instance.ActualEndOn = DateTime.UtcNow; + instance.EndedByUserId = userId; + instance.Notes = reason; + + await _routeInstancesRepository.SaveOrUpdateAsync(instance, cancellationToken); + return instance; + } + + public async Task PauseRouteAsync(string routeInstanceId, string userId, CancellationToken cancellationToken = default) + { + var instance = await _routeInstancesRepository.GetByIdAsync(routeInstanceId); + if (instance == null) + throw new ArgumentException("Route instance not found.", nameof(routeInstanceId)); + + if (instance.Status != (int)RouteInstanceStatus.InProgress) + throw new InvalidOperationException("Route instance is not in progress."); + + instance.Status = (int)RouteInstanceStatus.Paused; + await _routeInstancesRepository.SaveOrUpdateAsync(instance, cancellationToken); + return instance; + } + + public async Task ResumeRouteAsync(string routeInstanceId, string userId, CancellationToken cancellationToken = default) + { + var instance = await _routeInstancesRepository.GetByIdAsync(routeInstanceId); + if (instance == null) + throw new ArgumentException("Route instance not found.", nameof(routeInstanceId)); + + if (instance.Status != (int)RouteInstanceStatus.Paused) + throw new InvalidOperationException("Route instance is not paused."); + + instance.Status = (int)RouteInstanceStatus.InProgress; + await _routeInstancesRepository.SaveOrUpdateAsync(instance, cancellationToken); + return instance; + } + + public async Task GetActiveInstanceForUnitAsync(int unitId) + { + var instances = await _routeInstancesRepository.GetActiveInstancesByUnitIdAsync(unitId); + return instances?.FirstOrDefault(); + } + + public async Task GetInstanceByIdAsync(string routeInstanceId) + { + return await _routeInstancesRepository.GetByIdAsync(routeInstanceId); + } + + public async Task> GetInstancesForDepartmentAsync(int departmentId) + { + var instances = await _routeInstancesRepository.GetInstancesByDepartmentIdAsync(departmentId); + return instances?.ToList() ?? new List(); + } + + public async Task> GetInstancesByDateRangeAsync(int departmentId, DateTime startDate, DateTime endDate) + { + var instances = await _routeInstancesRepository.GetInstancesByDateRangeAsync(departmentId, startDate, endDate); + return instances?.ToList() ?? new List(); + } + + #endregion Route Instance Lifecycle + + #region Stop Check-in/Check-out + + public async Task CheckInAtStopAsync(string routeInstanceStopId, decimal latitude, decimal longitude, RouteStopCheckInType checkInType, CancellationToken cancellationToken = default) + { + var instanceStop = await _routeInstanceStopsRepository.GetByIdAsync(routeInstanceStopId); + if (instanceStop == null) + throw new ArgumentException("Route instance stop not found.", nameof(routeInstanceStopId)); + + var instance = await _routeInstancesRepository.GetByIdAsync(instanceStop.RouteInstanceId); + if (instance == null || instance.Status != (int)RouteInstanceStatus.InProgress) + throw new InvalidOperationException("Route instance is not in progress."); + + instanceStop.Status = 1; // CheckedIn + instanceStop.CheckInOn = DateTime.UtcNow; + instanceStop.CheckInType = (int)checkInType; + instanceStop.CheckInLatitude = latitude; + instanceStop.CheckInLongitude = longitude; + + if (instanceStop.EstimatedArrivalOn.HasValue) + instanceStop.ActualArrivalDeviation = (int)(DateTime.UtcNow - instanceStop.EstimatedArrivalOn.Value).TotalSeconds; + + await _routeInstanceStopsRepository.SaveOrUpdateAsync(instanceStop, cancellationToken); + return instanceStop; + } + + public async Task CheckOutFromStopAsync(string routeInstanceStopId, decimal latitude, decimal longitude, CancellationToken cancellationToken = default) + { + var instanceStop = await _routeInstanceStopsRepository.GetByIdAsync(routeInstanceStopId); + if (instanceStop == null) + throw new ArgumentException("Route instance stop not found.", nameof(routeInstanceStopId)); + + if (instanceStop.Status != 1) // Not CheckedIn + throw new InvalidOperationException("Must check in before checking out."); + + instanceStop.Status = 2; // CheckedOut + instanceStop.CheckOutOn = DateTime.UtcNow; + instanceStop.CheckOutLatitude = latitude; + instanceStop.CheckOutLongitude = longitude; + + if (instanceStop.CheckInOn.HasValue) + instanceStop.DwellSeconds = (int)(DateTime.UtcNow - instanceStop.CheckInOn.Value).TotalSeconds; + + await _routeInstanceStopsRepository.SaveOrUpdateAsync(instanceStop, cancellationToken); + + // Update instance completed count + var instance = await _routeInstancesRepository.GetByIdAsync(instanceStop.RouteInstanceId); + if (instance != null) + { + instance.StopsCompleted += 1; + await _routeInstancesRepository.SaveOrUpdateAsync(instance, cancellationToken); + } + + return instanceStop; + } + + public async Task SkipStopAsync(string routeInstanceStopId, string reason, CancellationToken cancellationToken = default) + { + var instanceStop = await _routeInstanceStopsRepository.GetByIdAsync(routeInstanceStopId); + if (instanceStop == null) + throw new ArgumentException("Route instance stop not found.", nameof(routeInstanceStopId)); + + instanceStop.Status = 3; // Skipped + instanceStop.SkipReason = reason; + + await _routeInstanceStopsRepository.SaveOrUpdateAsync(instanceStop, cancellationToken); + return instanceStop; + } + + public async Task> GetInstanceStopsAsync(string routeInstanceId) + { + var stops = await _routeInstanceStopsRepository.GetStopsByRouteInstanceIdAsync(routeInstanceId); + return stops?.OrderBy(x => x.StopOrder).ToList() ?? new List(); + } + + #endregion Stop Check-in/Check-out + + #region Deviation Tracking + + public async Task RecordDeviationAsync(RouteDeviation deviation, CancellationToken cancellationToken = default) + { + return await _routeDeviationsRepository.SaveOrUpdateAsync(deviation, cancellationToken); + } + + public async Task AcknowledgeDeviationAsync(string routeDeviationId, string userId, CancellationToken cancellationToken = default) + { + var deviation = await _routeDeviationsRepository.GetByIdAsync(routeDeviationId); + if (deviation == null) + throw new ArgumentException("Route deviation not found.", nameof(routeDeviationId)); + + deviation.IsAcknowledged = true; + deviation.AcknowledgedByUserId = userId; + deviation.AcknowledgedOn = DateTime.UtcNow; + + await _routeDeviationsRepository.SaveOrUpdateAsync(deviation, cancellationToken); + return deviation; + } + + public async Task> GetUnacknowledgedDeviationsAsync(int departmentId) + { + var deviations = await _routeDeviationsRepository.GetUnacknowledgedDeviationsByDepartmentAsync(departmentId); + return deviations?.ToList() ?? new List(); + } + + #endregion Deviation Tracking + + #region Mapbox Integration + + public async Task UpdateRouteGeometryAsync(string routePlanId, string geometry, double distance, double duration, CancellationToken cancellationToken = default) + { + var plan = await _routePlansRepository.GetByIdAsync(routePlanId); + if (plan == null) + throw new ArgumentException("Route plan not found.", nameof(routePlanId)); + + plan.MapboxRouteGeometry = geometry; + plan.EstimatedDistanceMeters = distance; + plan.EstimatedDurationSeconds = duration; + + await _routePlansRepository.SaveOrUpdateAsync(plan, cancellationToken); + return plan; + } + + #endregion Mapbox Integration + + #region Geofence + + public async Task CheckGeofenceProximityAsync(int unitId, decimal latitude, decimal longitude) + { + var activeInstance = await GetActiveInstanceForUnitAsync(unitId); + if (activeInstance == null) + return null; + + var plan = await _routePlansRepository.GetByIdAsync(activeInstance.RoutePlanId); + var instanceStops = await GetInstanceStopsAsync(activeInstance.RouteInstanceId); + var pendingStops = instanceStops.Where(s => s.Status == 0).ToList(); // Pending + + foreach (var instanceStop in pendingStops) + { + var routeStop = await _routeStopsRepository.GetByIdAsync(instanceStop.RouteStopId); + if (routeStop == null) continue; + + var radiusMeters = routeStop.GeofenceRadiusMeters ?? plan?.GeofenceRadiusMeters ?? 100; + var distance = HaversineDistance(latitude, longitude, routeStop.Latitude, routeStop.Longitude); + + if (distance <= radiusMeters) + return instanceStop; + } + + return null; + } + + private static double HaversineDistance(decimal lat1, decimal lon1, decimal lat2, decimal lon2) + { + const double R = 6371000; // Earth radius in meters + var dLat = ToRadians((double)(lat2 - lat1)); + var dLon = ToRadians((double)(lon2 - lon1)); + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(ToRadians((double)lat1)) * Math.Cos(ToRadians((double)lat2)) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + return R * c; + } + + private static double ToRadians(double degrees) + { + return degrees * Math.PI / 180; + } + + #endregion Geofence + } +} diff --git a/Core/Resgrid.Services/ServicesModule.cs b/Core/Resgrid.Services/ServicesModule.cs index 0d8fe648..671ab02e 100644 --- a/Core/Resgrid.Services/ServicesModule.cs +++ b/Core/Resgrid.Services/ServicesModule.cs @@ -83,7 +83,9 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); // UDF Services builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs index 7f0f9cd6..feb0f716 100644 --- a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs +++ b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs @@ -1535,5 +1535,57 @@ public static void AddUdfClaims(ClaimsIdentity identity, bool isAdmin, List + /// Adds Route claims based on department role. + /// Department admins always receive full access (View + Update + Create + Delete). + /// Group admins receive View + Update when the permission is DepartmentAndGroupAdmins or Everyone. + /// Regular users receive View only when the permission is Everyone. + /// Default (no permission record): Everyone — all users can view routes. + /// + public static void AddRouteClaims(ClaimsIdentity identity, bool isAdmin, List permissions, bool isGroupAdmin, List roles) + { + if (isAdmin) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.View)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Update)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Create)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Delete)); + return; + } + + if (permissions != null && permissions.Any(x => x.PermissionType == (int)PermissionTypes.ManageRoutes)) + { + var permission = permissions.First(x => x.PermissionType == (int)PermissionTypes.ManageRoutes); + + if (permission.Action == (int)PermissionActions.DepartmentAdminsOnly) + { + // Non-admins get no route claims. + } + else if (permission.Action == (int)PermissionActions.DepartmentAndGroupAdmins && isGroupAdmin) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.View)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Update)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Create)); + } + else if (permission.Action == (int)PermissionActions.Everyone) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.View)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Update)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Create)); + } + } + else + { + // Default: everyone can view routes, admins and group admins can manage. + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.View)); + + if (isGroupAdmin) + { + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Update)); + identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Create)); + } + } + } } } diff --git a/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs b/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs index 3bd1a9e8..03844026 100644 --- a/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs +++ b/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs @@ -202,6 +202,7 @@ public override async Task CreateAsync(TUser user) ClaimsLogic.AddWorkflowCredentialClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); ClaimsLogic.AddWorkflowRunClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); ClaimsLogic.AddUdfClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); + ClaimsLogic.AddRouteClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); } } diff --git a/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs b/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs index 79830bea..98a3a795 100644 --- a/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs +++ b/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs @@ -125,6 +125,7 @@ public async Task BuildTokenAsync(string userId, int departmentId) ClaimsLogic.AddWorkflowCredentialClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); ClaimsLogic.AddWorkflowRunClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); ClaimsLogic.AddUdfClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); + ClaimsLogic.AddRouteClaims(id, departmentAdmin, permissions, isGroupAdmin, roles); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfig.Key)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature); diff --git a/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs b/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs index 48e837f6..f4b66d8b 100644 --- a/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs +++ b/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs @@ -63,6 +63,7 @@ public static class Resources public const string Sso = "Sso"; public const string Scim = "Scim"; public const string Udf = "Udf"; + public const string Route = "Route"; } public static string CreateDepartmentClaimTypeString(int departmentId) diff --git a/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs b/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs index d052f2c9..2e57df03 100644 --- a/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs +++ b/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs @@ -1038,5 +1038,10 @@ where roleIds.Contains(r.PersonnelRoleId) AddClaim(new Claim(ResgridClaimTypes.Resources.Contacts, ResgridClaimTypes.Actions.Delete)); } } + + public void AddRouteClaims(bool isAdmin, List permissions, bool isGroupAdmin, List roles) + { + ClaimsLogic.AddRouteClaims(this, isAdmin, permissions, isGroupAdmin, roles); + } } } diff --git a/Providers/Resgrid.Providers.Claims/ResgridResources.cs b/Providers/Resgrid.Providers.Claims/ResgridResources.cs index 0a9c8df5..bef5362a 100644 --- a/Providers/Resgrid.Providers.Claims/ResgridResources.cs +++ b/Providers/Resgrid.Providers.Claims/ResgridResources.cs @@ -162,5 +162,10 @@ public static class ResgridResources public const string Udf_Update = "Udf_Update"; public const string Udf_Create = "Udf_Create"; public const string Udf_Delete = "Udf_Delete"; + + public const string Route_View = "Route_View"; + public const string Route_Update = "Route_Update"; + public const string Route_Create = "Route_Create"; + public const string Route_Delete = "Route_Delete"; } } diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0051_AddingIndoorMaps.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0051_AddingIndoorMaps.cs new file mode 100644 index 00000000..b85b036f --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0051_AddingIndoorMaps.cs @@ -0,0 +1,94 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(51)] + public class M0051_AddingIndoorMaps : Migration + { + public override void Up() + { + Create.Table("IndoorMaps") + .WithColumn("IndoorMapId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("Name").AsString(250).NotNullable() + .WithColumn("Description").AsString(1000).Nullable() + .WithColumn("CenterLatitude").AsDecimal(10, 7).NotNullable() + .WithColumn("CenterLongitude").AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsNELat").AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsNELon").AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsSWLat").AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsSWLon").AsDecimal(10, 7).NotNullable() + .WithColumn("DefaultFloorId").AsString(128).Nullable() + .WithColumn("IsDeleted").AsBoolean().NotNullable() + .WithColumn("AddedById").AsString(128).NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable() + .WithColumn("UpdatedById").AsString(128).Nullable() + .WithColumn("UpdatedOn").AsDateTime2().Nullable(); + + Create.ForeignKey("FK_IndoorMaps_Departments") + .FromTable("IndoorMaps").ForeignColumn("DepartmentId") + .ToTable("Departments").PrimaryColumn("DepartmentId"); + + Create.Table("IndoorMapFloors") + .WithColumn("IndoorMapFloorId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("IndoorMapId").AsString(128).NotNullable() + .WithColumn("Name").AsString(100).NotNullable() + .WithColumn("FloorOrder").AsInt32().NotNullable() + .WithColumn("ImageData").AsBinary(int.MaxValue).Nullable() + .WithColumn("ImageContentType").AsString(50).Nullable() + .WithColumn("BoundsNELat").AsDecimal(10, 7).Nullable() + .WithColumn("BoundsNELon").AsDecimal(10, 7).Nullable() + .WithColumn("BoundsSWLat").AsDecimal(10, 7).Nullable() + .WithColumn("BoundsSWLon").AsDecimal(10, 7).Nullable() + .WithColumn("Opacity").AsDecimal(3, 2).NotNullable().WithDefaultValue(0.8m) + .WithColumn("IsDeleted").AsBoolean().NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_IndoorMapFloors_IndoorMaps") + .FromTable("IndoorMapFloors").ForeignColumn("IndoorMapId") + .ToTable("IndoorMaps").PrimaryColumn("IndoorMapId"); + + Create.Table("IndoorMapZones") + .WithColumn("IndoorMapZoneId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("IndoorMapFloorId").AsString(128).NotNullable() + .WithColumn("Name").AsString(250).NotNullable() + .WithColumn("Description").AsString(500).Nullable() + .WithColumn("ZoneType").AsInt32().NotNullable() + .WithColumn("PixelGeometry").AsString(int.MaxValue).Nullable() + .WithColumn("GeoGeometry").AsString(int.MaxValue).Nullable() + .WithColumn("CenterPixelX").AsDouble().NotNullable() + .WithColumn("CenterPixelY").AsDouble().NotNullable() + .WithColumn("CenterLatitude").AsDecimal(10, 7).NotNullable() + .WithColumn("CenterLongitude").AsDecimal(10, 7).NotNullable() + .WithColumn("Color").AsString(50).Nullable() + .WithColumn("Metadata").AsString(int.MaxValue).Nullable() + .WithColumn("IsSearchable").AsBoolean().NotNullable() + .WithColumn("IsDeleted").AsBoolean().NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_IndoorMapZones_IndoorMapFloors") + .FromTable("IndoorMapZones").ForeignColumn("IndoorMapFloorId") + .ToTable("IndoorMapFloors").PrimaryColumn("IndoorMapFloorId"); + + Alter.Table("Calls") + .AddColumn("IndoorMapZoneId").AsString(128).Nullable(); + + Alter.Table("Calls") + .AddColumn("IndoorMapFloorId").AsString(128).Nullable(); + } + + public override void Down() + { + Delete.ForeignKey("FK_IndoorMapZones_IndoorMapFloors").OnTable("IndoorMapZones"); + Delete.ForeignKey("FK_IndoorMapFloors_IndoorMaps").OnTable("IndoorMapFloors"); + Delete.ForeignKey("FK_IndoorMaps_Departments").OnTable("IndoorMaps"); + + Delete.Column("IndoorMapZoneId").FromTable("Calls"); + Delete.Column("IndoorMapFloorId").FromTable("Calls"); + + Delete.Table("IndoorMapZones"); + Delete.Table("IndoorMapFloors"); + Delete.Table("IndoorMaps"); + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0052_AddingCustomMapSupport.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0052_AddingCustomMapSupport.cs new file mode 100644 index 00000000..f1526940 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0052_AddingCustomMapSupport.cs @@ -0,0 +1,95 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(52)] + public class M0052_AddingCustomMapSupport : Migration + { + public override void Up() + { + // ── IndoorMaps – new columns ───────────────────────────────────── + Alter.Table("IndoorMaps") + .AddColumn("MapType").AsInt32().NotNullable().WithDefaultValue(0) + .AddColumn("BoundsGeoJson").AsString(int.MaxValue).Nullable() + .AddColumn("ThumbnailData").AsBinary(int.MaxValue).Nullable() + .AddColumn("ThumbnailContentType").AsString(50).Nullable(); + + // ── IndoorMapFloors – new columns ──────────────────────────────── + Alter.Table("IndoorMapFloors") + .AddColumn("LayerType").AsInt32().NotNullable().WithDefaultValue(0) + .AddColumn("IsTiled").AsBoolean().NotNullable().WithDefaultValue(false) + .AddColumn("TileMinZoom").AsInt32().Nullable() + .AddColumn("TileMaxZoom").AsInt32().Nullable() + .AddColumn("SourceFileSize").AsInt64().Nullable() + .AddColumn("GeoJsonData").AsString(int.MaxValue).Nullable(); + + // ── IndoorMapZones – new columns ───────────────────────────────── + Alter.Table("IndoorMapZones") + .AddColumn("IsDispatchable").AsBoolean().NotNullable().WithDefaultValue(true); + + // ── CustomMapTiles ─────────────────────────────────────────────── + Create.Table("CustomMapTiles") + .WithColumn("CustomMapTileId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("CustomMapLayerId").AsString(128).NotNullable() + .WithColumn("ZoomLevel").AsInt32().NotNullable() + .WithColumn("TileX").AsInt32().NotNullable() + .WithColumn("TileY").AsInt32().NotNullable() + .WithColumn("TileData").AsBinary(int.MaxValue).NotNullable() + .WithColumn("TileContentType").AsString(50).NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_CustomMapTiles_IndoorMapFloors") + .FromTable("CustomMapTiles").ForeignColumn("CustomMapLayerId") + .ToTable("IndoorMapFloors").PrimaryColumn("IndoorMapFloorId"); + + Create.UniqueConstraint("UQ_CustomMapTiles_LayerZoomXY") + .OnTable("CustomMapTiles") + .Columns(new[] { "CustomMapLayerId", "ZoomLevel", "TileX", "TileY" }); + + // ── CustomMapImports ───────────────────────────────────────────── + Create.Table("CustomMapImports") + .WithColumn("CustomMapImportId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("CustomMapId").AsString(128).NotNullable() + .WithColumn("CustomMapLayerId").AsString(128).Nullable() + .WithColumn("SourceFileName").AsString(500).NotNullable() + .WithColumn("SourceFileType").AsInt32().NotNullable() + .WithColumn("Status").AsInt32().NotNullable() + .WithColumn("ErrorMessage").AsString(int.MaxValue).Nullable() + .WithColumn("ImportedById").AsString(128).NotNullable() + .WithColumn("ImportedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_CustomMapImports_IndoorMaps") + .FromTable("CustomMapImports").ForeignColumn("CustomMapId") + .ToTable("IndoorMaps").PrimaryColumn("IndoorMapId"); + + Create.ForeignKey("FK_CustomMapImports_IndoorMapFloors") + .FromTable("CustomMapImports").ForeignColumn("CustomMapLayerId") + .ToTable("IndoorMapFloors").PrimaryColumn("IndoorMapFloorId"); + } + + public override void Down() + { + Delete.ForeignKey("FK_CustomMapImports_IndoorMapFloors").OnTable("CustomMapImports"); + Delete.ForeignKey("FK_CustomMapImports_IndoorMaps").OnTable("CustomMapImports"); + Delete.Table("CustomMapImports"); + + Delete.UniqueConstraint("UQ_CustomMapTiles_LayerZoomXY").FromTable("CustomMapTiles"); + Delete.ForeignKey("FK_CustomMapTiles_IndoorMapFloors").OnTable("CustomMapTiles"); + Delete.Table("CustomMapTiles"); + + Delete.Column("IsDispatchable").FromTable("IndoorMapZones"); + + Delete.Column("LayerType").FromTable("IndoorMapFloors"); + Delete.Column("IsTiled").FromTable("IndoorMapFloors"); + Delete.Column("TileMinZoom").FromTable("IndoorMapFloors"); + Delete.Column("TileMaxZoom").FromTable("IndoorMapFloors"); + Delete.Column("SourceFileSize").FromTable("IndoorMapFloors"); + Delete.Column("GeoJsonData").FromTable("IndoorMapFloors"); + + Delete.Column("MapType").FromTable("IndoorMaps"); + Delete.Column("BoundsGeoJson").FromTable("IndoorMaps"); + Delete.Column("ThumbnailData").FromTable("IndoorMaps"); + Delete.Column("ThumbnailContentType").FromTable("IndoorMaps"); + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0053_AddingRoutePlanning.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0053_AddingRoutePlanning.cs new file mode 100644 index 00000000..c9f31d54 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0053_AddingRoutePlanning.cs @@ -0,0 +1,219 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(53)] + public class M0053_AddingRoutePlanning : Migration + { + public override void Up() + { + // ── RoutePlans ────────────────────────────────────────── + Create.Table("RoutePlans") + .WithColumn("RoutePlanId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("UnitId").AsInt32().Nullable() + .WithColumn("Name").AsString(250).NotNullable() + .WithColumn("Description").AsString(1000).Nullable() + .WithColumn("RouteStatus").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("RouteColor").AsString(50).Nullable() + .WithColumn("StartLatitude").AsDecimal(10, 7).Nullable() + .WithColumn("StartLongitude").AsDecimal(10, 7).Nullable() + .WithColumn("EndLatitude").AsDecimal(10, 7).Nullable() + .WithColumn("EndLongitude").AsDecimal(10, 7).Nullable() + .WithColumn("UseStationAsStart").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("UseStationAsEnd").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("OptimizeStopOrder").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("MapboxRouteProfile").AsString(50).Nullable() + .WithColumn("MapboxRouteGeometry").AsString(int.MaxValue).Nullable() + .WithColumn("EstimatedDistanceMeters").AsDouble().Nullable() + .WithColumn("EstimatedDurationSeconds").AsDouble().Nullable() + .WithColumn("GeofenceRadiusMeters").AsInt32().NotNullable().WithDefaultValue(100) + .WithColumn("IsDeleted").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("AddedById").AsString(128).NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable() + .WithColumn("UpdatedById").AsString(128).Nullable() + .WithColumn("UpdatedOn").AsDateTime2().Nullable(); + + Create.ForeignKey("FK_RoutePlans_Departments") + .FromTable("RoutePlans").ForeignColumn("DepartmentId") + .ToTable("Departments").PrimaryColumn("DepartmentId"); + + Create.Index("IX_RoutePlans_DepartmentId") + .OnTable("RoutePlans") + .OnColumn("DepartmentId"); + + // ── RouteStops ────────────────────────────────────────── + Create.Table("RouteStops") + .WithColumn("RouteStopId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("RoutePlanId").AsString(128).NotNullable() + .WithColumn("StopOrder").AsInt32().NotNullable() + .WithColumn("Name").AsString(250).NotNullable() + .WithColumn("Description").AsString(1000).Nullable() + .WithColumn("StopType").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CallId").AsInt32().Nullable() + .WithColumn("Latitude").AsDecimal(10, 7).NotNullable() + .WithColumn("Longitude").AsDecimal(10, 7).NotNullable() + .WithColumn("Address").AsString(500).Nullable() + .WithColumn("GeofenceRadiusMeters").AsInt32().Nullable() + .WithColumn("Priority").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("PlannedArrivalTime").AsDateTime2().Nullable() + .WithColumn("PlannedDepartureTime").AsDateTime2().Nullable() + .WithColumn("EstimatedDwellMinutes").AsInt32().Nullable() + .WithColumn("ContactName").AsString(250).Nullable() + .WithColumn("ContactNumber").AsString(50).Nullable() + .WithColumn("Notes").AsString(int.MaxValue).Nullable() + .WithColumn("IsDeleted").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_RouteStops_RoutePlans") + .FromTable("RouteStops").ForeignColumn("RoutePlanId") + .ToTable("RoutePlans").PrimaryColumn("RoutePlanId"); + + Create.Index("IX_RouteStops_RoutePlanId") + .OnTable("RouteStops") + .OnColumn("RoutePlanId"); + + // ── RouteSchedules ────────────────────────────────────── + Create.Table("RouteSchedules") + .WithColumn("RouteScheduleId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("RoutePlanId").AsString(128).NotNullable() + .WithColumn("RecurrenceType").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("RecurrenceCron").AsString(250).Nullable() + .WithColumn("DaysOfWeek").AsString(50).Nullable() + .WithColumn("DayOfMonth").AsInt32().Nullable() + .WithColumn("ScheduledStartTime").AsString(20).Nullable() + .WithColumn("EffectiveFrom").AsDateTime2().NotNullable() + .WithColumn("EffectiveTo").AsDateTime2().Nullable() + .WithColumn("IsActive").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("LastInstanceCreatedOn").AsDateTime2().Nullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_RouteSchedules_RoutePlans") + .FromTable("RouteSchedules").ForeignColumn("RoutePlanId") + .ToTable("RoutePlans").PrimaryColumn("RoutePlanId"); + + Create.Index("IX_RouteSchedules_RoutePlanId") + .OnTable("RouteSchedules") + .OnColumn("RoutePlanId"); + + // ── RouteInstances ────────────────────────────────────── + Create.Table("RouteInstances") + .WithColumn("RouteInstanceId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("RoutePlanId").AsString(128).NotNullable() + .WithColumn("RouteScheduleId").AsString(128).Nullable() + .WithColumn("UnitId").AsInt32().NotNullable() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("Status").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("StartedByUserId").AsString(128).NotNullable() + .WithColumn("ScheduledStartOn").AsDateTime2().Nullable() + .WithColumn("ActualStartOn").AsDateTime2().Nullable() + .WithColumn("ActualEndOn").AsDateTime2().Nullable() + .WithColumn("EndedByUserId").AsString(128).Nullable() + .WithColumn("TotalDistanceMeters").AsDouble().Nullable() + .WithColumn("TotalDurationSeconds").AsDouble().Nullable() + .WithColumn("ActualRouteGeometry").AsString(int.MaxValue).Nullable() + .WithColumn("StopsCompleted").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("StopsTotal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("Notes").AsString(int.MaxValue).Nullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_RouteInstances_RoutePlans") + .FromTable("RouteInstances").ForeignColumn("RoutePlanId") + .ToTable("RoutePlans").PrimaryColumn("RoutePlanId"); + + Create.ForeignKey("FK_RouteInstances_Departments") + .FromTable("RouteInstances").ForeignColumn("DepartmentId") + .ToTable("Departments").PrimaryColumn("DepartmentId"); + + Create.Index("IX_RouteInstances_RoutePlanId") + .OnTable("RouteInstances") + .OnColumn("RoutePlanId"); + + Create.Index("IX_RouteInstances_DepartmentId") + .OnTable("RouteInstances") + .OnColumn("DepartmentId"); + + Create.Index("IX_RouteInstances_UnitId") + .OnTable("RouteInstances") + .OnColumn("UnitId"); + + // ── RouteInstanceStops ────────────────────────────────── + Create.Table("RouteInstanceStops") + .WithColumn("RouteInstanceStopId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("RouteInstanceId").AsString(128).NotNullable() + .WithColumn("RouteStopId").AsString(128).NotNullable() + .WithColumn("StopOrder").AsInt32().NotNullable() + .WithColumn("Status").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CheckInOn").AsDateTime2().Nullable() + .WithColumn("CheckInType").AsInt32().Nullable() + .WithColumn("CheckInLatitude").AsDecimal(10, 7).Nullable() + .WithColumn("CheckInLongitude").AsDecimal(10, 7).Nullable() + .WithColumn("CheckOutOn").AsDateTime2().Nullable() + .WithColumn("CheckOutLatitude").AsDecimal(10, 7).Nullable() + .WithColumn("CheckOutLongitude").AsDecimal(10, 7).Nullable() + .WithColumn("DwellSeconds").AsInt32().Nullable() + .WithColumn("SkipReason").AsString(500).Nullable() + .WithColumn("Notes").AsString(int.MaxValue).Nullable() + .WithColumn("EstimatedArrivalOn").AsDateTime2().Nullable() + .WithColumn("ActualArrivalDeviation").AsInt32().Nullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable(); + + Create.ForeignKey("FK_RouteInstanceStops_RouteInstances") + .FromTable("RouteInstanceStops").ForeignColumn("RouteInstanceId") + .ToTable("RouteInstances").PrimaryColumn("RouteInstanceId"); + + Create.ForeignKey("FK_RouteInstanceStops_RouteStops") + .FromTable("RouteInstanceStops").ForeignColumn("RouteStopId") + .ToTable("RouteStops").PrimaryColumn("RouteStopId"); + + Create.Index("IX_RouteInstanceStops_RouteInstanceId") + .OnTable("RouteInstanceStops") + .OnColumn("RouteInstanceId"); + + // ── RouteDeviations ───────────────────────────────────── + Create.Table("RouteDeviations") + .WithColumn("RouteDeviationId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("RouteInstanceId").AsString(128).NotNullable() + .WithColumn("DetectedOn").AsDateTime2().NotNullable() + .WithColumn("Latitude").AsDecimal(10, 7).NotNullable() + .WithColumn("Longitude").AsDecimal(10, 7).NotNullable() + .WithColumn("DeviationDistanceMeters").AsDouble().NotNullable() + .WithColumn("DeviationType").AsInt32().NotNullable() + .WithColumn("IsAcknowledged").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("AcknowledgedByUserId").AsString(128).Nullable() + .WithColumn("AcknowledgedOn").AsDateTime2().Nullable() + .WithColumn("Notes").AsString(500).Nullable(); + + Create.ForeignKey("FK_RouteDeviations_RouteInstances") + .FromTable("RouteDeviations").ForeignColumn("RouteInstanceId") + .ToTable("RouteInstances").PrimaryColumn("RouteInstanceId"); + + Create.Index("IX_RouteDeviations_RouteInstanceId") + .OnTable("RouteDeviations") + .OnColumn("RouteInstanceId"); + } + + public override void Down() + { + Delete.ForeignKey("FK_RouteDeviations_RouteInstances").OnTable("RouteDeviations"); + Delete.Table("RouteDeviations"); + + Delete.ForeignKey("FK_RouteInstanceStops_RouteStops").OnTable("RouteInstanceStops"); + Delete.ForeignKey("FK_RouteInstanceStops_RouteInstances").OnTable("RouteInstanceStops"); + Delete.Table("RouteInstanceStops"); + + Delete.ForeignKey("FK_RouteInstances_Departments").OnTable("RouteInstances"); + Delete.ForeignKey("FK_RouteInstances_RoutePlans").OnTable("RouteInstances"); + Delete.Table("RouteInstances"); + + Delete.ForeignKey("FK_RouteSchedules_RoutePlans").OnTable("RouteSchedules"); + Delete.Table("RouteSchedules"); + + Delete.ForeignKey("FK_RouteStops_RoutePlans").OnTable("RouteStops"); + Delete.Table("RouteStops"); + + Delete.ForeignKey("FK_RoutePlans_Departments").OnTable("RoutePlans"); + Delete.Table("RoutePlans"); + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0051_AddingIndoorMapsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0051_AddingIndoorMapsPg.cs new file mode 100644 index 00000000..dfefa7d4 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0051_AddingIndoorMapsPg.cs @@ -0,0 +1,113 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + [Migration(51)] + public class M0051_AddingIndoorMapsPg : Migration + { + public override void Up() + { + // ── IndoorMaps ──────────────────────────────────────────────────── + Create.Table("IndoorMaps".ToLower()) + .WithColumn("IndoorMapId".ToLower()).AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("DepartmentId".ToLower()).AsInt32().NotNullable() + .WithColumn("Name".ToLower()).AsCustom("citext").NotNullable() + .WithColumn("Description".ToLower()).AsCustom("citext").Nullable() + .WithColumn("CenterLatitude".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("CenterLongitude".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsNELat".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsNELon".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsSWLat".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("BoundsSWLon".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("DefaultFloorId".ToLower()).AsCustom("citext").Nullable() + .WithColumn("IsDeleted".ToLower()).AsBoolean().NotNullable() + .WithColumn("AddedById".ToLower()).AsCustom("citext").NotNullable() + .WithColumn("AddedOn".ToLower()).AsDateTime().NotNullable() + .WithColumn("UpdatedById".ToLower()).AsCustom("citext").Nullable() + .WithColumn("UpdatedOn".ToLower()).AsDateTime().Nullable(); + + Create.ForeignKey("FK_IndoorMaps_Departments".ToLower()) + .FromTable("IndoorMaps".ToLower()).ForeignColumn("DepartmentId".ToLower()) + .ToTable("Departments".ToLower()).PrimaryColumn("DepartmentId".ToLower()); + + Create.Index("IX_IndoorMaps_DepartmentId".ToLower()) + .OnTable("IndoorMaps".ToLower()) + .OnColumn("DepartmentId".ToLower()); + + // ── IndoorMapFloors ─────────────────────────────────────────────── + Create.Table("IndoorMapFloors".ToLower()) + .WithColumn("IndoorMapFloorId".ToLower()).AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("IndoorMapId".ToLower()).AsCustom("citext").NotNullable() + .WithColumn("Name".ToLower()).AsCustom("citext").NotNullable() + .WithColumn("FloorOrder".ToLower()).AsInt32().NotNullable() + .WithColumn("ImageData".ToLower()).AsCustom("bytea").Nullable() + .WithColumn("ImageContentType".ToLower()).AsCustom("citext").Nullable() + .WithColumn("BoundsNELat".ToLower()).AsDecimal(10, 7).Nullable() + .WithColumn("BoundsNELon".ToLower()).AsDecimal(10, 7).Nullable() + .WithColumn("BoundsSWLat".ToLower()).AsDecimal(10, 7).Nullable() + .WithColumn("BoundsSWLon".ToLower()).AsDecimal(10, 7).Nullable() + .WithColumn("Opacity".ToLower()).AsDecimal(3, 2).NotNullable().WithDefaultValue(0.8m) + .WithColumn("IsDeleted".ToLower()).AsBoolean().NotNullable() + .WithColumn("AddedOn".ToLower()).AsDateTime().NotNullable(); + + Create.ForeignKey("FK_IndoorMapFloors_IndoorMaps".ToLower()) + .FromTable("IndoorMapFloors".ToLower()).ForeignColumn("IndoorMapId".ToLower()) + .ToTable("IndoorMaps".ToLower()).PrimaryColumn("IndoorMapId".ToLower()); + + Create.Index("IX_IndoorMapFloors_IndoorMapId".ToLower()) + .OnTable("IndoorMapFloors".ToLower()) + .OnColumn("IndoorMapId".ToLower()); + + // ── IndoorMapZones ──────────────────────────────────────────────── + Create.Table("IndoorMapZones".ToLower()) + .WithColumn("IndoorMapZoneId".ToLower()).AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("IndoorMapFloorId".ToLower()).AsCustom("citext").NotNullable() + .WithColumn("Name".ToLower()).AsCustom("citext").NotNullable() + .WithColumn("Description".ToLower()).AsCustom("citext").Nullable() + .WithColumn("ZoneType".ToLower()).AsInt32().NotNullable() + .WithColumn("PixelGeometry".ToLower()).AsCustom("text").Nullable() + .WithColumn("GeoGeometry".ToLower()).AsCustom("text").Nullable() + .WithColumn("CenterPixelX".ToLower()).AsDouble().NotNullable() + .WithColumn("CenterPixelY".ToLower()).AsDouble().NotNullable() + .WithColumn("CenterLatitude".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("CenterLongitude".ToLower()).AsDecimal(10, 7).NotNullable() + .WithColumn("Color".ToLower()).AsCustom("citext").Nullable() + .WithColumn("Metadata".ToLower()).AsCustom("text").Nullable() + .WithColumn("IsSearchable".ToLower()).AsBoolean().NotNullable() + .WithColumn("IsDeleted".ToLower()).AsBoolean().NotNullable() + .WithColumn("AddedOn".ToLower()).AsDateTime().NotNullable(); + + Create.ForeignKey("FK_IndoorMapZones_IndoorMapFloors".ToLower()) + .FromTable("IndoorMapZones".ToLower()).ForeignColumn("IndoorMapFloorId".ToLower()) + .ToTable("IndoorMapFloors".ToLower()).PrimaryColumn("IndoorMapFloorId".ToLower()); + + Create.Index("IX_IndoorMapZones_IndoorMapFloorId".ToLower()) + .OnTable("IndoorMapZones".ToLower()) + .OnColumn("IndoorMapFloorId".ToLower()); + + // ── Calls (additional columns) ──────────────────────────────────── + Alter.Table("Calls".ToLower()) + .AddColumn("IndoorMapZoneId".ToLower()).AsCustom("citext").Nullable() + .AddColumn("IndoorMapFloorId".ToLower()).AsCustom("citext").Nullable(); + } + + public override void Down() + { + Delete.Index("IX_IndoorMapZones_IndoorMapFloorId".ToLower()).OnTable("IndoorMapZones".ToLower()); + Delete.ForeignKey("FK_IndoorMapZones_IndoorMapFloors".ToLower()).OnTable("IndoorMapZones".ToLower()); + + Delete.Index("IX_IndoorMapFloors_IndoorMapId".ToLower()).OnTable("IndoorMapFloors".ToLower()); + Delete.ForeignKey("FK_IndoorMapFloors_IndoorMaps".ToLower()).OnTable("IndoorMapFloors".ToLower()); + + Delete.Index("IX_IndoorMaps_DepartmentId".ToLower()).OnTable("IndoorMaps".ToLower()); + Delete.ForeignKey("FK_IndoorMaps_Departments".ToLower()).OnTable("IndoorMaps".ToLower()); + + Delete.Column("IndoorMapZoneId".ToLower()).FromTable("Calls".ToLower()); + Delete.Column("IndoorMapFloorId".ToLower()).FromTable("Calls".ToLower()); + + Delete.Table("IndoorMapZones".ToLower()); + Delete.Table("IndoorMapFloors".ToLower()); + Delete.Table("IndoorMaps".ToLower()); + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0052_AddingCustomMapSupportPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0052_AddingCustomMapSupportPg.cs new file mode 100644 index 00000000..8cab749e --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0052_AddingCustomMapSupportPg.cs @@ -0,0 +1,104 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + [Migration(52)] + public class M0052_AddingCustomMapSupportPg : Migration + { + public override void Up() + { + // ── IndoorMaps – new columns ───────────────────────────────────── + Alter.Table("indoormaps") + .AddColumn("maptype").AsInt32().NotNullable().WithDefaultValue(0) + .AddColumn("boundsgeojson").AsCustom("text").Nullable() + .AddColumn("thumbnaildata").AsCustom("bytea").Nullable() + .AddColumn("thumbnailcontenttype").AsCustom("citext").Nullable(); + + // ── IndoorMapFloors – new columns ──────────────────────────────── + Alter.Table("indoormapfloors") + .AddColumn("layertype").AsInt32().NotNullable().WithDefaultValue(0) + .AddColumn("istiled").AsBoolean().NotNullable().WithDefaultValue(false) + .AddColumn("tileminzoom").AsInt32().Nullable() + .AddColumn("tilemaxzoom").AsInt32().Nullable() + .AddColumn("sourcefilesize").AsInt64().Nullable() + .AddColumn("geojsondata").AsCustom("text").Nullable(); + + // ── IndoorMapZones – new columns ───────────────────────────────── + Alter.Table("indoormapzones") + .AddColumn("isdispatchable").AsBoolean().NotNullable().WithDefaultValue(true); + + // ── CustomMapTiles ─────────────────────────────────────────────── + Create.Table("custommaptiles") + .WithColumn("custommaptileid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("custommaplayerid").AsCustom("citext").NotNullable() + .WithColumn("zoomlevel").AsInt32().NotNullable() + .WithColumn("tilex").AsInt32().NotNullable() + .WithColumn("tiley").AsInt32().NotNullable() + .WithColumn("tiledata").AsCustom("bytea").NotNullable() + .WithColumn("tilecontenttype").AsCustom("citext").NotNullable() + .WithColumn("addedon").AsDateTime().NotNullable(); + + Create.ForeignKey("fk_custommaptiles_indoormapfloors") + .FromTable("custommaptiles").ForeignColumn("custommaplayerid") + .ToTable("indoormapfloors").PrimaryColumn("indoormapfloorid"); + + Create.Index("ix_custommaptiles_layerzoomxy") + .OnTable("custommaptiles") + .OnColumn("custommaplayerid").Ascending() + .OnColumn("zoomlevel").Ascending() + .OnColumn("tilex").Ascending() + .OnColumn("tiley").Ascending() + .WithOptions().Unique(); + + // ── CustomMapImports ───────────────────────────────────────────── + Create.Table("custommapimports") + .WithColumn("custommapimportid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("custommapid").AsCustom("citext").NotNullable() + .WithColumn("custommaplayerid").AsCustom("citext").Nullable() + .WithColumn("sourcefilename").AsCustom("citext").NotNullable() + .WithColumn("sourcefiletype").AsInt32().NotNullable() + .WithColumn("status").AsInt32().NotNullable() + .WithColumn("errormessage").AsCustom("text").Nullable() + .WithColumn("importedbyid").AsCustom("citext").NotNullable() + .WithColumn("importedon").AsDateTime().NotNullable(); + + Create.ForeignKey("fk_custommapimports_indoormaps") + .FromTable("custommapimports").ForeignColumn("custommapid") + .ToTable("indoormaps").PrimaryColumn("indoormapid"); + + Create.ForeignKey("fk_custommapimports_indoormapfloors") + .FromTable("custommapimports").ForeignColumn("custommaplayerid") + .ToTable("indoormapfloors").PrimaryColumn("indoormapfloorid"); + + Create.Index("ix_custommapimports_custommapid") + .OnTable("custommapimports") + .OnColumn("custommapid"); + } + + public override void Down() + { + Delete.Index("ix_custommapimports_custommapid").OnTable("custommapimports"); + Delete.ForeignKey("fk_custommapimports_indoormapfloors").OnTable("custommapimports"); + Delete.ForeignKey("fk_custommapimports_indoormaps").OnTable("custommapimports"); + Delete.Table("custommapimports"); + + Delete.Index("ix_custommaptiles_layerzoomxy").OnTable("custommaptiles"); + Delete.ForeignKey("fk_custommaptiles_indoormapfloors").OnTable("custommaptiles"); + Delete.Table("custommaptiles"); + + Delete.Column("isdispatchable").FromTable("indoormapzones"); + + Delete.Column("layertype").FromTable("indoormapfloors"); + Delete.Column("istiled").FromTable("indoormapfloors"); + Delete.Column("tileminzoom").FromTable("indoormapfloors"); + Delete.Column("tilemaxzoom").FromTable("indoormapfloors"); + Delete.Column("sourcefilesize").FromTable("indoormapfloors"); + Delete.Column("geojsondata").FromTable("indoormapfloors"); + + Delete.Column("maptype").FromTable("indoormaps"); + Delete.Column("boundsgeojson").FromTable("indoormaps"); + Delete.Column("thumbnaildata").FromTable("indoormaps"); + Delete.Column("thumbnailcontenttype").FromTable("indoormaps"); + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0053_AddingRoutePlanningPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0053_AddingRoutePlanningPg.cs new file mode 100644 index 00000000..230cb9e8 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0053_AddingRoutePlanningPg.cs @@ -0,0 +1,227 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + [Migration(53)] + public class M0053_AddingRoutePlanningPg : Migration + { + public override void Up() + { + // ── RoutePlans ────────────────────────────────────────── + Create.Table("routeplans") + .WithColumn("routeplanid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("unitid").AsInt32().Nullable() + .WithColumn("name").AsCustom("citext").NotNullable() + .WithColumn("description").AsCustom("citext").Nullable() + .WithColumn("routestatus").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("routecolor").AsCustom("citext").Nullable() + .WithColumn("startlatitude").AsDecimal(10, 7).Nullable() + .WithColumn("startlongitude").AsDecimal(10, 7).Nullable() + .WithColumn("endlatitude").AsDecimal(10, 7).Nullable() + .WithColumn("endlongitude").AsDecimal(10, 7).Nullable() + .WithColumn("usestationasstart").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("usestationasend").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("optimizestoporder").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("mapboxrouteprofile").AsCustom("citext").Nullable() + .WithColumn("mapboxroutegeometry").AsCustom("text").Nullable() + .WithColumn("estimateddistancemeters").AsDouble().Nullable() + .WithColumn("estimateddurationseconds").AsDouble().Nullable() + .WithColumn("geofenceradiusmeters").AsInt32().NotNullable().WithDefaultValue(100) + .WithColumn("isdeleted").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("addedbyid").AsCustom("citext").NotNullable() + .WithColumn("addedon").AsDateTime().NotNullable() + .WithColumn("updatedbyid").AsCustom("citext").Nullable() + .WithColumn("updatedon").AsDateTime().Nullable(); + + Create.ForeignKey("fk_routeplans_departments") + .FromTable("routeplans").ForeignColumn("departmentid") + .ToTable("departments").PrimaryColumn("departmentid"); + + Create.Index("ix_routeplans_departmentid") + .OnTable("routeplans") + .OnColumn("departmentid"); + + // ── RouteStops ────────────────────────────────────────── + Create.Table("routestops") + .WithColumn("routestopid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("routeplanid").AsCustom("citext").NotNullable() + .WithColumn("stoporder").AsInt32().NotNullable() + .WithColumn("name").AsCustom("citext").NotNullable() + .WithColumn("description").AsCustom("citext").Nullable() + .WithColumn("stoptype").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("callid").AsInt32().Nullable() + .WithColumn("latitude").AsDecimal(10, 7).NotNullable() + .WithColumn("longitude").AsDecimal(10, 7).NotNullable() + .WithColumn("address").AsCustom("citext").Nullable() + .WithColumn("geofenceradiusmeters").AsInt32().Nullable() + .WithColumn("priority").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("plannedarrivaltime").AsDateTime().Nullable() + .WithColumn("planneddeparturetime").AsDateTime().Nullable() + .WithColumn("estimateddwellminutes").AsInt32().Nullable() + .WithColumn("contactname").AsCustom("citext").Nullable() + .WithColumn("contactnumber").AsCustom("citext").Nullable() + .WithColumn("notes").AsCustom("text").Nullable() + .WithColumn("isdeleted").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("addedon").AsDateTime().NotNullable(); + + Create.ForeignKey("fk_routestops_routeplans") + .FromTable("routestops").ForeignColumn("routeplanid") + .ToTable("routeplans").PrimaryColumn("routeplanid"); + + Create.Index("ix_routestops_routeplanid") + .OnTable("routestops") + .OnColumn("routeplanid"); + + // ── RouteSchedules ────────────────────────────────────── + Create.Table("routeschedules") + .WithColumn("routescheduleid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("routeplanid").AsCustom("citext").NotNullable() + .WithColumn("recurrencetype").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("recurrencecron").AsCustom("citext").Nullable() + .WithColumn("daysofweek").AsCustom("citext").Nullable() + .WithColumn("dayofmonth").AsInt32().Nullable() + .WithColumn("scheduledstarttime").AsCustom("citext").Nullable() + .WithColumn("effectivefrom").AsDateTime().NotNullable() + .WithColumn("effectiveto").AsDateTime().Nullable() + .WithColumn("isactive").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("lastinstancecreatedon").AsDateTime().Nullable() + .WithColumn("addedon").AsDateTime().NotNullable(); + + Create.ForeignKey("fk_routeschedules_routeplans") + .FromTable("routeschedules").ForeignColumn("routeplanid") + .ToTable("routeplans").PrimaryColumn("routeplanid"); + + Create.Index("ix_routeschedules_routeplanid") + .OnTable("routeschedules") + .OnColumn("routeplanid"); + + // ── RouteInstances ────────────────────────────────────── + Create.Table("routeinstances") + .WithColumn("routeinstanceid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("routeplanid").AsCustom("citext").NotNullable() + .WithColumn("routescheduleid").AsCustom("citext").Nullable() + .WithColumn("unitid").AsInt32().NotNullable() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("status").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("startedbyuserid").AsCustom("citext").NotNullable() + .WithColumn("scheduledstarton").AsDateTime().Nullable() + .WithColumn("actualstarton").AsDateTime().Nullable() + .WithColumn("actualendon").AsDateTime().Nullable() + .WithColumn("endedbyuserid").AsCustom("citext").Nullable() + .WithColumn("totaldistancemeters").AsDouble().Nullable() + .WithColumn("totaldurationseconds").AsDouble().Nullable() + .WithColumn("actualroutegeometry").AsCustom("text").Nullable() + .WithColumn("stopscompleted").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("stopstotal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("notes").AsCustom("text").Nullable() + .WithColumn("addedon").AsDateTime().NotNullable(); + + Create.ForeignKey("fk_routeinstances_routeplans") + .FromTable("routeinstances").ForeignColumn("routeplanid") + .ToTable("routeplans").PrimaryColumn("routeplanid"); + + Create.ForeignKey("fk_routeinstances_departments") + .FromTable("routeinstances").ForeignColumn("departmentid") + .ToTable("departments").PrimaryColumn("departmentid"); + + Create.Index("ix_routeinstances_routeplanid") + .OnTable("routeinstances") + .OnColumn("routeplanid"); + + Create.Index("ix_routeinstances_departmentid") + .OnTable("routeinstances") + .OnColumn("departmentid"); + + Create.Index("ix_routeinstances_unitid") + .OnTable("routeinstances") + .OnColumn("unitid"); + + // ── RouteInstanceStops ────────────────────────────────── + Create.Table("routeinstancestops") + .WithColumn("routeinstancestopid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("routeinstanceid").AsCustom("citext").NotNullable() + .WithColumn("routestopid").AsCustom("citext").NotNullable() + .WithColumn("stoporder").AsInt32().NotNullable() + .WithColumn("status").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("checkinon").AsDateTime().Nullable() + .WithColumn("checkintype").AsInt32().Nullable() + .WithColumn("checkinlatitude").AsDecimal(10, 7).Nullable() + .WithColumn("checkinlongitude").AsDecimal(10, 7).Nullable() + .WithColumn("checkouton").AsDateTime().Nullable() + .WithColumn("checkoutlatitude").AsDecimal(10, 7).Nullable() + .WithColumn("checkoutlongitude").AsDecimal(10, 7).Nullable() + .WithColumn("dwellseconds").AsInt32().Nullable() + .WithColumn("skipreason").AsCustom("citext").Nullable() + .WithColumn("notes").AsCustom("text").Nullable() + .WithColumn("estimatedarrivalon").AsDateTime().Nullable() + .WithColumn("actualarrivaldeviation").AsInt32().Nullable() + .WithColumn("addedon").AsDateTime().NotNullable(); + + Create.ForeignKey("fk_routeinstancestops_routeinstances") + .FromTable("routeinstancestops").ForeignColumn("routeinstanceid") + .ToTable("routeinstances").PrimaryColumn("routeinstanceid"); + + Create.ForeignKey("fk_routeinstancestops_routestops") + .FromTable("routeinstancestops").ForeignColumn("routestopid") + .ToTable("routestops").PrimaryColumn("routestopid"); + + Create.Index("ix_routeinstancestops_routeinstanceid") + .OnTable("routeinstancestops") + .OnColumn("routeinstanceid"); + + // ── RouteDeviations ───────────────────────────────────── + Create.Table("routedeviations") + .WithColumn("routedeviationid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("routeinstanceid").AsCustom("citext").NotNullable() + .WithColumn("detectedon").AsDateTime().NotNullable() + .WithColumn("latitude").AsDecimal(10, 7).NotNullable() + .WithColumn("longitude").AsDecimal(10, 7).NotNullable() + .WithColumn("deviationdistancemeters").AsDouble().NotNullable() + .WithColumn("deviationtype").AsInt32().NotNullable() + .WithColumn("isacknowledged").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("acknowledgedbyuserid").AsCustom("citext").Nullable() + .WithColumn("acknowledgedon").AsDateTime().Nullable() + .WithColumn("notes").AsCustom("citext").Nullable(); + + Create.ForeignKey("fk_routedeviations_routeinstances") + .FromTable("routedeviations").ForeignColumn("routeinstanceid") + .ToTable("routeinstances").PrimaryColumn("routeinstanceid"); + + Create.Index("ix_routedeviations_routeinstanceid") + .OnTable("routedeviations") + .OnColumn("routeinstanceid"); + } + + public override void Down() + { + Delete.Index("ix_routedeviations_routeinstanceid").OnTable("routedeviations"); + Delete.ForeignKey("fk_routedeviations_routeinstances").OnTable("routedeviations"); + Delete.Table("routedeviations"); + + Delete.Index("ix_routeinstancestops_routeinstanceid").OnTable("routeinstancestops"); + Delete.ForeignKey("fk_routeinstancestops_routestops").OnTable("routeinstancestops"); + Delete.ForeignKey("fk_routeinstancestops_routeinstances").OnTable("routeinstancestops"); + Delete.Table("routeinstancestops"); + + Delete.Index("ix_routeinstances_unitid").OnTable("routeinstances"); + Delete.Index("ix_routeinstances_departmentid").OnTable("routeinstances"); + Delete.Index("ix_routeinstances_routeplanid").OnTable("routeinstances"); + Delete.ForeignKey("fk_routeinstances_departments").OnTable("routeinstances"); + Delete.ForeignKey("fk_routeinstances_routeplans").OnTable("routeinstances"); + Delete.Table("routeinstances"); + + Delete.Index("ix_routeschedules_routeplanid").OnTable("routeschedules"); + Delete.ForeignKey("fk_routeschedules_routeplans").OnTable("routeschedules"); + Delete.Table("routeschedules"); + + Delete.Index("ix_routestops_routeplanid").OnTable("routestops"); + Delete.ForeignKey("fk_routestops_routeplans").OnTable("routestops"); + Delete.Table("routestops"); + + Delete.Index("ix_routeplans_departmentid").OnTable("routeplans"); + Delete.ForeignKey("fk_routeplans_departments").OnTable("routeplans"); + Delete.Table("routeplans"); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs index 5052655c..29df3157 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs @@ -453,6 +453,26 @@ protected SqlConfiguration() { } public string SelectAllCallContactsByCallIdQuery { get; set; } #endregion Contacts + #region IndoorMaps + public string IndoorMapsTableName { get; set; } + public string IndoorMapFloorsTableName { get; set; } + public string IndoorMapZonesTableName { get; set; } + public string SelectIndoorMapsByDepartmentIdQuery { get; set; } + public string SelectIndoorMapFloorsByMapIdQuery { get; set; } + public string SelectIndoorMapZonesByFloorIdQuery { get; set; } + public string SearchIndoorMapZonesQuery { get; set; } + #endregion IndoorMaps + + #region CustomMaps + public string CustomMapTilesTableName { get; set; } + public string CustomMapImportsTableName { get; set; } + public string SelectCustomMapTileQuery { get; set; } + public string SelectCustomMapTilesForLayerQuery { get; set; } + public string DeleteCustomMapTilesForLayerQuery { get; set; } + public string SelectCustomMapImportsForMapQuery { get; set; } + public string SelectPendingCustomMapImportsQuery { get; set; } + #endregion CustomMaps + #region User Defined Fields public string UdfDefinitionsTableName { get; set; } public string UdfFieldsTableName { get; set; } @@ -465,6 +485,29 @@ protected SqlConfiguration() { } public string DeleteUdfFieldValuesByEntityAndDefinitionQuery { get; set; } #endregion User Defined Fields + #region Routes + public string RoutePlansTableName { get; set; } + public string RouteStopsTableName { get; set; } + public string RouteSchedulesTableName { get; set; } + public string RouteInstancesTableName { get; set; } + public string RouteInstanceStopsTableName { get; set; } + public string RouteDeviationsTableName { get; set; } + public string SelectRoutePlansByDepartmentIdQuery { get; set; } + public string SelectRoutePlansByUnitIdQuery { get; set; } + public string SelectActiveRoutePlansByDepartmentIdQuery { get; set; } + public string SelectRouteStopsByRoutePlanIdQuery { get; set; } + public string SelectRouteStopsByCallIdQuery { get; set; } + public string SelectRouteSchedulesByRoutePlanIdQuery { get; set; } + public string SelectActiveSchedulesDueQuery { get; set; } + public string SelectRouteInstancesByDepartmentIdQuery { get; set; } + public string SelectActiveRouteInstancesByUnitIdQuery { get; set; } + public string SelectRouteInstancesByRoutePlanIdQuery { get; set; } + public string SelectRouteInstancesByDateRangeQuery { get; set; } + public string SelectRouteInstanceStopsByRouteInstanceIdQuery { get; set; } + public string SelectRouteDeviationsByRouteInstanceIdQuery { get; set; } + public string SelectUnacknowledgedRouteDeviationsByDepartmentQuery { get; set; } + #endregion Routes + // Identity #region Table Names diff --git a/Repositories/Resgrid.Repositories.DataRepository/CustomMapImportsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CustomMapImportsRepository.cs new file mode 100644 index 00000000..43dec2ef --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/CustomMapImportsRepository.cs @@ -0,0 +1,104 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using System; +using Dapper; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Queries.CustomMaps; + +namespace Resgrid.Repositories.DataRepository +{ + public class CustomMapImportsRepository : RepositoryBase, ICustomMapImportsRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public CustomMapImportsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetImportsForMapAsync(string mapId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CustomMapId", mapId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetPendingImportsAsync() + { + try + { + var selectFunction = new Func>>(async x => + { + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/CustomMapTilesRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CustomMapTilesRepository.cs new file mode 100644 index 00000000..59cbddf1 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/CustomMapTilesRepository.cs @@ -0,0 +1,153 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Linq; +using Dapper; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Queries.CustomMaps; + +namespace Resgrid.Repositories.DataRepository +{ + public class CustomMapTilesRepository : RepositoryBase, ICustomMapTilesRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public CustomMapTilesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task GetTileAsync(string layerId, int zoomLevel, int tileX, int tileY) + { + try + { + var selectFunction = new Func>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CustomMapLayerId", layerId); + dynamicParameters.Add("ZoomLevel", zoomLevel); + dynamicParameters.Add("TileX", tileX); + dynamicParameters.Add("TileY", tileY); + + var query = _queryFactory.GetQuery(); + + return await x.QueryFirstOrDefaultAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetTilesForLayerAsync(string layerId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CustomMapLayerId", layerId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task DeleteTilesForLayerAsync(string layerId, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + var deleteFunction = new Func>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CustomMapLayerId", layerId); + + var query = _queryFactory.GetQuery(); + + await x.ExecuteAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + + return true; + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await deleteFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await deleteFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/IndoorMapFloorsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/IndoorMapFloorsRepository.cs new file mode 100644 index 00000000..fa9aa1ab --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/IndoorMapFloorsRepository.cs @@ -0,0 +1,73 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using System; +using Dapper; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Queries.IndoorMaps; + +namespace Resgrid.Repositories.DataRepository +{ + public class IndoorMapFloorsRepository : RepositoryBase, IIndoorMapFloorsRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public IndoorMapFloorsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetFloorsByIndoorMapIdAsync(string indoorMapId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("IndoorMapId", indoorMapId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/IndoorMapZonesRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/IndoorMapZonesRepository.cs new file mode 100644 index 00000000..c29fe8ca --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/IndoorMapZonesRepository.cs @@ -0,0 +1,115 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using System; +using Dapper; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Queries.IndoorMaps; + +namespace Resgrid.Repositories.DataRepository +{ + public class IndoorMapZonesRepository : RepositoryBase, IIndoorMapZonesRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public IndoorMapZonesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetZonesByFloorIdAsync(string indoorMapFloorId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("IndoorMapFloorId", indoorMapFloorId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + + throw; + } + } + + public async Task> SearchZonesAsync(int departmentId, string searchTerm) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + dynamicParameters.Add("SearchTerm", searchTerm); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/IndoorMapsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/IndoorMapsRepository.cs new file mode 100644 index 00000000..3d5f8848 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/IndoorMapsRepository.cs @@ -0,0 +1,73 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using System; +using Dapper; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Queries.IndoorMaps; + +namespace Resgrid.Repositories.DataRepository +{ + public class IndoorMapsRepository : RepositoryBase, IIndoorMapsRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public IndoorMapsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetIndoorMapsByDepartmentIdAsync(int departmentId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs index ce60eba4..daeb4232 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs @@ -168,6 +168,23 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // Indoor Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Custom Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Route Planning Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + // SSO Repositories builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs index f97921c0..39ee08f5 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs @@ -167,6 +167,23 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // Indoor Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Custom Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Route Planning Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + // SSO Repositories builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs index 983f84db..18119d65 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs @@ -167,6 +167,23 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // Indoor Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Custom Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Route Planning Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + // SSO Repositories builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs index a08234f5..d0df4789 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs @@ -167,6 +167,23 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // Indoor Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Custom Maps Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + + // Route Planning Repositories + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + // SSO Repositories builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/DeleteCustomMapTilesForLayerQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/DeleteCustomMapTilesForLayerQuery.cs new file mode 100644 index 00000000..76969d84 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/DeleteCustomMapTilesForLayerQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CustomMaps +{ + public class DeleteCustomMapTilesForLayerQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public DeleteCustomMapTilesForLayerQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.DeleteCustomMapTilesForLayerQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CustomMapTilesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%LAYERID%" }, + new string[] { "CustomMapLayerId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapImportsForMapQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapImportsForMapQuery.cs new file mode 100644 index 00000000..83888f58 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapImportsForMapQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CustomMaps +{ + public class SelectCustomMapImportsForMapQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectCustomMapImportsForMapQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectCustomMapImportsForMapQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CustomMapImportsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%MAPID%" }, + new string[] { "CustomMapId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapTileQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapTileQuery.cs new file mode 100644 index 00000000..4a9b4d23 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapTileQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CustomMaps +{ + public class SelectCustomMapTileQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectCustomMapTileQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectCustomMapTileQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CustomMapTilesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%LAYERID%", "%ZOOM%", "%TX%", "%TY%" }, + new string[] { "CustomMapLayerId", "ZoomLevel", "TileX", "TileY" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapTilesForLayerQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapTilesForLayerQuery.cs new file mode 100644 index 00000000..601d579d --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectCustomMapTilesForLayerQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CustomMaps +{ + public class SelectCustomMapTilesForLayerQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectCustomMapTilesForLayerQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectCustomMapTilesForLayerQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CustomMapTilesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%LAYERID%" }, + new string[] { "CustomMapLayerId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectPendingCustomMapImportsQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectPendingCustomMapImportsQuery.cs new file mode 100644 index 00000000..9922ff4a --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CustomMaps/SelectPendingCustomMapImportsQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CustomMaps +{ + public class SelectPendingCustomMapImportsQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectPendingCustomMapImportsQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectPendingCustomMapImportsQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CustomMapImportsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { }, + new string[] { }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SearchIndoorMapZonesQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SearchIndoorMapZonesQuery.cs new file mode 100644 index 00000000..bbae5dd5 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SearchIndoorMapZonesQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.IndoorMaps +{ + public class SearchIndoorMapZonesQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SearchIndoorMapZonesQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SearchIndoorMapZonesQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.IndoorMapZonesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%TERM%", "%DID%" }, + new string[] { "SearchTerm", "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapFloorsByMapIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapFloorsByMapIdQuery.cs new file mode 100644 index 00000000..5cbd4f32 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapFloorsByMapIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.IndoorMaps +{ + public class SelectIndoorMapFloorsByMapIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectIndoorMapFloorsByMapIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectIndoorMapFloorsByMapIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.IndoorMapFloorsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%MAPID%" }, + new string[] { "IndoorMapId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapZonesByFloorIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapZonesByFloorIdQuery.cs new file mode 100644 index 00000000..2a45e40e --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapZonesByFloorIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.IndoorMaps +{ + public class SelectIndoorMapZonesByFloorIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectIndoorMapZonesByFloorIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectIndoorMapZonesByFloorIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.IndoorMapZonesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%FLOORID%" }, + new string[] { "IndoorMapFloorId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapsByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapsByDepartmentIdQuery.cs new file mode 100644 index 00000000..54bde910 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/IndoorMaps/SelectIndoorMapsByDepartmentIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.IndoorMaps +{ + public class SelectIndoorMapsByDepartmentIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectIndoorMapsByDepartmentIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectIndoorMapsByDepartmentIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.IndoorMapsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%DID%" }, + new string[] { "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveRouteInstancesByUnitIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveRouteInstancesByUnitIdQuery.cs new file mode 100644 index 00000000..22de36db --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveRouteInstancesByUnitIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectActiveRouteInstancesByUnitIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectActiveRouteInstancesByUnitIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectActiveRouteInstancesByUnitIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteInstancesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%UNITID%" }, + new string[] { "UnitId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveRoutePlansByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveRoutePlansByDepartmentIdQuery.cs new file mode 100644 index 00000000..2007964f --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveRoutePlansByDepartmentIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectActiveRoutePlansByDepartmentIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectActiveRoutePlansByDepartmentIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectActiveRoutePlansByDepartmentIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RoutePlansTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%DID%" }, + new string[] { "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveSchedulesDueQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveSchedulesDueQuery.cs new file mode 100644 index 00000000..acfd140f --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectActiveSchedulesDueQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectActiveSchedulesDueQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectActiveSchedulesDueQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectActiveSchedulesDueQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteSchedulesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%ASOF%" }, + new string[] { "AsOfDate" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteDeviationsByRouteInstanceIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteDeviationsByRouteInstanceIdQuery.cs new file mode 100644 index 00000000..a4cc4bbc --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteDeviationsByRouteInstanceIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteDeviationsByRouteInstanceIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteDeviationsByRouteInstanceIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteDeviationsByRouteInstanceIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteDeviationsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%RIID%" }, + new string[] { "RouteInstanceId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstanceStopsByRouteInstanceIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstanceStopsByRouteInstanceIdQuery.cs new file mode 100644 index 00000000..560a6499 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstanceStopsByRouteInstanceIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteInstanceStopsByRouteInstanceIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteInstanceStopsByRouteInstanceIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteInstanceStopsByRouteInstanceIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteInstanceStopsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%RIID%" }, + new string[] { "RouteInstanceId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByDateRangeQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByDateRangeQuery.cs new file mode 100644 index 00000000..65130553 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByDateRangeQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteInstancesByDateRangeQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteInstancesByDateRangeQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteInstancesByDateRangeQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteInstancesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%DID%", "%STARTDATE%", "%ENDDATE%" }, + new string[] { "DepartmentId", "StartDate", "EndDate" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByDepartmentIdQuery.cs new file mode 100644 index 00000000..c9b706bb --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByDepartmentIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteInstancesByDepartmentIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteInstancesByDepartmentIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteInstancesByDepartmentIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteInstancesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%DID%" }, + new string[] { "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByRoutePlanIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByRoutePlanIdQuery.cs new file mode 100644 index 00000000..69ffb243 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteInstancesByRoutePlanIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteInstancesByRoutePlanIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteInstancesByRoutePlanIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteInstancesByRoutePlanIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteInstancesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%RPID%" }, + new string[] { "RoutePlanId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRoutePlansByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRoutePlansByDepartmentIdQuery.cs new file mode 100644 index 00000000..e264f3f7 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRoutePlansByDepartmentIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRoutePlansByDepartmentIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRoutePlansByDepartmentIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRoutePlansByDepartmentIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RoutePlansTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%DID%" }, + new string[] { "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRoutePlansByUnitIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRoutePlansByUnitIdQuery.cs new file mode 100644 index 00000000..308b1219 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRoutePlansByUnitIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRoutePlansByUnitIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRoutePlansByUnitIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRoutePlansByUnitIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RoutePlansTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%UNITID%" }, + new string[] { "UnitId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteSchedulesByRoutePlanIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteSchedulesByRoutePlanIdQuery.cs new file mode 100644 index 00000000..3768ed2c --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteSchedulesByRoutePlanIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteSchedulesByRoutePlanIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteSchedulesByRoutePlanIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteSchedulesByRoutePlanIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteSchedulesTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%RPID%" }, + new string[] { "RoutePlanId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteStopsByCallIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteStopsByCallIdQuery.cs new file mode 100644 index 00000000..f54abbfb --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteStopsByCallIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteStopsByCallIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteStopsByCallIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteStopsByCallIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteStopsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%CALLID%" }, + new string[] { "CallId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteStopsByRoutePlanIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteStopsByRoutePlanIdQuery.cs new file mode 100644 index 00000000..41e3e4b8 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectRouteStopsByRoutePlanIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectRouteStopsByRoutePlanIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectRouteStopsByRoutePlanIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectRouteStopsByRoutePlanIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteStopsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%RPID%" }, + new string[] { "RoutePlanId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectUnacknowledgedRouteDeviationsByDepartmentQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectUnacknowledgedRouteDeviationsByDepartmentQuery.cs new file mode 100644 index 00000000..d6a87ffe --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Routes/SelectUnacknowledgedRouteDeviationsByDepartmentQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Routes +{ + public class SelectUnacknowledgedRouteDeviationsByDepartmentQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectUnacknowledgedRouteDeviationsByDepartmentQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectUnacknowledgedRouteDeviationsByDepartmentQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.RouteDeviationsTableName, + _sqlConfiguration.ParameterNotation, + new string[] { "%DID%" }, + new string[] { "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/RepositoryBase.cs b/Repositories/Resgrid.Repositories.DataRepository/RepositoryBase.cs index 9825174c..62417958 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/RepositoryBase.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/RepositoryBase.cs @@ -308,7 +308,11 @@ public virtual async Task SaveOrUpdateAsync(T entity, CancellationToken cance if (((IEntity)entity).IdValue != null) didParse = int.TryParse(entity.IdValue.ToString(), out idValue); - if (((IEntity)entity).IdValue == null || (didParse && idValue == 0)) + bool isNewEntity = ((IEntity)entity).IdValue == null + || (didParse && idValue == 0) + || (((IEntity)entity).IdType == 1 && string.IsNullOrWhiteSpace(((IEntity)entity).IdValue?.ToString())); + + if (isNewEntity) { if (((IEntity)entity).IdType == 1) ((IEntity)entity).IdValue = Guid.NewGuid().ToString(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/RouteDeviationsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/RouteDeviationsRepository.cs new file mode 100644 index 00000000..0b116c05 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RouteDeviationsRepository.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; +using Resgrid.Repositories.DataRepository.Queries.Routes; + +namespace Resgrid.Repositories.DataRepository +{ + public class RouteDeviationsRepository : RepositoryBase, IRouteDeviationsRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public RouteDeviationsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, + IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetDeviationsByRouteInstanceIdAsync(string routeInstanceId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("RouteInstanceId", routeInstanceId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetUnacknowledgedDeviationsByDepartmentAsync(int departmentId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/RouteInstanceStopsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/RouteInstanceStopsRepository.cs new file mode 100644 index 00000000..eb74b7bd --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RouteInstanceStopsRepository.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; +using Resgrid.Repositories.DataRepository.Queries.Routes; + +namespace Resgrid.Repositories.DataRepository +{ + public class RouteInstanceStopsRepository : RepositoryBase, IRouteInstanceStopsRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public RouteInstanceStopsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, + IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetStopsByRouteInstanceIdAsync(string routeInstanceId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("RouteInstanceId", routeInstanceId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/RouteInstancesRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/RouteInstancesRepository.cs new file mode 100644 index 00000000..129354d5 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RouteInstancesRepository.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; +using Resgrid.Repositories.DataRepository.Queries.Routes; + +namespace Resgrid.Repositories.DataRepository +{ + public class RouteInstancesRepository : RepositoryBase, IRouteInstancesRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public RouteInstancesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, + IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetInstancesByDepartmentIdAsync(int departmentId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetActiveInstancesByUnitIdAsync(int unitId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("UnitId", unitId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetInstancesByRoutePlanIdAsync(string routePlanId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("RoutePlanId", routePlanId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetInstancesByDateRangeAsync(int departmentId, DateTime startDate, DateTime endDate) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + dynamicParameters.Add("StartDate", startDate); + dynamicParameters.Add("EndDate", endDate); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/RoutePlansRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/RoutePlansRepository.cs new file mode 100644 index 00000000..a8f9ce31 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RoutePlansRepository.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; +using Resgrid.Repositories.DataRepository.Queries.Routes; + +namespace Resgrid.Repositories.DataRepository +{ + public class RoutePlansRepository : RepositoryBase, IRoutePlansRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public RoutePlansRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, + IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetRoutePlansByDepartmentIdAsync(int departmentId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetRoutePlansByUnitIdAsync(int unitId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("UnitId", unitId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetActiveRoutePlansByDepartmentIdAsync(int departmentId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/RouteSchedulesRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/RouteSchedulesRepository.cs new file mode 100644 index 00000000..6a093829 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RouteSchedulesRepository.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; +using Resgrid.Repositories.DataRepository.Queries.Routes; + +namespace Resgrid.Repositories.DataRepository +{ + public class RouteSchedulesRepository : RepositoryBase, IRouteSchedulesRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public RouteSchedulesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, + IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetSchedulesByRoutePlanIdAsync(string routePlanId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("RoutePlanId", routePlanId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetActiveSchedulesDueAsync(DateTime asOfDate) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("AsOfDate", asOfDate); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/RouteStopsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/RouteStopsRepository.cs new file mode 100644 index 00000000..42b0038b --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RouteStopsRepository.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Framework; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; +using Resgrid.Repositories.DataRepository.Queries.Routes; + +namespace Resgrid.Repositories.DataRepository +{ + public class RouteStopsRepository : RepositoryBase, IRouteStopsRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public RouteStopsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, + IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetStopsByRoutePlanIdAsync(string routePlanId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("RoutePlanId", routePlanId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetStopsByCallIdAsync(int callId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CallId", callId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs index 2eeaa7af..c8399371 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs @@ -1430,6 +1430,138 @@ SELECT dvc.* WHERE CallId = %CALLID%"; #endregion Contacts + #region IndoorMaps + IndoorMapsTableName = "IndoorMaps"; + IndoorMapFloorsTableName = "IndoorMapFloors"; + IndoorMapZonesTableName = "IndoorMapZones"; + SelectIndoorMapsByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE DepartmentId = %DID% AND IsDeleted = false"; + SelectIndoorMapFloorsByMapIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE IndoorMapId = %MAPID% AND IsDeleted = false + ORDER BY FloorOrder"; + SelectIndoorMapZonesByFloorIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE IndoorMapFloorId = %FLOORID% AND IsDeleted = false"; + SearchIndoorMapZonesQuery = @" + SELECT z.* + FROM %SCHEMA%.indoormapzones z + INNER JOIN %SCHEMA%.indoormapfloors f ON z.IndoorMapFloorId = f.IndoorMapFloorId + INNER JOIN %SCHEMA%.indoormaps m ON f.IndoorMapId = m.IndoorMapId + WHERE m.DepartmentId = %DID% + AND z.IsSearchable = true + AND z.IsDeleted = false + AND f.IsDeleted = false + AND m.IsDeleted = false + AND z.Name ILIKE '%' || %TERM% || '%'"; + #endregion IndoorMaps + + #region CustomMaps + CustomMapTilesTableName = "CustomMapTiles"; + CustomMapImportsTableName = "CustomMapImports"; + SelectCustomMapTileQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE CustomMapLayerId = %LAYERID% AND ZoomLevel = %ZOOM% AND TileX = %TX% AND TileY = %TY%"; + SelectCustomMapTilesForLayerQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE CustomMapLayerId = %LAYERID%"; + DeleteCustomMapTilesForLayerQuery = @" + DELETE FROM %SCHEMA%.%TABLENAME% + WHERE CustomMapLayerId = %LAYERID%"; + SelectCustomMapImportsForMapQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE CustomMapId = %MAPID% + ORDER BY ImportedOn DESC"; + SelectPendingCustomMapImportsQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE Status = 0 + ORDER BY ImportedOn"; + #endregion CustomMaps + + #region Routes + RoutePlansTableName = "RoutePlans"; + RouteStopsTableName = "RouteStops"; + RouteSchedulesTableName = "RouteSchedules"; + RouteInstancesTableName = "RouteInstances"; + RouteInstanceStopsTableName = "RouteInstanceStops"; + RouteDeviationsTableName = "RouteDeviations"; + + SelectRoutePlansByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE DepartmentId = %DID% AND IsDeleted = false"; + SelectRoutePlansByUnitIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE UnitId = %UNITID% AND IsDeleted = false"; + SelectActiveRoutePlansByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE DepartmentId = %DID% AND RouteStatus = 1 AND IsDeleted = false"; + SelectRouteStopsByRoutePlanIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE RoutePlanId = %RPID% AND IsDeleted = false + ORDER BY StopOrder"; + SelectRouteStopsByCallIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE CallId = %CALLID% AND IsDeleted = false"; + SelectRouteSchedulesByRoutePlanIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE RoutePlanId = %RPID%"; + SelectActiveSchedulesDueQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE IsActive = true AND EffectiveFrom <= %ASOF% + AND (EffectiveTo IS NULL OR EffectiveTo >= %ASOF%)"; + SelectRouteInstancesByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE DepartmentId = %DID% + ORDER BY AddedOn DESC"; + SelectActiveRouteInstancesByUnitIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE UnitId = %UNITID% AND Status = 1"; + SelectRouteInstancesByRoutePlanIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE RoutePlanId = %RPID% + ORDER BY AddedOn DESC"; + SelectRouteInstancesByDateRangeQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE DepartmentId = %DID% + AND AddedOn >= %STARTDATE% AND AddedOn <= %ENDDATE% + ORDER BY AddedOn DESC"; + SelectRouteInstanceStopsByRouteInstanceIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE RouteInstanceId = %RIID% + ORDER BY StopOrder"; + SelectRouteDeviationsByRouteInstanceIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE RouteInstanceId = %RIID% + ORDER BY DetectedOn DESC"; + SelectUnacknowledgedRouteDeviationsByDepartmentQuery = @" + SELECT d.* + FROM %SCHEMA%.RouteDeviations d + INNER JOIN %SCHEMA%.RouteInstances i ON d.RouteInstanceId = i.RouteInstanceId + WHERE i.DepartmentId = %DID% AND d.IsAcknowledged = false + ORDER BY d.DetectedOn DESC"; + #endregion Routes + #region User Defined Fields UdfDefinitionsTableName = "UdfDefinitions"; UdfFieldsTableName = "UdfFields"; diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs index 64393956..7e25ef0f 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs @@ -1394,6 +1394,138 @@ SELECT dvc.* WHERE CallId = %CALLID%"; #endregion Contacts + #region IndoorMaps + IndoorMapsTableName = "IndoorMaps"; + IndoorMapFloorsTableName = "IndoorMapFloors"; + IndoorMapZonesTableName = "IndoorMapZones"; + SelectIndoorMapsByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [DepartmentId] = %DID% AND [IsDeleted] = 0"; + SelectIndoorMapFloorsByMapIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [IndoorMapId] = %MAPID% AND [IsDeleted] = 0 + ORDER BY [FloorOrder]"; + SelectIndoorMapZonesByFloorIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [IndoorMapFloorId] = %FLOORID% AND [IsDeleted] = 0"; + SearchIndoorMapZonesQuery = @" + SELECT z.* + FROM %SCHEMA%.IndoorMapZones z + INNER JOIN %SCHEMA%.IndoorMapFloors f ON z.[IndoorMapFloorId] = f.[IndoorMapFloorId] + INNER JOIN %SCHEMA%.IndoorMaps m ON f.[IndoorMapId] = m.[IndoorMapId] + WHERE m.[DepartmentId] = %DID% + AND z.[IsSearchable] = 1 + AND z.[IsDeleted] = 0 + AND f.[IsDeleted] = 0 + AND m.[IsDeleted] = 0 + AND z.[Name] LIKE '%' + %TERM% + '%'"; + #endregion IndoorMaps + + #region CustomMaps + CustomMapTilesTableName = "CustomMapTiles"; + CustomMapImportsTableName = "CustomMapImports"; + SelectCustomMapTileQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [CustomMapLayerId] = %LAYERID% AND [ZoomLevel] = %ZOOM% AND [TileX] = %TX% AND [TileY] = %TY%"; + SelectCustomMapTilesForLayerQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [CustomMapLayerId] = %LAYERID%"; + DeleteCustomMapTilesForLayerQuery = @" + DELETE FROM %SCHEMA%.%TABLENAME% + WHERE [CustomMapLayerId] = %LAYERID%"; + SelectCustomMapImportsForMapQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [CustomMapId] = %MAPID% + ORDER BY [ImportedOn] DESC"; + SelectPendingCustomMapImportsQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [Status] = 0 + ORDER BY [ImportedOn]"; + #endregion CustomMaps + + #region Routes + RoutePlansTableName = "RoutePlans"; + RouteStopsTableName = "RouteStops"; + RouteSchedulesTableName = "RouteSchedules"; + RouteInstancesTableName = "RouteInstances"; + RouteInstanceStopsTableName = "RouteInstanceStops"; + RouteDeviationsTableName = "RouteDeviations"; + + SelectRoutePlansByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [DepartmentId] = %DID% AND [IsDeleted] = 0"; + SelectRoutePlansByUnitIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [UnitId] = %UNITID% AND [IsDeleted] = 0"; + SelectActiveRoutePlansByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [DepartmentId] = %DID% AND [RouteStatus] = 1 AND [IsDeleted] = 0"; + SelectRouteStopsByRoutePlanIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [RoutePlanId] = %RPID% AND [IsDeleted] = 0 + ORDER BY [StopOrder]"; + SelectRouteStopsByCallIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [CallId] = %CALLID% AND [IsDeleted] = 0"; + SelectRouteSchedulesByRoutePlanIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [RoutePlanId] = %RPID%"; + SelectActiveSchedulesDueQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [IsActive] = 1 AND [EffectiveFrom] <= %ASOF% + AND ([EffectiveTo] IS NULL OR [EffectiveTo] >= %ASOF%)"; + SelectRouteInstancesByDepartmentIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [DepartmentId] = %DID% + ORDER BY [AddedOn] DESC"; + SelectActiveRouteInstancesByUnitIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [UnitId] = %UNITID% AND [Status] = 1"; + SelectRouteInstancesByRoutePlanIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [RoutePlanId] = %RPID% + ORDER BY [AddedOn] DESC"; + SelectRouteInstancesByDateRangeQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [DepartmentId] = %DID% + AND [AddedOn] >= %STARTDATE% AND [AddedOn] <= %ENDDATE% + ORDER BY [AddedOn] DESC"; + SelectRouteInstanceStopsByRouteInstanceIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [RouteInstanceId] = %RIID% + ORDER BY [StopOrder]"; + SelectRouteDeviationsByRouteInstanceIdQuery = @" + SELECT * + FROM %SCHEMA%.%TABLENAME% + WHERE [RouteInstanceId] = %RIID% + ORDER BY [DetectedOn] DESC"; + SelectUnacknowledgedRouteDeviationsByDepartmentQuery = @" + SELECT d.* + FROM %SCHEMA%.RouteDeviations d + INNER JOIN %SCHEMA%.RouteInstances i ON d.[RouteInstanceId] = i.[RouteInstanceId] + WHERE i.[DepartmentId] = %DID% AND d.[IsAcknowledged] = 0 + ORDER BY d.[DetectedOn] DESC"; + #endregion Routes + #region User Defined Fields UdfDefinitionsTableName = "UdfDefinitions"; UdfFieldsTableName = "UdfFields"; diff --git a/Tests/Resgrid.Tests/Services/CustomMapServiceTests.cs b/Tests/Resgrid.Tests/Services/CustomMapServiceTests.cs new file mode 100644 index 00000000..b8bc468e --- /dev/null +++ b/Tests/Resgrid.Tests/Services/CustomMapServiceTests.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Services; + +namespace Resgrid.Tests.Services +{ + [TestFixture] + public class CustomMapServiceTests + { + private Mock _mapsRepo; + private Mock _floorsRepo; + private Mock _zonesRepo; + private Mock _tilesRepo; + private Mock _importsRepo; + private CustomMapService _service; + + [SetUp] + public void SetUp() + { + _mapsRepo = new Mock(); + _floorsRepo = new Mock(); + _zonesRepo = new Mock(); + _tilesRepo = new Mock(); + _importsRepo = new Mock(); + _service = new CustomMapService(_mapsRepo.Object, _floorsRepo.Object, _zonesRepo.Object, _tilesRepo.Object, _importsRepo.Object); + } + + #region Maps CRUD + + [Test] + public async Task SaveCustomMapAsync_ShouldCallRepository() + { + var map = new IndoorMap { IndoorMapId = "m1", Name = "Test" }; + _mapsRepo.Setup(x => x.SaveOrUpdateAsync(map, It.IsAny(), It.IsAny())).ReturnsAsync(map); + + var result = await _service.SaveCustomMapAsync(map); + + result.Should().NotBeNull(); + result.IndoorMapId.Should().Be("m1"); + _mapsRepo.Verify(x => x.SaveOrUpdateAsync(map, It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task GetCustomMapByIdAsync_ShouldReturnMap() + { + var map = new IndoorMap { IndoorMapId = "m1" }; + _mapsRepo.Setup(x => x.GetByIdAsync("m1")).ReturnsAsync(map); + + var result = await _service.GetCustomMapByIdAsync("m1"); + + result.Should().NotBeNull(); + result.IndoorMapId.Should().Be("m1"); + } + + [Test] + public async Task GetCustomMapsForDepartmentAsync_ShouldFilterByType() + { + var maps = new List + { + new IndoorMap { IndoorMapId = "m1", MapType = 0, IsDeleted = false }, + new IndoorMap { IndoorMapId = "m2", MapType = 1, IsDeleted = false }, + new IndoorMap { IndoorMapId = "m3", MapType = 0, IsDeleted = true } + }; + _mapsRepo.Setup(x => x.GetIndoorMapsByDepartmentIdAsync(1)).ReturnsAsync(maps); + + var result = await _service.GetCustomMapsForDepartmentAsync(1, CustomMapType.Indoor); + + result.Should().HaveCount(1); + result[0].IndoorMapId.Should().Be("m1"); + } + + [Test] + public async Task GetCustomMapsForDepartmentAsync_NoFilter_ShouldReturnAllNonDeleted() + { + var maps = new List + { + new IndoorMap { IndoorMapId = "m1", MapType = 0, IsDeleted = false }, + new IndoorMap { IndoorMapId = "m2", MapType = 1, IsDeleted = false }, + new IndoorMap { IndoorMapId = "m3", MapType = 0, IsDeleted = true } + }; + _mapsRepo.Setup(x => x.GetIndoorMapsByDepartmentIdAsync(1)).ReturnsAsync(maps); + + var result = await _service.GetCustomMapsForDepartmentAsync(1); + + result.Should().HaveCount(2); + } + + [Test] + public async Task DeleteCustomMapAsync_ShouldSoftDelete() + { + var map = new IndoorMap { IndoorMapId = "m1", IsDeleted = false }; + _mapsRepo.Setup(x => x.GetByIdAsync("m1")).ReturnsAsync(map); + _mapsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(map); + + var result = await _service.DeleteCustomMapAsync("m1"); + + result.Should().BeTrue(); + map.IsDeleted.Should().BeTrue(); + } + + [Test] + public async Task DeleteCustomMapAsync_MapNotFound_ShouldReturnFalse() + { + _mapsRepo.Setup(x => x.GetByIdAsync("m99")).ReturnsAsync((IndoorMap)null); + + var result = await _service.DeleteCustomMapAsync("m99"); + + result.Should().BeFalse(); + } + + #endregion Maps CRUD + + #region Layers CRUD + + [Test] + public async Task GetLayersForMapAsync_ShouldReturnOrderedNonDeleted() + { + var layers = new List + { + new IndoorMapFloor { IndoorMapFloorId = "l1", FloorOrder = 2, IsDeleted = false }, + new IndoorMapFloor { IndoorMapFloorId = "l2", FloorOrder = 1, IsDeleted = false }, + new IndoorMapFloor { IndoorMapFloorId = "l3", FloorOrder = 0, IsDeleted = true } + }; + _floorsRepo.Setup(x => x.GetFloorsByIndoorMapIdAsync("m1")).ReturnsAsync(layers); + + var result = await _service.GetLayersForMapAsync("m1"); + + result.Should().HaveCount(2); + result[0].IndoorMapFloorId.Should().Be("l2"); + result[1].IndoorMapFloorId.Should().Be("l1"); + } + + [Test] + public async Task DeleteLayerAsync_ShouldSoftDelete() + { + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1", IsDeleted = false }; + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _floorsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(layer); + + var result = await _service.DeleteLayerAsync("l1"); + + result.Should().BeTrue(); + layer.IsDeleted.Should().BeTrue(); + } + + #endregion Layers CRUD + + #region Regions CRUD + + [Test] + public async Task GetRegionsForLayerAsync_ShouldReturnNonDeleted() + { + var regions = new List + { + new IndoorMapZone { IndoorMapZoneId = "r1", IsDeleted = false }, + new IndoorMapZone { IndoorMapZoneId = "r2", IsDeleted = true } + }; + _zonesRepo.Setup(x => x.GetZonesByFloorIdAsync("l1")).ReturnsAsync(regions); + + var result = await _service.GetRegionsForLayerAsync("l1"); + + result.Should().HaveCount(1); + result[0].IndoorMapZoneId.Should().Be("r1"); + } + + [Test] + public async Task DeleteRegionAsync_ShouldSoftDelete() + { + var region = new IndoorMapZone { IndoorMapZoneId = "r1", IsDeleted = false }; + _zonesRepo.Setup(x => x.GetByIdAsync("r1")).ReturnsAsync(region); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(region); + + var result = await _service.DeleteRegionAsync("r1"); + + result.Should().BeTrue(); + region.IsDeleted.Should().BeTrue(); + } + + #endregion Regions CRUD + + #region Dispatch Integration + + [Test] + public async Task GetRegionDisplayNameAsync_ShouldBuildFullPath() + { + var region = new IndoorMapZone { IndoorMapZoneId = "r1", IndoorMapFloorId = "l1", Name = "Room 201" }; + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1", IndoorMapId = "m1", Name = "Floor 2" }; + var map = new IndoorMap { IndoorMapId = "m1", Name = "Main Hospital" }; + + _zonesRepo.Setup(x => x.GetByIdAsync("r1")).ReturnsAsync(region); + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _mapsRepo.Setup(x => x.GetByIdAsync("m1")).ReturnsAsync(map); + + var result = await _service.GetRegionDisplayNameAsync("r1"); + + result.Should().Be("Main Hospital > Floor 2 > Room 201"); + } + + [Test] + public async Task GetRegionDisplayNameAsync_NoMap_ShouldReturnLayerAndRegion() + { + var region = new IndoorMapZone { IndoorMapZoneId = "r1", IndoorMapFloorId = "l1", Name = "Stage A" }; + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1", IndoorMapId = "m1", Name = "Base" }; + + _zonesRepo.Setup(x => x.GetByIdAsync("r1")).ReturnsAsync(region); + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _mapsRepo.Setup(x => x.GetByIdAsync("m1")).ReturnsAsync((IndoorMap)null); + + var result = await _service.GetRegionDisplayNameAsync("r1"); + + result.Should().Be("Base > Stage A"); + } + + [Test] + public async Task GetRegionDisplayNameAsync_RegionNotFound_ShouldReturnNull() + { + _zonesRepo.Setup(x => x.GetByIdAsync("r99")).ReturnsAsync((IndoorMapZone)null); + + var result = await _service.GetRegionDisplayNameAsync("r99"); + + result.Should().BeNull(); + } + + [Test] + public async Task SearchRegionsAsync_ShouldReturnResults() + { + var zones = new List + { + new IndoorMapZone { IndoorMapZoneId = "r1", Name = "Room 101" } + }; + _zonesRepo.Setup(x => x.SearchZonesAsync(1, "Room")).ReturnsAsync(zones); + + var result = await _service.SearchRegionsAsync(1, "Room"); + + result.Should().HaveCount(1); + result[0].Name.Should().Be("Room 101"); + } + + #endregion Dispatch Integration + + #region Tiles + + [Test] + public async Task GetTileAsync_ShouldDelegateToRepository() + { + var tile = new CustomMapTile { CustomMapTileId = "t1", TileData = new byte[] { 1, 2, 3 } }; + _tilesRepo.Setup(x => x.GetTileAsync("l1", 2, 3, 4)).ReturnsAsync(tile); + + var result = await _service.GetTileAsync("l1", 2, 3, 4); + + result.Should().NotBeNull(); + result.TileData.Should().HaveCount(3); + } + + [Test] + public async Task DeleteTilesForLayerAsync_ShouldCallRepository() + { + _tilesRepo.Setup(x => x.DeleteTilesForLayerAsync("l1", It.IsAny())).ReturnsAsync(true); + + await _service.DeleteTilesForLayerAsync("l1"); + + _tilesRepo.Verify(x => x.DeleteTilesForLayerAsync("l1", It.IsAny()), Times.Once); + } + + #endregion Tiles + + #region Imports + + [Test] + public async Task GetImportsForMapAsync_ShouldReturnResults() + { + var imports = new List + { + new CustomMapImport { CustomMapImportId = "i1", SourceFileName = "test.geojson" } + }; + _importsRepo.Setup(x => x.GetImportsForMapAsync("m1")).ReturnsAsync(imports); + + var result = await _service.GetImportsForMapAsync("m1"); + + result.Should().HaveCount(1); + result[0].SourceFileName.Should().Be("test.geojson"); + } + + [Test] + public async Task ImportGeoJsonAsync_ValidGeoJson_ShouldCreateRegions() + { + var geoJson = @"{ + ""type"": ""FeatureCollection"", + ""features"": [ + { + ""type"": ""Feature"", + ""geometry"": { + ""type"": ""Point"", + ""coordinates"": [-104.99, 39.74] + }, + ""properties"": { + ""name"": ""Test Point"" + } + } + ] + }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapZone z, CancellationToken c, bool f) => z); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + result.Should().NotBeNull(); + result.Status.Should().Be((int)CustomMapImportStatus.Complete); + _zonesRepo.Verify(x => x.SaveOrUpdateAsync(It.Is(z => z.Name == "Test Point"), It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task ImportGeoJsonAsync_InvalidJson_ShouldSetStatusFailed() + { + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", "not-valid-json{{{", "user1"); + + result.Should().NotBeNull(); + result.Status.Should().Be((int)CustomMapImportStatus.Failed); + result.ErrorMessage.Should().NotBeNullOrEmpty(); + } + + #endregion Imports + } +} diff --git a/Tests/Resgrid.Tests/Services/GeoImportServiceTests.cs b/Tests/Resgrid.Tests/Services/GeoImportServiceTests.cs new file mode 100644 index 00000000..4bbaf353 --- /dev/null +++ b/Tests/Resgrid.Tests/Services/GeoImportServiceTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Services; + +namespace Resgrid.Tests.Services +{ + [TestFixture] + public class GeoImportServiceTests + { + private Mock _mapsRepo; + private Mock _floorsRepo; + private Mock _zonesRepo; + private Mock _tilesRepo; + private Mock _importsRepo; + private CustomMapService _service; + + [SetUp] + public void SetUp() + { + _mapsRepo = new Mock(); + _floorsRepo = new Mock(); + _zonesRepo = new Mock(); + _tilesRepo = new Mock(); + _importsRepo = new Mock(); + _service = new CustomMapService(_mapsRepo.Object, _floorsRepo.Object, _zonesRepo.Object, _tilesRepo.Object, _importsRepo.Object); + } + + #region GeoJSON Import + + [Test] + public async Task ImportGeoJson_WithPointFeature_ShouldCreateRegion() + { + var geoJson = @"{ + ""type"": ""FeatureCollection"", + ""features"": [{ + ""type"": ""Feature"", + ""geometry"": { ""type"": ""Point"", ""coordinates"": [-104.99, 39.74] }, + ""properties"": { ""name"": ""Fire Hydrant 42"" } + }] + }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapZone z, CancellationToken c, bool f) => z); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + result.Status.Should().Be((int)CustomMapImportStatus.Complete); + _zonesRepo.Verify(x => x.SaveOrUpdateAsync( + It.Is(z => z.Name == "Fire Hydrant 42" && z.CenterLatitude == 39.74m && z.CenterLongitude == -104.99m), + It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task ImportGeoJson_WithPolygonFeature_ShouldCreateRegion() + { + var geoJson = @"{ + ""type"": ""FeatureCollection"", + ""features"": [{ + ""type"": ""Feature"", + ""geometry"": { + ""type"": ""Polygon"", + ""coordinates"": [[[-104.0, 39.0], [-104.0, 40.0], [-103.0, 40.0], [-103.0, 39.0], [-104.0, 39.0]]] + }, + ""properties"": { ""name"": ""District A"" } + }] + }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapZone z, CancellationToken c, bool f) => z); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + result.Status.Should().Be((int)CustomMapImportStatus.Complete); + _zonesRepo.Verify(x => x.SaveOrUpdateAsync( + It.Is(z => z.Name == "District A" && z.GeoGeometry != null), + It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task ImportGeoJson_WithLineStringFeature_ShouldCreateRegion() + { + var geoJson = @"{ + ""type"": ""FeatureCollection"", + ""features"": [{ + ""type"": ""Feature"", + ""geometry"": { + ""type"": ""LineString"", + ""coordinates"": [[-104.0, 39.0], [-103.0, 40.0]] + }, + ""properties"": { ""name"": ""Emergency Route"" } + }] + }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapZone z, CancellationToken c, bool f) => z); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + result.Status.Should().Be((int)CustomMapImportStatus.Complete); + _zonesRepo.Verify(x => x.SaveOrUpdateAsync( + It.Is(z => z.Name == "Emergency Route"), + It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task ImportGeoJson_MultipleFeatures_ShouldCreateMultipleRegions() + { + var geoJson = @"{ + ""type"": ""FeatureCollection"", + ""features"": [ + { + ""type"": ""Feature"", + ""geometry"": { ""type"": ""Point"", ""coordinates"": [-104.0, 39.0] }, + ""properties"": { ""name"": ""Point A"" } + }, + { + ""type"": ""Feature"", + ""geometry"": { ""type"": ""Point"", ""coordinates"": [-105.0, 40.0] }, + ""properties"": { ""name"": ""Point B"" } + } + ] + }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapZone z, CancellationToken c, bool f) => z); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + result.Status.Should().Be((int)CustomMapImportStatus.Complete); + _zonesRepo.Verify(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Test] + public async Task ImportGeoJson_InvalidJson_ShouldFailWithErrorMessage() + { + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", "{{invalid json", "user1"); + + result.Status.Should().Be((int)CustomMapImportStatus.Failed); + result.ErrorMessage.Should().NotBeNullOrEmpty(); + } + + [Test] + public async Task ImportGeoJson_ShouldCreateAuditRecord() + { + var geoJson = @"{ ""type"": ""FeatureCollection"", ""features"": [] }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + + var result = await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + result.Should().NotBeNull(); + result.CustomMapId.Should().Be("m1"); + result.CustomMapLayerId.Should().Be("l1"); + result.ImportedById.Should().Be("user1"); + result.SourceFileType.Should().Be((int)CustomMapImportFileType.GeoJSON); + } + + [Test] + public async Task ImportGeoJson_StatusTransition_ShouldGoFromProcessingToComplete() + { + var geoJson = @"{ ""type"": ""FeatureCollection"", ""features"": [] }"; + + var statusHistory = new List(); + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((i, c, f) => statusHistory.Add(i.Status)) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + + await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + statusHistory.Should().HaveCount(2); + statusHistory[0].Should().Be((int)CustomMapImportStatus.Processing); + statusHistory[1].Should().Be((int)CustomMapImportStatus.Complete); + } + + [Test] + public async Task ImportGeoJson_RegionsShouldBeDispatchableByDefault() + { + var geoJson = @"{ + ""type"": ""FeatureCollection"", + ""features"": [{ + ""type"": ""Feature"", + ""geometry"": { ""type"": ""Point"", ""coordinates"": [-104.0, 39.0] }, + ""properties"": { ""name"": ""Test"" } + }] + }"; + + _importsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapImport i, CancellationToken c, bool f) => i); + _zonesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapZone z, CancellationToken c, bool f) => z); + + await _service.ImportGeoJsonAsync("m1", "l1", geoJson, "user1"); + + _zonesRepo.Verify(x => x.SaveOrUpdateAsync( + It.Is(z => z.IsDispatchable && z.IsSearchable), + It.IsAny(), It.IsAny()), Times.Once); + } + + #endregion GeoJSON Import + } +} diff --git a/Tests/Resgrid.Tests/Services/RouteServiceTests.cs b/Tests/Resgrid.Tests/Services/RouteServiceTests.cs new file mode 100644 index 00000000..ab7c2c2a --- /dev/null +++ b/Tests/Resgrid.Tests/Services/RouteServiceTests.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Services; + +namespace Resgrid.Tests.Services +{ + [TestFixture] + public class RouteServiceTests + { + private Mock _plansRepo; + private Mock _stopsRepo; + private Mock _schedulesRepo; + private Mock _instancesRepo; + private Mock _instanceStopsRepo; + private Mock _deviationsRepo; + private RouteService _service; + + [SetUp] + public void SetUp() + { + _plansRepo = new Mock(); + _stopsRepo = new Mock(); + _schedulesRepo = new Mock(); + _instancesRepo = new Mock(); + _instanceStopsRepo = new Mock(); + _deviationsRepo = new Mock(); + _service = new RouteService( + _plansRepo.Object, _stopsRepo.Object, _schedulesRepo.Object, + _instancesRepo.Object, _instanceStopsRepo.Object, _deviationsRepo.Object); + } + + #region Route Plan CRUD + + [Test] + public async Task SaveRoutePlanAsync_ShouldCallRepository() + { + var plan = new RoutePlan { RoutePlanId = "rp1", Name = "Test Route" }; + _plansRepo.Setup(x => x.SaveOrUpdateAsync(plan, It.IsAny(), It.IsAny())).ReturnsAsync(plan); + + var result = await _service.SaveRoutePlanAsync(plan); + + result.Should().NotBeNull(); + result.RoutePlanId.Should().Be("rp1"); + _plansRepo.Verify(x => x.SaveOrUpdateAsync(plan, It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task GetRoutePlanByIdAsync_ShouldReturnPlan() + { + var plan = new RoutePlan { RoutePlanId = "rp1" }; + _plansRepo.Setup(x => x.GetByIdAsync("rp1")).ReturnsAsync(plan); + + var result = await _service.GetRoutePlanByIdAsync("rp1"); + + result.Should().NotBeNull(); + result.RoutePlanId.Should().Be("rp1"); + } + + [Test] + public async Task GetRoutePlansForDepartmentAsync_ShouldReturnNonDeleted() + { + var plans = new List + { + new RoutePlan { RoutePlanId = "rp1", DepartmentId = 1, IsDeleted = false }, + new RoutePlan { RoutePlanId = "rp2", DepartmentId = 1, IsDeleted = true }, + new RoutePlan { RoutePlanId = "rp3", DepartmentId = 1, IsDeleted = false } + }; + _plansRepo.Setup(x => x.GetRoutePlansByDepartmentIdAsync(1)).ReturnsAsync(plans); + + var result = await _service.GetRoutePlansForDepartmentAsync(1); + + result.Should().HaveCount(2); + result.All(p => !p.IsDeleted).Should().BeTrue(); + } + + [Test] + public async Task GetRoutePlansForUnitAsync_ShouldFilterByUnit() + { + var plans = new List + { + new RoutePlan { RoutePlanId = "rp1", UnitId = 10, IsDeleted = false } + }; + _plansRepo.Setup(x => x.GetRoutePlansByUnitIdAsync(10)).ReturnsAsync(plans); + + var result = await _service.GetRoutePlansForUnitAsync(10); + + result.Should().HaveCount(1); + result[0].UnitId.Should().Be(10); + } + + [Test] + public async Task DeleteRoutePlanAsync_ShouldSoftDelete() + { + var plan = new RoutePlan { RoutePlanId = "rp1", IsDeleted = false }; + _plansRepo.Setup(x => x.GetByIdAsync("rp1")).ReturnsAsync(plan); + _plansRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RoutePlan p, CancellationToken ct, bool b) => p); + + var result = await _service.DeleteRoutePlanAsync("rp1"); + + result.Should().BeTrue(); + plan.IsDeleted.Should().BeTrue(); + } + + [Test] + public async Task DeleteRoutePlanAsync_NotFound_ShouldReturnFalse() + { + _plansRepo.Setup(x => x.GetByIdAsync("rp999")).ReturnsAsync((RoutePlan)null); + + var result = await _service.DeleteRoutePlanAsync("rp999"); + + result.Should().BeFalse(); + } + + #endregion + + #region Route Stop CRUD + + [Test] + public async Task SaveRouteStopAsync_ShouldCallRepository() + { + var stop = new RouteStop { RouteStopId = "rs1", Name = "Stop 1" }; + _stopsRepo.Setup(x => x.SaveOrUpdateAsync(stop, It.IsAny(), It.IsAny())).ReturnsAsync(stop); + + var result = await _service.SaveRouteStopAsync(stop); + + result.Should().NotBeNull(); + _stopsRepo.Verify(x => x.SaveOrUpdateAsync(stop, It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task GetStopsForPlanAsync_ShouldReturnOrderedNonDeleted() + { + var stops = new List + { + new RouteStop { RouteStopId = "rs2", StopOrder = 1, IsDeleted = false }, + new RouteStop { RouteStopId = "rs1", StopOrder = 0, IsDeleted = false }, + new RouteStop { RouteStopId = "rs3", StopOrder = 2, IsDeleted = true } + }; + _stopsRepo.Setup(x => x.GetStopsByRoutePlanIdAsync("rp1")).ReturnsAsync(stops); + + var result = await _service.GetRouteStopsForPlanAsync("rp1"); + + result.Should().HaveCount(2); + result[0].RouteStopId.Should().Be("rs1"); + result[1].RouteStopId.Should().Be("rs2"); + } + + [Test] + public async Task ReorderStopsAsync_ShouldUpdateStopOrders() + { + var stops = new List + { + new RouteStop { RouteStopId = "rs1", StopOrder = 0, IsDeleted = false }, + new RouteStop { RouteStopId = "rs2", StopOrder = 1, IsDeleted = false } + }; + _stopsRepo.Setup(x => x.GetStopsByRoutePlanIdAsync("rp1")).ReturnsAsync(stops); + _stopsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteStop s, CancellationToken ct, bool b) => s); + + var result = await _service.ReorderRouteStopsAsync("rp1", new List { "rs2", "rs1" }); + + result.Should().BeTrue(); + stops.First(s => s.RouteStopId == "rs2").StopOrder.Should().Be(0); + stops.First(s => s.RouteStopId == "rs1").StopOrder.Should().Be(1); + } + + [Test] + public async Task DeleteRouteStopAsync_ShouldSoftDelete() + { + var stop = new RouteStop { RouteStopId = "rs1", IsDeleted = false }; + _stopsRepo.Setup(x => x.GetByIdAsync("rs1")).ReturnsAsync(stop); + _stopsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteStop s, CancellationToken ct, bool b) => s); + + var result = await _service.DeleteRouteStopAsync("rs1"); + + result.Should().BeTrue(); + stop.IsDeleted.Should().BeTrue(); + } + + #endregion + + #region Route Instance Lifecycle + + [Test] + public async Task StartRouteAsync_ShouldCreateInstance_WithStops() + { + var plan = new RoutePlan { RoutePlanId = "rp1", DepartmentId = 1 }; + var stops = new List + { + new RouteStop { RouteStopId = "rs1", StopOrder = 0, IsDeleted = false }, + new RouteStop { RouteStopId = "rs2", StopOrder = 1, IsDeleted = false } + }; + + _plansRepo.Setup(x => x.GetByIdAsync("rp1")).ReturnsAsync(plan); + _stopsRepo.Setup(x => x.GetStopsByRoutePlanIdAsync("rp1")).ReturnsAsync(stops); + _instancesRepo.Setup(x => x.GetActiveInstancesByUnitIdAsync(10)).ReturnsAsync(new List()); + _instancesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstance i, CancellationToken ct, bool b) => i); + _instanceStopsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstanceStop s, CancellationToken ct, bool b) => s); + + var result = await _service.StartRouteAsync("rp1", 10, "user1"); + + result.Should().NotBeNull(); + result.Status.Should().Be((int)RouteInstanceStatus.InProgress); + result.StopsTotal.Should().Be(2); + _instanceStopsRepo.Verify(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void StartRouteAsync_AlreadyActive_ShouldThrow() + { + var existing = new List { new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.InProgress } }; + _instancesRepo.Setup(x => x.GetActiveInstancesByUnitIdAsync(10)).ReturnsAsync(existing); + + Func act = async () => await _service.StartRouteAsync("rp1", 10, "user1"); + + act.Should().ThrowAsync(); + } + + [Test] + public async Task EndRouteAsync_ShouldSetCompleted() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.InProgress, ActualStartOn = DateTime.UtcNow.AddHours(-1) }; + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + _instancesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstance i, CancellationToken ct, bool b) => i); + + var result = await _service.EndRouteAsync("ri1", "user1"); + + result.Status.Should().Be((int)RouteInstanceStatus.Completed); + result.ActualEndOn.Should().NotBeNull(); + result.TotalDurationSeconds.Should().BeGreaterThan(0); + } + + [Test] + public void EndRouteAsync_NotInProgress_ShouldThrow() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.Completed }; + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + + Func act = async () => await _service.EndRouteAsync("ri1", "user1"); + + act.Should().ThrowAsync(); + } + + [Test] + public async Task PauseRouteAsync_ShouldSetPaused() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.InProgress }; + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + _instancesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstance i, CancellationToken ct, bool b) => i); + + var result = await _service.PauseRouteAsync("ri1", "user1"); + + result.Status.Should().Be((int)RouteInstanceStatus.Paused); + } + + [Test] + public void PauseRouteAsync_NotInProgress_ShouldThrow() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.Paused }; + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + + Func act = async () => await _service.PauseRouteAsync("ri1", "user1"); + + act.Should().ThrowAsync(); + } + + [Test] + public async Task ResumeRouteAsync_ShouldSetInProgress() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.Paused }; + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + _instancesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstance i, CancellationToken ct, bool b) => i); + + var result = await _service.ResumeRouteAsync("ri1", "user1"); + + result.Status.Should().Be((int)RouteInstanceStatus.InProgress); + } + + [Test] + public async Task CancelRouteAsync_ShouldSetCancelled() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.InProgress }; + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + _instancesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstance i, CancellationToken ct, bool b) => i); + + var result = await _service.CancelRouteAsync("ri1", "user1", "Testing cancel"); + + result.Status.Should().Be((int)RouteInstanceStatus.Cancelled); + result.Notes.Should().Be("Testing cancel"); + } + + [Test] + public async Task GetActiveInstanceForUnitAsync_NoActive_ShouldReturnNull() + { + _instancesRepo.Setup(x => x.GetActiveInstancesByUnitIdAsync(10)).ReturnsAsync(new List()); + + var result = await _service.GetActiveInstanceForUnitAsync(10); + + result.Should().BeNull(); + } + + #endregion + + #region Check-in/Check-out + + [Test] + public async Task CheckInAtStopAsync_ShouldCreateCheckIn() + { + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", RouteInstanceId = "ri1", Status = 0 }; + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.InProgress }; + _instanceStopsRepo.Setup(x => x.GetByIdAsync("ris1")).ReturnsAsync(instanceStop); + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + _instanceStopsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstanceStop s, CancellationToken ct, bool b) => s); + + var result = await _service.CheckInAtStopAsync("ris1", 40.7128m, -74.0060m, RouteStopCheckInType.Manual); + + result.Status.Should().Be(1); // CheckedIn + result.CheckInOn.Should().NotBeNull(); + result.CheckInLatitude.Should().Be(40.7128m); + } + + [Test] + public void CheckInAtStopAsync_InvalidInstance_ShouldThrow() + { + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", RouteInstanceId = "ri1" }; + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.Completed }; + _instanceStopsRepo.Setup(x => x.GetByIdAsync("ris1")).ReturnsAsync(instanceStop); + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + + Func act = async () => await _service.CheckInAtStopAsync("ris1", 40.7128m, -74.0060m, RouteStopCheckInType.Manual); + + act.Should().ThrowAsync(); + } + + [Test] + public async Task CheckOutFromStopAsync_ShouldSetCheckOutTime() + { + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", RouteInstanceId = "ri1", Status = 1, CheckInOn = DateTime.UtcNow.AddMinutes(-5) }; + var instance = new RouteInstance { RouteInstanceId = "ri1", Status = (int)RouteInstanceStatus.InProgress, StopsCompleted = 0 }; + _instanceStopsRepo.Setup(x => x.GetByIdAsync("ris1")).ReturnsAsync(instanceStop); + _instancesRepo.Setup(x => x.GetByIdAsync("ri1")).ReturnsAsync(instance); + _instanceStopsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstanceStop s, CancellationToken ct, bool b) => s); + _instancesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstance i, CancellationToken ct, bool b) => i); + + var result = await _service.CheckOutFromStopAsync("ris1", 40.7128m, -74.0060m); + + result.Status.Should().Be(2); // CheckedOut + result.CheckOutOn.Should().NotBeNull(); + result.DwellSeconds.Should().BeGreaterThan(0); + } + + [Test] + public void CheckOutFromStopAsync_NotCheckedIn_ShouldThrow() + { + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", Status = 0 }; // Pending + _instanceStopsRepo.Setup(x => x.GetByIdAsync("ris1")).ReturnsAsync(instanceStop); + + Func act = async () => await _service.CheckOutFromStopAsync("ris1", 40.7128m, -74.0060m); + + act.Should().ThrowAsync(); + } + + [Test] + public async Task SkipStopAsync_ShouldSetSkippedWithReason() + { + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", Status = 0 }; + _instanceStopsRepo.Setup(x => x.GetByIdAsync("ris1")).ReturnsAsync(instanceStop); + _instanceStopsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RouteInstanceStop s, CancellationToken ct, bool b) => s); + + var result = await _service.SkipStopAsync("ris1", "Road closed"); + + result.Status.Should().Be(3); // Skipped + result.SkipReason.Should().Be("Road closed"); + } + + #endregion + + #region Geofence + + [Test] + public async Task CheckGeofenceProximityAsync_WithinRadius_ShouldReturnStop() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", RoutePlanId = "rp1" }; + var plan = new RoutePlan { RoutePlanId = "rp1", GeofenceRadiusMeters = 200 }; + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", RouteStopId = "rs1", Status = 0, StopOrder = 0 }; + var routeStop = new RouteStop { RouteStopId = "rs1", Latitude = 40.7128m, Longitude = -74.0060m }; + + _instancesRepo.Setup(x => x.GetActiveInstancesByUnitIdAsync(10)).ReturnsAsync(new List { instance }); + _plansRepo.Setup(x => x.GetByIdAsync("rp1")).ReturnsAsync(plan); + _instanceStopsRepo.Setup(x => x.GetStopsByRouteInstanceIdAsync("ri1")).ReturnsAsync(new List { instanceStop }); + _stopsRepo.Setup(x => x.GetByIdAsync("rs1")).ReturnsAsync(routeStop); + + // Position very close to the stop + var result = await _service.CheckGeofenceProximityAsync(10, 40.7128m, -74.0060m); + + result.Should().NotBeNull(); + result.RouteInstanceStopId.Should().Be("ris1"); + } + + [Test] + public async Task CheckGeofenceProximityAsync_OutsideRadius_ShouldReturnNull() + { + var instance = new RouteInstance { RouteInstanceId = "ri1", RoutePlanId = "rp1" }; + var plan = new RoutePlan { RoutePlanId = "rp1", GeofenceRadiusMeters = 50 }; + var instanceStop = new RouteInstanceStop { RouteInstanceStopId = "ris1", RouteStopId = "rs1", Status = 0, StopOrder = 0 }; + var routeStop = new RouteStop { RouteStopId = "rs1", Latitude = 40.7128m, Longitude = -74.0060m }; + + _instancesRepo.Setup(x => x.GetActiveInstancesByUnitIdAsync(10)).ReturnsAsync(new List { instance }); + _plansRepo.Setup(x => x.GetByIdAsync("rp1")).ReturnsAsync(plan); + _instanceStopsRepo.Setup(x => x.GetStopsByRouteInstanceIdAsync("ri1")).ReturnsAsync(new List { instanceStop }); + _stopsRepo.Setup(x => x.GetByIdAsync("rs1")).ReturnsAsync(routeStop); + + // Position far from the stop (different city) + var result = await _service.CheckGeofenceProximityAsync(10, 34.0522m, -118.2437m); + + result.Should().BeNull(); + } + + [Test] + public async Task CheckGeofenceProximityAsync_NoActiveRoute_ShouldReturnNull() + { + _instancesRepo.Setup(x => x.GetActiveInstancesByUnitIdAsync(10)).ReturnsAsync(new List()); + + var result = await _service.CheckGeofenceProximityAsync(10, 40.7128m, -74.0060m); + + result.Should().BeNull(); + } + + #endregion + + #region Progress + + [Test] + public async Task GetRouteProgressAsync_ShouldCalculateCompletedStops() + { + var stops = new List + { + new RouteInstanceStop { RouteInstanceStopId = "ris1", Status = 2, StopOrder = 0 }, // CheckedOut + new RouteInstanceStop { RouteInstanceStopId = "ris2", Status = 2, StopOrder = 1 }, // CheckedOut + new RouteInstanceStop { RouteInstanceStopId = "ris3", Status = 0, StopOrder = 2 } // Pending + }; + _instanceStopsRepo.Setup(x => x.GetStopsByRouteInstanceIdAsync("ri1")).ReturnsAsync(stops); + + var result = await _service.GetInstanceStopsAsync("ri1"); + + result.Should().HaveCount(3); + result.Count(s => s.Status == 2).Should().Be(2); // 2 completed + result.Count(s => s.Status == 0).Should().Be(1); // 1 pending + } + + [Test] + public async Task GetRouteProgressAsync_NoCheckIns_ShouldShowZero() + { + var stops = new List + { + new RouteInstanceStop { RouteInstanceStopId = "ris1", Status = 0, StopOrder = 0 }, + new RouteInstanceStop { RouteInstanceStopId = "ris2", Status = 0, StopOrder = 1 } + }; + _instanceStopsRepo.Setup(x => x.GetStopsByRouteInstanceIdAsync("ri1")).ReturnsAsync(stops); + + var result = await _service.GetInstanceStopsAsync("ri1"); + + result.Should().HaveCount(2); + result.All(s => s.Status == 0).Should().BeTrue(); + } + + #endregion + } +} diff --git a/Tests/Resgrid.Tests/Services/TileProcessingTests.cs b/Tests/Resgrid.Tests/Services/TileProcessingTests.cs new file mode 100644 index 00000000..52f04979 --- /dev/null +++ b/Tests/Resgrid.Tests/Services/TileProcessingTests.cs @@ -0,0 +1,168 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Services; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Formats.Png; +using System.IO; + +namespace Resgrid.Tests.Services +{ + [TestFixture] + public class TileProcessingTests + { + private Mock _mapsRepo; + private Mock _floorsRepo; + private Mock _zonesRepo; + private Mock _tilesRepo; + private Mock _importsRepo; + private CustomMapService _service; + + [SetUp] + public void SetUp() + { + _mapsRepo = new Mock(); + _floorsRepo = new Mock(); + _zonesRepo = new Mock(); + _tilesRepo = new Mock(); + _importsRepo = new Mock(); + _service = new CustomMapService(_mapsRepo.Object, _floorsRepo.Object, _zonesRepo.Object, _tilesRepo.Object, _importsRepo.Object); + } + + private byte[] CreateTestImage(int width, int height) + { + using (var image = new Image(width, height)) + { + using (var ms = new MemoryStream()) + { + image.SaveAsPng(ms); + return ms.ToArray(); + } + } + } + + [Test] + public async Task ProcessAndStoreTiles_SmallImage_ShouldNotTile() + { + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1" }; + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _floorsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapFloor f, CancellationToken c, bool fl) => f); + + var imageData = CreateTestImage(1024, 768); + + await _service.ProcessAndStoreTilesAsync("l1", imageData); + + layer.IsTiled.Should().BeFalse(); + layer.ImageData.Should().NotBeNull(); + layer.SourceFileSize.Should().Be(imageData.Length); + _tilesRepo.Verify(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task ProcessAndStoreTiles_LargeImage_ShouldTile() + { + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1" }; + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _floorsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapFloor f, CancellationToken c, bool fl) => f); + _tilesRepo.Setup(x => x.DeleteTilesForLayerAsync("l1", It.IsAny())).ReturnsAsync(true); + _tilesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapTile t, CancellationToken c, bool f) => t); + + var imageData = CreateTestImage(4096, 3072); + + await _service.ProcessAndStoreTilesAsync("l1", imageData); + + layer.IsTiled.Should().BeTrue(); + layer.ImageData.Should().BeNull(); + layer.TileMinZoom.Should().NotBeNull(); + layer.TileMaxZoom.Should().NotBeNull(); + layer.TileMinZoom.Value.Should().BeLessThanOrEqualTo(layer.TileMaxZoom.Value); + _tilesRepo.Verify(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.AtLeastOnce); + } + + [Test] + public async Task ProcessAndStoreTiles_LargeImage_ShouldHaveCorrectZoomLevels() + { + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1" }; + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _floorsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapFloor f, CancellationToken c, bool fl) => f); + _tilesRepo.Setup(x => x.DeleteTilesForLayerAsync("l1", It.IsAny())).ReturnsAsync(true); + _tilesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapTile t, CancellationToken c, bool f) => t); + + var imageData = CreateTestImage(2560, 2560); + + await _service.ProcessAndStoreTilesAsync("l1", imageData); + + layer.TileMinZoom.Should().Be(0); + // maxZoom = ceil(log2(2560/256)) = ceil(log2(10)) = ceil(3.32) = 4 + layer.TileMaxZoom.Should().Be(4); + } + + [Test] + public async Task ProcessAndStoreTiles_NullLayer_ShouldNotThrow() + { + _floorsRepo.Setup(x => x.GetByIdAsync("l99")).ReturnsAsync((IndoorMapFloor)null); + + await _service.Invoking(s => s.ProcessAndStoreTilesAsync("l99", new byte[] { 1, 2, 3 })) + .Should().NotThrowAsync(); + } + + [Test] + public async Task ProcessAndStoreTiles_LargeImage_TilesShouldBe256x256() + { + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1" }; + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _floorsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapFloor f, CancellationToken c, bool fl) => f); + _tilesRepo.Setup(x => x.DeleteTilesForLayerAsync("l1", It.IsAny())).ReturnsAsync(true); + + CustomMapTile savedTile = null; + _tilesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((t, c, f) => savedTile = t) + .ReturnsAsync((CustomMapTile t, CancellationToken c, bool f) => t); + + var imageData = CreateTestImage(3000, 3000); + + await _service.ProcessAndStoreTilesAsync("l1", imageData); + + savedTile.Should().NotBeNull(); + savedTile.TileData.Should().NotBeNull(); + savedTile.TileContentType.Should().Be("image/png"); + + // Verify the saved tile image is 256x256 + using (var tileImage = Image.Load(savedTile.TileData)) + { + tileImage.Width.Should().Be(256); + tileImage.Height.Should().Be(256); + } + } + + [Test] + public async Task ProcessAndStoreTiles_LargeImage_ShouldDeleteExistingTilesFirst() + { + var layer = new IndoorMapFloor { IndoorMapFloorId = "l1" }; + _floorsRepo.Setup(x => x.GetByIdAsync("l1")).ReturnsAsync(layer); + _floorsRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((IndoorMapFloor f, CancellationToken c, bool fl) => f); + _tilesRepo.Setup(x => x.DeleteTilesForLayerAsync("l1", It.IsAny())).ReturnsAsync(true); + _tilesRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((CustomMapTile t, CancellationToken c, bool f) => t); + + var imageData = CreateTestImage(4096, 4096); + + await _service.ProcessAndStoreTilesAsync("l1", imageData); + + _tilesRepo.Verify(x => x.DeleteTilesForLayerAsync("l1", It.IsAny()), Times.Once); + } + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs index 2a20c7a5..790c63f0 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs @@ -576,6 +576,12 @@ public async Task> SaveCall([FromBody] NewCallInput if (!string.IsNullOrWhiteSpace(newCallInput.CallFormData)) call.CallFormData = newCallInput.CallFormData; + if (!string.IsNullOrWhiteSpace(newCallInput.IndoorMapZoneId)) + call.IndoorMapZoneId = newCallInput.IndoorMapZoneId; + + if (!string.IsNullOrWhiteSpace(newCallInput.IndoorMapFloorId)) + call.IndoorMapFloorId = newCallInput.IndoorMapFloorId; + if (newCallInput.DispatchOn.HasValue) { call.DispatchOn = DateTimeHelpers.ConvertToUtc(newCallInput.DispatchOn.Value, department.TimeZone); diff --git a/Web/Resgrid.Web.Services/Controllers/v4/MappingController.cs b/Web/Resgrid.Web.Services/Controllers/v4/MappingController.cs index 2e93087c..e0495292 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/MappingController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/MappingController.cs @@ -43,6 +43,8 @@ public class MappingController : V4AuthenticatedApiControllerbase private readonly IGeoLocationProvider _geoLocationProvider; private readonly IMappingService _mappingService; private readonly Model.Services.IAuthorizationService _authorizationService; + private readonly IIndoorMapService _indoorMapService; + private readonly ICustomMapService _customMapService; public MappingController( IUsersService usersService, @@ -58,7 +60,9 @@ public MappingController( IDepartmentSettingsService departmentSettingsService, IGeoLocationProvider geoLocationProvider, IMappingService mappingService, - Model.Services.IAuthorizationService authorizationService + Model.Services.IAuthorizationService authorizationService, + IIndoorMapService indoorMapService, + ICustomMapService customMapService ) { _usersService = usersService; @@ -75,6 +79,8 @@ Model.Services.IAuthorizationService authorizationService _geoLocationProvider = geoLocationProvider; _mappingService = mappingService; _authorizationService = authorizationService; + _indoorMapService = indoorMapService; + _customMapService = customMapService; } #endregion Members and Constructors @@ -510,5 +516,530 @@ public static GetMapLayersData ConvertMapLayerData(MapLayer layer) return result; } + + /// + /// Gets all indoor maps for the department. + /// + /// GetIndoorMapsResult object with list of indoor maps + [HttpGet("GetIndoorMaps")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetIndoorMaps() + { + var result = new GetIndoorMapsResult(); + + var maps = await _indoorMapService.GetIndoorMapsForDepartmentAsync(DepartmentId); + + if (maps != null && maps.Count > 0) + { + foreach (var m in maps) + { + result.Data.Add(new IndoorMapResultData + { + IndoorMapId = m.IndoorMapId, + Name = m.Name, + Description = m.Description, + CenterLatitude = m.CenterLatitude, + CenterLongitude = m.CenterLongitude, + BoundsNELat = m.BoundsNELat, + BoundsNELon = m.BoundsNELon, + BoundsSWLat = m.BoundsSWLat, + BoundsSWLon = m.BoundsSWLon, + DefaultFloorId = m.DefaultFloorId + }); + } + } + + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets a specific indoor map with its floors. + /// + /// Indoor map id + /// GetIndoorMapResult object with map and floor data + [HttpGet("GetIndoorMap/{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetIndoorMap(string id) + { + var result = new GetIndoorMapResult(); + + var map = await _indoorMapService.GetIndoorMapByIdAsync(id); + + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + result.Data.Map = new IndoorMapResultData + { + IndoorMapId = map.IndoorMapId, + Name = map.Name, + Description = map.Description, + CenterLatitude = map.CenterLatitude, + CenterLongitude = map.CenterLongitude, + BoundsNELat = map.BoundsNELat, + BoundsNELon = map.BoundsNELon, + BoundsSWLat = map.BoundsSWLat, + BoundsSWLon = map.BoundsSWLon, + DefaultFloorId = map.DefaultFloorId + }; + + var floors = await _indoorMapService.GetFloorsForMapAsync(id); + + if (floors != null && floors.Count > 0) + { + foreach (var f in floors) + { + result.Data.Floors.Add(new IndoorMapFloorResultData + { + IndoorMapFloorId = f.IndoorMapFloorId, + IndoorMapId = f.IndoorMapId, + Name = f.Name, + FloorOrder = f.FloorOrder, + HasImage = f.ImageData != null && f.ImageData.Length > 0, + BoundsNELat = f.BoundsNELat, + BoundsNELon = f.BoundsNELon, + BoundsSWLat = f.BoundsSWLat, + BoundsSWLon = f.BoundsSWLon, + Opacity = f.Opacity + }); + } + } + + result.PageSize = 1; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets a specific indoor map floor with its zones. + /// + /// Indoor map floor id + /// GetIndoorMapFloorResult object with floor and zone data + [HttpGet("GetIndoorMapFloor/{floorId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetIndoorMapFloor(string floorId) + { + var result = new GetIndoorMapFloorResult(); + + var floor = await _indoorMapService.GetFloorByIdAsync(floorId); + + if (floor == null) + return NotFound(); + + var map = await _indoorMapService.GetIndoorMapByIdAsync(floor.IndoorMapId); + + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + result.Data = new IndoorMapFloorResultData + { + IndoorMapFloorId = floor.IndoorMapFloorId, + IndoorMapId = floor.IndoorMapId, + Name = floor.Name, + FloorOrder = floor.FloorOrder, + HasImage = floor.ImageData != null && floor.ImageData.Length > 0, + BoundsNELat = floor.BoundsNELat, + BoundsNELon = floor.BoundsNELon, + BoundsSWLat = floor.BoundsSWLat, + BoundsSWLon = floor.BoundsSWLon, + Opacity = floor.Opacity, + Zones = new List() + }; + + var zones = await _indoorMapService.GetZonesForFloorAsync(floorId); + + if (zones != null && zones.Count > 0) + { + foreach (var z in zones) + { + result.Data.Zones.Add(new IndoorMapZoneResultData + { + IndoorMapZoneId = z.IndoorMapZoneId, + IndoorMapFloorId = z.IndoorMapFloorId, + Name = z.Name, + Description = z.Description, + ZoneType = z.ZoneType, + PixelGeometry = z.PixelGeometry, + GeoGeometry = z.GeoGeometry, + CenterPixelX = z.CenterPixelX, + CenterPixelY = z.CenterPixelY, + CenterLatitude = z.CenterLatitude, + CenterLongitude = z.CenterLongitude, + Color = z.Color, + Metadata = z.Metadata, + IsSearchable = z.IsSearchable + }); + } + } + + result.PageSize = 1; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets the image for a specific indoor map floor. + /// + /// Indoor map floor id + /// Floor image file + [HttpGet("GetIndoorMapFloorImage/{floorId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task GetIndoorMapFloorImage(string floorId) + { + var floor = await _indoorMapService.GetFloorByIdAsync(floorId); + + if (floor == null) + return NotFound(); + + var map = await _indoorMapService.GetIndoorMapByIdAsync(floor.IndoorMapId); + + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + if (floor.ImageData == null || floor.ImageData.Length == 0) + return NotFound(); + + return File(floor.ImageData, floor.ImageContentType ?? "image/png"); + } + + /// + /// Searches for indoor map zones matching the specified term. + /// + /// Search term + /// SearchIndoorLocationsResult object with matching zones + [HttpGet("SearchIndoorLocations")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> SearchIndoorLocations(string term) + { + var result = new SearchIndoorLocationsResult(); + + if (!string.IsNullOrWhiteSpace(term)) + { + var zones = await _indoorMapService.SearchZonesAsync(DepartmentId, term); + + if (zones != null && zones.Count > 0) + { + foreach (var z in zones) + { + result.Data.Add(new IndoorMapZoneResultData + { + IndoorMapZoneId = z.IndoorMapZoneId, + IndoorMapFloorId = z.IndoorMapFloorId, + Name = z.Name, + Description = z.Description, + ZoneType = z.ZoneType, + PixelGeometry = z.PixelGeometry, + GeoGeometry = z.GeoGeometry, + CenterPixelX = z.CenterPixelX, + CenterPixelY = z.CenterPixelY, + CenterLatitude = z.CenterLatitude, + CenterLongitude = z.CenterLongitude, + Color = z.Color, + Metadata = z.Metadata, + IsSearchable = z.IsSearchable + }); + } + } + } + + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets all custom maps for the department, with optional type filter. + /// + /// Optional map type filter (0=Indoor, 1=Outdoor, 2=Event, 3=Custom) + /// GetCustomMapsResult object + [HttpGet("GetCustomMaps")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetCustomMaps(int? type) + { + var result = new GetCustomMapsResult(); + + CustomMapType? filterType = type.HasValue ? (CustomMapType)type.Value : null; + var maps = await _customMapService.GetCustomMapsForDepartmentAsync(DepartmentId, filterType); + + if (maps != null && maps.Count > 0) + { + foreach (var m in maps) + { + result.Data.Add(new CustomMapResultData + { + IndoorMapId = m.IndoorMapId, + Name = m.Name, + Description = m.Description, + MapType = m.MapType, + CenterLatitude = m.CenterLatitude, + CenterLongitude = m.CenterLongitude, + BoundsNELat = m.BoundsNELat, + BoundsNELon = m.BoundsNELon, + BoundsSWLat = m.BoundsSWLat, + BoundsSWLon = m.BoundsSWLon, + BoundsGeoJson = m.BoundsGeoJson, + DefaultFloorId = m.DefaultFloorId + }); + } + } + + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets a specific custom map with its layers. + /// + /// Custom map id + /// GetCustomMapResult object + [HttpGet("GetCustomMap/{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetCustomMap(string id) + { + var result = new GetCustomMapResult(); + + var map = await _customMapService.GetCustomMapByIdAsync(id); + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + result.Data.Map = new CustomMapResultData + { + IndoorMapId = map.IndoorMapId, + Name = map.Name, + Description = map.Description, + MapType = map.MapType, + CenterLatitude = map.CenterLatitude, + CenterLongitude = map.CenterLongitude, + BoundsNELat = map.BoundsNELat, + BoundsNELon = map.BoundsNELon, + BoundsSWLat = map.BoundsSWLat, + BoundsSWLon = map.BoundsSWLon, + BoundsGeoJson = map.BoundsGeoJson, + DefaultFloorId = map.DefaultFloorId + }; + + var layers = await _customMapService.GetLayersForMapAsync(id); + if (layers != null && layers.Count > 0) + { + foreach (var l in layers) + { + result.Data.Layers.Add(new CustomMapLayerResultData + { + IndoorMapFloorId = l.IndoorMapFloorId, + IndoorMapId = l.IndoorMapId, + Name = l.Name, + FloorOrder = l.FloorOrder, + LayerType = l.LayerType, + HasImage = (l.ImageData != null && l.ImageData.Length > 0) || l.IsTiled, + IsTiled = l.IsTiled, + TileMinZoom = l.TileMinZoom, + TileMaxZoom = l.TileMaxZoom, + BoundsNELat = l.BoundsNELat, + BoundsNELon = l.BoundsNELon, + BoundsSWLat = l.BoundsSWLat, + BoundsSWLon = l.BoundsSWLon, + Opacity = l.Opacity + }); + } + } + + result.PageSize = 1; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets a specific custom map layer with its regions. + /// + /// Layer id + /// GetCustomMapLayerResult object + [HttpGet("GetCustomMapLayer/{layerId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetCustomMapLayer(string layerId) + { + var result = new GetCustomMapLayerResult(); + + var layer = await _customMapService.GetLayerByIdAsync(layerId); + if (layer == null) + return NotFound(); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + result.Data = new CustomMapLayerResultData + { + IndoorMapFloorId = layer.IndoorMapFloorId, + IndoorMapId = layer.IndoorMapId, + Name = layer.Name, + FloorOrder = layer.FloorOrder, + LayerType = layer.LayerType, + HasImage = (layer.ImageData != null && layer.ImageData.Length > 0) || layer.IsTiled, + IsTiled = layer.IsTiled, + TileMinZoom = layer.TileMinZoom, + TileMaxZoom = layer.TileMaxZoom, + BoundsNELat = layer.BoundsNELat, + BoundsNELon = layer.BoundsNELon, + BoundsSWLat = layer.BoundsSWLat, + BoundsSWLon = layer.BoundsSWLon, + Opacity = layer.Opacity, + Regions = new System.Collections.Generic.List() + }; + + var regions = await _customMapService.GetRegionsForLayerAsync(layerId); + if (regions != null && regions.Count > 0) + { + foreach (var r in regions) + { + result.Data.Regions.Add(new CustomMapRegionResultData + { + IndoorMapZoneId = r.IndoorMapZoneId, + IndoorMapFloorId = r.IndoorMapFloorId, + Name = r.Name, + Description = r.Description, + ZoneType = r.ZoneType, + PixelGeometry = r.PixelGeometry, + GeoGeometry = r.GeoGeometry, + CenterPixelX = r.CenterPixelX, + CenterPixelY = r.CenterPixelY, + CenterLatitude = r.CenterLatitude, + CenterLongitude = r.CenterLongitude, + Color = r.Color, + Metadata = r.Metadata, + IsSearchable = r.IsSearchable, + IsDispatchable = r.IsDispatchable + }); + } + } + + result.PageSize = 1; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Gets a tile image for a specific custom map layer. + /// + /// Layer id + /// Zoom level + /// Tile X + /// Tile Y + /// Tile image file + [HttpGet("GetCustomMapTile/{layerId}/{z}/{x}/{y}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task GetCustomMapTile(string layerId, int z, int x, int y) + { + var tile = await _customMapService.GetTileAsync(layerId, z, x, y); + + if (tile == null) + return NotFound(); + + return File(tile.TileData, tile.TileContentType ?? "image/png"); + } + + /// + /// Gets the full image for a non-tiled custom map layer. + /// + /// Layer id + /// Layer image file + [HttpGet("GetCustomMapLayerImage/{layerId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task GetCustomMapLayerImage(string layerId) + { + var layer = await _customMapService.GetLayerByIdAsync(layerId); + if (layer == null) + return NotFound(); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + if (layer.ImageData == null || layer.ImageData.Length == 0) + return NotFound(); + + return File(layer.ImageData, layer.ImageContentType ?? "image/png"); + } + + /// + /// Searches for custom map regions matching the specified term. + /// + /// Search term + /// SearchCustomMapRegionsResult object + [HttpGet("SearchCustomMapRegions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> SearchCustomMapRegions(string term) + { + var result = new SearchCustomMapRegionsResult(); + + if (!string.IsNullOrWhiteSpace(term)) + { + var regions = await _customMapService.SearchRegionsAsync(DepartmentId, term); + + if (regions != null && regions.Count > 0) + { + foreach (var r in regions) + { + result.Data.Add(new CustomMapRegionResultData + { + IndoorMapZoneId = r.IndoorMapZoneId, + IndoorMapFloorId = r.IndoorMapFloorId, + Name = r.Name, + Description = r.Description, + ZoneType = r.ZoneType, + PixelGeometry = r.PixelGeometry, + GeoGeometry = r.GeoGeometry, + CenterPixelX = r.CenterPixelX, + CenterPixelY = r.CenterPixelY, + CenterLatitude = r.CenterLatitude, + CenterLongitude = r.CenterLongitude, + Color = r.Color, + Metadata = r.Metadata, + IsSearchable = r.IsSearchable, + IsDispatchable = r.IsDispatchable + }); + } + } + } + + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } } } diff --git a/Web/Resgrid.Web.Services/Controllers/v4/RoutesController.cs b/Web/Resgrid.Web.Services/Controllers/v4/RoutesController.cs new file mode 100644 index 00000000..b9dc605f --- /dev/null +++ b/Web/Resgrid.Web.Services/Controllers/v4/RoutesController.cs @@ -0,0 +1,617 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Model; +using Resgrid.Model.Services; +using Resgrid.Providers.Claims; +using Resgrid.Web.Services.Models.v4.Routes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Resgrid.Web.Services.Controllers.v4 +{ + /// + /// Route planning operations + /// + [Route("api/v{VersionId:apiVersion}/[controller]")] + [ApiVersion("4.0")] + [ApiExplorerSettings(GroupName = "v4")] + public class RoutesController : V4AuthenticatedApiControllerbase + { + private readonly IRouteService _routeService; + + public RoutesController(IRouteService routeService) + { + _routeService = routeService; + } + + /// + /// Gets all route plans for the department + /// + [HttpGet("GetRoutePlans")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetRoutePlans() + { + var plans = await _routeService.GetRoutePlansForDepartmentAsync(DepartmentId); + var result = new GetRoutePlansResult(); + + foreach (var plan in plans) + { + var stops = await _routeService.GetRouteStopsForPlanAsync(plan.RoutePlanId); + result.Data.Add(new RoutePlanResultData + { + RoutePlanId = plan.RoutePlanId, + Name = plan.Name, + Description = plan.Description, + UnitId = plan.UnitId, + RouteStatus = plan.RouteStatus, + RouteColor = plan.RouteColor, + StopsCount = stops.Count, + EstimatedDistanceMeters = plan.EstimatedDistanceMeters, + EstimatedDurationSeconds = plan.EstimatedDurationSeconds, + MapboxRouteProfile = plan.MapboxRouteProfile, + AddedOn = plan.AddedOn + }); + } + + result.PageSize = result.Data.Count; + return Ok(result); + } + + /// + /// Gets route plans for a specific unit + /// + [HttpGet("GetRoutePlansForUnit/{unitId}")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetRoutePlansForUnit(int unitId) + { + var plans = await _routeService.GetRoutePlansForUnitAsync(unitId); + var result = new GetRoutePlansResult(); + + foreach (var plan in plans.Where(p => p.DepartmentId == DepartmentId)) + { + result.Data.Add(new RoutePlanResultData + { + RoutePlanId = plan.RoutePlanId, + Name = plan.Name, + Description = plan.Description, + UnitId = plan.UnitId, + RouteStatus = plan.RouteStatus, + RouteColor = plan.RouteColor, + MapboxRouteProfile = plan.MapboxRouteProfile, + AddedOn = plan.AddedOn + }); + } + + result.PageSize = result.Data.Count; + return Ok(result); + } + + /// + /// Gets a single route plan with stops and schedules + /// + [HttpGet("GetRoutePlan/{id}")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetRoutePlan(string id) + { + var plan = await _routeService.GetRoutePlanByIdAsync(id); + if (plan == null || plan.DepartmentId != DepartmentId) + return NotFound(); + + var stops = await _routeService.GetRouteStopsForPlanAsync(id); + var schedules = await _routeService.GetSchedulesForPlanAsync(id); + + var result = new GetRoutePlanResult(); + result.Data = new RoutePlanDetailResultData + { + RoutePlanId = plan.RoutePlanId, + Name = plan.Name, + Description = plan.Description, + DepartmentId = plan.DepartmentId, + UnitId = plan.UnitId, + RouteStatus = plan.RouteStatus, + RouteColor = plan.RouteColor, + StartLatitude = plan.StartLatitude, + StartLongitude = plan.StartLongitude, + EndLatitude = plan.EndLatitude, + EndLongitude = plan.EndLongitude, + UseStationAsStart = plan.UseStationAsStart, + UseStationAsEnd = plan.UseStationAsEnd, + OptimizeStopOrder = plan.OptimizeStopOrder, + MapboxRouteProfile = plan.MapboxRouteProfile, + MapboxRouteGeometry = plan.MapboxRouteGeometry, + EstimatedDistanceMeters = plan.EstimatedDistanceMeters, + EstimatedDurationSeconds = plan.EstimatedDurationSeconds, + GeofenceRadiusMeters = plan.GeofenceRadiusMeters, + AddedOn = plan.AddedOn, + Stops = stops.Select(s => new RouteStopResultData + { + RouteStopId = s.RouteStopId, + StopOrder = s.StopOrder, + Name = s.Name, + Description = s.Description, + StopType = s.StopType, + CallId = s.CallId, + Latitude = s.Latitude, + Longitude = s.Longitude, + Address = s.Address, + GeofenceRadiusMeters = s.GeofenceRadiusMeters, + Priority = s.Priority, + PlannedArrivalTime = s.PlannedArrivalTime, + PlannedDepartureTime = s.PlannedDepartureTime, + EstimatedDwellMinutes = s.EstimatedDwellMinutes, + ContactName = s.ContactName, + ContactNumber = s.ContactNumber, + Notes = s.Notes + }).ToList(), + Schedules = schedules.Select(s => new RouteScheduleResultData + { + RouteScheduleId = s.RouteScheduleId, + RecurrenceType = s.RecurrenceType, + RecurrenceCron = s.RecurrenceCron, + DaysOfWeek = s.DaysOfWeek, + DayOfMonth = s.DayOfMonth, + ScheduledStartTime = s.ScheduledStartTime, + EffectiveFrom = s.EffectiveFrom, + EffectiveTo = s.EffectiveTo, + IsActive = s.IsActive + }).ToList() + }; + + return Ok(result); + } + + /// + /// Creates a new route plan with stops + /// + [HttpPost("CreateRoutePlan")] + [Authorize(Policy = ResgridResources.Route_Create)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task> CreateRoutePlan([FromBody] NewRoutePlanInput input) + { + var plan = new RoutePlan + { + DepartmentId = DepartmentId, + UnitId = input.UnitId, + Name = input.Name, + Description = input.Description, + RouteStatus = input.RouteStatus, + RouteColor = input.RouteColor, + StartLatitude = input.StartLatitude, + StartLongitude = input.StartLongitude, + EndLatitude = input.EndLatitude, + EndLongitude = input.EndLongitude, + UseStationAsStart = input.UseStationAsStart, + UseStationAsEnd = input.UseStationAsEnd, + OptimizeStopOrder = input.OptimizeStopOrder, + MapboxRouteProfile = input.MapboxRouteProfile, + GeofenceRadiusMeters = input.GeofenceRadiusMeters, + AddedById = UserId, + AddedOn = DateTime.UtcNow + }; + + plan = await _routeService.SaveRoutePlanAsync(plan); + + if (input.Stops != null) + { + for (int i = 0; i < input.Stops.Count; i++) + { + var stopInput = input.Stops[i]; + var stop = new RouteStop + { + RoutePlanId = plan.RoutePlanId, + StopOrder = i, + Name = stopInput.Name, + Description = stopInput.Description, + StopType = stopInput.StopType, + CallId = stopInput.CallId, + Latitude = stopInput.Latitude, + Longitude = stopInput.Longitude, + Address = stopInput.Address, + GeofenceRadiusMeters = stopInput.GeofenceRadiusMeters, + Priority = stopInput.Priority, + PlannedArrivalTime = stopInput.PlannedArrivalTime, + PlannedDepartureTime = stopInput.PlannedDepartureTime, + EstimatedDwellMinutes = stopInput.EstimatedDwellMinutes, + ContactName = stopInput.ContactName, + ContactNumber = stopInput.ContactNumber, + Notes = stopInput.Notes, + AddedOn = DateTime.UtcNow + }; + + await _routeService.SaveRouteStopAsync(stop); + } + } + + if (input.Schedules != null) + { + foreach (var schedInput in input.Schedules) + { + var schedule = new RouteSchedule + { + RoutePlanId = plan.RoutePlanId, + RecurrenceType = schedInput.RecurrenceType, + RecurrenceCron = schedInput.RecurrenceCron, + DaysOfWeek = schedInput.DaysOfWeek, + DayOfMonth = schedInput.DayOfMonth, + ScheduledStartTime = schedInput.ScheduledStartTime, + EffectiveFrom = schedInput.EffectiveFrom, + EffectiveTo = schedInput.EffectiveTo, + IsActive = true, + AddedOn = DateTime.UtcNow + }; + + await _routeService.SaveRouteScheduleAsync(schedule); + } + } + + return CreatedAtAction(nameof(GetRoutePlan), new { id = plan.RoutePlanId }, + new SaveRoutePlanResult { Id = plan.RoutePlanId, Status = "Created" }); + } + + /// + /// Updates an existing route plan with stops + /// + [HttpPut("UpdateRoutePlan")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> UpdateRoutePlan([FromBody] UpdateRoutePlanInput input) + { + var plan = await _routeService.GetRoutePlanByIdAsync(input.RoutePlanId); + if (plan == null || plan.DepartmentId != DepartmentId) + return NotFound(); + + plan.UnitId = input.UnitId; + plan.Name = input.Name; + plan.Description = input.Description; + plan.RouteStatus = input.RouteStatus; + plan.RouteColor = input.RouteColor; + plan.StartLatitude = input.StartLatitude; + plan.StartLongitude = input.StartLongitude; + plan.EndLatitude = input.EndLatitude; + plan.EndLongitude = input.EndLongitude; + plan.UseStationAsStart = input.UseStationAsStart; + plan.UseStationAsEnd = input.UseStationAsEnd; + plan.OptimizeStopOrder = input.OptimizeStopOrder; + plan.MapboxRouteProfile = input.MapboxRouteProfile; + plan.GeofenceRadiusMeters = input.GeofenceRadiusMeters; + plan.UpdatedById = UserId; + plan.UpdatedOn = DateTime.UtcNow; + + await _routeService.SaveRoutePlanAsync(plan); + + return Ok(new SaveRoutePlanResult { Id = plan.RoutePlanId, Status = "Updated" }); + } + + /// + /// Soft-deletes a route plan + /// + [HttpDelete("DeleteRoutePlan/{id}")] + [Authorize(Policy = ResgridResources.Route_Delete)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteRoutePlan(string id) + { + var plan = await _routeService.GetRoutePlanByIdAsync(id); + if (plan == null || plan.DepartmentId != DepartmentId) + return NotFound(); + + await _routeService.DeleteRoutePlanAsync(id); + return Ok(); + } + + /// + /// Starts a route instance + /// + [HttpPost("StartRoute")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> StartRoute([FromBody] StartRouteInput input) + { + var instance = await _routeService.StartRouteAsync(input.RoutePlanId, input.UnitId, UserId); + + var result = new GetRouteInstanceResult(); + result.Data = MapInstanceToResult(instance); + return Ok(result); + } + + /// + /// Ends a route instance + /// + [HttpPost("EndRoute")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> EndRoute([FromBody] EndRouteInput input) + { + var instance = await _routeService.EndRouteAsync(input.RouteInstanceId, UserId); + + var result = new GetRouteInstanceResult(); + result.Data = MapInstanceToResult(instance); + return Ok(result); + } + + /// + /// Pauses a route instance + /// + [HttpPost("PauseRoute")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PauseRoute([FromBody] PauseRouteInput input) + { + var instance = await _routeService.PauseRouteAsync(input.RouteInstanceId, UserId); + + var result = new GetRouteInstanceResult(); + result.Data = MapInstanceToResult(instance); + return Ok(result); + } + + /// + /// Resumes a paused route instance + /// + [HttpPost("ResumeRoute")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ResumeRoute([FromBody] ResumeRouteInput input) + { + var instance = await _routeService.ResumeRouteAsync(input.RouteInstanceId, UserId); + + var result = new GetRouteInstanceResult(); + result.Data = MapInstanceToResult(instance); + return Ok(result); + } + + /// + /// Cancels a route instance + /// + [HttpPost("CancelRoute")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CancelRoute([FromBody] CancelRouteInput input) + { + var instance = await _routeService.CancelRouteAsync(input.RouteInstanceId, UserId, input.Reason); + + var result = new GetRouteInstanceResult(); + result.Data = MapInstanceToResult(instance); + return Ok(result); + } + + /// + /// Gets the active route instance for a unit + /// + [HttpGet("GetActiveRouteForUnit/{unitId}")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetActiveRouteForUnit(int unitId) + { + var instance = await _routeService.GetActiveInstanceForUnitAsync(unitId); + if (instance == null || instance.DepartmentId != DepartmentId) + return NotFound(); + + return Ok(await BuildProgressResult(instance)); + } + + /// + /// Gets all active route instances for the department + /// + [HttpGet("GetActiveRoutes")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetActiveRoutes() + { + var instances = await _routeService.GetInstancesForDepartmentAsync(DepartmentId); + var active = instances.Where(i => i.Status == (int)RouteInstanceStatus.InProgress || i.Status == (int)RouteInstanceStatus.Paused); + + var result = new GetRouteInstancesResult(); + foreach (var instance in active) + { + result.Data.Add(MapInstanceToResult(instance)); + } + + result.PageSize = result.Data.Count; + return Ok(result); + } + + /// + /// Gets route instance history for a plan + /// + [HttpGet("GetRouteHistory/{routePlanId}")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetRouteHistory(string routePlanId) + { + var plan = await _routeService.GetRoutePlanByIdAsync(routePlanId); + if (plan == null || plan.DepartmentId != DepartmentId) + return NotFound(); + + var instances = await _routeService.GetInstancesForDepartmentAsync(DepartmentId); + var filtered = instances.Where(i => i.RoutePlanId == routePlanId); + + var result = new GetRouteInstancesResult(); + foreach (var instance in filtered) + { + var data = MapInstanceToResult(instance); + data.RoutePlanName = plan.Name; + result.Data.Add(data); + } + + result.PageSize = result.Data.Count; + return Ok(result); + } + + /// + /// Gets progress for a route instance + /// + [HttpGet("GetRouteProgress/{instanceId}")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetRouteProgress(string instanceId) + { + var instance = await _routeService.GetInstanceByIdAsync(instanceId); + if (instance == null || instance.DepartmentId != DepartmentId) + return NotFound(); + + return Ok(await BuildProgressResult(instance)); + } + + /// + /// Check in at a stop + /// + [HttpPost("CheckInAtStop")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task CheckInAtStop([FromBody] CheckInInput input) + { + await _routeService.CheckInAtStopAsync(input.RouteInstanceStopId, input.Latitude, input.Longitude, RouteStopCheckInType.Manual); + return Ok(); + } + + /// + /// Check out from a stop + /// + [HttpPost("CheckOutFromStop")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task CheckOutFromStop([FromBody] CheckOutInput input) + { + await _routeService.CheckOutFromStopAsync(input.RouteInstanceStopId, input.Latitude, input.Longitude); + return Ok(); + } + + /// + /// Skip a stop with reason + /// + [HttpPost("SkipStop")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task SkipStop([FromBody] SkipStopInput input) + { + await _routeService.SkipStopAsync(input.RouteInstanceStopId, input.Reason); + return Ok(); + } + + /// + /// Auto check-in from geofence proximity + /// + [HttpPost("GeofenceCheckIn")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GeofenceCheckIn([FromBody] GeofenceCheckInInput input) + { + var stop = await _routeService.CheckGeofenceProximityAsync(input.UnitId, input.Latitude, input.Longitude); + if (stop != null) + { + await _routeService.CheckInAtStopAsync(stop.RouteInstanceStopId, input.Latitude, input.Longitude, RouteStopCheckInType.Geofence); + } + + return Ok(); + } + + /// + /// Gets unacknowledged deviations for the department + /// + [HttpGet("GetUnacknowledgedDeviations")] + [Authorize(Policy = ResgridResources.Route_View)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetUnacknowledgedDeviations() + { + var deviations = await _routeService.GetUnacknowledgedDeviationsAsync(DepartmentId); + + var result = new GetRouteDeviationsResult(); + result.Data = deviations.Select(d => new RouteDeviationResultData + { + RouteDeviationId = d.RouteDeviationId, + RouteInstanceId = d.RouteInstanceId, + DetectedOn = d.DetectedOn, + Latitude = d.Latitude, + Longitude = d.Longitude, + DeviationDistanceMeters = d.DeviationDistanceMeters, + DeviationType = d.DeviationType, + IsAcknowledged = d.IsAcknowledged, + Notes = d.Notes + }).ToList(); + + result.PageSize = result.Data.Count; + return Ok(result); + } + + /// + /// Acknowledge a deviation + /// + [HttpPost("AcknowledgeDeviation/{id}")] + [Authorize(Policy = ResgridResources.Route_Update)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task AcknowledgeDeviation(string id) + { + await _routeService.AcknowledgeDeviationAsync(id, UserId); + return Ok(); + } + + #region Helpers + + private RouteInstanceResultData MapInstanceToResult(RouteInstance instance) + { + return new RouteInstanceResultData + { + RouteInstanceId = instance.RouteInstanceId, + RoutePlanId = instance.RoutePlanId, + UnitId = instance.UnitId, + Status = instance.Status, + ActualStartOn = instance.ActualStartOn, + ActualEndOn = instance.ActualEndOn, + StopsCompleted = instance.StopsCompleted, + StopsTotal = instance.StopsTotal, + TotalDistanceMeters = instance.TotalDistanceMeters, + TotalDurationSeconds = instance.TotalDurationSeconds, + Notes = instance.Notes, + AddedOn = instance.AddedOn + }; + } + + private async Task BuildProgressResult(RouteInstance instance) + { + var instanceStops = await _routeService.GetInstanceStopsAsync(instance.RouteInstanceId); + + var result = new GetRouteProgressResult(); + result.Data = new RouteProgressResultData + { + Instance = MapInstanceToResult(instance), + TotalStops = instanceStops.Count, + CompletedStops = instanceStops.Count(s => s.Status == 2), // CheckedOut + SkippedStops = instanceStops.Count(s => s.Status == 3), // Skipped + PendingStops = instanceStops.Count(s => s.Status == 0), // Pending + Stops = instanceStops.Select(s => new RouteInstanceStopResultData + { + RouteInstanceStopId = s.RouteInstanceStopId, + RouteStopId = s.RouteStopId, + StopOrder = s.StopOrder, + Status = s.Status, + CheckInOn = s.CheckInOn, + CheckInType = s.CheckInType, + CheckInLatitude = s.CheckInLatitude, + CheckInLongitude = s.CheckInLongitude, + CheckOutOn = s.CheckOutOn, + CheckOutLatitude = s.CheckOutLatitude, + CheckOutLongitude = s.CheckOutLongitude, + DwellSeconds = s.DwellSeconds, + SkipReason = s.SkipReason, + Notes = s.Notes, + EstimatedArrivalOn = s.EstimatedArrivalOn, + ActualArrivalDeviation = s.ActualArrivalDeviation + }).ToList() + }; + + return result; + } + + #endregion + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Calls/NewCallInput.cs b/Web/Resgrid.Web.Services/Models/v4/Calls/NewCallInput.cs index 5bc9ec61..6185d6a1 100644 --- a/Web/Resgrid.Web.Services/Models/v4/Calls/NewCallInput.cs +++ b/Web/Resgrid.Web.Services/Models/v4/Calls/NewCallInput.cs @@ -102,5 +102,15 @@ public class NewCallInput /// User Defined Field values for this call /// public List UdfValues { get; set; } + + /// + /// Indoor Map Zone Id for the call location + /// + public string IndoorMapZoneId { get; set; } + + /// + /// Indoor Map Floor Id for the call location + /// + public string IndoorMapFloorId { get; set; } } } diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapLayerResultData.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapLayerResultData.cs new file mode 100644 index 00000000..f5af9db5 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapLayerResultData.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class CustomMapLayerResultData + { + public string IndoorMapFloorId { get; set; } + public string IndoorMapId { get; set; } + public string Name { get; set; } + public int FloorOrder { get; set; } + public int LayerType { get; set; } + public bool HasImage { get; set; } + public bool IsTiled { get; set; } + public int? TileMinZoom { get; set; } + public int? TileMaxZoom { get; set; } + public decimal? BoundsNELat { get; set; } + public decimal? BoundsNELon { get; set; } + public decimal? BoundsSWLat { get; set; } + public decimal? BoundsSWLon { get; set; } + public decimal Opacity { get; set; } + public List Regions { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapRegionResultData.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapRegionResultData.cs new file mode 100644 index 00000000..475dbb62 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapRegionResultData.cs @@ -0,0 +1,21 @@ +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class CustomMapRegionResultData + { + public string IndoorMapZoneId { get; set; } + public string IndoorMapFloorId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int ZoneType { get; set; } + public string PixelGeometry { get; set; } + public string GeoGeometry { get; set; } + public double CenterPixelX { get; set; } + public double CenterPixelY { get; set; } + public decimal CenterLatitude { get; set; } + public decimal CenterLongitude { get; set; } + public string Color { get; set; } + public string Metadata { get; set; } + public bool IsSearchable { get; set; } + public bool IsDispatchable { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapResultData.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapResultData.cs new file mode 100644 index 00000000..7bb904dc --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/CustomMapResultData.cs @@ -0,0 +1,18 @@ +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class CustomMapResultData + { + public string IndoorMapId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int MapType { get; set; } + public decimal CenterLatitude { get; set; } + public decimal CenterLongitude { get; set; } + public decimal BoundsNELat { get; set; } + public decimal BoundsNELon { get; set; } + public decimal BoundsSWLat { get; set; } + public decimal BoundsSWLon { get; set; } + public string BoundsGeoJson { get; set; } + public string DefaultFloorId { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/GetCustomMapsResult.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/GetCustomMapsResult.cs new file mode 100644 index 00000000..e9d5c89c --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/GetCustomMapsResult.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class GetCustomMapsResult : StandardApiResponseV4Base + { + public List Data { get; set; } + + public GetCustomMapsResult() + { + Data = new List(); + } + } + + public class GetCustomMapResult : StandardApiResponseV4Base + { + public GetCustomMapResultData Data { get; set; } + + public GetCustomMapResult() + { + Data = new GetCustomMapResultData(); + } + } + + public class GetCustomMapResultData + { + public CustomMapResultData Map { get; set; } + public List Layers { get; set; } + + public GetCustomMapResultData() + { + Layers = new List(); + } + } + + public class GetCustomMapLayerResult : StandardApiResponseV4Base + { + public CustomMapLayerResultData Data { get; set; } + + public GetCustomMapLayerResult() + { + Data = new CustomMapLayerResultData(); + } + } + + public class SearchCustomMapRegionsResult : StandardApiResponseV4Base + { + public List Data { get; set; } + + public SearchCustomMapRegionsResult() + { + Data = new List(); + } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/GetIndoorMapsResult.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/GetIndoorMapsResult.cs new file mode 100644 index 00000000..ebb77068 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/GetIndoorMapsResult.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class GetIndoorMapsResult : StandardApiResponseV4Base + { + /// + /// Response Data + /// + public List Data { get; set; } + + /// + /// Default constructor + /// + public GetIndoorMapsResult() + { + Data = new List(); + } + } + + public class GetIndoorMapResult : StandardApiResponseV4Base + { + /// + /// Response Data + /// + public GetIndoorMapResultData Data { get; set; } + + /// + /// Default constructor + /// + public GetIndoorMapResult() + { + Data = new GetIndoorMapResultData(); + } + } + + public class GetIndoorMapResultData + { + public IndoorMapResultData Map { get; set; } + public List Floors { get; set; } + + public GetIndoorMapResultData() + { + Floors = new List(); + } + } + + public class GetIndoorMapFloorResult : StandardApiResponseV4Base + { + /// + /// Response Data + /// + public IndoorMapFloorResultData Data { get; set; } + + /// + /// Default constructor + /// + public GetIndoorMapFloorResult() + { + Data = new IndoorMapFloorResultData(); + } + } + + public class SearchIndoorLocationsResult : StandardApiResponseV4Base + { + /// + /// Response Data + /// + public List Data { get; set; } + + /// + /// Default constructor + /// + public SearchIndoorLocationsResult() + { + Data = new List(); + } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapFloorResultData.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapFloorResultData.cs new file mode 100644 index 00000000..1b5b080c --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapFloorResultData.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class IndoorMapFloorResultData + { + public string IndoorMapFloorId { get; set; } + public string IndoorMapId { get; set; } + public string Name { get; set; } + public int FloorOrder { get; set; } + public bool HasImage { get; set; } + public decimal? BoundsNELat { get; set; } + public decimal? BoundsNELon { get; set; } + public decimal? BoundsSWLat { get; set; } + public decimal? BoundsSWLon { get; set; } + public decimal Opacity { get; set; } + public List Zones { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapResultData.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapResultData.cs new file mode 100644 index 00000000..f9ae5251 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapResultData.cs @@ -0,0 +1,16 @@ +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class IndoorMapResultData + { + public string IndoorMapId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal CenterLatitude { get; set; } + public decimal CenterLongitude { get; set; } + public decimal BoundsNELat { get; set; } + public decimal BoundsNELon { get; set; } + public decimal BoundsSWLat { get; set; } + public decimal BoundsSWLon { get; set; } + public string DefaultFloorId { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapZoneResultData.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapZoneResultData.cs new file mode 100644 index 00000000..c5ab9e19 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/IndoorMapZoneResultData.cs @@ -0,0 +1,20 @@ +namespace Resgrid.Web.Services.Models.v4.Mapping +{ + public class IndoorMapZoneResultData + { + public string IndoorMapZoneId { get; set; } + public string IndoorMapFloorId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int ZoneType { get; set; } + public string PixelGeometry { get; set; } + public string GeoGeometry { get; set; } + public double CenterPixelX { get; set; } + public double CenterPixelY { get; set; } + public decimal CenterLatitude { get; set; } + public decimal CenterLongitude { get; set; } + public string Color { get; set; } + public string Metadata { get; set; } + public bool IsSearchable { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Routes/RouteInputModels.cs b/Web/Resgrid.Web.Services/Models/v4/Routes/RouteInputModels.cs new file mode 100644 index 00000000..8ee3dac9 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Routes/RouteInputModels.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Resgrid.Web.Services.Models.v4.Routes +{ + public class NewRoutePlanInput + { + [Required] + public string Name { get; set; } + public string Description { get; set; } + public int? UnitId { get; set; } + public int RouteStatus { get; set; } + public string RouteColor { get; set; } + public decimal? StartLatitude { get; set; } + public decimal? StartLongitude { get; set; } + public decimal? EndLatitude { get; set; } + public decimal? EndLongitude { get; set; } + public bool UseStationAsStart { get; set; } = true; + public bool UseStationAsEnd { get; set; } = true; + public bool OptimizeStopOrder { get; set; } + public string MapboxRouteProfile { get; set; } + public int GeofenceRadiusMeters { get; set; } = 100; + public List Stops { get; set; } + public List Schedules { get; set; } + } + + public class UpdateRoutePlanInput : NewRoutePlanInput + { + [Required] + public string RoutePlanId { get; set; } + } + + public class NewRouteStopInput + { + [Required] + public string Name { get; set; } + public string Description { get; set; } + public int StopType { get; set; } + public int? CallId { get; set; } + [Required] + public decimal Latitude { get; set; } + [Required] + public decimal Longitude { get; set; } + public string Address { get; set; } + public int? GeofenceRadiusMeters { get; set; } + public int Priority { get; set; } + public DateTime? PlannedArrivalTime { get; set; } + public DateTime? PlannedDepartureTime { get; set; } + public int? EstimatedDwellMinutes { get; set; } + public string ContactName { get; set; } + public string ContactNumber { get; set; } + public string Notes { get; set; } + } + + public class RouteScheduleInput + { + public int RecurrenceType { get; set; } + public string RecurrenceCron { get; set; } + public string DaysOfWeek { get; set; } + public int? DayOfMonth { get; set; } + public string ScheduledStartTime { get; set; } + [Required] + public DateTime EffectiveFrom { get; set; } + public DateTime? EffectiveTo { get; set; } + } + + public class StartRouteInput + { + [Required] + public string RoutePlanId { get; set; } + [Required] + public int UnitId { get; set; } + } + + public class EndRouteInput + { + [Required] + public string RouteInstanceId { get; set; } + } + + public class PauseRouteInput + { + [Required] + public string RouteInstanceId { get; set; } + } + + public class ResumeRouteInput + { + [Required] + public string RouteInstanceId { get; set; } + } + + public class CancelRouteInput + { + [Required] + public string RouteInstanceId { get; set; } + public string Reason { get; set; } + } + + public class CheckInInput + { + [Required] + public string RouteInstanceStopId { get; set; } + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + } + + public class CheckOutInput + { + [Required] + public string RouteInstanceStopId { get; set; } + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + } + + public class SkipStopInput + { + [Required] + public string RouteInstanceStopId { get; set; } + public string Reason { get; set; } + } + + public class GeofenceCheckInInput + { + [Required] + public int UnitId { get; set; } + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + } + + public class UpdateRouteGeometryInput + { + [Required] + public string RoutePlanId { get; set; } + [Required] + public string Geometry { get; set; } + public double DistanceMeters { get; set; } + public double DurationSeconds { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Routes/RouteResultModels.cs b/Web/Resgrid.Web.Services/Models/v4/Routes/RouteResultModels.cs new file mode 100644 index 00000000..7f073a6d --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Routes/RouteResultModels.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Routes +{ + public class GetRoutePlansResult : StandardApiResponseV4Base + { + public List Data { get; set; } + public GetRoutePlansResult() { Data = new List(); } + } + + public class GetRoutePlanResult : StandardApiResponseV4Base + { + public RoutePlanDetailResultData Data { get; set; } + public GetRoutePlanResult() { Data = new RoutePlanDetailResultData(); } + } + + public class SaveRoutePlanResult : StandardApiResponseV4Base + { + public string Id { get; set; } + public string Status { get; set; } + } + + public class GetRouteInstanceResult : StandardApiResponseV4Base + { + public RouteInstanceResultData Data { get; set; } + public GetRouteInstanceResult() { Data = new RouteInstanceResultData(); } + } + + public class GetRouteInstancesResult : StandardApiResponseV4Base + { + public List Data { get; set; } + public GetRouteInstancesResult() { Data = new List(); } + } + + public class GetRouteProgressResult : StandardApiResponseV4Base + { + public RouteProgressResultData Data { get; set; } + public GetRouteProgressResult() { Data = new RouteProgressResultData(); } + } + + public class GetRouteDeviationsResult : StandardApiResponseV4Base + { + public List Data { get; set; } + public GetRouteDeviationsResult() { Data = new List(); } + } + + public class RoutePlanResultData + { + public string RoutePlanId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int? UnitId { get; set; } + public int RouteStatus { get; set; } + public string RouteColor { get; set; } + public int StopsCount { get; set; } + public double? EstimatedDistanceMeters { get; set; } + public double? EstimatedDurationSeconds { get; set; } + public string MapboxRouteProfile { get; set; } + public DateTime AddedOn { get; set; } + } + + public class RoutePlanDetailResultData + { + public string RoutePlanId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int DepartmentId { get; set; } + public int? UnitId { get; set; } + public int RouteStatus { get; set; } + public string RouteColor { get; set; } + public decimal? StartLatitude { get; set; } + public decimal? StartLongitude { get; set; } + public decimal? EndLatitude { get; set; } + public decimal? EndLongitude { get; set; } + public bool UseStationAsStart { get; set; } + public bool UseStationAsEnd { get; set; } + public bool OptimizeStopOrder { get; set; } + public string MapboxRouteProfile { get; set; } + public string MapboxRouteGeometry { get; set; } + public double? EstimatedDistanceMeters { get; set; } + public double? EstimatedDurationSeconds { get; set; } + public int GeofenceRadiusMeters { get; set; } + public DateTime AddedOn { get; set; } + public List Stops { get; set; } + public List Schedules { get; set; } + + public RoutePlanDetailResultData() + { + Stops = new List(); + Schedules = new List(); + } + } + + public class RouteStopResultData + { + public string RouteStopId { get; set; } + public int StopOrder { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int StopType { get; set; } + public int? CallId { get; set; } + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + public string Address { get; set; } + public int? GeofenceRadiusMeters { get; set; } + public int Priority { get; set; } + public DateTime? PlannedArrivalTime { get; set; } + public DateTime? PlannedDepartureTime { get; set; } + public int? EstimatedDwellMinutes { get; set; } + public string ContactName { get; set; } + public string ContactNumber { get; set; } + public string Notes { get; set; } + } + + public class RouteScheduleResultData + { + public string RouteScheduleId { get; set; } + public int RecurrenceType { get; set; } + public string RecurrenceCron { get; set; } + public string DaysOfWeek { get; set; } + public int? DayOfMonth { get; set; } + public string ScheduledStartTime { get; set; } + public DateTime EffectiveFrom { get; set; } + public DateTime? EffectiveTo { get; set; } + public bool IsActive { get; set; } + } + + public class RouteInstanceResultData + { + public string RouteInstanceId { get; set; } + public string RoutePlanId { get; set; } + public string RoutePlanName { get; set; } + public int UnitId { get; set; } + public int Status { get; set; } + public DateTime? ActualStartOn { get; set; } + public DateTime? ActualEndOn { get; set; } + public int StopsCompleted { get; set; } + public int StopsTotal { get; set; } + public double? TotalDistanceMeters { get; set; } + public double? TotalDurationSeconds { get; set; } + public string Notes { get; set; } + public DateTime AddedOn { get; set; } + } + + public class RouteProgressResultData + { + public RouteInstanceResultData Instance { get; set; } + public int TotalStops { get; set; } + public int CompletedStops { get; set; } + public int SkippedStops { get; set; } + public int PendingStops { get; set; } + public List Stops { get; set; } + + public RouteProgressResultData() + { + Stops = new List(); + } + } + + public class RouteInstanceStopResultData + { + public string RouteInstanceStopId { get; set; } + public string RouteStopId { get; set; } + public int StopOrder { get; set; } + public int Status { get; set; } + public DateTime? CheckInOn { get; set; } + public int? CheckInType { get; set; } + public decimal? CheckInLatitude { get; set; } + public decimal? CheckInLongitude { get; set; } + public DateTime? CheckOutOn { get; set; } + public decimal? CheckOutLatitude { get; set; } + public decimal? CheckOutLongitude { get; set; } + public int? DwellSeconds { get; set; } + public string SkipReason { get; set; } + public string Notes { get; set; } + public DateTime? EstimatedArrivalOn { get; set; } + public int? ActualArrivalDeviation { get; set; } + } + + public class RouteDeviationResultData + { + public string RouteDeviationId { get; set; } + public string RouteInstanceId { get; set; } + public DateTime DetectedOn { get; set; } + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + public double DeviationDistanceMeters { get; set; } + public int DeviationType { get; set; } + public bool IsAcknowledged { get; set; } + public string AcknowledgedByUserId { get; set; } + public DateTime? AcknowledgedOn { get; set; } + public string Notes { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml index 6d176db2..2a28e0d3 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml @@ -630,6 +630,85 @@ GetMapLayersResult object + + + Gets all indoor maps for the department. + + GetIndoorMapsResult object with list of indoor maps + + + + Gets a specific indoor map with its floors. + + Indoor map id + GetIndoorMapResult object with map and floor data + + + + Gets a specific indoor map floor with its zones. + + Indoor map floor id + GetIndoorMapFloorResult object with floor and zone data + + + + Gets the image for a specific indoor map floor. + + Indoor map floor id + Floor image file + + + + Searches for indoor map zones matching the specified term. + + Search term + SearchIndoorLocationsResult object with matching zones + + + + Gets all custom maps for the department, with optional type filter. + + Optional map type filter (0=Indoor, 1=Outdoor, 2=Event, 3=Custom) + GetCustomMapsResult object + + + + Gets a specific custom map with its layers. + + Custom map id + GetCustomMapResult object + + + + Gets a specific custom map layer with its regions. + + Layer id + GetCustomMapLayerResult object + + + + Gets a tile image for a specific custom map layer. + + Layer id + Zoom level + Tile X + Tile Y + Tile image file + + + + Gets the full image for a non-tiled custom map layer. + + Layer id + Layer image file + + + + Searches for custom map regions matching the specified term. + + Search term + SearchCustomMapRegionsResult object + Messaging system interaction @@ -865,6 +944,116 @@ + + + Route planning operations + + + + + Gets all route plans for the department + + + + + Gets route plans for a specific unit + + + + + Gets a single route plan with stops and schedules + + + + + Creates a new route plan with stops + + + + + Updates an existing route plan with stops + + + + + Soft-deletes a route plan + + + + + Starts a route instance + + + + + Ends a route instance + + + + + Pauses a route instance + + + + + Resumes a paused route instance + + + + + Cancels a route instance + + + + + Gets the active route instance for a unit + + + + + Gets all active route instances for the department + + + + + Gets route instance history for a plan + + + + + Gets progress for a route instance + + + + + Check in at a stop + + + + + Check out from a stop + + + + + Skip a stop with reason + + + + + Auto check-in from geofence proximity + + + + + Gets unacknowledged deviations for the department + + + + + Acknowledge a deviation + + SCIM 2.0 provisioning endpoint for automated user lifecycle management @@ -4672,6 +4861,16 @@ User Defined Field values for this call + + + Indoor Map Zone Id for the call location + + + + + Indoor Map Floor Id for the call location + + Gets the calls current scheduled but not yet dispatched @@ -5727,6 +5926,46 @@ Can the API services talk to the cache + + + Response Data + + + + + Default constructor + + + + + Response Data + + + + + Default constructor + + + + + Response Data + + + + + Default constructor + + + + + Response Data + + + + + Default constructor + + Response Data diff --git a/Web/Resgrid.Web.Services/Startup.cs b/Web/Resgrid.Web.Services/Startup.cs index 9dc93145..c947c3f2 100644 --- a/Web/Resgrid.Web.Services/Startup.cs +++ b/Web/Resgrid.Web.Services/Startup.cs @@ -409,6 +409,11 @@ public void ConfigureServices(IServiceCollection services) options.AddPolicy(ResgridResources.Udf_Update, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Udf, ResgridClaimTypes.Actions.Update)); options.AddPolicy(ResgridResources.Udf_Create, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Udf, ResgridClaimTypes.Actions.Create)); options.AddPolicy(ResgridResources.Udf_Delete, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Udf, ResgridClaimTypes.Actions.Delete)); + + options.AddPolicy(ResgridResources.Route_View, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.View)); + options.AddPolicy(ResgridResources.Route_Update, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Update)); + options.AddPolicy(ResgridResources.Route_Create, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Create)); + options.AddPolicy(ResgridResources.Route_Delete, policy => policy.RequireClaim(ResgridClaimTypes.Resources.Route, ResgridClaimTypes.Actions.Delete)); }); #endregion Auth Roles diff --git a/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs index 10d57dc9..e74e5452 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs @@ -359,6 +359,8 @@ public async Task Edit(string contactId) if (model.Contact.DepartmentId != DepartmentId) return Unauthorized(); + + if (!String.IsNullOrWhiteSpace(model.Contact.EntranceGpsCoordinates)) { var entranceGpsCoordinates = model.Contact.EntranceGpsCoordinates.Split(','); model.LocationGpsLatitude = entranceGpsCoordinates[0]; diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs new file mode 100644 index 00000000..8c74b38a --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs @@ -0,0 +1,353 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Model; +using Resgrid.Model.Services; +using Resgrid.Web.Areas.User.Models.CustomMaps; +using Resgrid.Web.Helpers; +using System.Collections.Generic; + +namespace Resgrid.Web.Areas.User.Controllers +{ + [Area("User")] + public class CustomMapsController : SecureBaseController + { + private readonly ICustomMapService _customMapService; + + public CustomMapsController(ICustomMapService customMapService) + { + _customMapService = customMapService; + } + + #region Map CRUD + + [HttpGet] + public async Task Index(int? type) + { + var model = new CustomMapIndexView(); + CustomMapType? filterType = type.HasValue ? (CustomMapType)type.Value : null; + model.FilterType = filterType; + model.Maps = await _customMapService.GetCustomMapsForDepartmentAsync(DepartmentId, filterType); + return View(model); + } + + [HttpGet] + public IActionResult New(int? type) + { + var model = new CustomMapNewView(); + if (type.HasValue) + model.Map.MapType = type.Value; + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task New(CustomMapNewView model, CancellationToken cancellationToken) + { + if (ModelState.IsValid) + { + model.Map.DepartmentId = DepartmentId; + model.Map.AddedById = UserId; + model.Map.AddedOn = DateTime.UtcNow; + model.Map.IsDeleted = false; + + await _customMapService.SaveCustomMapAsync(model.Map, cancellationToken); + + return RedirectToAction("Index"); + } + + return View(model); + } + + [HttpGet] + public async Task Edit(string id) + { + var map = await _customMapService.GetCustomMapByIdAsync(id); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var model = new CustomMapNewView(); + model.Map = map; + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(CustomMapNewView model, CancellationToken cancellationToken) + { + var existing = await _customMapService.GetCustomMapByIdAsync(model.Map.IndoorMapId); + if (existing == null || existing.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + if (ModelState.IsValid) + { + model.Map.DepartmentId = DepartmentId; + model.Map.AddedById = existing.AddedById; + model.Map.AddedOn = existing.AddedOn; + model.Map.UpdatedById = UserId; + model.Map.UpdatedOn = DateTime.UtcNow; + model.Map.IsDeleted = existing.IsDeleted; + + await _customMapService.SaveCustomMapAsync(model.Map, cancellationToken); + + return RedirectToAction("Index"); + } + + return View(model); + } + + [HttpGet] + public async Task Delete(string id, CancellationToken cancellationToken) + { + var map = await _customMapService.GetCustomMapByIdAsync(id); + if (map != null && map.DepartmentId == DepartmentId) + { + await _customMapService.DeleteCustomMapAsync(id, cancellationToken); + } + + return RedirectToAction("Index"); + } + + #endregion Map CRUD + + #region Layers + + [HttpGet] + public async Task Layers(string id) + { + var map = await _customMapService.GetCustomMapByIdAsync(id); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var model = new CustomMapLayersView(); + model.Map = map; + model.Layers = await _customMapService.GetLayersForMapAsync(id); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task NewLayer(string indoorMapId, string name, int floorOrder, int layerType, IFormFile layerImage, CancellationToken cancellationToken) + { + var map = await _customMapService.GetCustomMapByIdAsync(indoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var layer = new IndoorMapFloor(); + layer.IndoorMapId = indoorMapId; + layer.Name = name; + layer.FloorOrder = floorOrder; + layer.LayerType = layerType; + layer.Opacity = 0.8m; + layer.IsDeleted = false; + layer.AddedOn = DateTime.UtcNow; + + byte[] imageData = null; + if (layerImage != null && layerImage.Length > 0) + { + using (var ms = new MemoryStream()) + { + await layerImage.CopyToAsync(ms); + imageData = ms.ToArray(); + } + layer.ImageContentType = layerImage.ContentType; + layer.SourceFileSize = layerImage.Length; + } + + // Save the layer first to get it in DB + await _customMapService.SaveLayerAsync(layer, cancellationToken); + + // Process tiles if image provided + if (imageData != null) + { + await _customMapService.ProcessAndStoreTilesAsync(layer.IndoorMapFloorId, imageData, cancellationToken); + } + + return RedirectToAction("Layers", new { id = indoorMapId }); + } + + [HttpGet] + public async Task DeleteLayer(string id, CancellationToken cancellationToken) + { + var layer = await _customMapService.GetLayerByIdAsync(id); + if (layer == null) + return RedirectToAction("Index"); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + await _customMapService.DeleteLayerAsync(id, cancellationToken); + + return RedirectToAction("Layers", new { id = layer.IndoorMapId }); + } + + #endregion Layers + + #region Region Editor + + [HttpGet] + public async Task RegionEditor(string id) + { + var layer = await _customMapService.GetLayerByIdAsync(id); + if (layer == null) + return RedirectToAction("Index"); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var model = new CustomMapRegionEditorView(); + model.Layer = layer; + model.Map = map; + model.Regions = await _customMapService.GetRegionsForLayerAsync(id); + return View(model); + } + + [HttpPost] + public async Task SaveRegion([FromBody] IndoorMapZone region, CancellationToken cancellationToken) + { + var layer = await _customMapService.GetLayerByIdAsync(region.IndoorMapFloorId); + if (layer == null) + return Json(new { success = false, message = "Layer not found" }); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return Json(new { success = false, message = "Unauthorized" }); + + if (string.IsNullOrWhiteSpace(region.IndoorMapZoneId)) + { + region.IndoorMapZoneId = null; + region.AddedOn = DateTime.UtcNow; + } + region.IsDeleted = false; + + var saved = await _customMapService.SaveRegionAsync(region, cancellationToken); + return Json(new { success = true, regionId = saved.IndoorMapZoneId }); + } + + [HttpPost] + public async Task DeleteRegion([FromBody] DeleteRegionRequest request, CancellationToken cancellationToken) + { + var region = await _customMapService.GetRegionByIdAsync(request.RegionId); + if (region == null) + return Json(new { success = false }); + + var layer = await _customMapService.GetLayerByIdAsync(region.IndoorMapFloorId); + if (layer == null) + return Json(new { success = false }); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return Json(new { success = false }); + + await _customMapService.DeleteRegionAsync(request.RegionId, cancellationToken); + return Json(new { success = true }); + } + + #endregion Region Editor + + #region Import + + [HttpGet] + public async Task Import(string id) + { + var map = await _customMapService.GetCustomMapByIdAsync(id); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var model = new CustomMapImportView(); + model.Map = map; + model.Layers = await _customMapService.GetLayersForMapAsync(id); + model.Imports = await _customMapService.GetImportsForMapAsync(id); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ImportUpload(string mapId, string layerId, IFormFile importFile, CancellationToken cancellationToken) + { + var map = await _customMapService.GetCustomMapByIdAsync(mapId); + if (map == null || map.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + if (importFile == null || importFile.Length == 0) + return RedirectToAction("Import", new { id = mapId }); + + var fileName = importFile.FileName.ToLowerInvariant(); + + if (fileName.EndsWith(".geojson") || fileName.EndsWith(".json")) + { + using (var reader = new StreamReader(importFile.OpenReadStream())) + { + var geoJson = await reader.ReadToEndAsync(); + await _customMapService.ImportGeoJsonAsync(mapId, layerId, geoJson, UserId, cancellationToken); + } + } + else if (fileName.EndsWith(".kml")) + { + await _customMapService.ImportKmlAsync(mapId, layerId, importFile.OpenReadStream(), false, UserId, cancellationToken); + } + else if (fileName.EndsWith(".kmz")) + { + await _customMapService.ImportKmlAsync(mapId, layerId, importFile.OpenReadStream(), true, UserId, cancellationToken); + } + + return RedirectToAction("Import", new { id = mapId }); + } + + #endregion Import + + #region Search & Image Endpoints + + [HttpGet] + public async Task SearchRegions(string term) + { + var regions = await _customMapService.SearchRegionsAsync(DepartmentId, term); + var results = new List(); + + foreach (var region in regions) + { + var displayName = await _customMapService.GetRegionDisplayNameAsync(region.IndoorMapZoneId); + results.Add(new { id = region.IndoorMapZoneId, text = displayName, layerId = region.IndoorMapFloorId }); + } + + return Json(new { results = results }); + } + + [HttpGet] + public async Task GetLayerImage(string id) + { + var layer = await _customMapService.GetLayerByIdAsync(id); + if (layer == null || layer.ImageData == null) + return NotFound(); + + var map = await _customMapService.GetCustomMapByIdAsync(layer.IndoorMapId); + if (map == null || map.DepartmentId != DepartmentId) + return NotFound(); + + return File(layer.ImageData, layer.ImageContentType ?? "image/png"); + } + + [HttpGet] + public async Task GetLayerTile(string id, int z, int x, int y) + { + var tile = await _customMapService.GetTileAsync(id, z, x, y); + if (tile == null) + return NotFound(); + + return File(tile.TileData, tile.TileContentType ?? "image/png"); + } + + #endregion Search & Image Endpoints + } + + public class DeleteRegionRequest + { + public string RegionId { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs b/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs index 186777f2..381012d4 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs @@ -227,6 +227,15 @@ public async Task NewCall(NewCallView model, IFormCollection coll if (!String.IsNullOrEmpty(model.Latitude) && !String.IsNullOrEmpty(model.Longitude)) model.Call.GeoLocationData = string.Format("{0},{1}", model.Latitude, model.Longitude); + // Indoor map zone + var indoorMapZoneId = collection["IndoorMapZoneId"].FirstOrDefault(); + var indoorMapFloorId = collection["IndoorMapFloorId"].FirstOrDefault(); + if (!String.IsNullOrWhiteSpace(indoorMapZoneId)) + { + model.Call.IndoorMapZoneId = indoorMapZoneId; + model.Call.IndoorMapFloorId = indoorMapFloorId; + } + List dispatchingUserIds = new List(); List dispatchingGroupIds = new List(); List dispatchingUnitIds = new List(); @@ -608,6 +617,15 @@ public async Task UpdateCall(UpdateCallView model, IFormCollectio if (!String.IsNullOrEmpty(model.Latitude) && !String.IsNullOrEmpty(model.Longitude)) call.GeoLocationData = string.Format("{0},{1}", model.Latitude, model.Longitude); + // Indoor map zone + var indoorMapZoneId = collection["IndoorMapZoneId"].FirstOrDefault(); + var indoorMapFloorId = collection["IndoorMapFloorId"].FirstOrDefault(); + if (!String.IsNullOrWhiteSpace(indoorMapZoneId)) + { + call.IndoorMapZoneId = indoorMapZoneId; + call.IndoorMapFloorId = indoorMapFloorId; + } + List existingDispatches = new List(call.Dispatches); List dispatchingUserIds = new List(); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/IndoorMapsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/IndoorMapsController.cs new file mode 100644 index 00000000..3dfc50bb --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Controllers/IndoorMapsController.cs @@ -0,0 +1,52 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Resgrid.Web.Areas.User.Controllers +{ + [Area("User")] + public class IndoorMapsController : SecureBaseController + { + [HttpGet] + public IActionResult Index() + { + return RedirectToAction("Index", "CustomMaps", new { area = "User", type = 0 }); + } + + [HttpGet] + public IActionResult New() + { + return RedirectToAction("New", "CustomMaps", new { area = "User", type = 0 }); + } + + [HttpGet] + public IActionResult Edit(string id) + { + return RedirectToAction("Edit", "CustomMaps", new { area = "User", id = id }); + } + + [HttpGet] + public IActionResult Delete(string id) + { + return RedirectToAction("Delete", "CustomMaps", new { area = "User", id = id }); + } + + [HttpGet] + public IActionResult Floors(string id) + { + return RedirectToAction("Layers", "CustomMaps", new { area = "User", id = id }); + } + + [HttpGet] + public IActionResult ZoneEditor(string id) + { + return RedirectToAction("RegionEditor", "CustomMaps", new { area = "User", id = id }); + } + + [HttpGet] + public IActionResult GetFloorImage(string id) + { + return RedirectToAction("GetLayerImage", "CustomMaps", new { area = "User", id = id }); + } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Controllers/RoutesController.cs b/Web/Resgrid.Web/Areas/User/Controllers/RoutesController.cs new file mode 100644 index 00000000..7ce2f096 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Controllers/RoutesController.cs @@ -0,0 +1,172 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Model; +using Resgrid.Model.Services; +using Resgrid.Web.Areas.User.Models.Routes; + +namespace Resgrid.Web.Areas.User.Controllers +{ + [Area("User")] + public class RoutesController : SecureBaseController + { + private readonly IRouteService _routeService; + private readonly IUnitsService _unitsService; + + public RoutesController(IRouteService routeService, IUnitsService unitsService) + { + _routeService = routeService; + _unitsService = unitsService; + } + + [HttpGet] + public async Task Index() + { + var model = new RouteIndexView(); + model.Plans = await _routeService.GetRoutePlansForDepartmentAsync(DepartmentId); + return View(model); + } + + [HttpGet] + public async Task New() + { + var model = new RouteNewView(); + model.Units = (await _unitsService.GetUnitsForDepartmentAsync(DepartmentId)).ToList(); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task New(RouteNewView model, CancellationToken cancellationToken) + { + if (ModelState.IsValid) + { + model.Plan.DepartmentId = DepartmentId; + model.Plan.AddedById = UserId; + model.Plan.AddedOn = DateTime.UtcNow; + model.Plan.IsDeleted = false; + + await _routeService.SaveRoutePlanAsync(model.Plan, cancellationToken); + + return RedirectToAction("Index"); + } + + model.Units = (await _unitsService.GetUnitsForDepartmentAsync(DepartmentId)).ToList(); + return View(model); + } + + [HttpGet] + public async Task Edit(string id) + { + var plan = await _routeService.GetRoutePlanByIdAsync(id); + if (plan == null || plan.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var model = new RouteEditView(); + model.Plan = plan; + model.Stops = await _routeService.GetRouteStopsForPlanAsync(id); + model.Schedules = await _routeService.GetSchedulesForPlanAsync(id); + model.Units = (await _unitsService.GetUnitsForDepartmentAsync(DepartmentId)).ToList(); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(RouteEditView model, CancellationToken cancellationToken) + { + if (ModelState.IsValid) + { + var existing = await _routeService.GetRoutePlanByIdAsync(model.Plan.RoutePlanId); + if (existing == null || existing.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + model.Plan.DepartmentId = DepartmentId; + model.Plan.UpdatedById = UserId; + model.Plan.UpdatedOn = DateTime.UtcNow; + model.Plan.AddedById = existing.AddedById; + model.Plan.AddedOn = existing.AddedOn; + + await _routeService.SaveRoutePlanAsync(model.Plan, cancellationToken); + + return RedirectToAction("Index"); + } + + model.Units = (await _unitsService.GetUnitsForDepartmentAsync(DepartmentId)).ToList(); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(string id, CancellationToken cancellationToken) + { + var plan = await _routeService.GetRoutePlanByIdAsync(id); + if (plan != null && plan.DepartmentId == DepartmentId) + { + await _routeService.DeleteRoutePlanAsync(id, cancellationToken); + } + + return RedirectToAction("Index"); + } + + [HttpGet] + public async Task View(string id) + { + var plan = await _routeService.GetRoutePlanByIdAsync(id); + if (plan == null || plan.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var model = new RouteDetailView(); + model.Plan = plan; + model.Stops = await _routeService.GetRouteStopsForPlanAsync(id); + return View(model); + } + + [HttpGet] + public async Task Instances(string routePlanId) + { + var plan = await _routeService.GetRoutePlanByIdAsync(routePlanId); + if (plan == null || plan.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var instances = await _routeService.GetInstancesForDepartmentAsync(DepartmentId); + var filtered = instances.Where(i => i.RoutePlanId == routePlanId).ToList(); + + var model = new RouteInstancesView(); + model.Plan = plan; + model.Instances = filtered; + return View(model); + } + + [HttpGet] + public async Task ActiveRoutes() + { + var instances = await _routeService.GetInstancesForDepartmentAsync(DepartmentId); + var active = instances.Where(i => i.Status == (int)RouteInstanceStatus.InProgress || i.Status == (int)RouteInstanceStatus.Paused).ToList(); + var plans = await _routeService.GetRoutePlansForDepartmentAsync(DepartmentId); + + var model = new ActiveRoutesView(); + model.Instances = active; + model.Plans = plans; + return View(model); + } + + [HttpGet] + public async Task InstanceDetail(string instanceId) + { + var instance = await _routeService.GetInstanceByIdAsync(instanceId); + if (instance == null || instance.DepartmentId != DepartmentId) + return RedirectToAction("Index"); + + var plan = await _routeService.GetRoutePlanByIdAsync(instance.RoutePlanId); + var stops = await _routeService.GetInstanceStopsAsync(instanceId); + + var model = new RouteInstanceDetailView(); + model.Instance = instance; + model.Plan = plan; + model.Stops = stops; + return View(model); + } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapImportView.cs b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapImportView.cs new file mode 100644 index 00000000..43da4f47 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapImportView.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.CustomMaps +{ + public class CustomMapImportView : BaseUserModel + { + public IndoorMap Map { get; set; } + public List Layers { get; set; } + public List Imports { get; set; } + public string Message { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapIndexView.cs b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapIndexView.cs new file mode 100644 index 00000000..41ee3559 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapIndexView.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.CustomMaps +{ + public class CustomMapIndexView : BaseUserModel + { + public List Maps { get; set; } + public CustomMapType? FilterType { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapLayersView.cs b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapLayersView.cs new file mode 100644 index 00000000..78b8ee12 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapLayersView.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.CustomMaps +{ + public class CustomMapLayersView : BaseUserModel + { + public IndoorMap Map { get; set; } + public List Layers { get; set; } + public string Message { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapNewView.cs b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapNewView.cs new file mode 100644 index 00000000..7af078d2 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapNewView.cs @@ -0,0 +1,15 @@ +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.CustomMaps +{ + public class CustomMapNewView : BaseUserModel + { + public IndoorMap Map { get; set; } + public string Message { get; set; } + + public CustomMapNewView() + { + Map = new IndoorMap(); + } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapRegionEditorView.cs b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapRegionEditorView.cs new file mode 100644 index 00000000..c2ab1c61 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/CustomMaps/CustomMapRegionEditorView.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.CustomMaps +{ + public class CustomMapRegionEditorView : BaseUserModel + { + public IndoorMapFloor Layer { get; set; } + public IndoorMap Map { get; set; } + public List Regions { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapFloorsView.cs b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapFloorsView.cs new file mode 100644 index 00000000..b5e50f25 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapFloorsView.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.IndoorMaps +{ + public class IndoorMapFloorsView : BaseUserModel + { + public IndoorMap IndoorMap { get; set; } + public List Floors { get; set; } + public string Message { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapIndexView.cs b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapIndexView.cs new file mode 100644 index 00000000..9e3735f5 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapIndexView.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.IndoorMaps +{ + public class IndoorMapIndexView : BaseUserModel + { + public List IndoorMaps { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapNewView.cs b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapNewView.cs new file mode 100644 index 00000000..cb369504 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapNewView.cs @@ -0,0 +1,15 @@ +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.IndoorMaps +{ + public class IndoorMapNewView : BaseUserModel + { + public IndoorMap IndoorMap { get; set; } + public string Message { get; set; } + + public IndoorMapNewView() + { + IndoorMap = new IndoorMap(); + } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapZoneEditorView.cs b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapZoneEditorView.cs new file mode 100644 index 00000000..85a0c50c --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/IndoorMaps/IndoorMapZoneEditorView.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.IndoorMaps +{ + public class IndoorMapZoneEditorView : BaseUserModel + { + public IndoorMapFloor Floor { get; set; } + public IndoorMap IndoorMap { get; set; } + public List Zones { get; set; } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Models/Routes/RouteViewModels.cs b/Web/Resgrid.Web/Areas/User/Models/Routes/RouteViewModels.cs new file mode 100644 index 00000000..13f5f057 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/Routes/RouteViewModels.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.Routes +{ + public class RouteIndexView : BaseUserModel + { + public List Plans { get; set; } + public RouteIndexView() { Plans = new List(); } + } + + public class RouteNewView : BaseUserModel + { + public RoutePlan Plan { get; set; } + public List Units { get; set; } + public RouteNewView() + { + Plan = new RoutePlan(); + Units = new List(); + } + } + + public class RouteEditView : BaseUserModel + { + public RoutePlan Plan { get; set; } + public List Stops { get; set; } + public List Schedules { get; set; } + public List Units { get; set; } + public RouteEditView() + { + Plan = new RoutePlan(); + Stops = new List(); + Schedules = new List(); + Units = new List(); + } + } + + public class RouteDetailView : BaseUserModel + { + public RoutePlan Plan { get; set; } + public List Stops { get; set; } + public RouteDetailView() + { + Plan = new RoutePlan(); + Stops = new List(); + } + } + + public class RouteInstancesView : BaseUserModel + { + public RoutePlan Plan { get; set; } + public List Instances { get; set; } + public RouteInstancesView() + { + Plan = new RoutePlan(); + Instances = new List(); + } + } + + public class ActiveRoutesView : BaseUserModel + { + public List Instances { get; set; } + public List Plans { get; set; } + public ActiveRoutesView() + { + Instances = new List(); + Plans = new List(); + } + } + + public class RouteInstanceDetailView : BaseUserModel + { + public RouteInstance Instance { get; set; } + public RoutePlan Plan { get; set; } + public List Stops { get; set; } + public RouteInstanceDetailView() + { + Instance = new RouteInstance(); + Plan = new RoutePlan(); + Stops = new List(); + } + } +} diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Edit.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Edit.cshtml new file mode 100644 index 00000000..3d65cd66 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Edit.cshtml @@ -0,0 +1,99 @@ +@model Resgrid.Web.Areas.User.Models.CustomMaps.CustomMapNewView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | Edit Custom Map"; +} + +@section Styles { + + +} + +
+
+

Edit Custom Map

+ +
+
+ +
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(Model.Message)) + { +
@Model.Message
+ } +
+ @Html.AntiForgeryToken() + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+

Draw a rectangle on the map below to update the bounds. Click the map to set the center point.

+
+
+ +
+ + + + + + + + + +
+
+
+ Cancel + +
+
+
+
+
+
+
+
+ +@section Scripts { + + + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml new file mode 100644 index 00000000..e07bc1ba --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml @@ -0,0 +1,106 @@ +@model Resgrid.Web.Areas.User.Models.CustomMaps.CustomMapImportView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | Import - " + Model.Map.Name; +} + +
+
+

Import - @Model.Map.Name

+ +
+
+ +
+
+
+
+
+
Import Geospatial Data
+
+
+ @if (!string.IsNullOrWhiteSpace(Model.Message)) + { +
@Model.Message
+ } +
+ @Html.AntiForgeryToken() + +
+ +
+ +
+
+
+ +
+ + Supported formats: GeoJSON (.geojson, .json), KML (.kml), KMZ (.kmz) +
+
+ +
+
+
+
+
+ +
+
+
Import History
+
+
+
+ + + + + + + + + + + + @if (Model.Imports != null) + { + foreach (var imp in Model.Imports) + { + + + + + + + + } + } + +
FileTypeStatusDateError
@imp.SourceFileName@((CustomMapImportFileType)imp.SourceFileType) + @{ + var status = (CustomMapImportStatus)imp.Status; + var labelClass = status == CustomMapImportStatus.Complete ? "label-success" : + status == CustomMapImportStatus.Failed ? "label-danger" : + status == CustomMapImportStatus.Processing ? "label-warning" : "label-default"; + } + @status + @imp.ImportedOn.ToString("g")@imp.ErrorMessage
+
+
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Index.cshtml new file mode 100644 index 00000000..a47ecc41 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Index.cshtml @@ -0,0 +1,89 @@ +@model Resgrid.Web.Areas.User.Models.CustomMaps.CustomMapIndexView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | Custom Maps"; +} + +
+
+

Custom Maps

+ +
+
+ +
+
+ +
+
+
+
+
+ +
+ + + + + + + + + + + + @if (Model.Maps != null) + { + foreach (var map in Model.Maps) + { + + + + + + + + } + } + +
NameTypeDescriptionCreated
@map.Name@((CustomMapType)map.MapType)@map.Description@map.AddedOn.ToString("g") + Layers + Import + Edit + Delete +
+
+
+
+
+
+
+ +@section Scripts { + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Layers.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Layers.cshtml new file mode 100644 index 00000000..681e0ba6 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Layers.cshtml @@ -0,0 +1,113 @@ +@model Resgrid.Web.Areas.User.Models.CustomMaps.CustomMapLayersView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | Layers - " + Model.Map.Name; +} + +
+
+

Layers - @Model.Map.Name

+ +
+
+ +
+
+
+
+
+
Add Layer
+
+
+
+ @Html.AntiForgeryToken() + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ + Images larger than 2048px will be automatically tiled. +
+
+ +
+
+
+
+
+ +
+
+
Layers
+
+
+
+ + + + + + + + + + + + + @if (Model.Layers != null) + { + foreach (var layer in Model.Layers) + { + + + + + + + + + } + } + +
OrderNameTypeHas ImageTiled
@layer.FloorOrder@layer.Name@((CustomMapLayerType)layer.LayerType)@(layer.ImageData != null || layer.IsTiled ? "Yes" : "No") + @if (layer.IsTiled) + { + Tiled (z@(layer.TileMinZoom)-@(layer.TileMaxZoom)) + } + else + { + No + } + + Region Editor + Delete +
+
+
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/New.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/New.cshtml new file mode 100644 index 00000000..788e9edc --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/New.cshtml @@ -0,0 +1,98 @@ +@model Resgrid.Web.Areas.User.Models.CustomMaps.CustomMapNewView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | New Custom Map"; +} + +@section Styles { + + +} + +
+
+

New Custom Map

+ +
+
+ +
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(Model.Message)) + { +
@Model.Message
+ } +
+ @Html.AntiForgeryToken() +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+

Draw a rectangle on the map below to define the bounds. Click the map to set the center point.

+
+
+ +
+ + + + + + + + + +
+
+
+ Cancel + +
+
+
+
+
+
+
+
+ +@section Scripts { + + + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/RegionEditor.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/RegionEditor.cshtml new file mode 100644 index 00000000..81957703 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/RegionEditor.cshtml @@ -0,0 +1,138 @@ +@model Resgrid.Web.Areas.User.Models.CustomMaps.CustomMapRegionEditorView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | Region Editor - " + Model.Layer.Name; +} + +@section Styles { + + + +} + +
+
+

Region Editor - @Model.Map.Name > @Model.Layer.Name

+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
Regions
+
+
+ @if (Model.Regions != null) + { + foreach (var region in Model.Regions) + { +
+ @region.Name + + + +
@((IndoorMapZoneType)region.ZoneType) + @if (region.IsDispatchable) + { + Dispatchable + } +
+ } + } +
+
+ +
+
+
Region Properties
+
+ +
+
+
+
+ +@section Scripts { + + + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/Dashboard.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/Dashboard.cshtml index 39efb095..641cf8b2 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/Dashboard.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/Dashboard.cshtml @@ -45,8 +45,12 @@ @localizer["ArchivedCalls"] @if (ClaimsAuthorizationHelper.CanCreateCall()) { - @localizer["NewCall"] + @localizer["NewCall"] } + @if (ClaimsAuthorizationHelper.CanViewRoutes()) + { + Routes + } diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/NewCall.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/NewCall.cshtml index 7c21f043..d99e37ab 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/NewCall.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/NewCall.cshtml @@ -177,6 +177,18 @@
+
+ +
+
+
+ +
+
+ + +
+
@@ -615,6 +627,34 @@ $("#alertNotesModal").modal("show"); } }); + + $('#indoorZoneSearch').select2({ + placeholder: 'Search indoor locations...', + allowClear: true, + minimumInputLength: 2, + ajax: { + url: '@Url.Action("SearchZones", "IndoorMaps", new { area = "User" })', + dataType: 'json', + delay: 300, + data: function(params) { + return { term: params.term }; + }, + processResults: function(data) { + return { results: data.results }; + } + } + }); + + $('#indoorZoneSearch').on('select2:select', function(e) { + var data = e.params.data; + $('#IndoorMapZoneId').val(data.id); + $('#IndoorMapFloorId').val(data.floorId); + }); + + $('#indoorZoneSearch').on('select2:clear', function() { + $('#IndoorMapZoneId').val(''); + $('#IndoorMapFloorId').val(''); + }); }); } diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml index 6301496a..7058a2bf 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml @@ -178,6 +178,18 @@
+
+ +
+
+
+ +
+
+ + +
+
@@ -485,6 +497,34 @@ $("#AdditionalContacts :selected").map(function(i, el) { checkAlertNotes($(el).val(), "#additionalNotesIcon"); }).get(); + + $('#indoorZoneSearch').select2({ + placeholder: 'Search indoor locations...', + allowClear: true, + minimumInputLength: 2, + ajax: { + url: '@Url.Action("SearchZones", "IndoorMaps", new { area = "User" })', + dataType: 'json', + delay: 300, + data: function(params) { + return { term: params.term }; + }, + processResults: function(data) { + return { results: data.results }; + } + } + }); + + $('#indoorZoneSearch').on('select2:select', function(e) { + var data = e.params.data; + $('#IndoorMapZoneId').val(data.id); + $('#IndoorMapFloorId').val(data.floorId); + }); + + $('#indoorZoneSearch').on('select2:clear', function() { + $('#IndoorMapZoneId').val(''); + $('#IndoorMapFloorId').val(''); + }); }); } diff --git a/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Edit.cshtml b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Edit.cshtml new file mode 100644 index 00000000..f812b6e3 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Edit.cshtml @@ -0,0 +1,88 @@ +@model Resgrid.Web.Areas.User.Models.IndoorMaps.IndoorMapNewView +@{ + ViewBag.Title = "Resgrid | Edit Indoor Map"; +} + +
+
+

Edit Indoor Map

+ +
+
+ +
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(Model.Message)) + { +
@Model.Message
+ } +
+ @Html.AntiForgeryToken() + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+ Cancel + +
+
+
+
+
+
+
+
+
+ +@section Scripts { + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Floors.cshtml b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Floors.cshtml new file mode 100644 index 00000000..a82165f8 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Floors.cshtml @@ -0,0 +1,89 @@ +@model Resgrid.Web.Areas.User.Models.IndoorMaps.IndoorMapFloorsView +@{ + ViewBag.Title = "Resgrid | Floors - " + Model.IndoorMap.Name; +} + +
+
+

Floors - @Model.IndoorMap.Name

+ +
+
+ +
+
+
+
+
+
Add Floor
+
+
+
+ @Html.AntiForgeryToken() + +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+
Floors
+
+
+
+ + + + + + + + + + + @if (Model.Floors != null) + { + foreach (var floor in Model.Floors) + { + + + + + + + } + } + +
OrderNameHas Image
@floor.FloorOrder@floor.Name@(floor.ImageData != null ? "Yes" : "No") + Zone Editor + Delete +
+
+
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Index.cshtml new file mode 100644 index 00000000..30a2457a --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/Index.cshtml @@ -0,0 +1,74 @@ +@model Resgrid.Web.Areas.User.Models.IndoorMaps.IndoorMapIndexView +@{ + ViewBag.Title = "Resgrid | Indoor Maps"; +} + +
+
+

Indoor Maps

+ +
+
+ +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + @if (Model.IndoorMaps != null) + { + foreach (var map in Model.IndoorMaps) + { + + + + + + + } + } + +
NameDescriptionCreated
@map.Name@map.Description@map.AddedOn.ToString("g") + Floors + Edit + Delete +
+
+
+
+
+
+
+ +@section Scripts { + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/New.cshtml b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/New.cshtml new file mode 100644 index 00000000..31336db9 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/New.cshtml @@ -0,0 +1,87 @@ +@model Resgrid.Web.Areas.User.Models.IndoorMaps.IndoorMapNewView +@{ + ViewBag.Title = "Resgrid | New Indoor Map"; +} + +
+
+

New Indoor Map

+ +
+
+ +
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(Model.Message)) + { +
@Model.Message
+ } +
+ @Html.AntiForgeryToken() +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+ Cancel + +
+
+
+
+
+
+
+
+
+ +@section Scripts { + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/ZoneEditor.cshtml b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/ZoneEditor.cshtml new file mode 100644 index 00000000..361594c6 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IndoorMaps/ZoneEditor.cshtml @@ -0,0 +1,119 @@ +@model Resgrid.Web.Areas.User.Models.IndoorMaps.IndoorMapZoneEditorView +@{ + ViewBag.Title = "Resgrid | Zone Editor - " + Model.Floor.Name; +} + +@section Styles { + + + +} + +
+
+

Zone Editor - @Model.IndoorMap.Name > @Model.Floor.Name

+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
Zones
+
+
+ @if (Model.Zones != null) + { + foreach (var zone in Model.Zones) + { +
+ @zone.Name + + + +
@((IndoorMapZoneType)zone.ZoneType) +
+ } + } +
+
+ +
+
+
Zone Properties
+
+ +
+
+
+
+ +@section Scripts { + + + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Mapping/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Mapping/Index.cshtml index ec0d298f..e9dfde19 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Mapping/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Mapping/Index.cshtml @@ -41,7 +41,10 @@ { } diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/ActiveRoutes.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/ActiveRoutes.cshtml new file mode 100644 index 00000000..9866f83b --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/ActiveRoutes.cshtml @@ -0,0 +1,69 @@ +@model Resgrid.Web.Areas.User.Models.Routes.ActiveRoutesView +@{ + ViewBag.Title = "Resgrid | Active Routes"; +} + +
+
+

Active Routes

+ +
+
+ +
+
+ @if (Model.Instances.Count == 0) + { +
+
+
+

No Active Routes

+

There are no route instances currently in progress.

+
+
+
+ } + @foreach (var instance in Model.Instances) + { + var plan = Model.Plans.FirstOrDefault(p => p.RoutePlanId == instance.RoutePlanId); + var pct = instance.StopsTotal > 0 ? (instance.StopsCompleted * 100 / instance.StopsTotal) : 0; +
+
+
+
@(plan?.Name ?? "Unknown Route")
+ @((Resgrid.Model.RouteInstanceStatus)instance.Status) +
+
+
+
+ Unit +

@instance.UnitId

+
+
+ Stops +

@instance.StopsCompleted / @instance.StopsTotal

+
+
+
+
+
+
+ Started: @(instance.ActualStartOn?.ToString("HH:mm") ?? "-") +
+ + View Details + +
+
+
+ } +
+
+ +@section Scripts { + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/Edit.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/Edit.cshtml new file mode 100644 index 00000000..8266d80d --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/Edit.cshtml @@ -0,0 +1,160 @@ +@model Resgrid.Web.Areas.User.Models.Routes.RouteEditView +@{ + ViewBag.Title = "Resgrid | Edit Route"; +} + +
+
+

Edit Route Plan

+ +
+
+ +
+
+
+
+
Route Details
+
+
+ @Html.AntiForgeryToken() + + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+
+ +
+ +

Stops (@Model.Stops.Count)

+
+ + + + + + + + + + + + @foreach (var stop in Model.Stops) + { + + + + + + + + } + +
#NameTypeAddressPriority
@stop.StopOrder@stop.Name@((Resgrid.Model.RouteStopType)stop.StopType)@stop.Address@((Resgrid.Model.RouteStopPriority)stop.Priority)
+
+ +
+ +
+ +
+
+ Cancel + +
+
+
+
+
+
+
+
+ +@section Scripts { + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/Index.cshtml new file mode 100644 index 00000000..3d27dd6c --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/Index.cshtml @@ -0,0 +1,84 @@ +@model Resgrid.Web.Areas.User.Models.Routes.RouteIndexView +@using Resgrid.Model +@{ + ViewBag.Title = "Resgrid | Routes"; +} + +
+
+

Routes

+ +
+ +
+ +
+
+
+
+
+
+ + + + + + + + + + + + + @foreach (var plan in Model.Plans) + { + + + + + + + + + } + +
NameStatusStopsProfileCreatedActions
@plan.Name@((RouteStatus)plan.RouteStatus)-@(plan.MapboxRouteProfile ?? "driving")@plan.AddedOn.ToString("yyyy-MM-dd") + + View + + + Edit + + + History + +
+ @Html.AntiForgeryToken() + + +
+
+
+
+
+
+
+
+ +@section Scripts { + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/InstanceDetail.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/InstanceDetail.cshtml new file mode 100644 index 00000000..72333d6c --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/InstanceDetail.cshtml @@ -0,0 +1,100 @@ +@model Resgrid.Web.Areas.User.Models.Routes.RouteInstanceDetailView +@{ + ViewBag.Title = "Resgrid | Route Instance Detail"; +} + +
+
+

Route Instance - @Model.Plan.Name

+ +
+
+ +
+
+
+
+
Route Map
+
+
+
+
+
+
+
+
Instance Info
+
+
+
Status
+
@((Resgrid.Model.RouteInstanceStatus)Model.Instance.Status)
+
Unit
+
@Model.Instance.UnitId
+
Started
+
@(Model.Instance.ActualStartOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")
+
Ended
+
@(Model.Instance.ActualEndOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")
+
Progress
+
@Model.Instance.StopsCompleted / @Model.Instance.StopsTotal
+
+
+ @{ + var pct = Model.Instance.StopsTotal > 0 ? (Model.Instance.StopsCompleted * 100 / Model.Instance.StopsTotal) : 0; + } +
+
+
+
+ +
+
Stop Timeline
+
+
+ @foreach (var stop in Model.Stops.OrderBy(s => s.StopOrder)) + { + var statusClass = stop.Status switch { 1 => "lazur-bg", 2 => "navy-bg", 3 => "warning-bg", _ => "default-bg" }; + var statusText = stop.Status switch { 0 => "Pending", 1 => "Checked In", 2 => "Completed", 3 => "Skipped", _ => "Unknown" }; +
+
+ +
+
+

Stop @(stop.StopOrder + 1)

+

@statusText

+ @if (stop.CheckInOn.HasValue) + { +

In: @stop.CheckInOn.Value.ToString("HH:mm:ss")

+ } + @if (stop.CheckOutOn.HasValue) + { +

Out: @stop.CheckOutOn.Value.ToString("HH:mm:ss")

+ } + @if (stop.DwellSeconds.HasValue) + { +

Dwell: @(stop.DwellSeconds.Value / 60)m @(stop.DwellSeconds.Value % 60)s

+ } + @if (!string.IsNullOrEmpty(stop.SkipReason)) + { +

Skip: @stop.SkipReason

+ } +
+
+ } +
+
+
+
+
+
+ +@section Scripts { + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/Instances.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/Instances.cshtml new file mode 100644 index 00000000..feb90027 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/Instances.cshtml @@ -0,0 +1,57 @@ +@model Resgrid.Web.Areas.User.Models.Routes.RouteInstancesView +@{ + ViewBag.Title = "Resgrid | Route History"; +} + +
+
+

Route History - @Model.Plan.Name

+ +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + @foreach (var instance in Model.Instances) + { + + + + + + + + + } + +
StatusUnitStartedEndedStopsActions
@((Resgrid.Model.RouteInstanceStatus)instance.Status)@instance.UnitId@(instance.ActualStartOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")@(instance.ActualEndOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")@instance.StopsCompleted / @instance.StopsTotal + + Detail + +
+
+
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/New.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/New.cshtml new file mode 100644 index 00000000..09b248b9 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/New.cshtml @@ -0,0 +1,133 @@ +@model Resgrid.Web.Areas.User.Models.Routes.RouteNewView +@{ + ViewBag.Title = "Resgrid | New Route"; +} + +
+
+

New Route Plan

+ +
+
+ +
+
+
+
+
+
Route Details
+
+
+
+ @Html.AntiForgeryToken() + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+
+ +
+ +

Map & Stops

+

Stops can be managed after creating the route via the API or edit form.

+
+ +
+ +
+
+ Cancel + +
+
+
+
+
+
+
+
+ +@section Scripts { + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Routes/View.cshtml b/Web/Resgrid.Web/Areas/User/Views/Routes/View.cshtml new file mode 100644 index 00000000..9050d60e --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/Routes/View.cshtml @@ -0,0 +1,84 @@ +@model Resgrid.Web.Areas.User.Models.Routes.RouteDetailView +@{ + ViewBag.Title = "Resgrid | Route Detail"; +} + +
+
+

@Model.Plan.Name

+ +
+
+ +
+
+
+
+
Route Map
+
+
+
+
+
+
+
+
Details
+
+
+
Status
+
@((Resgrid.Model.RouteStatus)Model.Plan.RouteStatus)
+
Profile
+
@(Model.Plan.MapboxRouteProfile ?? "driving")
+
Geofence
+
@Model.Plan.GeofenceRadiusMeters m
+ @if (Model.Plan.EstimatedDistanceMeters.HasValue) + { +
Distance
+
@(Math.Round(Model.Plan.EstimatedDistanceMeters.Value / 1000, 1)) km
+ } + @if (Model.Plan.EstimatedDurationSeconds.HasValue) + { +
Duration
+
@(Math.Round(Model.Plan.EstimatedDurationSeconds.Value / 60, 0)) min
+ } +
+
+
+ +
+
Stops (@Model.Stops.Count)
+
+
+ @foreach (var stop in Model.Stops.OrderBy(s => s.StopOrder)) + { +
+
+ @(stop.StopOrder + 1). @stop.Name + @if (!string.IsNullOrEmpty(stop.Address)) + { +
@stop.Address + } +
+
+ } +
+
+
+
+
+
+ +@section Scripts { + + +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml index db4abd64..e0743208 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml @@ -36,33 +36,22 @@ { } - @if (!String.IsNullOrWhiteSpace(Resgrid.Config.TelemetryConfig.PostHogUrl) && !String.IsNullOrWhiteSpace(Resgrid.Config.TelemetryConfig.PostHogApiKey)) - { - - } - @if (!String.IsNullOrWhiteSpace(Resgrid.Config.TelemetryConfig.CountlyUrl) && !String.IsNullOrWhiteSpace(Resgrid.Config.TelemetryConfig.CountlyWebKey)) {