From 6b704aa9e708a193b4cae52cdb37eb53556feec0 Mon Sep 17 00:00:00 2001 From: nairdo Date: Wed, 22 Oct 2025 16:49:14 -0700 Subject: [PATCH 001/559] - Code Generation, Migration rollups, and version update for 17.6 alpha packaging. --- Rock.Version/AssemblySharedInfo.cs | 8 ++++---- Rock.Version/AssemblySharedInfo.tt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Rock.Version/AssemblySharedInfo.cs b/Rock.Version/AssemblySharedInfo.cs index 9b071ee5d37..ae137c3316b 100644 --- a/Rock.Version/AssemblySharedInfo.cs +++ b/Rock.Version/AssemblySharedInfo.cs @@ -1,4 +1,4 @@ -//2 +//1 // ^^^ This number above is the build number used by the T4 template responsible for generating this file. // Do Not Remove! // @@ -42,13 +42,13 @@ // The AssemblyVersion number should change only when we are // making a breaking change and need the runtime binding to fail if it does not // match the correct version exactly. -[assembly: AssemblyVersion( "17.5.2" )] +[assembly: AssemblyVersion( "17.6.1" )] -[assembly: AssemblyFileVersion( "17.5.2" )] +[assembly: AssemblyFileVersion( "17.6.1" )] // This is the "official" product name that will be shown to people. // It's shown in the SystemInfo details and perhaps the RockUpdate page. -[assembly: AssemblyInformationalVersion( "Rock McKinley 17.5" )] +[assembly: AssemblyInformationalVersion( "Rock McKinley 17.6" )] diff --git a/Rock.Version/AssemblySharedInfo.tt b/Rock.Version/AssemblySharedInfo.tt index ebef1275489..6629a3129bd 100644 --- a/Rock.Version/AssemblySharedInfo.tt +++ b/Rock.Version/AssemblySharedInfo.tt @@ -4,7 +4,7 @@ <#@ output extension=".cs" #> <# int major = 17; - int minor = 5; + int minor = 6; int build = 0; try From 8ab284c1ae1b152f63881d06ea5d5bd9ee9219e9 Mon Sep 17 00:00:00 2001 From: nairdo Date: Fri, 23 Jan 2026 13:32:22 -0700 Subject: [PATCH 002/559] - (Event) Fixed a regression in the Registration Entry block where registrants were not added to groups when using an external 'redirect' type of payment gateway. The bug prevented the registrant from being added to the group after returning from payment. The fix ensures the method honors the slug when resolving the group for linkage, even if a GroupId is in the query string. (Fixes #5409) (cherry picked from commit 45f1e0f6f11a1f50a4ca995fc73bd9ca6cdd57ad) (cherry picked from commit e316324736529000e566c0d122f0dbc7d810a496) --- Rock.Blocks/Event/RegistrationEntry.cs | 68 +++++++++---------- .../Blocks/Event/RegistrationEntry.ascx.cs | 25 +++++-- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/Rock.Blocks/Event/RegistrationEntry.cs b/Rock.Blocks/Event/RegistrationEntry.cs index 226499e275e..19f8313a377 100644 --- a/Rock.Blocks/Event/RegistrationEntry.cs +++ b/Rock.Blocks/Event/RegistrationEntry.cs @@ -381,7 +381,7 @@ public BlockActionResult GetPaymentRedirect( RegistrationEntryArgsBag args, stri if ( PageParameter( PageParameterKey.GroupId ).AsIntegerOrNull() == null ) { - var groupId = GetRegistrationGroupId( rockContext, context?.Registration?.RegistrationInstanceId ); + var groupId = GetRegistrationGroupId( rockContext, context?.Registration?.RegistrationInstanceId, allowParameterGroupId: false ); if ( groupId.HasValue ) { RequestContext.PageParameters.Add( PageParameterKey.GroupId, groupId.ToString() ); @@ -668,7 +668,7 @@ public BlockActionResult GetSignatureDocumentData( RegistrationEntryArgsBag args // Process the GroupMember so we have data for the Lava merge. GroupMember groupMember = null; - var groupId = GetRegistrationGroupId( rockContext, context.Registration.RegistrationInstanceId ); + var groupId = GetRegistrationGroupId( rockContext, context.Registration.RegistrationInstanceId, allowParameterGroupId: false ); if ( groupId.HasValue ) { @@ -806,7 +806,7 @@ public BlockActionResult GetDefaultAttributeFieldValues( RegistrationEntryArgsBa // try getting the group member for the registrant person. if ( groupMember == null && person != null ) { - var groupId = GetRegistrationGroupId( rockContext, GetRegistrationInstanceId( rockContext ) ); + var groupId = GetRegistrationGroupId( rockContext, GetRegistrationInstanceId( rockContext ), allowParameterGroupId: false ); if ( groupId.HasValue ) { @@ -1885,48 +1885,48 @@ private Person GetExistingRegistrarPerson( RegistrationContext context, Person c /// The rock context. /// The registration instance identifier. /// The identifier or null if one is not available. - private int? GetRegistrationGroupId( RockContext rockContext, int? registrationInstanceId ) + private int? GetRegistrationGroupId( RockContext rockContext, int? registrationInstanceId, bool allowParameterGroupId = true ) { var groupId = PageParameter( PageParameterKey.GroupId ).AsIntegerOrNull(); var registrationSlug = PageParameter( PageParameterKey.Slug ); var eventOccurrenceId = this.EventOccurrenceIdPageParameter; - if ( !groupId.HasValue ) + if ( !registrationSlug.IsNullOrWhiteSpace() ) { - if ( !registrationSlug.IsNullOrWhiteSpace() ) - { - var dateTime = RockDateTime.Now; - var linkage = new EventItemOccurrenceGroupMapService( rockContext ) - .Queryable().AsNoTracking() - .Where( l => - l.UrlSlug == registrationSlug && - l.RegistrationInstance != null && - l.RegistrationInstance.IsActive && - l.RegistrationInstance.RegistrationTemplate != null && - l.RegistrationInstance.RegistrationTemplate.IsActive && - ( !l.RegistrationInstance.StartDateTime.HasValue || l.RegistrationInstance.StartDateTime <= dateTime ) && - ( !l.RegistrationInstance.EndDateTime.HasValue || l.RegistrationInstance.EndDateTime > dateTime ) ) - .FirstOrDefault(); + var dateTime = RockDateTime.Now; + var linkage = new EventItemOccurrenceGroupMapService( rockContext ) + .Queryable().AsNoTracking() + .Where( l => + l.UrlSlug == registrationSlug && + l.RegistrationInstance != null && + l.RegistrationInstance.IsActive && + l.RegistrationInstance.RegistrationTemplate != null && + l.RegistrationInstance.RegistrationTemplate.IsActive && + ( !l.RegistrationInstance.StartDateTime.HasValue || l.RegistrationInstance.StartDateTime <= dateTime ) && + ( !l.RegistrationInstance.EndDateTime.HasValue || l.RegistrationInstance.EndDateTime > dateTime ) ) + .FirstOrDefault(); - return linkage?.GroupId; - } - else if ( eventOccurrenceId.HasValue && registrationInstanceId.HasValue ) - { - var linkageGroupId = new EventItemOccurrenceService( rockContext ) - .Queryable() - .Where( o => o.Id == eventOccurrenceId.Value ) - .SelectMany( o => o.Linkages ) - .Where( l => l.RegistrationInstanceId == registrationInstanceId.Value ) - .Select( l => l.GroupId ) - .FirstOrDefault(); + return linkage?.GroupId; + } + else if ( eventOccurrenceId.HasValue && registrationInstanceId.HasValue ) + { + var linkageGroupId = new EventItemOccurrenceService( rockContext ) + .Queryable() + .Where( o => o.Id == eventOccurrenceId.Value ) + .SelectMany( o => o.Linkages ) + .Where( l => l.RegistrationInstanceId == registrationInstanceId.Value ) + .Select( l => l.GroupId ) + .FirstOrDefault(); - return linkageGroupId; - } + return linkageGroupId; + } + + if ( allowParameterGroupId && groupId.HasValue ) + { + return groupId.Value; } // If there is no slug or event occurrence id then don't use/trust the groupId in the query string - // There is some if logic refactoring that could be done here but leaving as we're only addressing a - // security concern and don't want to inadvertently change behavior. return null; } diff --git a/RockWeb/Blocks/Event/RegistrationEntry.ascx.cs b/RockWeb/Blocks/Event/RegistrationEntry.ascx.cs index 3e3c113a1d6..21f9bcc183a 100644 --- a/RockWeb/Blocks/Event/RegistrationEntry.ascx.cs +++ b/RockWeb/Blocks/Event/RegistrationEntry.ascx.cs @@ -2083,13 +2083,7 @@ private bool SetRegistrationState() numHowMany.Value = registration.Registrants.Count(); // set group id - if ( groupId.HasValue ) - { - // Commenting out line below to improve security. More refactoring could be made to clean up this - // section of code but limiting changes for now to reduce chance of introducing a bug. - // GroupId = groupId; - } - else if ( !string.IsNullOrWhiteSpace( registrationSlug ) ) + if ( !string.IsNullOrWhiteSpace( registrationSlug ) ) { var dateTime = RockDateTime.Now; var linkage = new EventItemOccurrenceGroupMapService( rockContext ) @@ -2113,6 +2107,23 @@ private bool SetRegistrationState() } } } + else if ( eventOccurrenceId.HasValue && registrationInstanceId.HasValue ) + { + var linkageGroupId = new EventItemOccurrenceService( rockContext ) + .Queryable() + .Where( o => o.Id == eventOccurrenceId.Value ) + .SelectMany( o => o.Linkages ) + .Where( l => l.RegistrationInstanceId == registrationInstanceId.Value ) + .Select( l => l.GroupId ) + .FirstOrDefault(); + + GroupId = linkageGroupId; + } + else if ( groupId.HasValue ) + { + // If there is no slug or event occurrence id then don't use/trust the groupId in the query string + // GroupId = groupId; + } } // A registration slug was specified From bc575ab2acae7dd981267f60e18623e028e33a2c Mon Sep 17 00:00:00 2001 From: nairdo Date: Fri, 23 Jan 2026 14:40:30 -0700 Subject: [PATCH 003/559] - Code Generation, Migration rollups, and version update for 17.7 alpha packaging. --- Rock.Version/AssemblySharedInfo.cs | 8 ++++---- Rock.Version/AssemblySharedInfo.tt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Rock.Version/AssemblySharedInfo.cs b/Rock.Version/AssemblySharedInfo.cs index ae137c3316b..b9f7e2f3e67 100644 --- a/Rock.Version/AssemblySharedInfo.cs +++ b/Rock.Version/AssemblySharedInfo.cs @@ -1,4 +1,4 @@ -//1 +//0 // ^^^ This number above is the build number used by the T4 template responsible for generating this file. // Do Not Remove! // @@ -42,13 +42,13 @@ // The AssemblyVersion number should change only when we are // making a breaking change and need the runtime binding to fail if it does not // match the correct version exactly. -[assembly: AssemblyVersion( "17.6.1" )] +[assembly: AssemblyVersion( "17.7.0" )] -[assembly: AssemblyFileVersion( "17.6.1" )] +[assembly: AssemblyFileVersion( "17.7.0" )] // This is the "official" product name that will be shown to people. // It's shown in the SystemInfo details and perhaps the RockUpdate page. -[assembly: AssemblyInformationalVersion( "Rock McKinley 17.6" )] +[assembly: AssemblyInformationalVersion( "Rock McKinley 17.7" )] diff --git a/Rock.Version/AssemblySharedInfo.tt b/Rock.Version/AssemblySharedInfo.tt index 6629a3129bd..5b609157b26 100644 --- a/Rock.Version/AssemblySharedInfo.tt +++ b/Rock.Version/AssemblySharedInfo.tt @@ -4,7 +4,7 @@ <#@ output extension=".cs" #> <# int major = 17; - int minor = 6; + int minor = 7; int build = 0; try From 55a81cc4d7c86c373b491fbb1291e123512bc95c Mon Sep 17 00:00:00 2001 From: CarterHenning Date: Thu, 26 Feb 2026 15:11:33 -0700 Subject: [PATCH 004/559] - very early start on block conversion --- Rock.Blocks/Crm/NcoaResults.cs | 228 ++++++++++++++++++ .../src/Crm/NcoaResults/types.partial.ts | 20 ++ .../src/Crm/ncoaResults.obs | 85 +++++++ .../Blocks/Crm/NcoaResults/NcoaResultsBag.cs | 16 ++ .../Crm/NcoaResults/NcoaResultsOptionsBag.cs | 27 +++ .../Blocks/Crm/NcoaResults/NcoaRowBag.cs | 12 + 6 files changed, 388 insertions(+) create mode 100644 Rock.Blocks/Crm/NcoaResults.cs create mode 100644 Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts create mode 100644 Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs create mode 100644 Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs create mode 100644 Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs create mode 100644 Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs diff --git a/Rock.Blocks/Crm/NcoaResults.cs b/Rock.Blocks/Crm/NcoaResults.cs new file mode 100644 index 00000000000..2c27c8ab3c3 --- /dev/null +++ b/Rock.Blocks/Crm/NcoaResults.cs @@ -0,0 +1,228 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data.Entity; +using System.Linq; + +using Rock.Attribute; +using Rock.Constants; +using Rock.Data; +using Rock.Model; +using Rock.Obsidian.UI; +using Rock.Security; +using Rock.Store; +using Rock.Utility; +using Rock.ViewModels.Blocks; +using Rock.ViewModels.Blocks.Crm.NcoaResults; +using Rock.ViewModels.Blocks.Store.PackageDetail; +using Rock.ViewModels.Controls; +using Rock.ViewModels.Utility; +using Rock.Web.Cache; + +using static Rock.Blocks.Security.Oidc.AuthClientList; + +namespace Rock.Blocks.Crm +{ + /// + /// Displays a list of people. + /// + + [DisplayName( "NcoaResults" )] + [Category( "CRM" )] + [Description( "Displays a list of ncoa results." )] + [IconCssClass( "fa fa-list" )] + // [SupportedSiteTypes( Model.SiteType.Web )] + + #region Block Attributes + + [IntegerField( "Result Count", + Description = "How many results to show per page.", + DefaultIntegerValue = 20, + Key = AttributeKey.ResultCount )] + + #endregion + + [Rock.SystemGuid.EntityTypeGuid( "01a7925e-2532-4a9a-9dc6-8bef835761de" )] + [Rock.SystemGuid.BlockTypeGuid( "69c53367-0d4a-49f1-b64b-863f08c2fc0b" )] + [CustomizedGrid] + public class NcoaResults : RockBlockType + { + #region Keys + + private static class AttributeKey + { + public const string DetailPage = "DetailPage"; + public const string ResultCount = "ResultCount"; + } + + private static class NavigationUrlKey + { + public const string DetailPage = "DetailPage"; + } + + private static class PreferenceKey + { + public const string FilterProcessed = "filter-processed"; + public const string FilterMoveDate = "filter-move-date"; + public const string FilterNcoaProcessedDate = "filter-ncoa-processed-date"; + public const string FilterMoveType = "filter-move-type"; + public const string FilterAddressStatus = "filter-address-status"; + public const string FilterInvalidReason = "filter-invalid-reason"; + public const string FilterMoveDistance = "filter-move-distance"; + public const string FilterLastName = "filter-last-name"; + public const string FilterCampus = "filter-campus"; + } + + #endregion Keys + + #region Fields + + /// + /// The Short Link attributes that are configured to show on the grid. + /// + private readonly Lazy> _gridAttributes = new Lazy>( BuildGridAttributes ); + + private PersonPreferenceCollection _personPreferences; + + #endregion + + #region Properties + + public PersonPreferenceCollection PersonPreferences + { + get + { + if ( _personPreferences == null ) + { + _personPreferences = this.GetBlockPersonPreferences(); + } + + return _personPreferences; + } + } + + protected ListItemBag FilterProcessed => PersonPreferences + .GetValue( PreferenceKey.FilterProcessed ) + .FromJsonOrNull(); + + protected ListItemBag FilterMoveDate => PersonPreferences + .GetValue( PreferenceKey.FilterMoveDate ) + .FromJsonOrNull(); + + private ListItemBag FilterNcoaProcessedDateRange => PersonPreferences + .GetValue( PreferenceKey.FilterNcoaProcessedDate ) + .FromJsonOrNull(); + + protected ListItemBag FilterMoveType => PersonPreferences + .GetValue( PreferenceKey.FilterMoveType ) + .FromJsonOrNull(); + + protected ListItemBag FilterAddressStatus => PersonPreferences + .GetValue( PreferenceKey.FilterAddressStatus ) + .FromJsonOrNull(); + + protected ListItemBag FilterInvalidReason => PersonPreferences + .GetValue( PreferenceKey.FilterInvalidReason ) + .FromJsonOrNull(); + + protected ListItemBag FilterMoveDistance => PersonPreferences + .GetValue( PreferenceKey.FilterMoveDistance ) + .FromJsonOrNull(); + + protected ListItemBag FilterLastName => PersonPreferences + .GetValue( PreferenceKey.FilterLastName ) + .FromJsonOrNull(); + + protected ListItemBag FilterCampus => PersonPreferences + .GetValue( PreferenceKey.FilterCampus ) + .FromJsonOrNull(); + + + + #endregion + + #region Methods + + /// + public override object GetObsidianBlockInitialization() + { + var box = new CustomBlockBox(); + + box.NavigationUrls = GetBoxNavigationUrls(); + box.Options = GetBoxOptions(); + + return box; + } + + /// + /// Gets the box options required for the component to render the list. + /// + /// The options that provide additional details to the block. + private NcoaResultsOptionsBag GetBoxOptions() + { + var options = new NcoaResultsOptionsBag(); + options.ResultCount = GetAttributeValue( AttributeKey.ResultCount ).AsIntegerOrNull() ?? 20; + + return options; + } + + + /// + /// Gets the box navigation URLs required for the page to operate. + /// + /// A dictionary of key names and URL values. + private Dictionary GetBoxNavigationUrls() + { + return new Dictionary + { + [NavigationUrlKey.DetailPage] = this.GetLinkedPageUrl( AttributeKey.DetailPage, "NcoaRowId", "((Key))" ) + }; + } + + + /// + /// Builds the list of grid attributes that should be included on the Grid. + /// + /// + /// The default implementation returns only attributes that are not qualified. + /// + /// A list of objects. + private static List BuildGridAttributes() + { + var entityTypeId = EntityTypeCache.Get( false )?.Id; + + if ( entityTypeId.HasValue ) + { + return AttributeCache.GetOrderedGridAttributes( entityTypeId, string.Empty, string.Empty ); + } + + return new List(); + } + + + #endregion + + #region Block Actions + + //[BlockAction] + //public BlockActionResult GetNcoaResults() + #endregion + } +} diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts new file mode 100644 index 00000000000..e303f0fbe3b --- /dev/null +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts @@ -0,0 +1,20 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +export const enum NavigationUrlKey { + DetailPage = "DetailPage" +} diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs b/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs new file mode 100644 index 00000000000..bfe4e06e9a9 --- /dev/null +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs @@ -0,0 +1,85 @@ + + + + diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs new file mode 100644 index 00000000000..ce357617cde --- /dev/null +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rock.ViewModels.Blocks.Crm.NcoaResults +{ + internal class NcoaResultsBag : BlockBox + { + /// + /// + /// + public List Rows { get; set; } + } +} diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs new file mode 100644 index 00000000000..dfe2e83025e --- /dev/null +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs @@ -0,0 +1,27 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +namespace Rock.ViewModels.Blocks.Crm.NcoaResults +{ + /// + /// The additional configuration options for the NcoaResults block. + /// + public class NcoaResultsOptionsBag + { + public int ResultCount { get; set; } + } +} diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs new file mode 100644 index 00000000000..5da72206ba8 --- /dev/null +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rock.ViewModels.Blocks.Crm.NcoaResults +{ + internal class NcoaRowBag + { + } +} From e9dbc75ac1b538862433e4710e960f0a950479b5 Mon Sep 17 00:00:00 2001 From: CarterHenning Date: Mon, 2 Mar 2026 15:41:53 -0700 Subject: [PATCH 005/559] - data calls, person preference based filtering, started organizing UI. --- Rock.Blocks/Crm/NcoaResults.cs | 136 ++++++++++++++++-- .../NcoaResults/gridSettingsModal.partial.obs | 136 ++++++++++++++++++ .../src/Crm/NcoaResults/types.partial.ts | 24 +++- .../src/Crm/ncoaResults.obs | 129 +++++++++++------ .../Blocks/Crm/NcoaResults/ncoaDataBag.d.ts | 40 ++++++ .../Crm/NcoaResults/ncoaResultsBag.d.ts | 28 ++++ .../NcoaResults/ncoaResultsOptionsBag.d.ts | 28 ++++ .../Blocks/Crm/NcoaResults/NcoaDataBag.cs | 55 +++++++ .../Blocks/Crm/NcoaResults/NcoaResultsBag.cs | 7 +- .../Crm/NcoaResults/NcoaResultsOptionsBag.cs | 3 + .../Blocks/Crm/NcoaResults/NcoaRowBag.cs | 12 -- 11 files changed, 533 insertions(+), 65 deletions(-) create mode 100644 Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs create mode 100644 Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts create mode 100644 Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts create mode 100644 Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsOptionsBag.d.ts create mode 100644 Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs delete mode 100644 Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs diff --git a/Rock.Blocks/Crm/NcoaResults.cs b/Rock.Blocks/Crm/NcoaResults.cs index 2c27c8ab3c3..566a59e6b82 100644 --- a/Rock.Blocks/Crm/NcoaResults.cs +++ b/Rock.Blocks/Crm/NcoaResults.cs @@ -44,11 +44,11 @@ namespace Rock.Blocks.Crm /// Displays a list of people. /// - [DisplayName( "NcoaResults" )] + [DisplayName( "NCOA Results" )] [Category( "CRM" )] [Description( "Displays a list of ncoa results." )] [IconCssClass( "fa fa-list" )] - // [SupportedSiteTypes( Model.SiteType.Web )] + [SupportedSiteTypes( Model.SiteType.Web )] #region Block Attributes @@ -118,15 +118,32 @@ public PersonPreferenceCollection PersonPreferences } } - protected ListItemBag FilterProcessed => PersonPreferences - .GetValue( PreferenceKey.FilterProcessed ) - .FromJsonOrNull(); + protected Processed? FilterProcessed + { + get + { + var processedValue = PersonPreferences + .GetValue( PreferenceKey.FilterProcessed ); + + if ( processedValue == null ) + { + return null; + } + + if ( int.TryParse( processedValue, out var intValue ) ) + { + return ( Processed ) intValue; + } + + return null; + } + } protected ListItemBag FilterMoveDate => PersonPreferences .GetValue( PreferenceKey.FilterMoveDate ) .FromJsonOrNull(); - private ListItemBag FilterNcoaProcessedDateRange => PersonPreferences + protected ListItemBag FilterNcoaProcessedDate => PersonPreferences .GetValue( PreferenceKey.FilterNcoaProcessedDate ) .FromJsonOrNull(); @@ -206,7 +223,7 @@ private Dictionary GetBoxNavigationUrls() /// A list of objects. private static List BuildGridAttributes() { - var entityTypeId = EntityTypeCache.Get( false )?.Id; + var entityTypeId = EntityTypeCache.Get( false )?.Id; if ( entityTypeId.HasValue ) { @@ -217,12 +234,113 @@ private static List BuildGridAttributes() } + /// + /// Formats the address. + /// + /// The street1. + /// The street2. + /// The city. + /// The state. + /// The postal code. + /// The formated address + private string FormattedAddress( string street1, string street2, string city, string state, string postalCode ) + { + if ( string.IsNullOrWhiteSpace( street1 ) && + string.IsNullOrWhiteSpace( street2 ) && + string.IsNullOrWhiteSpace( city ) ) + { + return string.Empty; + } + + string result = string.Format( "{0} {1} {2}, {3} {4}", + street1, street2, city, state, postalCode ).ReplaceWhileExists( " ", " " ); + + // Remove blank lines + while ( result.Contains( Environment.NewLine + Environment.NewLine ) ) + { + result = result.Replace( Environment.NewLine + Environment.NewLine, Environment.NewLine ); + } + while ( result.Contains( "\x0A\x0A" ) ) + { + result = result.Replace( "\x0A\x0A", "\x0A" ); + } + + if ( string.IsNullOrWhiteSpace( result.Replace( ",", string.Empty ) ) ) + { + return string.Empty; + } + + return result; + } + #endregion #region Block Actions - //[BlockAction] - //public BlockActionResult GetNcoaResults() + [BlockAction] + public BlockActionResult GetNcoaData() + { + int resultCount = GetAttributeValue( AttributeKey.ResultCount ).AsIntegerOrNull() ?? 20; + + var query = new NcoaHistoryService( RockContext ).Queryable(); + + var processed = FilterProcessed; + + if ( processed.HasValue ) + { + if ( processed.Value != Processed.All && processed.Value != Processed.ManualUpdateRequiredOrNotProcessed ) + { + query = query.Where( i => i.Processed == processed ); + } + else if ( processed.Value == Processed.ManualUpdateRequiredOrNotProcessed ) + { + query = query.Where( i => i.Processed == Processed.ManualUpdateRequired || i.Processed == Processed.NotProcessed ); + } + } + + var ncoaHistoryData = query + .Select( i => new + { + i.Id, + i.NcoaType, + i.Processed, + i.MoveDate, + i.MoveDistance, + + i.OriginalStreet1, + i.OriginalStreet2, + i.OriginalCity, + i.OriginalState, + i.OriginalPostalCode, + + i.UpdatedStreet1, + i.UpdatedStreet2, + i.UpdatedCity, + i.UpdatedState, + i.UpdatedPostalCode + } ) + .ToList(); + + var bag = new NcoaResultsBag + { + NcoaList = ncoaHistoryData.Select( i => new NcoaDataBag + { + IdKey = i.Id.AsIdKey(), + Type = i.NcoaType.ToString(), + OriginalAddress = FormattedAddress( + i.OriginalStreet1, i.OriginalStreet2, i.OriginalCity, i.OriginalState, i.OriginalPostalCode ) + .ConvertCrLfToHtmlBr(), + NewAddress = FormattedAddress( + i.UpdatedStreet1, i.UpdatedStreet2, i.UpdatedCity, i.UpdatedState, i.UpdatedPostalCode ) + .ConvertCrLfToHtmlBr(), + MoveDate = i.MoveDate, + MoveDistance = i.MoveDistance, + Status = i.Processed == Processed.Complete ? "Processed" : "Not Processed" + } ).ToList() + }; + + return ActionOk( bag ); + } #endregion } } diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs new file mode 100644 index 00000000000..ec6561ad571 --- /dev/null +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs @@ -0,0 +1,136 @@ + + + + diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts index e303f0fbe3b..481c1c96075 100644 --- a/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/types.partial.ts @@ -15,6 +15,26 @@ // // -export const enum NavigationUrlKey { - DetailPage = "DetailPage" +export const enum PreferenceKey { + FilterProcessed = "filter-processed", + FilterMoveDate = "filter-move-date", + FilterNcoaProcessedDate = "filter-ncoa-processed-date", + FilterMoveType = "filter-move-type", + FilterAddressStatus = "filter-address-status", + FilterInvalidReason = "filter-invalid-reason", + FilterMoveDistance = "filter-move-distance", + FilterLastName = "filter-last-name", + FilterCampus = "filter-campus" } + +export type GridSettingsOptions = { + filterProcessed: string; + filterMoveDate: string; + filterNcoaProcessedDate: string; + filterMoveType: string; + filterAddressStatus: string; + filterInvalidReason: string; + filterMoveDistance: string; + filterLastName: string; + filterCampus: string; +}; diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs b/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs index bfe4e06e9a9..b0ab82fda2f 100644 --- a/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs @@ -1,30 +1,61 @@ diff --git a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts new file mode 100644 index 00000000000..2df3c7df27a --- /dev/null +++ b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts @@ -0,0 +1,40 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the Rock.CodeGeneration project +// Changes to this file will be lost when the code is regenerated. +// +//------------------------------------------------------------------------------ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +export type NcoaDataBag = { + familyMembers?: string | null; + + idKey?: string | null; + + moveDate?: string | null; + + moveDistance?: number | null; + + newAddress?: string | null; + + originalAddress?: string | null; + + status?: string | null; + + type?: string | null; +}; diff --git a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts new file mode 100644 index 00000000000..515dacac92f --- /dev/null +++ b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts @@ -0,0 +1,28 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the Rock.CodeGeneration project +// Changes to this file will be lost when the code is regenerated. +// +//------------------------------------------------------------------------------ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +import { NcoaDataBag } from "@Obsidian/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag"; + +export type NcoaResultsBag = { + ncoaList?: NcoaDataBag[] | null; +}; diff --git a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsOptionsBag.d.ts b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsOptionsBag.d.ts new file mode 100644 index 00000000000..f3bc8f146ff --- /dev/null +++ b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsOptionsBag.d.ts @@ -0,0 +1,28 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the Rock.CodeGeneration project +// Changes to this file will be lost when the code is regenerated. +// +//------------------------------------------------------------------------------ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +/** The additional configuration options for the NcoaResults block. */ +export type NcoaResultsOptionsBag = { + /** Number of results displayed on the page */ + resultCount: number; +}; diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs new file mode 100644 index 00000000000..587ce902665 --- /dev/null +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rock.ViewModels.Blocks.Crm.NcoaResults +{ + /// + /// + /// + public class NcoaDataBag + { + /// + /// + /// + public string IdKey { get; set; } + + /// + /// + /// + public string Type { get; set; } + + /// + /// + /// + public string FamilyMembers { get; set; } + + /// + /// + /// + public string OriginalAddress { get; set; } + + /// + /// + /// + public string NewAddress { get; set; } + + /// + /// + /// + public DateTime? MoveDate { get; set; } + + /// + /// + /// + public decimal? MoveDistance { get; set; } + + /// + /// + /// + public string Status { get; set; } + + } +} diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs index ce357617cde..26aab2ec422 100644 --- a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs @@ -6,11 +6,14 @@ namespace Rock.ViewModels.Blocks.Crm.NcoaResults { - internal class NcoaResultsBag : BlockBox + /// + /// + /// + public class NcoaResultsBag { /// /// /// - public List Rows { get; set; } + public List NcoaList { get; set; } } } diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs index dfe2e83025e..2197ed112e9 100644 --- a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs @@ -22,6 +22,9 @@ namespace Rock.ViewModels.Blocks.Crm.NcoaResults /// public class NcoaResultsOptionsBag { + /// + /// Number of results displayed on the page + /// public int ResultCount { get; set; } } } diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs deleted file mode 100644 index 5da72206ba8..00000000000 --- a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaRowBag.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Rock.ViewModels.Blocks.Crm.NcoaResults -{ - internal class NcoaRowBag - { - } -} From b15a753a91a72e0440bb36d768ab0d2ff5a3b391 Mon Sep 17 00:00:00 2001 From: CarterHenning Date: Tue, 3 Mar 2026 16:20:05 -0700 Subject: [PATCH 006/559] - Filtering and Family Data calls --- Rock.Blocks/Crm/NcoaResults.cs | 279 +++++++++++++----- .../NcoaResults/gridSettingsModal.partial.obs | 104 ++++++- .../src/Crm/NcoaResults/types.partial.ts | 11 +- .../src/Crm/ncoaResults.obs | 17 +- .../Blocks/Crm/NcoaResults/ncoaDataBag.d.ts | 2 + .../Blocks/Crm/NcoaResults/NcoaDataBag.cs | 5 + 6 files changed, 327 insertions(+), 91 deletions(-) diff --git a/Rock.Blocks/Crm/NcoaResults.cs b/Rock.Blocks/Crm/NcoaResults.cs index 566a59e6b82..468b7e92957 100644 --- a/Rock.Blocks/Crm/NcoaResults.cs +++ b/Rock.Blocks/Crm/NcoaResults.cs @@ -24,6 +24,7 @@ using Rock.Attribute; using Rock.Constants; using Rock.Data; +using Rock.Enums.Controls; using Rock.Model; using Rock.Obsidian.UI; using Rock.Security; @@ -118,58 +119,40 @@ public PersonPreferenceCollection PersonPreferences } } - protected Processed? FilterProcessed - { - get - { - var processedValue = PersonPreferences - .GetValue( PreferenceKey.FilterProcessed ); + private Processed? FilterProcessed => PersonPreferences + .GetValue( PreferenceKey.FilterProcessed ) + .ConvertToEnumOrNull(); - if ( processedValue == null ) - { - return null; - } + private MoveType? FilterMoveType => PersonPreferences + .GetValue( PreferenceKey.FilterMoveType ) + .ConvertToEnumOrNull(); - if ( int.TryParse( processedValue, out var intValue ) ) - { - return ( Processed ) intValue; - } + private AddressStatus? FilterAddressStatus => PersonPreferences + .GetValue( PreferenceKey.FilterAddressStatus ) + .ConvertToEnumOrNull(); - return null; - } - } + private AddressInvalidReason? FilterInvalidReason => PersonPreferences + .GetValue( PreferenceKey.FilterInvalidReason ) + .ConvertToEnumOrNull(); - protected ListItemBag FilterMoveDate => PersonPreferences + private SlidingDateRangeBag FilterMoveDate => PersonPreferences .GetValue( PreferenceKey.FilterMoveDate ) - .FromJsonOrNull(); + .ToSlidingDateRangeBagOrNull(); - protected ListItemBag FilterNcoaProcessedDate => PersonPreferences + private SlidingDateRangeBag FilterNcoaProcessedDate => PersonPreferences .GetValue( PreferenceKey.FilterNcoaProcessedDate ) - .FromJsonOrNull(); + .ToSlidingDateRangeBagOrNull(); - protected ListItemBag FilterMoveType => PersonPreferences - .GetValue( PreferenceKey.FilterMoveType ) - .FromJsonOrNull(); - - protected ListItemBag FilterAddressStatus => PersonPreferences - .GetValue( PreferenceKey.FilterAddressStatus ) - .FromJsonOrNull(); - - protected ListItemBag FilterInvalidReason => PersonPreferences - .GetValue( PreferenceKey.FilterInvalidReason ) - .FromJsonOrNull(); - - protected ListItemBag FilterMoveDistance => PersonPreferences + protected Decimal? FilterMoveDistance => PersonPreferences .GetValue( PreferenceKey.FilterMoveDistance ) - .FromJsonOrNull(); + .AsDecimalOrNull(); - protected ListItemBag FilterLastName => PersonPreferences - .GetValue( PreferenceKey.FilterLastName ) - .FromJsonOrNull(); + protected string FilterLastName => PersonPreferences + .GetValue( PreferenceKey.FilterLastName ); - protected ListItemBag FilterCampus => PersonPreferences + protected Guid? FilterCampus => PersonPreferences .GetValue( PreferenceKey.FilterCampus ) - .FromJsonOrNull(); + .FromJsonOrNull()?.Value.AsGuidOrNull(); @@ -273,6 +256,30 @@ private string FormattedAddress( string street1, string street2, string city, st return result; } + public Dictionary GetPersonNamesForFamilies( List familyIds ) + { + var groupMemberService = new GroupMemberService( RockContext ); + var familyGroupTypeGuid = Rock.SystemGuid.GroupType.GROUPTYPE_FAMILY.AsGuid(); + + var families = groupMemberService.Queryable() + .Where( gm => + familyIds.Contains( gm.GroupId ) && + gm.Group.GroupType.Guid == familyGroupTypeGuid ) + .Select( gm => new + { + gm.GroupId, + FullName = gm.Person.NickName + " " + gm.Person.LastName + } ) + .ToList() + .GroupBy( x => x.GroupId ) + .ToDictionary( + g => g.Key, + g => string.Join( ", ", g.Select( x => x.FullName ) ) + ); + + return families; + } + #endregion #region Block Actions @@ -282,60 +289,180 @@ public BlockActionResult GetNcoaData() { int resultCount = GetAttributeValue( AttributeKey.ResultCount ).AsIntegerOrNull() ?? 20; - var query = new NcoaHistoryService( RockContext ).Queryable(); + var ncoaQuery = new NcoaHistoryService( RockContext ).Queryable(); var processed = FilterProcessed; + var moveType = FilterMoveType; + var moveDate = FilterMoveDate; + var ncoaProcessedDate = FilterNcoaProcessedDate; + var addressStatus = FilterAddressStatus; + var addressInvalidReason = FilterInvalidReason; + var moveDistance = FilterMoveDistance; + var lastName = FilterLastName; + int? campusId = null; + + if ( FilterCampus.HasValue ) + { + campusId = CampusCache.GetId( FilterCampus.Value ); + } + // Processed Status Filtering if ( processed.HasValue ) { if ( processed.Value != Processed.All && processed.Value != Processed.ManualUpdateRequiredOrNotProcessed ) { - query = query.Where( i => i.Processed == processed ); + ncoaQuery = ncoaQuery.Where( i => i.Processed == processed ); } else if ( processed.Value == Processed.ManualUpdateRequiredOrNotProcessed ) { - query = query.Where( i => i.Processed == Processed.ManualUpdateRequired || i.Processed == Processed.NotProcessed ); + ncoaQuery = ncoaQuery.Where( i => i.Processed == Processed.ManualUpdateRequired || i.Processed == Processed.NotProcessed ); + } + } + + // Move Type Filtering + if ( moveType.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.MoveType == moveType ); + } + + // Move Date Filtering + if ( moveDate != null ) + { + // Default to the last 180 days if a null/invalid range was selected. + var defaultSlidingDateRange = new SlidingDateRangeBag + { + RangeType = SlidingDateRangeType.Last, + TimeUnit = TimeUnitType.Day, + TimeValue = 180 + }; + + var dateRange = moveDate.Validate( defaultSlidingDateRange ).ActualDateRange; + + if ( dateRange.Start.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.MoveDate >= dateRange.Start ); + } + + if ( dateRange.End.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.MoveDate <= dateRange.End ); } } - var ncoaHistoryData = query - .Select( i => new + // NCOA Processed Date Filtering + if ( ncoaProcessedDate != null ) { - i.Id, - i.NcoaType, - i.Processed, - i.MoveDate, - i.MoveDistance, - - i.OriginalStreet1, - i.OriginalStreet2, - i.OriginalCity, - i.OriginalState, - i.OriginalPostalCode, - - i.UpdatedStreet1, - i.UpdatedStreet2, - i.UpdatedCity, - i.UpdatedState, - i.UpdatedPostalCode - } ) - .ToList(); + // Default to the last 180 days if a null/invalid range was selected. + var defaultSlidingDateRange = new SlidingDateRangeBag + { + RangeType = SlidingDateRangeType.Last, + TimeUnit = TimeUnitType.Day, + TimeValue = 180 + }; + + var dateRange = ncoaProcessedDate.Validate( defaultSlidingDateRange ).ActualDateRange; + + if ( dateRange.Start.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.NcoaRunDateTime >= dateRange.Start ); + } + + if ( dateRange.End.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.NcoaRunDateTime <= dateRange.End ); + } + } + + // Address Status Filtering + if ( addressStatus.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.AddressStatus == addressStatus ); + } + + // Address Invalid Reason Filtering + if ( addressInvalidReason.HasValue ) + { + ncoaQuery = ncoaQuery.Where( i => i.AddressInvalidReason == addressInvalidReason ); + } + + // Move Distance Filtering + if ( moveDistance != null ) + { + ncoaQuery = ncoaQuery.Where( i => i.MoveDistance <= moveDistance ); + } + + // Last Name Filtering + if ( lastName.IsNotNullOrWhiteSpace() ) + { + var personAliasQuery = new PersonAliasService( RockContext ).Queryable().Where( p => p.Person.LastName.Contains( lastName ) ).Select( p => p.Id); + + ncoaQuery = ncoaQuery.Where( i => personAliasQuery.Contains( i.PersonAliasId ) ); + } + + //Campus Filtering + if ( campusId.HasValue ) + { + var familyGroupType = GroupTypeCache.Get( Rock.SystemGuid.GroupType.GROUPTYPE_FAMILY.AsGuid() ); + var personAliasQuery = new PersonAliasService( RockContext ).Queryable().AsNoTracking(); + var campusQuery = new GroupMemberService( RockContext ) + .Queryable().AsNoTracking() + .Where( m => + m.Group.GroupTypeId == familyGroupType.Id && + m.Group.CampusId.HasValue && + m.Group.CampusId.Value == campusId ) + .Select( m => m.PersonId ) + .Join( personAliasQuery, m => m, p => p.PersonId, ( m, p ) => p.Id ); + + ncoaQuery = ncoaQuery.Where( i => campusQuery.Contains( i.PersonAliasId ) ); + } + + var ncoaHistoryData = ncoaQuery.ToList(); + + // Records that are not individual move types and will represent family moves. + var familyIds = ncoaHistoryData + .Where( h => h.MoveType != MoveType.Individual ) + .GroupBy( h => new { h.FamilyId, h.MoveType, h.MoveDate } ) + .Select( g => g.Max( x => x.FamilyId ) ).ToList(); + + var familyNamesKey = GetPersonNamesForFamilies( familyIds ); + + var ncoaPersonAliasIds = ncoaHistoryData.Select( d => d.PersonAliasId ).ToList(); + + var personData = new PersonAliasService( RockContext ).Queryable().AsNoTracking() + .Where( p => ncoaPersonAliasIds.Contains( p.Id ) ) + .Select( p => new + { + p.Id, + p.Person.FirstName, + p.Person.LastName, + } ).ToList(); var bag = new NcoaResultsBag { - NcoaList = ncoaHistoryData.Select( i => new NcoaDataBag + NcoaList = ncoaHistoryData.Select( i => { - IdKey = i.Id.AsIdKey(), - Type = i.NcoaType.ToString(), - OriginalAddress = FormattedAddress( - i.OriginalStreet1, i.OriginalStreet2, i.OriginalCity, i.OriginalState, i.OriginalPostalCode ) - .ConvertCrLfToHtmlBr(), - NewAddress = FormattedAddress( - i.UpdatedStreet1, i.UpdatedStreet2, i.UpdatedCity, i.UpdatedState, i.UpdatedPostalCode ) - .ConvertCrLfToHtmlBr(), - MoveDate = i.MoveDate, - MoveDistance = i.MoveDistance, - Status = i.Processed == Processed.Complete ? "Processed" : "Not Processed" + var individual = personData.Where( p => p.Id == i.PersonAliasId ).FirstOrDefault(); + + return new NcoaDataBag + { + IdKey = i.Id.AsIdKey(), + Type = i.NcoaType.ToString(), + + Individual = individual.FirstName + ' ' + individual.LastName, + FamilyMembers = familyNamesKey.ContainsKey(i.FamilyId) ? familyNamesKey[i.FamilyId] : string.Empty, + + OriginalAddress = FormattedAddress( + i.OriginalStreet1, i.OriginalStreet2, i.OriginalCity, i.OriginalState, i.OriginalPostalCode ) + .ConvertCrLfToHtmlBr(), + + NewAddress = FormattedAddress( + i.UpdatedStreet1, i.UpdatedStreet2, i.UpdatedCity, i.UpdatedState, i.UpdatedPostalCode ) + .ConvertCrLfToHtmlBr(), + + MoveDate = i.MoveDate, + MoveDistance = i.MoveDistance, + Status = i.Processed == Processed.Complete ? "Processed" : "Not Processed" + }; } ).ToList() }; diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs index ec6561ad571..233463c500d 100644 --- a/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/NcoaResults/gridSettingsModal.partial.obs @@ -12,18 +12,68 @@
-
+
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+ +
+ + diff --git a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts index 3eab1bffc2f..14098766fa1 100644 --- a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts +++ b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaDataBag.d.ts @@ -22,11 +22,15 @@ // export type NcoaDataBag = { + addressStatus?: string | null; + familyMembers?: string | null; idKey?: string | null; - individual?: string | null; + individualIdKey?: string | null; + + individualName?: string | null; moveDate?: string | null; @@ -36,7 +40,7 @@ export type NcoaDataBag = { originalAddress?: string | null; - status?: string | null; + processStatus?: string | null; type?: string | null; }; diff --git a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts index 515dacac92f..edd1c2e7ccb 100644 --- a/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts +++ b/Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaResultsBag.d.ts @@ -25,4 +25,6 @@ import { NcoaDataBag } from "@Obsidian/ViewModels/Blocks/Crm/NcoaResults/ncoaDat export type NcoaResultsBag = { ncoaList?: NcoaDataBag[] | null; + + totalResults?: number | null; }; diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs index 186b1e5bc46..c6ea91aed17 100644 --- a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaDataBag.cs @@ -24,7 +24,12 @@ public class NcoaDataBag /// /// /// - public string Individual { get; set; } + public string IndividualName { get; set; } + + /// + /// + /// + public string IndividualIdKey { get; set; } /// /// @@ -44,7 +49,7 @@ public class NcoaDataBag /// /// /// - public DateTime? MoveDate { get; set; } + public string MoveDate { get; set; } /// /// @@ -54,7 +59,12 @@ public class NcoaDataBag /// /// /// - public string Status { get; set; } + public string ProcessStatus { get; set; } + + /// + /// + /// + public string AddressStatus { get; set; } } } diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs index 26aab2ec422..7fc4f2fcd23 100644 --- a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsBag.cs @@ -15,5 +15,10 @@ public class NcoaResultsBag /// /// public List NcoaList { get; set; } + + /// + /// + /// + public int? TotalResults { get; set; } } } diff --git a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs index 2197ed112e9..253f50d4bb4 100644 --- a/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs +++ b/Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaResultsOptionsBag.cs @@ -26,5 +26,6 @@ public class NcoaResultsOptionsBag /// Number of results displayed on the page /// public int ResultCount { get; set; } + } } From 68242621cfe1bbe697fd8e454da9ed44385d5f50 Mon Sep 17 00:00:00 2001 From: CarterHenning Date: Mon, 16 Mar 2026 10:48:56 -0700 Subject: [PATCH 008/559] - family grouping, remove duplicate moves from results --- Rock.Blocks/Crm/NcoaResults.cs | 92 ++++++--- .../src/Crm/ncoaResults.obs | 193 ++++++++++-------- .../Blocks/Crm/NcoaResults/ncoaDataBag.d.ts | 2 + .../Crm/NcoaResults/ncoaFamilyGroupBag.d.ts | 31 +++ .../Crm/NcoaResults/ncoaResultsBag.d.ts | 4 +- .../Blocks/Crm/NcoaResults/NcoaDataBag.cs | 5 + .../Crm/NcoaResults/NcoaFamilyGroupBag.cs | 23 +++ .../Blocks/Crm/NcoaResults/NcoaResultsBag.cs | 2 +- 8 files changed, 232 insertions(+), 120 deletions(-) create mode 100644 Rock.JavaScript.Obsidian/Framework/ViewModels/Blocks/Crm/NcoaResults/ncoaFamilyGroupBag.d.ts create mode 100644 Rock.ViewModels/Blocks/Crm/NcoaResults/NcoaFamilyGroupBag.cs diff --git a/Rock.Blocks/Crm/NcoaResults.cs b/Rock.Blocks/Crm/NcoaResults.cs index 992a811642c..59dc50dd6a0 100644 --- a/Rock.Blocks/Crm/NcoaResults.cs +++ b/Rock.Blocks/Crm/NcoaResults.cs @@ -419,11 +419,29 @@ public BlockActionResult GetNcoaData(int pageNumber) ncoaQuery = ncoaQuery.Where( i => campusQuery.Contains( i.PersonAliasId ) ); } - var totalResults = ncoaQuery.Count(); - - ncoaQuery = ncoaQuery.OrderBy( i => i.Id ).Skip( ( pageNumber - 1 ) * resultCount ).Take( resultCount ); - - var ncoaHistoryData = ncoaQuery.ToList(); + // Group duplicate family moves. + var groupedFamilyMoves = ncoaQuery + .Where( i => i.MoveType != MoveType.Individual ) + .GroupBy( i => new { i.FamilyId, i.MoveType, i.MoveDate } ) + .Select( g => g.OrderByDescending( x => x.Id ).FirstOrDefault() ) + .ToList(); + + var individualMoves = ncoaQuery + .Where( i => i.MoveType == MoveType.Individual ) + .ToList(); + + var combinedData = groupedFamilyMoves + .Concat( individualMoves ) + .OrderBy( i => i.FamilyId ) + .ToList(); + + var totalResults = combinedData.Count(); + + var ncoaHistoryData = combinedData + .OrderBy(i => i.Id) + .Skip( ( pageNumber - 1 ) * resultCount ) + .Take( resultCount ) + .ToList(); // Records that are not individual move types and will represent family moves. var familyIds = ncoaHistoryData @@ -445,36 +463,48 @@ public BlockActionResult GetNcoaData(int pageNumber) p.Person.LastName, } ).ToList(); - var bag = new NcoaResultsBag + var ncoaItems = ncoaHistoryData.Select( i => { - TotalResults = totalResults, - NcoaList = ncoaHistoryData.Select( i => + var individual = personData.Where( p => p.personAliasId == i.PersonAliasId ).FirstOrDefault(); + + + return new NcoaDataBag + { + IdKey = i.Id.AsIdKey(), + FamilyId = i.FamilyId, + Type = i.NcoaType.ToString(), + IndividualIdKey = individual.personId.AsIdKey(), + IndividualName = individual.NickName + ' ' + individual.LastName, + FamilyMembers = familyNamesKey.ContainsKey( i.FamilyId ) ? familyNamesKey[i.FamilyId] : string.Empty, + + OriginalAddress = FormattedAddress( + i.OriginalStreet1, i.OriginalStreet2, i.OriginalCity, i.OriginalState, i.OriginalPostalCode ) + .ConvertCrLfToHtmlBr(), + + NewAddress = FormattedAddress( + i.UpdatedStreet1, i.UpdatedStreet2, i.UpdatedCity, i.UpdatedState, i.UpdatedPostalCode ) + .ConvertCrLfToHtmlBr(), + + MoveDate = i.MoveDate.ToShortDateString(), + MoveDistance = i.MoveDistance, + ProcessStatus = i.Processed == Processed.Complete ? "Processed" : "Not Processed", + AddressStatus = i.AddressStatus.ToString() + }; + } ).ToList(); + + var groupedNcoaItems = ncoaItems + .GroupBy( n => n.FamilyId ) + .Select( g => new NcoaFamilyGroupBag { - var individual = personData.Where( p => p.personAliasId == i.PersonAliasId ).FirstOrDefault(); + FamilyName = familyNamesKey.ContainsKey( g.Key ) ? familyNamesKey[g.Key].Split( ' ' ).Last() : null, + NcoaItems = g.ToList() + } ).ToList(); - return new NcoaDataBag - { - IdKey = i.Id.AsIdKey(), - Type = i.NcoaType.ToString(), - IndividualIdKey = individual.personId.AsIdKey(), - IndividualName = individual.NickName + ' ' + individual.LastName, - FamilyMembers = familyNamesKey.ContainsKey( i.FamilyId ) ? familyNamesKey[i.FamilyId] : string.Empty, - - OriginalAddress = FormattedAddress( - i.OriginalStreet1, i.OriginalStreet2, i.OriginalCity, i.OriginalState, i.OriginalPostalCode ) - .ConvertCrLfToHtmlBr(), - - NewAddress = FormattedAddress( - i.UpdatedStreet1, i.UpdatedStreet2, i.UpdatedCity, i.UpdatedState, i.UpdatedPostalCode ) - .ConvertCrLfToHtmlBr(), - - MoveDate = i.MoveDate.ToShortDateString(), - MoveDistance = i.MoveDistance, - ProcessStatus = i.Processed == Processed.Complete ? "Processed" : "Not Processed", - AddressStatus = i.AddressStatus.ToString() - }; - } ).ToList() + var bag = new NcoaResultsBag + { + TotalResults = totalResults, + NcoaList = groupedNcoaItems }; return ActionOk( bag ); diff --git a/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs b/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs index 02ceb18688d..695055f1e73 100644 --- a/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs +++ b/Rock.JavaScript.Obsidian.Blocks/src/Crm/ncoaResults.obs @@ -4,102 +4,92 @@ type="block"> -

