Skip to content

Commit 43aeac4

Browse files
committed
Merge branch 'hotfix-20.1' into develop
1 parent 14369bd commit 43aeac4

23 files changed

Lines changed: 1408 additions & 52 deletions

File tree

‎Rock.Blocks/Event/RegistrationInstanceRegistrantList.cs‎

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -762,18 +762,27 @@ private List<RegistrantPlacementBag> GetRegistrantPlacements( RegistrationRegist
762762
return GetRegistrationTemplatePlacements()
763763
.Select( placement =>
764764
{
765-
var placedGroupNames = personId.HasValue && _placementGroupsByPlacementId.TryGetValue( placement.Id, out var placementGroups )
766-
? placementGroups
767-
.Where( g => g.PersonIds.Contains( personId.Value ) )
768-
.Select( g => g.GroupName )
769-
.ToList()
770-
: new List<string>();
765+
var placementGroups = personId.HasValue && _placementGroupsByPlacementId.TryGetValue( placement.Id, out var groupsForPlacement )
766+
? groupsForPlacement
767+
: new List<PlacementGroupInfo>();
768+
769+
var activeGroupNames = placementGroups
770+
.Where( g => g.ActivePersonIds.Contains( personId.Value ) )
771+
.Select( g => g.GroupName )
772+
.ToList();
773+
774+
// An inactive membership still counts as placed, but only the active ones drive the count and green state.
775+
var inactiveGroupNames = placementGroups
776+
.Where( g => g.InactivePersonIds.Contains( personId.Value ) )
777+
.Select( g => g.GroupName )
778+
.ToList();
771779

772780
return new RegistrantPlacementBag
773781
{
774782
PlacementId = placement.Id,
775-
GroupCount = placedGroupNames.Count,
776-
GroupNames = placedGroupNames
783+
GroupCount = activeGroupNames.Count,
784+
GroupNames = activeGroupNames,
785+
InactiveGroupNames = inactiveGroupNames
777786
};
778787
} )
779788
.ToList();
@@ -1181,7 +1190,12 @@ private void LoadPlacementGroupInfo( RockContext rockContext )
11811190
{
11821191
g.Id,
11831192
g.Name,
1184-
PersonIds = g.Members.Select( m => m.PersonId )
1193+
ActivePersonIds = g.Members
1194+
.Where( m => !m.IsArchived && m.GroupMemberStatus != GroupMemberStatus.Inactive )
1195+
.Select( m => m.PersonId ),
1196+
InactivePersonIds = g.Members
1197+
.Where( m => !m.IsArchived && m.GroupMemberStatus == GroupMemberStatus.Inactive )
1198+
.Select( m => m.PersonId )
11851199
} )
11861200
.ToList();
11871201

@@ -1192,7 +1206,12 @@ private void LoadPlacementGroupInfo( RockContext rockContext )
11921206
{
11931207
g.Id,
11941208
g.Name,
1195-
PersonIds = g.Members.Select( m => m.PersonId )
1209+
ActivePersonIds = g.Members
1210+
.Where( m => !m.IsArchived && m.GroupMemberStatus != GroupMemberStatus.Inactive )
1211+
.Select( m => m.PersonId ),
1212+
InactivePersonIds = g.Members
1213+
.Where( m => !m.IsArchived && m.GroupMemberStatus == GroupMemberStatus.Inactive )
1214+
.Select( m => m.PersonId )
11961215
} )
11971216
.ToList();
11981217

@@ -1220,7 +1239,8 @@ lists are deduped by group identifier here.
12201239
placementGroups.Add( new PlacementGroupInfo
12211240
{
12221241
GroupName = groupInfo.Name,
1223-
PersonIds = new HashSet<int>( groupInfo.PersonIds )
1242+
ActivePersonIds = new HashSet<int>( groupInfo.ActivePersonIds ),
1243+
InactivePersonIds = new HashSet<int>( groupInfo.InactivePersonIds )
12241244
} );
12251245
}
12261246

