diff --git a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx index 92620391..15025adf 100644 --- a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx @@ -118,4 +118,6 @@ الحالة التاريخ خطأ + لا توجد طبقات متاحة. يرجى إنشاء طبقة أولاً قبل الاستيراد. + يرجى تحديد طبقة الهدف قبل الاستيراد. أنشئ طبقة أولاً إذا لم تكن موجودة. diff --git a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx index d9fdbed5..471295e3 100644 --- a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx +++ b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx @@ -365,4 +365,10 @@ Error + + No layers available. Please create a layer first before importing. + + + Please select a target layer before importing. Create a layer first if none exist. + diff --git a/Core/Resgrid.Model/Helpers/SerializerHelper.cs b/Core/Resgrid.Model/Helpers/SerializerHelper.cs index 74d7a3ec..50bb5704 100644 --- a/Core/Resgrid.Model/Helpers/SerializerHelper.cs +++ b/Core/Resgrid.Model/Helpers/SerializerHelper.cs @@ -11,7 +11,9 @@ public static void WarmUpProtobufSerializer() Serializer.PrepareSerializer
(); Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); + Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); + Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); diff --git a/Core/Resgrid.Model/PlanAddon.cs b/Core/Resgrid.Model/PlanAddon.cs index 18b82219..fca7d9d3 100644 --- a/Core/Resgrid.Model/PlanAddon.cs +++ b/Core/Resgrid.Model/PlanAddon.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using ProtoBuf; using Resgrid.Framework; using System; using System.Collections.Generic; @@ -6,20 +7,28 @@ namespace Resgrid.Model { + [ProtoContract] public class PlanAddon : IEntity { + [ProtoMember(1)] public string PlanAddonId { get; set; } + [ProtoMember(2)] public int? PlanId { get; set; } + [ProtoMember(3)] public virtual Plan Plan { get; set; } + [ProtoMember(4)] public int AddonType { get; set; } + [ProtoMember(5)] public double Cost { get; set; } + [ProtoMember(6)] public string ExternalId { get; set; } + [ProtoMember(7)] public string TestExternalId { get; set; } [NotMapped] @@ -33,10 +42,13 @@ public class PlanAddon : IEntity public string GetExternalKey() { - if (Config.PaymentProviderConfig.IsTestMode) - return TestExternalId; - else + if (!string.IsNullOrEmpty(ExternalId)) return ExternalId; + + if (Config.PaymentProviderConfig.IsTestMode && !string.IsNullOrEmpty(TestExternalId)) + return TestExternalId; + + return null; } [NotMapped] diff --git a/Core/Resgrid.Services/CustomMapService.cs b/Core/Resgrid.Services/CustomMapService.cs index e0d024f1..aa513542 100644 --- a/Core/Resgrid.Services/CustomMapService.cs +++ b/Core/Resgrid.Services/CustomMapService.cs @@ -311,6 +311,9 @@ public async Task GetTileAsync(string layerId, int z, int x, int public async Task ImportGeoJsonAsync(string mapId, string layerId, string geoJsonString, string userId, CancellationToken cancellationToken = default(CancellationToken)) { + if (string.IsNullOrWhiteSpace(layerId)) + throw new ArgumentException("A target layer must be specified for import.", nameof(layerId)); + var import = new CustomMapImport { CustomMapId = mapId, @@ -353,6 +356,9 @@ public async Task GetTileAsync(string layerId, int z, int x, int public async Task ImportKmlAsync(string mapId, string layerId, Stream kmlStream, bool isKmz, string userId, CancellationToken cancellationToken = default(CancellationToken)) { + if (string.IsNullOrWhiteSpace(layerId)) + throw new ArgumentException("A target layer must be specified for import.", nameof(layerId)); + var import = new CustomMapImport { CustomMapId = mapId, diff --git a/Core/Resgrid.Services/UsersService.cs b/Core/Resgrid.Services/UsersService.cs index 78b26b2f..10d05126 100644 --- a/Core/Resgrid.Services/UsersService.cs +++ b/Core/Resgrid.Services/UsersService.cs @@ -105,6 +105,9 @@ public async Task DoesUserHaveAnyActiveDepartments(string userName) public IdentityUser GetUserById(string userId, bool bypassCache = true) { + if (string.IsNullOrWhiteSpace(userId)) + return null; + if (!bypassCache && Config.SystemBehaviorConfig.CacheEnabled) { Func getUser = delegate () diff --git a/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs b/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs index aee2efe3..fc849dbe 100644 --- a/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs +++ b/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs @@ -1,4 +1,5 @@ using System; +using Resgrid.Framework; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -79,16 +80,35 @@ public async Task> FetchAlertsAsync(WeatherAlertSource source if (response.StatusCode == System.Net.HttpStatusCode.NotModified) return alerts; // No changes since last poll - response.EnsureSuccessStatusCode(); + // Read response body before checking status for diagnostic context + var json = response.Content != null ? await response.Content.ReadAsStringAsync() : string.Empty; + + if (!response.IsSuccessStatusCode) + { + // For client errors (4xx), the request is malformed — retrying won't help. + // Log the full diagnostic context and return empty rather than crashing the poller. + var snippet = json.Length > 500 ? json.Substring(0, 500) : json; + var errorMsg = $"NWS API returned {(int)response.StatusCode} ({response.ReasonPhrase}) " + + $"for URL '{url}', departmentId='{source.DepartmentId}', areaFilter='{source.AreaFilter}'. " + + $"Response body: {snippet}"; + + if ((int)response.StatusCode >= 500) + { + // Server errors are transient — throw so the poller can retry + throw new HttpRequestException(errorMsg); + } + + // Client errors (400, etc.) are permanent — log and return empty + Logging.LogError(errorMsg); + return alerts; + } // Update ETag on source if (response.Headers.ETag != null) source.LastETag = response.Headers.ETag.Tag; - var json = await response.Content.ReadAsStringAsync(); - // Validate response content-type is JSON before parsing - var contentType = response.Content.Headers.ContentType?.MediaType ?? ""; + var contentType = response.Content?.Headers.ContentType?.MediaType ?? ""; if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase)) { var snippet = json.Length > 200 ? json.Substring(0, 200) : json; diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs index 1b93ba53..12afbb3c 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs @@ -9,6 +9,8 @@ using Resgrid.Model; using Resgrid.Model.Services; using Resgrid.Web.Areas.User.Models.CustomMaps; +using Microsoft.Extensions.Localization; +using Resgrid.Localization.Areas.User.CustomMaps; using Resgrid.Web.Helpers; namespace Resgrid.Web.Areas.User.Controllers @@ -17,10 +19,12 @@ namespace Resgrid.Web.Areas.User.Controllers public class CustomMapsController : SecureBaseController { private readonly ICustomMapService _customMapService; + private readonly IStringLocalizer _localizer; - public CustomMapsController(ICustomMapService customMapService) + public CustomMapsController(ICustomMapService customMapService, IStringLocalizer localizer) { _customMapService = customMapService; + _localizer = localizer; } #region Map CRUD @@ -277,6 +281,16 @@ public async Task ImportUpload(string mapId, string layerId, IFor if (map == null || map.DepartmentId != DepartmentId) return RedirectToAction("Index"); + if (string.IsNullOrWhiteSpace(layerId)) + { + var model = new CustomMapImportView(); + model.Map = map; + model.Layers = await _customMapService.GetLayersForMapAsync(mapId); + model.Imports = await _customMapService.GetImportsForMapAsync(mapId); + model.Message = _localizer["PleaseSelectTargetLayer"]; + return View("Import", model); + } + if (importFile == null || importFile.Length == 0) return RedirectToAction("Import", new { id = mapId }); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs b/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs index b1100005..74a9ca34 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs @@ -649,6 +649,9 @@ public async Task AddPerson(AddPersonModel model, IFormCollection [Authorize(Policy = ResgridResources.Personnel_Delete)] public async Task DeletePerson(string userId) { + if (string.IsNullOrWhiteSpace(userId)) + return RedirectToAction("Index", "Personnel", new { area = "User" }); + if (!await _authorizationService.CanUserDeleteUserAsync(DepartmentId, UserId, userId)) return Unauthorized(); @@ -666,6 +669,9 @@ public async Task DeletePerson(string userId) [RequiresRecentTwoFactor] public async Task DeletePerson(DeletePersonModel model, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(model?.UserId)) + return RedirectToAction("Index", "Personnel", new { area = "User" }); + if (!await _authorizationService.CanUserDeleteUserAsync(DepartmentId, UserId, model.UserId)) return Unauthorized(); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs b/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs index 18d05758..01b03fe7 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs @@ -59,7 +59,7 @@ public async Task New() model.Template = new CallQuickTemplate(); var priorites = await _callsService.GetActiveCallPrioritiesForDepartmentAsync(DepartmentId); - model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)); + model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)?.DepartmentCallPriorityId); List types = new List(); @@ -70,6 +70,17 @@ public async Task New() return View(model); } + private async Task PopulateDropdowns(NewTemplateModel model) + { + var priorites = await _callsService.GetActiveCallPrioritiesForDepartmentAsync(DepartmentId); + model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)?.DepartmentCallPriorityId); + + List types = new List(); + types.Add(new CallType { CallTypeId = 0, Type = "No Type" }); + types.AddRange(await _callsService.GetCallTypesForDepartmentAsync(DepartmentId)); + model.CallTypes = new SelectList(types, "Type", "Type"); + } + [HttpPost] [Authorize(Policy = ResgridResources.Department_Update)] public async Task New(NewTemplateModel model, CancellationToken cancellationToken) @@ -77,6 +88,7 @@ public async Task New(NewTemplateModel model, CancellationToken c if (String.IsNullOrWhiteSpace(model.Template.CallName) && String.IsNullOrWhiteSpace(model.Template.CallNature)) { + await PopulateDropdowns(model); model.Message = "You must specify a call name and/or call nature to set to save the template"; return View(model); } @@ -91,6 +103,7 @@ public async Task New(NewTemplateModel model, CancellationToken c return RedirectToAction("Index"); } + await PopulateDropdowns(model); return View(model); } @@ -125,6 +138,7 @@ public async Task Edit(NewTemplateModel model, CancellationToken if (String.IsNullOrWhiteSpace(model.Template.CallName) && String.IsNullOrWhiteSpace(model.Template.CallNature)) { + await PopulateDropdowns(model); model.Message = "You must specify a call name and/or call nature to set to save the template"; return View(model); } @@ -139,6 +153,7 @@ public async Task Edit(NewTemplateModel model, CancellationToken return RedirectToAction("Index"); } + await PopulateDropdowns(model); return View(model); } diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml index cecdcb09..43448ad4 100644 --- a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml @@ -34,15 +34,19 @@
- + @foreach (var layer in Model.Layers) { } - } - + + }
@@ -52,7 +56,7 @@ @localizer["ImportFileHelp"]
- +