Results: {{ totalCount }}

-
-

{{ row.individualName?.split(' ')?.[1] }} Family

- -
-
-
-

- - {{ row.familyMembers?.trim() ? "Family" : "Individual" }} - +
+

{{ family.familyName || family.ncoaItems?.[0]?.individualName?.split(' ')?.[1] }} Family

+ +
+
+ +
+
+

+ + {{ row.familyMembers?.trim() ? "Family" : "Individual" }} + +

+
+ + {{ row.moveDate }} +
+
+

+ 48 Month Move

-
-
Move Date
-
{{ row.moveDate }}
+

+ Invalid Address +

+ +
+
{{ row.familyMembers?.trim() ? "Family Members" : "Individual" }}
+
+ {{ member }} + + + +
-

- - 48 Month Move - -

- -

- - Invalid Address - -

-
- -
-
-
{{ row.familyMembers?.trim() ? "Family Members" : "Individual" }}
- -
- - {{ member }} - - - - - -
-
-
- -
-
-
Original Address
-
{{ row.originalAddress }}
-
- -
- -
-
-
New Address
-
{{ row.newAddress }}
-
-
- -
-
-
Move Distance
-
{{ row.moveDistance }}
-
-
- -
-
- + +
+
+
Original Address
+
{{ row.originalAddress }}
+
+
+
New Address
+
{{ row.newAddress }}
+
- - Mark Address As Previous - - - Mark Processed - -
-

{{ warnings[row.idKey ?? ''] }}

+ + +
+
+
Move Distance
+
{{ row.moveDistance }}
+
+
+ + + Mark Address As Previous + + + Mark Processed + +
+

{{ warnings[row.idKey ?? ''] }}

+
+
-
+
+
-