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 @@