From f6365390645a5ddd0ddf86b84708582fbdb2be4d Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Mon, 17 Aug 2026 10:04:43 -0700 Subject: [PATCH] RG-T131 Map fix, removed forms pages. --- .../Controllers/v4/CallsController.cs | 8 +++--- .../Controllers/v4/FormsController.cs | 22 ++++++---------- .../Models/v4/Mapping/GetMapDataResult.cs | 4 +-- .../Models/v4/Mapping/PoiResultModels.cs | 4 +-- .../Resgrid.Web.Services.xml | 8 +++--- .../src/components/map/LeafletMapView.tsx | 8 +++--- .../Apps/src/components/map/MapboxMapView.tsx | 6 ++++- .../User/Apps/src/components/map/mapTypes.ts | 25 ++++++++++++++++--- .../User/Controllers/DispatchController.cs | 21 ++++++++-------- .../Areas/User/Controllers/FormsController.cs | 12 +++++++++ .../Views/Dispatch/AddArchivedCall.cshtml | 2 +- .../Areas/User/Views/Dispatch/NewCall.cshtml | 3 +-- .../User/Views/Dispatch/UpdateCall.cshtml | 2 +- .../Areas/User/Views/Forms/View.cshtml | 7 +++++- .../Areas/User/Views/Shared/_TopNavbar.cshtml | 1 - 15 files changed, 83 insertions(+), 50 deletions(-) diff --git a/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs index d253e2f9d..a16d1d8d2 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs @@ -672,8 +672,8 @@ public async Task> SaveCall([FromBody] NewCallInput if (!string.IsNullOrWhiteSpace(newCallInput.What3Words)) call.W3W = newCallInput.What3Words; - if (!string.IsNullOrWhiteSpace(newCallInput.CallFormData)) - call.CallFormData = newCallInput.CallFormData; + // Forms module is disabled: CallFormData is read-only now. The input property stays on the + // contract so older clients keep deserializing, but anything they send is dropped. if (!string.IsNullOrWhiteSpace(newCallInput.IndoorMapZoneId)) call.IndoorMapZoneId = newCallInput.IndoorMapZoneId; @@ -992,8 +992,8 @@ public async Task> EditCall([FromBody] EditCallInpu if (!string.IsNullOrWhiteSpace(editCallInput.What3Words)) call.W3W = editCallInput.What3Words; - if (!string.IsNullOrWhiteSpace(editCallInput.CallFormData)) - call.CallFormData = editCallInput.CallFormData; + // Forms module is disabled: ignoring the posted value leaves whatever form data the call + // already carries intact, so an edit from an older client can't wipe or replace it. if (editCallInput.DispatchOn.HasValue) { diff --git a/Web/Resgrid.Web.Services/Controllers/v4/FormsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/FormsController.cs index efab05dd0..dc32ae81e 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/FormsController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/FormsController.cs @@ -35,26 +35,18 @@ public FormsController(IFormsService formsService) [HttpGet("GetNewCallForm")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = ResgridResources.Forms_View)] - public async Task> GetNewCallForm() + public Task> GetNewCallForm() { + // Forms module is disabled. Reporting "no form configured" is the same shape clients + // already handle for departments that never built one, so shipped app versions stop + // offering call form entry without needing a release. var result = new FormResult(); - var form = await _formsService.GetNewCallFormByDepartmentIdAsync(DepartmentId); - - if (form != null) - { - result.Data = ConvertFormResultData(form); - result.PageSize = 1; - result.Status = ResponseHelper.Success; - } - else - { - result.PageSize = 0; - result.Status = ResponseHelper.NotFound; - } + result.PageSize = 0; + result.Status = ResponseHelper.NotFound; ResponseHelper.PopulateV4ResponseData(result); - return Ok(result); + return Task.FromResult>(Ok(result)); } /// diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/GetMapDataResult.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/GetMapDataResult.cs index 648541e73..83c0f80b4 100644 --- a/Web/Resgrid.Web.Services/Models/v4/Mapping/GetMapDataResult.cs +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/GetMapDataResult.cs @@ -55,7 +55,7 @@ public class MapMakerInfoData /// /// The POI-specific custom icon image name (only set for POI markers, Type=4). /// New app versions should use this field instead of ImagePath for POI icons. - /// ImagePath is set to null for POI markers so old apps fall back to their default icon. + /// ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. /// public string PoiImage { get; set; } } @@ -72,7 +72,7 @@ public class PoiLayerData /// /// The POI-specific custom icon image name. /// New app versions should use this field for POI type icons. - /// ImagePath is set to null so old apps fall back to their default icon. + /// ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. /// public string PoiImage { get; set; } } diff --git a/Web/Resgrid.Web.Services/Models/v4/Mapping/PoiResultModels.cs b/Web/Resgrid.Web.Services/Models/v4/Mapping/PoiResultModels.cs index b3e380ec4..b71d66bc2 100644 --- a/Web/Resgrid.Web.Services/Models/v4/Mapping/PoiResultModels.cs +++ b/Web/Resgrid.Web.Services/Models/v4/Mapping/PoiResultModels.cs @@ -14,7 +14,7 @@ public class PoiTypeResultData /// /// The POI-specific custom icon image name. /// New app versions should use this field for POI type icons. - /// ImagePath is set to null so old apps fall back to their default icon. + /// ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. /// public string PoiImage { get; set; } } @@ -37,7 +37,7 @@ public class PoiResultData /// /// The POI-specific custom icon image name. /// New app versions should use this field for POI icons. - /// ImagePath is set to null so old apps fall back to their default icon. + /// ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. /// public string PoiImage { get; set; } } diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml index 5b0c295aa..69fd4ac7c 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml @@ -10057,14 +10057,14 @@ The POI-specific custom icon image name (only set for POI markers, Type=4). New app versions should use this field instead of ImagePath for POI icons. - ImagePath is set to null for POI markers so old apps fall back to their default icon. + ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. The POI-specific custom icon image name. New app versions should use this field for POI type icons. - ImagePath is set to null so old apps fall back to their default icon. + ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. @@ -10091,14 +10091,14 @@ The POI-specific custom icon image name. New app versions should use this field for POI type icons. - ImagePath is set to null so old apps fall back to their default icon. + ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. The POI-specific custom icon image name. New app versions should use this field for POI icons. - ImagePath is set to null so old apps fall back to their default icon. + ImagePath carries the bitmap-asset name resolved by PoiIconHelper for clients that ship PNGs. diff --git a/Web/Resgrid.Web/Areas/User/Apps/src/components/map/LeafletMapView.tsx b/Web/Resgrid.Web/Areas/User/Apps/src/components/map/LeafletMapView.tsx index 4aeba4bce..3c731978b 100644 --- a/Web/Resgrid.Web/Areas/User/Apps/src/components/map/LeafletMapView.tsx +++ b/Web/Resgrid.Web/Areas/User/Apps/src/components/map/LeafletMapView.tsx @@ -5,6 +5,7 @@ import 'leaflet/dist/leaflet.css'; import { getLayerColor, getMarkerIconUrl, + getPoiIconClass, getPoiMarkerShapePath, isPoiMarker, type MapMarkerInfo, @@ -17,6 +18,7 @@ interface MarkerState { longitude: number; title: string; imagePath: string; + poiImage: string; markerShape: string; color: string; markerType: number; @@ -26,9 +28,7 @@ interface MarkerState { function createMarkerIcon(marker: MapMarkerInfo): L.Icon | L.DivIcon { if (isPoiMarker(marker)) { - const iconClass = typeof marker.ImagePath === 'string' && marker.ImagePath.length > 0 - ? marker.ImagePath - : 'map-icon-map-pin'; + const iconClass = getPoiIconClass(marker); const color = marker.Color || '#2563eb'; return L.divIcon({ @@ -175,6 +175,7 @@ export default function LeafletMapView({ !existingMarkerState || existingMarkerState.title !== markerInfo.Title || existingMarkerState.imagePath !== markerInfo.ImagePath || + existingMarkerState.poiImage !== (markerInfo.PoiImage ?? '') || existingMarkerState.markerShape !== (markerInfo.Marker ?? '') || existingMarkerState.color !== (markerInfo.Color ?? '') || existingMarkerState.markerType !== markerInfo.Type || @@ -193,6 +194,7 @@ export default function LeafletMapView({ longitude: markerInfo.Longitude, title: markerInfo.Title, imagePath: markerInfo.ImagePath, + poiImage: markerInfo.PoiImage ?? '', markerShape: markerInfo.Marker ?? '', color: markerInfo.Color ?? '', markerType: markerInfo.Type, diff --git a/Web/Resgrid.Web/Areas/User/Apps/src/components/map/MapboxMapView.tsx b/Web/Resgrid.Web/Areas/User/Apps/src/components/map/MapboxMapView.tsx index 2134bf54a..954dfdf21 100644 --- a/Web/Resgrid.Web/Areas/User/Apps/src/components/map/MapboxMapView.tsx +++ b/Web/Resgrid.Web/Areas/User/Apps/src/components/map/MapboxMapView.tsx @@ -3,6 +3,7 @@ import 'mapbox-gl/dist/mapbox-gl.css'; import { getLayerColor, getMarkerIconUrl, + getPoiIconClass, getPoiMarkerShapePath, isPoiMarker, type MapMarkerInfo, @@ -19,6 +20,7 @@ interface MarkerState { longitude: number; title: string; imagePath: string; + poiImage: string; markerShape: string; color: string; markerType: number; @@ -44,7 +46,7 @@ function createMarkerElement(markerInfo: MapMarkerInfo, hideLabels: boolean): HT markerShape.appendChild(path); const icon = document.createElement('span'); - icon.className = `map-icon ${markerInfo.ImagePath || 'map-icon-map-pin'} rg-map__poi-marker-icon`; + icon.className = `map-icon ${getPoiIconClass(markerInfo)} rg-map__poi-marker-icon`; icon.setAttribute('aria-hidden', 'true'); wrapper.appendChild(markerShape); @@ -327,6 +329,7 @@ export default function MapboxMapView({ !existingMarkerState || existingMarkerState.title !== markerInfo.Title || existingMarkerState.imagePath !== markerInfo.ImagePath || + existingMarkerState.poiImage !== (markerInfo.PoiImage ?? '') || existingMarkerState.markerShape !== (markerInfo.Marker ?? '') || existingMarkerState.color !== (markerInfo.Color ?? '') || existingMarkerState.markerType !== markerInfo.Type || @@ -353,6 +356,7 @@ export default function MapboxMapView({ longitude: markerInfo.Longitude, title: markerInfo.Title, imagePath: markerInfo.ImagePath, + poiImage: markerInfo.PoiImage ?? '', markerShape: markerInfo.Marker ?? '', color: markerInfo.Color ?? '', markerType: markerInfo.Type, diff --git a/Web/Resgrid.Web/Areas/User/Apps/src/components/map/mapTypes.ts b/Web/Resgrid.Web/Areas/User/Apps/src/components/map/mapTypes.ts index 280660ee8..08bbd4a01 100644 --- a/Web/Resgrid.Web/Areas/User/Apps/src/components/map/mapTypes.ts +++ b/Web/Resgrid.Web/Areas/User/Apps/src/components/map/mapTypes.ts @@ -15,6 +15,7 @@ export interface MapMarkerInfo { Title: string; zIndex: number; ImagePath: string; + PoiImage?: string; InfoWindowContent: string; Color: string; Type: number | string; @@ -32,6 +33,7 @@ export interface PoiLayerInfo { Name: string; Color: string; ImagePath: string; + PoiImage?: string; Marker: string; IsDestination: boolean; } @@ -120,6 +122,8 @@ export interface MapRendererProps { } const defaultPoiMarkerShape = 'MAP_PIN'; +const poiIconClassPrefix = 'map-icon-'; +const defaultPoiIconClass = 'map-icon-map-pin'; const poiMarkerPaths: Record = { MAP_PIN: 'M0-48c-9.8 0-17.7 7.8-17.7 17.4 0 15.5 17.7 30.6 17.7 30.6s17.7-15.4 17.7-30.6c0-9.6-7.9-17.4-17.7-17.4z', @@ -204,7 +208,7 @@ function getMarkerTypeValue(marker: Pick): number | null } export function isPoiMarker( - marker: Pick, + marker: Pick, ): boolean { if (getMarkerTypeValue(marker) === mapMarkerTypes.poi) { return true; @@ -218,8 +222,23 @@ export function isPoiMarker( return true; } - return typeof marker.ImagePath === 'string' - && marker.ImagePath.trim().toLowerCase().startsWith('map-icon-'); + return isPoiIconClass(getStringValue(marker.PoiImage, marker.ImagePath)); +} + +function isPoiIconClass(candidate: string): boolean { + return candidate.toLowerCase().startsWith(poiIconClassPrefix); +} + +/** + * POI glyphs come from the map-icons web font, so the marker needs a "map-icon-*" class. The API + * carries that class in PoiImage; ImagePath holds a short PNG name ("hospital", "firstaid") for the + * apps that ship bitmap assets and is not a valid font class. Older API responses put the class in + * ImagePath, so that is still accepted when it looks like one. + */ +export function getPoiIconClass(marker: Pick): string { + const candidate = getStringValue(marker.PoiImage, marker.ImagePath); + + return isPoiIconClass(candidate) ? candidate : defaultPoiIconClass; } export function getPoiLayerId(layer: Pick | Pick): string { diff --git a/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs b/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs index 9e3d1311a..3c57983e0 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs @@ -62,7 +62,6 @@ public class DispatchController : SecureBaseController private readonly ITemplatesService _templatesService; private readonly IPdfProvider _pdfProvider; private readonly IProtocolsService _protocolsService; - private readonly IFormsService _formsService; private readonly IShiftsService _shiftsService; private readonly IContactsService _contactsService; private readonly IMappingService _mappingService; @@ -82,7 +81,7 @@ public DispatchController(IDepartmentsService departmentsService, IUsersService Model.Services.IAuthorizationService authorizationService, IWorkLogsService workLogsService, IGeoLocationProvider geoLocationProvider, IPersonnelRolesService personnelRolesService, IDepartmentSettingsService departmentSettingsService, IUserProfileService userProfileService, IUnitsService unitsService, IActionLogsService actionLogsService, IEventAggregator eventAggregator, ICustomStateService customStateService, - ITemplatesService templatesService, IPdfProvider pdfProvider, IProtocolsService protocolsService, IFormsService formsService, + ITemplatesService templatesService, IPdfProvider pdfProvider, IProtocolsService protocolsService, IShiftsService shiftsService, IContactsService contactsService, IMappingService mappingService, IUserDefinedFieldsService userDefinedFieldsService, IUdfRenderingService udfRenderingService, ICheckInTimerService checkInTimerService, IWeatherAlertService weatherAlertService, @@ -109,7 +108,6 @@ public DispatchController(IDepartmentsService departmentsService, IUsersService _templatesService = templatesService; _pdfProvider = pdfProvider; _protocolsService = protocolsService; - _formsService = formsService; _shiftsService = shiftsService; _contactsService = contactsService; _mappingService = mappingService; @@ -298,6 +296,10 @@ public async Task NewCall(NewCallView model, IFormCollection coll model.Call.DepartmentId = DepartmentId; model.Call.Priority = (int)model.CallPriority; model.Call.State = 0; + + // Forms module is disabled. The hidden field is model-bound, so drop anything posted + // into it rather than trusting the client not to send form data. + model.Call.CallFormData = null; model.Call.NatureOfCall = System.Net.WebUtility.HtmlDecode(model.Call.NatureOfCall); model.Call.Notes = System.Net.WebUtility.HtmlDecode(model.Call.Notes); @@ -1377,6 +1379,7 @@ public async Task AddArchivedCall(NewCallView model, IFormCollect model.Call.Priority = (int)model.CallPriority; model.Call.State = 0; model.Call.LoggedOn = model.Call.LoggedOn.ToUniversalTime(); + model.Call.CallFormData = null; model.Call.NatureOfCall = System.Net.WebUtility.HtmlDecode(model.Call.NatureOfCall); model.Call.Notes = System.Net.WebUtility.HtmlDecode(model.Call.Notes); @@ -3095,10 +3098,9 @@ private async Task FillNewCallView(NewCallView model) model.Call.ReportingUserId = UserId; - var form = await _formsService.GetNewCallFormByDepartmentIdAsync(DepartmentId); - - if (form != null) - model.NewCallFormData = form.Data; + // Forms module is disabled: leaving NewCallFormData empty drops the call form button, + // its modal and the formRender call from the new/archived call views. Form data already + // on existing calls is untouched and still renders read-only on the call detail view. model.Contacts = await _contactsService.GetAllContactsForDepartmentAsync(DepartmentId); if (model.Contacts != null && model.Contacts.Any()) @@ -3150,9 +3152,8 @@ private async Task FillUpdateCallView(UpdateCallView model) if (templates != null) model.CallTemplates = new SelectList(templates, "CallQuickTemplateId", "Name"); - var form = await _formsService.GetNewCallFormByDepartmentIdAsync(DepartmentId); - if (form != null) - model.NewCallFormData = form.Data; + // Forms module is disabled, see FillNewCallView. UpdateCall never copies CallFormData + // from the posted model onto the stored call, so existing form data survives an edit. var allUsers = await _departmentsService.GetAllUsersForDepartmentAsync(model.Department.DepartmentId); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/FormsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/FormsController.cs index cae4d2469..6a362e193 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/FormsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/FormsController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Resgrid.Model; using Resgrid.Model.Services; using Resgrid.Providers.Claims; @@ -31,6 +32,17 @@ public FormsController(IFormsService formsService, ICallsService callsService, I _departmentsService = departmentsService; } + /// + /// The Forms module is disabled. Existing form data stays in the database and still renders + /// read-only on the call detail view, but no new form templates can be listed, created or + /// edited, and no new form data can be captured. The actions below are left intact so the + /// module can be turned back on by removing this override. + /// + public override void OnActionExecuting(ActionExecutingContext context) + { + context.Result = NotFound(); + } + [HttpGet] [Authorize(Policy = ResgridResources.Forms_View)] public async Task Index() diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/AddArchivedCall.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/AddArchivedCall.cshtml index 24bcb3ec4..a3913c101 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/AddArchivedCall.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/AddArchivedCall.cshtml @@ -414,7 +414,7 @@ } diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml index 5361e6b42..69ae1de3b 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/UpdateCall.cshtml @@ -474,7 +474,7 @@ diff --git a/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml index b03ab13c1..1414b24d2 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml @@ -28,7 +28,6 @@
  • @commonLocalizer["TextMessaging"]
  • @commonLocalizer["Templates"]
  • @commonLocalizer["Protocols"]
  • -
  • @commonLocalizer["Forms"]
  • @commonLocalizer["Types"]
  • @commonLocalizer["DistributionLists"]
  • Workflows