@@ -1781,9 +1801,17 @@ private class PlacementGroupInfo
17811801
public string GroupName { get; set; }
17821802

17831803
/// <summary>
1784-
/// Gets or sets the person identifiers of the group's members.
1804+
/// Gets or sets the person identifiers of the group's non-archived
1805+
/// members whose status is Active or Pending.
1806+
/// </summary>
1807+
public HashSet<int> ActivePersonIds { get; set; }
1808+
1809+
/// <summary>
1810+
/// Gets or sets the person identifiers of the group's non-archived
1811+
/// members whose status is Inactive. These people are still placed
1812+
/// but are shown in a muted state.
17851813
/// </summary>
1786-
public HashSet<int> PersonIds { get; set; }
1814+
public HashSet<int> InactivePersonIds { get; set; }
17871815
}
17881816

17891817
/// <summary>

‎Rock.Blocks/Group/GroupPlacement.cs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,9 +650,12 @@ private bool IsGroupRoleCapacityAvailable( int? roleId, int groupId, int pending
650650

651651
if ( role?.MaxCount is int maxCount )
652652
{
653+
// Inactive members are shown as placed but do not take up a spot in the role.
653654
var currentCount = new GroupMemberService( RockContext )
654655
.Queryable()
655-
.Count( gm => gm.GroupId == group.Id && gm.GroupRoleId == roleId.Value );
656+
.Count( gm => gm.GroupId == group.Id
657+
&& gm.GroupRoleId == roleId.Value
658+
&& gm.GroupMemberStatus != GroupMemberStatus.Inactive );
656659

657660
if ( currentCount + pendingGroupMemberCount > maxCount )
658661
{
@@ -1348,6 +1351,7 @@ public BlockActionResult GetPlacementPeople( GroupPlacementKeysBag groupPlacemen
13481351
GroupRoleIdKey = row.GroupRoleId.HasValue
13491352
? IdHasher.Instance.GetHash( row.GroupRoleId.Value )
13501353
: null,
1354+
IsInactive = row.GroupMemberStatus == ( int ) GroupMemberStatus.Inactive,
13511355
DateTimeAdded = row.DateTimeAdded?.ToRockDateTimeOffset(),
13521356
Attributes = destinationGroupMember.GetPublicAttributesForView( GetCurrentPerson(), true, attributeFilter: a => displayedDestinationGroupMemberAttributeIds.Contains( a.Id ) ),
13531357
AttributeValues = destinationGroupMember.GetPublicAttributeValuesForView( GetCurrentPerson(), true, attributeFilter: a => displayedDestinationGroupMemberAttributeIds.Contains( a.Id ) ),
@@ -2318,6 +2322,12 @@ private class PlacementPeopleResult
23182322
/// </summary>
23192323
public int? GroupRoleId { get; set; }
23202324

2325+
/// <summary>
2326+
/// Gets or sets the group member status as the integer value of <see cref="Rock.Model.GroupMemberStatus"/>.
2327+
/// This is null for people who are not in a destination group.
2328+
/// </summary>
2329+
public int? GroupMemberStatus { get; set; }
2330+
23212331
/// <summary>
23222332
/// Gets or sets the Date Time the Group Member was added.
23232333
/// </summary>

‎Rock.JavaScript.Obsidian.Blocks/src/Event/registrationInstanceRegistrantList.obs‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@
301301
:deep(.placement-list .btn-placement-status) {
302302
margin-right: 4px;
303303
}
304+
305+
:deep(.placement-list .registrant-is-placed-inactive) {
306+
opacity: 0.5;
307+
}
304308
</style>
305309

306310
<script setup lang="ts">
@@ -546,24 +550,47 @@
546550
}
547551

548552
/**
549-
* Gets the button CSS classes for a placement, varying by whether the
550-
* registrant has been placed in at least one group.
553+
* Gets the button CSS classes for a placement. Green when the registrant
554+
* is an active member of at least one group, muted green when their only
555+
* memberships are Inactive, and default when they are not placed.
551556
*/
552557
function getPlacementButtonCssClass(row: Record<string, unknown>, placement: RegistrantPlacementConfigBag): string {
553-
const groupCount = getRowPlacement(row, placement.id)?.groupCount ?? 0;
558+
const rowPlacement = getRowPlacement(row, placement.id);
559+
560+
if ((rowPlacement?.groupCount ?? 0) > 0) {
561+
return "btn btn-success btn-xs btn-placement-status registrant-is-placed";
562+
}
554563

555-
return groupCount > 0
556-
? "btn btn-success btn-xs btn-placement-status registrant-is-placed"
557-
: "btn btn-default btn-xs btn-placement-status registrant-not-placed";
564+
if ((rowPlacement?.inactiveGroupNames ?? []).length > 0) {
565+
return "btn btn-success btn-xs btn-placement-status registrant-is-placed-inactive";
566+
}
567+
568+
return "btn btn-default btn-xs btn-placement-status registrant-not-placed";
558569
}
559570

560571
/**
561572
* Gets the tooltip for a placement button: the names of the groups the
562-
* registrant is placed in, e.g. "Bus 1, Bus 2 and Bus 3".
573+
* registrant is placed in, e.g. "Bus 1, Bus 2 and Bus 3". When the
574+
* registrant has only Inactive memberships, each name is suffixed with
575+
* "(Inactive)" so the muted state is explained.
563576
*/
564577
function getPlacementTooltip(row: Record<string, unknown>, placement: RegistrantPlacementConfigBag): string {
565-
const groupNames = getRowPlacement(row, placement.id)?.groupNames ?? [];
578+
const rowPlacement = getRowPlacement(row, placement.id);
579+
const groupNames = rowPlacement?.groupNames ?? [];
566580

581+
if (groupNames.length > 0) {
582+
return formatGroupNames(groupNames);
583+
}
584+
585+
const inactiveGroupNames = (rowPlacement?.inactiveGroupNames ?? []).map(name => `${name} (Inactive)`);
586+
587+
return formatGroupNames(inactiveGroupNames);
588+
}
589+
590+
/**
591+
* Joins group names for display, e.g. "Bus 1, Bus 2 and Bus 3".
592+
*/
593+
function formatGroupNames(groupNames: string[]): string {
567594
if (groupNames.length <= 1) {
568595
return groupNames[0] ?? "";
569596
}

‎Rock.JavaScript.Obsidian.Blocks/src/Group/GroupPlacement/destinationGroup.partial.obs‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
<Badge :class="[capacityStatusClass]">
1111
{{ capacityText }}
1212
</Badge>
13-
<template v-for="role in groupTypeRoles"
14-
:key="role.idKey">
13+
<template v-for="(role, index) in groupTypeRoles"
14+
:key="role.idKey ?? index">
1515
<span v-if="role.idKey"
1616
:class="['role-count', getRoleCountOverUnderCapacityClass(role.idKey)]">
1717
{{ pluralize(role.name ?? "") }}: {{ getRoleCount(role.idKey).toLocaleString() }}
@@ -49,8 +49,8 @@
4949
@dismiss="clearGroupError(destinationGroup.groupIdKey)">
5050
{{ getGroupError(destinationGroup.groupIdKey) }}
5151
</NotificationBox>
52-
<template v-for="role in groupTypeRoles"
53-
:key=role.idKey>
52+
<template v-for="(role, roleIndex) in groupTypeRoles"
53+
:key="role.idKey ?? roleIndex">
5454
<div v-if="role.idKey"
5555
class="panel-inline">
5656
<div class="panel-heading panel-heading-bottom">
@@ -83,12 +83,12 @@
8383
<div v-if="getGroupMembersFilteredByRoleAndGender(role.idKey, section.value).length > 0">
8484
<HighlightLabel :customClass="`gender-highlight-label ${section.class}`"
8585
labelType="custom">
86-
{{ section.label }} ({{ getGroupMembersFilteredByRoleAndGender(role.idKey, section.value).length.toLocaleString() }})
86+
{{ section.label }} ({{ getActiveGroupMemberCountByRoleAndGender(role.idKey, section.value).toLocaleString() }})
8787
</HighlightLabel>
8888
<div class="group-role-wrapper"
8989
v-drag-source="groupMemberDragOptions">
9090
<template v-for="(groupMember, index) in getGroupMembersFilteredByRoleAndGender(role.idKey, section.value)"
91-
:key="groupMember.groupMemberIdKey">
91+
:key="groupMember.groupMemberIdKey ?? index">
9292
<PersonCard v-if="groupMember.person"
9393
:isCardExpanded="arePersonCardsExpanded"
9494
:person="groupMember.person"
@@ -158,7 +158,6 @@
158158
</template>
159159

160160
<style scoped>
161-
162161
:deep(.panel.panel-block) {
163162
height: 100%;
164163
margin-bottom: var(--spacing-large);
@@ -191,7 +190,6 @@
191190
font-size: var(--font-size-small);
192191
font-weight: var(--font-weight-semibold);
193192
}
194-
195193
</style>
196194

197195
<script setup lang="ts">
@@ -352,12 +350,17 @@
352350
return fieldType?.getTextValue?.(rawValue, configValues) ?? rawValue;
353351
}
354352

353+
// Inactive members are displayed in the group but do not count toward capacity, role, or gender totals.
354+
const activeGroupMembers = computed((): GroupMemberBag[] => {
355+
return destinationGroup.value.groupMembers?.filter(gm => !gm.isInactive) ?? [];
356+
});
357+
355358
const capacityStatusClass = computed(() => {
356359
if (!destinationGroup.value.groupCapacity) {
357360
return "status-under";
358361
}
359362

360-
const groupMemberCount = destinationGroup.value.groupMembers?.length ?? 0;
363+
const groupMemberCount = activeGroupMembers.value.length;
361364
const groupCapacityPercent = (groupMemberCount / destinationGroup.value.groupCapacity) * 100;
362365

363366
if (groupCapacityPercent > 100) {
@@ -381,11 +384,7 @@
381384
unknown: 0
382385
};
383386

384-
if (!destinationGroup.value.groupMembers) {
385-
return counts;
386-
}
387-
388-
for (const groupMember of destinationGroup.value.groupMembers) {
387+
for (const groupMember of activeGroupMembers.value) {
389388
switch (groupMember.person?.gender) {
390389
case Gender.Male:
391390
counts.males++;
@@ -403,7 +402,7 @@
403402
});
404403

405404
const capacityText = computed(() => {
406-
const groupMemberCount = destinationGroup.value.groupMembers?.length ?? 0;
405+
const groupMemberCount = activeGroupMembers.value.length;
407406

408407
if (!destinationGroup.value.groupCapacity) {
409408
return `${groupMemberCount.toLocaleString()}`;
@@ -441,7 +440,7 @@
441440
});
442441

443442
function getRoleCount(roleIdKey: string): number {
444-
return destinationGroup.value.groupMembers?.filter(gm => gm.groupRoleIdKey === roleIdKey).length ?? 0;
443+
return activeGroupMembers.value.filter(gm => gm.groupRoleIdKey === roleIdKey).length;
445444
}
446445

447446
function getRoleCountOverUnderCapacityClass(roleIdKey: string): string {
@@ -473,6 +472,10 @@
473472
return destinationGroup.value.groupMembers?.filter(gm => gm.groupRoleIdKey === roleIdKey && gm.person?.gender === gender) ?? [];
474473
}
475474

475+
function getActiveGroupMemberCountByRoleAndGender(roleIdKey: string, gender: Gender): number {
476+
return activeGroupMembers.value.filter(gm => gm.groupRoleIdKey === roleIdKey && gm.person?.gender === gender).length;
477+
}
478+
476479
function getRoleCountTooltipText(roleIdKey: string): string {
477480
const role = groupTypeRoles.value.find(r => r.idKey === roleIdKey);
478481
if (!role) {

‎Rock.JavaScript.Obsidian.Blocks/src/Group/GroupPlacement/personCard.partial.obs‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
<template>
22
<div v-if="person.personIdKey"
3-
:class="['person-card', { 'selected': isSelected }]"
3+
:class="['person-card', { 'selected': isSelected, 'person-card-inactive': groupMember?.isInactive }]"
44
:style="{ borderLeftColor: getPersonBorderColor(person.gender) }"
55
@click="onPersonCardClick"
66
@mouseenter="togglePersonCardHover(true)"
77
@mouseleave="togglePersonCardHover(false)">
88
<div class="d-flex justify-content-between align-items-center person-row">
9-
<div class="d-flex align-items-center w-100">
9+
<div :class="['d-flex align-items-center w-100', { 'is-inactive': groupMember?.isInactive }]">
1010
<div class="mr-3 flex-shrink-0"
1111
:style="{ 'width': '32px', 'height': '32px', 'border-radius': '18px', 'background-image': `url('${person.photoUrl}')`, 'background-size': 'cover' }">
1212
</div>
1313
<span :ref="(el) => addTooltipIfOverflow(el as HTMLElement, displayedPersonName)"
1414
class="overflow-ellipsis">
1515
{{ displayedPersonName }}
1616
</span>
17+
<span v-if="groupMember?.isInactive"
18+
class="label label-default ml-2 flex-shrink-0"
19+
:ref="(el) => addTooltip(el as Element)"
20+
data-toggle="tooltip"
21+
data-original-title="This group member's status is Inactive. They are still placed but are not counted toward capacity.">
22+
Inactive
23+
</span>
1724
</div>
1825
<div v-if="isGroupMemberCard && groupMember?.groupMemberIdKey"
1926
:class="['dropdown', 'dropdown-right', { 'open': openDropdownIndex === index }]">
@@ -45,6 +52,7 @@
4552
</div>
4653
<TransitionVerticalCollapse v-if="isTransitionEnabled">
4754
<CardDetails v-if="isCardExpanded"
55+
:class="{ 'is-inactive': groupMember?.isInactive }"
4856
:person="person"
4957
:isInstanceDisplayed="isInstanceDisplayed"
5058
:areFeesDisplayed="areFeesDisplayed"
@@ -55,6 +63,7 @@
5563
</TransitionVerticalCollapse>
5664
<template v-else>
5765
<CardDetails v-if="isCardExpanded"
66+
:class="{ 'is-inactive': groupMember?.isInactive }"
5867
:person="person"
5968
:isInstanceDisplayed="isInstanceDisplayed"
6069
:areFeesDisplayed="areFeesDisplayed"
@@ -104,6 +113,7 @@
104113
import { PlacementMode } from "@Obsidian/Enums/Group/placementMode";
105114
import { useVModelPassthrough } from "@Obsidian/Utility/component";
106115
import { addTooltipIfOverflow } from "./types.partial";
116+
import { tooltip } from "@Obsidian/Utility/tooltip";
107117
import { Gender } from "@Obsidian/Enums/Crm/gender";
108118
import { PropType, ref, computed } from "vue";
109119
import CardDetails from "./cardDetails.partial.obs";
@@ -240,4 +250,10 @@
240250
emit("removeGroupMember", props.groupMember?.groupMemberIdKey ?? "");
241251
openDropdownIndex.value = null;
242252
}
253+
254+
function addTooltip(element: Element | null): void {
255+
if (element) {
256+
tooltip(element);
257+
}
258+
}
243259
</script>

0 commit comments

Comments
 (0)