@memberjunction/ng-explorer-settings 6.1.4 → 6.2.0-edge.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/lib/account-info/account-info.component.d.ts +1 -1
  2. package/dist/lib/account-info/account-info.component.js +2 -2
  3. package/dist/lib/account-info/account-info.component.js.map +1 -1
  4. package/dist/lib/application-management/application-dialog/application-dialog.component.d.ts +93 -10
  5. package/dist/lib/application-management/application-dialog/application-dialog.component.d.ts.map +1 -1
  6. package/dist/lib/application-management/application-dialog/application-dialog.component.js +232 -80
  7. package/dist/lib/application-management/application-dialog/application-dialog.component.js.map +1 -1
  8. package/dist/lib/application-management/application-management.component.d.ts +94 -11
  9. package/dist/lib/application-management/application-management.component.d.ts.map +1 -1
  10. package/dist/lib/application-management/application-management.component.js +231 -67
  11. package/dist/lib/application-management/application-management.component.js.map +1 -1
  12. package/dist/lib/application-settings/application-settings.component.d.ts +4 -4
  13. package/dist/lib/application-settings/application-settings.component.js +10 -10
  14. package/dist/lib/application-settings/application-settings.component.js.map +1 -1
  15. package/dist/lib/entity-permissions/entity-permissions.component.d.ts +88 -10
  16. package/dist/lib/entity-permissions/entity-permissions.component.d.ts.map +1 -1
  17. package/dist/lib/entity-permissions/entity-permissions.component.js +219 -63
  18. package/dist/lib/entity-permissions/entity-permissions.component.js.map +1 -1
  19. package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.d.ts +44 -6
  20. package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.d.ts.map +1 -1
  21. package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.js +117 -43
  22. package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.js.map +1 -1
  23. package/dist/lib/notification-preferences/notification-preferences.component.d.ts +25 -3
  24. package/dist/lib/notification-preferences/notification-preferences.component.d.ts.map +1 -1
  25. package/dist/lib/notification-preferences/notification-preferences.component.js +62 -18
  26. package/dist/lib/notification-preferences/notification-preferences.component.js.map +1 -1
  27. package/dist/lib/role-management/role-dialog/role-dialog.component.d.ts +32 -4
  28. package/dist/lib/role-management/role-dialog/role-dialog.component.d.ts.map +1 -1
  29. package/dist/lib/role-management/role-dialog/role-dialog.component.js +84 -30
  30. package/dist/lib/role-management/role-dialog/role-dialog.component.js.map +1 -1
  31. package/dist/lib/role-management/role-management-agent-context.d.ts +12 -4
  32. package/dist/lib/role-management/role-management-agent-context.d.ts.map +1 -1
  33. package/dist/lib/role-management/role-management-agent-context.js +26 -10
  34. package/dist/lib/role-management/role-management-agent-context.js.map +1 -1
  35. package/dist/lib/role-management/role-management.component.d.ts +95 -11
  36. package/dist/lib/role-management/role-management.component.d.ts.map +1 -1
  37. package/dist/lib/role-management/role-management.component.js +259 -91
  38. package/dist/lib/role-management/role-management.component.js.map +1 -1
  39. package/dist/lib/settings/settings.component.d.ts +87 -11
  40. package/dist/lib/settings/settings.component.d.ts.map +1 -1
  41. package/dist/lib/settings/settings.component.js +211 -65
  42. package/dist/lib/settings/settings.component.js.map +1 -1
  43. package/dist/lib/shared/settings-card.component.d.ts +24 -4
  44. package/dist/lib/shared/settings-card.component.d.ts.map +1 -1
  45. package/dist/lib/shared/settings-card.component.js +46 -6
  46. package/dist/lib/shared/settings-card.component.js.map +1 -1
  47. package/dist/lib/sql-logging/sql-logging.component.d.ts +172 -18
  48. package/dist/lib/sql-logging/sql-logging.component.d.ts.map +1 -1
  49. package/dist/lib/sql-logging/sql-logging.component.js +320 -108
  50. package/dist/lib/sql-logging/sql-logging.component.js.map +1 -1
  51. package/dist/lib/user-app-config/user-app-config-content.component.js +3 -3
  52. package/dist/lib/user-app-config/user-app-config-content.component.js.map +1 -1
  53. package/dist/lib/user-app-config/user-app-config-reorder.d.ts +4 -0
  54. package/dist/lib/user-app-config/user-app-config-reorder.d.ts.map +1 -1
  55. package/dist/lib/user-app-config/user-app-config-reorder.js +11 -3
  56. package/dist/lib/user-app-config/user-app-config-reorder.js.map +1 -1
  57. package/dist/lib/user-management/transaction-group-refusals.d.ts +45 -0
  58. package/dist/lib/user-management/transaction-group-refusals.d.ts.map +1 -0
  59. package/dist/lib/user-management/transaction-group-refusals.js +49 -0
  60. package/dist/lib/user-management/transaction-group-refusals.js.map +1 -0
  61. package/dist/lib/user-management/user-dialog/user-dialog.component.d.ts +44 -6
  62. package/dist/lib/user-management/user-dialog/user-dialog.component.d.ts.map +1 -1
  63. package/dist/lib/user-management/user-dialog/user-dialog.component.js +134 -45
  64. package/dist/lib/user-management/user-dialog/user-dialog.component.js.map +1 -1
  65. package/dist/lib/user-management/user-management-agent-context.d.ts +10 -0
  66. package/dist/lib/user-management/user-management-agent-context.d.ts.map +1 -1
  67. package/dist/lib/user-management/user-management-agent-context.js +26 -6
  68. package/dist/lib/user-management/user-management-agent-context.js.map +1 -1
  69. package/dist/lib/user-management/user-management.component.d.ts +174 -16
  70. package/dist/lib/user-management/user-management.component.d.ts.map +1 -1
  71. package/dist/lib/user-management/user-management.component.js +501 -177
  72. package/dist/lib/user-management/user-management.component.js.map +1 -1
  73. package/dist/lib/user-profile-settings/user-profile-settings.component.d.ts +97 -15
  74. package/dist/lib/user-profile-settings/user-profile-settings.component.d.ts.map +1 -1
  75. package/dist/lib/user-profile-settings/user-profile-settings.component.js +280 -116
  76. package/dist/lib/user-profile-settings/user-profile-settings.component.js.map +1 -1
  77. package/package.json +20 -20
@@ -9,8 +9,9 @@ import { Subject, BehaviorSubject } from 'rxjs';
9
9
  import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';
10
10
  import { RunView, CompositeKey } from '@memberjunction/core';
11
11
  import { BaseDashboard } from '@memberjunction/ng-shared';
12
- import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
13
- import { buildUserManagementAgentContext, isValidUserStatusFilter, resolveUserByIDOrName, resolveRoleByIDOrName, } from './user-management-agent-context';
12
+ import { NormalizeUUID, RegisterClass, UUIDsEqual } from '@memberjunction/global';
13
+ import { ServerRefusalReasons } from './transaction-group-refusals';
14
+ import { BuildUserManagementAgentContext, IsValidUserStatusFilter, ResolveUserByIDOrName, ResolveRoleByIDOrName, } from './user-management-agent-context';
14
15
  import * as i0 from "@angular/core";
15
16
  import * as i1 from "@angular/forms";
16
17
  import * as i2 from "@memberjunction/ng-shared-generic";
@@ -513,49 +514,177 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
513
514
  cdr;
514
515
  ngZone;
515
516
  // State management
516
- users = [];
517
- filteredUsers = [];
518
- roles = [];
519
- selectedUser = null;
517
+ Users = [];
518
+ /** @deprecated Use {@link Users}. */
519
+ get users() {
520
+ return this.Users;
521
+ }
522
+ /** @deprecated Use {@link Users}. */
523
+ set users(value) {
524
+ this.Users = value;
525
+ }
526
+ FilteredUsers = [];
527
+ /** @deprecated Use {@link FilteredUsers}. */
528
+ get filteredUsers() {
529
+ return this.FilteredUsers;
530
+ }
531
+ /** @deprecated Use {@link FilteredUsers}. */
532
+ set filteredUsers(value) {
533
+ this.FilteredUsers = value;
534
+ }
535
+ Roles = [];
536
+ /** @deprecated Use {@link Roles}. */
537
+ get roles() {
538
+ return this.Roles;
539
+ }
540
+ /** @deprecated Use {@link Roles}. */
541
+ set roles(value) {
542
+ this.Roles = value;
543
+ }
544
+ SelectedUser = null;
545
+ /** @deprecated Use {@link SelectedUser}. */
546
+ get selectedUser() {
547
+ return this.SelectedUser;
548
+ }
549
+ /** @deprecated Use {@link SelectedUser}. */
550
+ set selectedUser(value) {
551
+ this.SelectedUser = value;
552
+ }
520
553
  isLoading = false;
521
554
  error = null;
522
555
  // Selection state for bulk actions
523
- selectedUserIds = new Set();
556
+ SelectedUserIds = new Set();
557
+ /** @deprecated Use {@link SelectedUserIds}. */
558
+ get selectedUserIds() {
559
+ return this.SelectedUserIds;
560
+ }
561
+ /** @deprecated Use {@link SelectedUserIds}. */
562
+ set selectedUserIds(value) {
563
+ this.SelectedUserIds = value;
564
+ }
524
565
  // Dialog state
525
- showUserDialog = false;
526
- userDialogData = null;
566
+ ShowUserDialog = false;
567
+ /** @deprecated Use {@link ShowUserDialog}. */
568
+ get showUserDialog() {
569
+ return this.ShowUserDialog;
570
+ }
571
+ /** @deprecated Use {@link ShowUserDialog}. */
572
+ set showUserDialog(value) {
573
+ this.ShowUserDialog = value;
574
+ }
575
+ UserDialogData = null;
576
+ /** @deprecated Use {@link UserDialogData}. */
577
+ get userDialogData() {
578
+ return this.UserDialogData;
579
+ }
580
+ /** @deprecated Use {@link UserDialogData}. */
581
+ set userDialogData(value) {
582
+ this.UserDialogData = value;
583
+ }
527
584
  // Bulk action dialog state
528
- showBulkActionConfirm = false;
529
- bulkActionType = null;
530
- showBulkRoleAssign = false;
531
- bulkRoleId = '';
585
+ ShowBulkActionConfirm = false;
586
+ /** @deprecated Use {@link ShowBulkActionConfirm}. */
587
+ get showBulkActionConfirm() {
588
+ return this.ShowBulkActionConfirm;
589
+ }
590
+ /** @deprecated Use {@link ShowBulkActionConfirm}. */
591
+ set showBulkActionConfirm(value) {
592
+ this.ShowBulkActionConfirm = value;
593
+ }
594
+ BulkActionType = null;
595
+ /** @deprecated Use {@link BulkActionType}. */
596
+ get bulkActionType() {
597
+ return this.BulkActionType;
598
+ }
599
+ /** @deprecated Use {@link BulkActionType}. */
600
+ set bulkActionType(value) {
601
+ this.BulkActionType = value;
602
+ }
603
+ ShowBulkRoleAssign = false;
604
+ /** @deprecated Use {@link ShowBulkRoleAssign}. */
605
+ get showBulkRoleAssign() {
606
+ return this.ShowBulkRoleAssign;
607
+ }
608
+ /** @deprecated Use {@link ShowBulkRoleAssign}. */
609
+ set showBulkRoleAssign(value) {
610
+ this.ShowBulkRoleAssign = value;
611
+ }
612
+ BulkRoleId = '';
613
+ /** @deprecated Use {@link BulkRoleId}. */
614
+ get bulkRoleId() {
615
+ return this.BulkRoleId;
616
+ }
617
+ /** @deprecated Use {@link BulkRoleId}. */
618
+ set bulkRoleId(value) {
619
+ this.BulkRoleId = value;
620
+ }
532
621
  // Stats
533
- stats = {
622
+ Stats = {
534
623
  totalUsers: 0,
535
624
  activeUsers: 0,
536
625
  inactiveUsers: 0,
537
626
  adminUsers: 0 // This will be based on roles, not Type
538
627
  };
628
+ /** @deprecated Use {@link Stats}. */
629
+ get stats() {
630
+ return this.Stats;
631
+ }
632
+ /** @deprecated Use {@link Stats}. */
633
+ set stats(value) {
634
+ this.Stats = value;
635
+ }
539
636
  // Filters
540
- filters$ = new BehaviorSubject({
637
+ Filters$ = new BehaviorSubject({
541
638
  status: 'all',
542
639
  role: '',
543
640
  search: ''
544
641
  });
642
+ /** @deprecated Use {@link Filters$}. */
643
+ get filters$() {
644
+ return this.Filters$;
645
+ }
646
+ /** @deprecated Use {@link Filters$}. */
647
+ set filters$(value) {
648
+ this.Filters$ = value;
649
+ }
545
650
  // UI State
546
651
  showCreateDialog = false;
547
- showEditDialog = false;
548
- showDeleteConfirm = false;
652
+ ShowEditDialog = false;
653
+ /** @deprecated Use {@link ShowEditDialog}. */
654
+ get showEditDialog() {
655
+ return this.ShowEditDialog;
656
+ }
657
+ /** @deprecated Use {@link ShowEditDialog}. */
658
+ set showEditDialog(value) {
659
+ this.ShowEditDialog = value;
660
+ }
661
+ ShowDeleteConfirm = false;
662
+ /** @deprecated Use {@link ShowDeleteConfirm}. */
663
+ get showDeleteConfirm() {
664
+ return this.ShowDeleteConfirm;
665
+ }
666
+ /** @deprecated Use {@link ShowDeleteConfirm}. */
667
+ set showDeleteConfirm(value) {
668
+ this.ShowDeleteConfirm = value;
669
+ }
549
670
  // Mobile expansion state
550
671
  expandedUserIds = new Set();
551
672
  // User-Role mapping
552
673
  userRoleMap = new Map(); // userId -> roleIds[]
553
674
  // Grid configuration
554
- gridConfig = {
675
+ GridConfig = {
555
676
  pageSize: 20,
556
677
  sortField: 'Name',
557
678
  sortDirection: 'asc'
558
679
  };
680
+ /** @deprecated Use {@link GridConfig}. */
681
+ get gridConfig() {
682
+ return this.GridConfig;
683
+ }
684
+ /** @deprecated Use {@link GridConfig}. */
685
+ set gridConfig(value) {
686
+ this.GridConfig = value;
687
+ }
559
688
  destroy$ = new Subject();
560
689
  get metadata() { return this.ProviderToUse; }
561
690
  constructor(cdr, ngZone) {
@@ -571,7 +700,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
571
700
  this.registerAgentClientTools();
572
701
  }
573
702
  loadData() {
574
- this.loadInitialData();
703
+ this.LoadInitialData();
575
704
  }
576
705
  // ================================================================
577
706
  // AI Agent Context & Client Tools
@@ -598,25 +727,25 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
598
727
  * {@link buildUserManagementAgentContext} helper (unit-tested in isolation).
599
728
  */
600
729
  publishAgentContext() {
601
- const filters = this.filters$.value;
602
- const roleFilter = filters.role ? this.roles.find(r => UUIDsEqual(r.ID, filters.role)) : null;
730
+ const filters = this.Filters$.value;
731
+ const roleFilter = filters.role ? this.Roles.find(r => UUIDsEqual(r.ID, filters.role)) : null;
603
732
  const input = {
604
- TotalUserCount: this.users.length,
605
- FilteredUserCount: this.filteredUsers.length,
606
- ActiveUserCount: this.stats.activeUsers,
607
- InactiveUserCount: this.stats.inactiveUsers,
733
+ TotalUserCount: this.Users.length,
734
+ FilteredUserCount: this.FilteredUsers.length,
735
+ ActiveUserCount: this.Stats.activeUsers,
736
+ InactiveUserCount: this.Stats.inactiveUsers,
608
737
  StatusFilter: filters.status,
609
738
  RoleFilterId: filters.role || null,
610
739
  RoleFilterName: roleFilter?.Name ?? null,
611
740
  SearchText: filters.search,
612
- SelectedUserId: this.selectedUser?.ID ?? null,
613
- SelectedUserName: this.selectedUser?.Name ?? null,
614
- VisibleUserNames: this.filteredUsers.map(u => u.Name ?? '').filter(n => n !== ''),
615
- VisibleUserEmails: this.filteredUsers.map(u => u.Email ?? '').filter(e => e !== ''),
616
- AvailableRoleNames: this.roles.map(r => r.Name ?? '').filter(n => n !== ''),
741
+ SelectedUserId: this.SelectedUser?.ID ?? null,
742
+ SelectedUserName: this.SelectedUser?.Name ?? null,
743
+ VisibleUserNames: this.FilteredUsers.map(u => u.Name ?? '').filter(n => n !== ''),
744
+ VisibleUserEmails: this.FilteredUsers.map(u => u.Email ?? '').filter(e => e !== ''),
745
+ AvailableRoleNames: this.Roles.map(r => r.Name ?? '').filter(n => n !== ''),
617
746
  VisibleColumns: ['Name', 'Email', 'Type', 'Status', 'Roles'],
618
747
  };
619
- this.navigationService.SetAgentContext(this, buildUserManagementAgentContext(input));
748
+ this.navigationService.SetAgentContext(this, BuildUserManagementAgentContext(input));
620
749
  }
621
750
  /**
622
751
  * Register the read-only / navigational client tools the agent may invoke.
@@ -683,10 +812,10 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
683
812
  }
684
813
  handleSwitchStatusFilterTool(params) {
685
814
  const status = params?.['status'];
686
- if (!isValidUserStatusFilter(status)) {
815
+ if (!IsValidUserStatusFilter(status)) {
687
816
  return { Success: false, ErrorMessage: `Invalid status "${String(status)}". Expected one of: all, active, inactive.` };
688
817
  }
689
- this.onStatusFilterChange(status);
818
+ this.OnStatusFilterChange(status);
690
819
  this.publishAgentContext();
691
820
  return { Success: true };
692
821
  }
@@ -694,15 +823,15 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
694
823
  const raw = String(params?.['role'] ?? '');
695
824
  if (!raw.trim()) {
696
825
  // Empty string explicitly clears the role filter.
697
- this.updateFilter({ role: '' });
826
+ this.UpdateFilter({ role: '' });
698
827
  this.publishAgentContext();
699
828
  return { Success: true };
700
829
  }
701
- const resolved = resolveRoleByIDOrName(raw, this.roles.map(r => ({ ID: r.ID, Name: r.Name ?? '' })));
830
+ const resolved = ResolveRoleByIDOrName(raw, this.Roles.map(r => ({ ID: r.ID, Name: r.Name ?? '' })));
702
831
  if (!resolved.ok) {
703
832
  return { Success: false, ErrorMessage: resolved.error };
704
833
  }
705
- this.updateFilter({ role: resolved.match.ID });
834
+ this.UpdateFilter({ role: resolved.match.ID });
706
835
  this.publishAgentContext();
707
836
  return { Success: true };
708
837
  }
@@ -711,20 +840,20 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
711
840
  if (typeof searchText !== 'string') {
712
841
  return { Success: false, ErrorMessage: 'searchText must be a string.' };
713
842
  }
714
- this.updateFilter({ search: searchText });
843
+ this.UpdateFilter({ search: searchText });
715
844
  this.publishAgentContext();
716
845
  return { Success: true };
717
846
  }
718
847
  handleSelectUserTool(params) {
719
848
  const raw = String(params?.['user'] ?? '');
720
- const resolved = resolveUserByIDOrName(raw, this.users.map(u => ({
849
+ const resolved = ResolveUserByIDOrName(raw, this.Users.map(u => ({
721
850
  ID: u.ID, Name: u.Name ?? '', Email: u.Email, FirstName: u.FirstName, LastName: u.LastName,
722
851
  })));
723
852
  if (!resolved.ok) {
724
853
  return { Success: false, ErrorMessage: resolved.error };
725
854
  }
726
- const match = this.users.find(u => UUIDsEqual(u.ID, resolved.match.ID)) ?? null;
727
- this.selectedUser = match;
855
+ const match = this.Users.find(u => UUIDsEqual(u.ID, resolved.match.ID)) ?? null;
856
+ this.SelectedUser = match;
728
857
  this.cdr.markForCheck();
729
858
  this.publishAgentContext();
730
859
  return { Success: true };
@@ -745,7 +874,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
745
874
  }
746
875
  handleNavigateToUserRecordTool(params) {
747
876
  const raw = String(params?.['user'] ?? '');
748
- const resolved = resolveUserByIDOrName(raw, this.users.map(u => ({
877
+ const resolved = ResolveUserByIDOrName(raw, this.Users.map(u => ({
749
878
  ID: u.ID, Name: u.Name ?? '', Email: u.Email, FirstName: u.FirstName, LastName: u.LastName,
750
879
  })));
751
880
  if (!resolved.ok) {
@@ -758,7 +887,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
758
887
  applySortToVisibleUsers() {
759
888
  const dir = this.agentSortDirection === 'desc' ? -1 : 1;
760
889
  const field = this.agentSortField;
761
- this.filteredUsers = [...this.filteredUsers].sort((a, b) => {
890
+ this.FilteredUsers = [...this.FilteredUsers].sort((a, b) => {
762
891
  const av = this.sortKeyForUser(a, field);
763
892
  const bv = this.sortKeyForUser(b, field);
764
893
  return av.localeCompare(bv) * dir;
@@ -774,13 +903,13 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
774
903
  }
775
904
  }
776
905
  handleClearFiltersTool() {
777
- this.resetAllFiltersAndSearch();
906
+ this.ResetAllFiltersAndSearch();
778
907
  this.publishAgentContext();
779
908
  return { Success: true };
780
909
  }
781
910
  async handleRefreshTool() {
782
911
  try {
783
- await this.loadInitialData();
912
+ await this.LoadInitialData();
784
913
  this.publishAgentContext();
785
914
  return { Success: true };
786
915
  }
@@ -789,10 +918,10 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
789
918
  }
790
919
  }
791
920
  handleExportTool() {
792
- if (this.filteredUsers.length === 0) {
921
+ if (this.FilteredUsers.length === 0) {
793
922
  return { Success: false, ErrorMessage: 'No users to export.' };
794
923
  }
795
- this.exportUsers();
924
+ this.ExportUsers();
796
925
  return { Success: true };
797
926
  }
798
927
  ngOnDestroy() {
@@ -800,7 +929,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
800
929
  this.destroy$.complete();
801
930
  super.ngOnDestroy();
802
931
  }
803
- async loadInitialData() {
932
+ async LoadInitialData() {
804
933
  try {
805
934
  this.isLoading = true;
806
935
  this.error = null;
@@ -810,8 +939,8 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
810
939
  this.loadRoles(),
811
940
  this.loadUserRoles()
812
941
  ]);
813
- this.users = users;
814
- this.roles = roles;
942
+ this.Users = users;
943
+ this.Roles = roles;
815
944
  // Build user-role mapping
816
945
  this.buildUserRoleMapping(userRoles);
817
946
  this.calculateStats();
@@ -829,6 +958,10 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
829
958
  });
830
959
  }
831
960
  }
961
+ /** @deprecated Use {@link LoadInitialData}. */
962
+ async loadInitialData() {
963
+ return this.LoadInitialData();
964
+ }
832
965
  async loadUsers() {
833
966
  const rv = RunView.FromMetadataProvider(this.ProviderToUse);
834
967
  const result = await rv.RunView({
@@ -866,7 +999,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
866
999
  });
867
1000
  }
868
1001
  setupFilterSubscription() {
869
- this.filters$
1002
+ this.Filters$
870
1003
  .pipe(debounceTime(300), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), takeUntil(this.destroy$))
871
1004
  .subscribe(() => {
872
1005
  this.applyFilters();
@@ -874,8 +1007,8 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
874
1007
  });
875
1008
  }
876
1009
  applyFilters() {
877
- const filters = this.filters$.value;
878
- let filtered = [...this.users];
1010
+ const filters = this.Filters$.value;
1011
+ let filtered = [...this.Users];
879
1012
  // Apply status filter
880
1013
  if (filters.status !== 'all') {
881
1014
  filtered = filtered.filter(user => filters.status === 'active' ? user.IsActive : !user.IsActive);
@@ -895,25 +1028,29 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
895
1028
  user.FirstName?.toLowerCase().includes(searchLower) ||
896
1029
  user.LastName?.toLowerCase().includes(searchLower));
897
1030
  }
898
- this.filteredUsers = filtered;
1031
+ this.FilteredUsers = filtered;
899
1032
  // Preserve any agent-requested sort across filter changes (read-only display ordering).
900
1033
  this.applySortToVisibleUsers();
901
1034
  }
902
1035
  calculateStats() {
903
- this.stats = {
904
- totalUsers: this.users.length,
905
- activeUsers: this.users.filter(u => u.IsActive).length,
906
- inactiveUsers: this.users.filter(u => !u.IsActive).length,
907
- adminUsers: this.users.filter(u => u.Type === 'Owner').length // Using Owner as admin type
1036
+ this.Stats = {
1037
+ totalUsers: this.Users.length,
1038
+ activeUsers: this.Users.filter(u => u.IsActive).length,
1039
+ inactiveUsers: this.Users.filter(u => !u.IsActive).length,
1040
+ adminUsers: this.Users.filter(u => u.Type === 'Owner').length // Using Owner as admin type
908
1041
  };
909
1042
  }
910
1043
  // Public methods for template
1044
+ OnStatusFilterChange(status) {
1045
+ this.UpdateFilter({ status });
1046
+ }
1047
+ /** @deprecated Use {@link OnStatusFilterChange}. */
911
1048
  onStatusFilterChange(status) {
912
- this.updateFilter({ status });
1049
+ return this.OnStatusFilterChange(status);
913
1050
  }
914
- updateFilter(partial) {
915
- this.filters$.next({
916
- ...this.filters$.value,
1051
+ UpdateFilter(partial) {
1052
+ this.Filters$.next({
1053
+ ...this.Filters$.value,
917
1054
  ...partial
918
1055
  });
919
1056
  // Discrete changes (chips, popover dropdowns) apply immediately. Text search
@@ -924,42 +1061,58 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
924
1061
  this.cdr.markForCheck();
925
1062
  }
926
1063
  }
1064
+ /** @deprecated Use {@link UpdateFilter}. */
1065
+ updateFilter(partial) {
1066
+ return this.UpdateFilter(partial);
1067
+ }
1068
+ SelectUser(user) {
1069
+ this.SelectedUser = user;
1070
+ this.ShowEditDialog = true;
1071
+ }
1072
+ /** @deprecated Use {@link SelectUser}. */
927
1073
  selectUser(user) {
928
- this.selectedUser = user;
929
- this.showEditDialog = true;
1074
+ return this.SelectUser(user);
930
1075
  }
931
1076
  createNewUser() {
932
- this.userDialogData = {
1077
+ this.UserDialogData = {
933
1078
  mode: 'create',
934
- availableRoles: this.roles
1079
+ availableRoles: this.Roles
935
1080
  };
936
- this.showUserDialog = true;
1081
+ this.ShowUserDialog = true;
937
1082
  }
938
- editUser(user) {
939
- this.userDialogData = {
1083
+ EditUser(user) {
1084
+ this.UserDialogData = {
940
1085
  user: user,
941
1086
  mode: 'edit',
942
- availableRoles: this.roles
1087
+ availableRoles: this.Roles
943
1088
  };
944
- this.showUserDialog = true;
1089
+ this.ShowUserDialog = true;
1090
+ }
1091
+ /** @deprecated Use {@link EditUser}. */
1092
+ editUser(user) {
1093
+ return this.EditUser(user);
1094
+ }
1095
+ ConfirmDeleteUser(user) {
1096
+ this.SelectedUser = user;
1097
+ this.ShowDeleteConfirm = true;
945
1098
  }
1099
+ /** @deprecated Use {@link ConfirmDeleteUser}. */
946
1100
  confirmDeleteUser(user) {
947
- this.selectedUser = user;
948
- this.showDeleteConfirm = true;
1101
+ return this.ConfirmDeleteUser(user);
949
1102
  }
950
- async deleteUser() {
951
- if (!this.selectedUser)
1103
+ async DeleteUser() {
1104
+ if (!this.SelectedUser)
952
1105
  return;
953
1106
  try {
954
1107
  // Load user entity to delete
955
1108
  const user = await this.metadata.GetEntityObject('MJ: Users');
956
- const loadResult = await user.Load(this.selectedUser.ID);
1109
+ const loadResult = await user.Load(this.SelectedUser.ID);
957
1110
  if (loadResult) {
958
1111
  const deleteResult = await user.Delete();
959
1112
  if (deleteResult) {
960
- this.showDeleteConfirm = false;
961
- this.selectedUser = null;
962
- await this.loadInitialData();
1113
+ this.ShowDeleteConfirm = false;
1114
+ this.SelectedUser = null;
1115
+ await this.LoadInitialData();
963
1116
  }
964
1117
  else {
965
1118
  throw new Error(user.LatestResult?.Message || 'Failed to delete user');
@@ -977,7 +1130,11 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
977
1130
  });
978
1131
  }
979
1132
  }
980
- async toggleUserStatus(user) {
1133
+ /** @deprecated Use {@link DeleteUser}. */
1134
+ async deleteUser() {
1135
+ return this.DeleteUser();
1136
+ }
1137
+ async ToggleUserStatus(user) {
981
1138
  try {
982
1139
  user.IsActive = !user.IsActive;
983
1140
  // BaseEntity.Save() returns false on a validation failure — it does NOT throw. Discarding
@@ -1008,8 +1165,12 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1008
1165
  });
1009
1166
  }
1010
1167
  }
1011
- exportUsers() {
1012
- if (this.filteredUsers.length === 0) {
1168
+ /** @deprecated Use {@link ToggleUserStatus}. */
1169
+ async toggleUserStatus(user) {
1170
+ return this.ToggleUserStatus(user);
1171
+ }
1172
+ ExportUsers() {
1173
+ if (this.FilteredUsers.length === 0) {
1013
1174
  this.error = 'No users to export';
1014
1175
  return;
1015
1176
  }
@@ -1018,7 +1179,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1018
1179
  const headers = ['Name', 'First Name', 'Last Name', 'Email', 'Type', 'Status', 'Created', 'Updated'];
1019
1180
  const csvRows = [headers.join(',')];
1020
1181
  // Add user data
1021
- this.filteredUsers.forEach(user => {
1182
+ this.FilteredUsers.forEach(user => {
1022
1183
  const row = [
1023
1184
  this.escapeCSV(user.Name || ''),
1024
1185
  this.escapeCSV(user.FirstName || ''),
@@ -1050,22 +1211,38 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1050
1211
  this.error = 'Failed to export users';
1051
1212
  }
1052
1213
  }
1214
+ /** @deprecated Use {@link ExportUsers}. */
1215
+ exportUsers() {
1216
+ return this.ExportUsers();
1217
+ }
1053
1218
  escapeCSV(value) {
1054
1219
  if (value.includes(',') || value.includes('"') || value.includes('\n')) {
1055
1220
  return `"${value.replace(/"/g, '""')}"`;
1056
1221
  }
1057
1222
  return value;
1058
1223
  }
1224
+ RefreshData() {
1225
+ this.LoadInitialData();
1226
+ }
1227
+ /** @deprecated Use {@link RefreshData}. */
1059
1228
  refreshData() {
1060
- this.loadInitialData();
1229
+ return this.RefreshData();
1061
1230
  }
1062
- getStatusIcon(user) {
1231
+ GetStatusIcon(user) {
1063
1232
  return user.IsActive ? 'fa-check-circle' : 'fa-times-circle';
1064
1233
  }
1065
- getStatusClass(user) {
1234
+ /** @deprecated Use {@link GetStatusIcon}. */
1235
+ getStatusIcon(user) {
1236
+ return this.GetStatusIcon(user);
1237
+ }
1238
+ GetStatusClass(user) {
1066
1239
  return user.IsActive ? 'status-active' : 'status-inactive';
1067
1240
  }
1068
- getUserTypeIcon(user) {
1241
+ /** @deprecated Use {@link GetStatusClass}. */
1242
+ getStatusClass(user) {
1243
+ return this.GetStatusClass(user);
1244
+ }
1245
+ GetUserTypeIcon(user) {
1069
1246
  switch (user.Type) {
1070
1247
  case 'Owner':
1071
1248
  return 'fa-shield-halved';
@@ -1075,56 +1252,96 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1075
1252
  return 'fa-user';
1076
1253
  }
1077
1254
  }
1078
- getUserInitials(user) {
1255
+ /** @deprecated Use {@link GetUserTypeIcon}. */
1256
+ getUserTypeIcon(user) {
1257
+ return this.GetUserTypeIcon(user);
1258
+ }
1259
+ GetUserInitials(user) {
1079
1260
  const first = user.FirstName?.charAt(0) || '';
1080
1261
  const last = user.LastName?.charAt(0) || '';
1081
1262
  return (first + last).toUpperCase() || user.Name?.charAt(0).toUpperCase() || 'U';
1082
1263
  }
1083
- onUserDialogResult(result) {
1084
- this.showUserDialog = false;
1085
- this.userDialogData = null;
1264
+ /** @deprecated Use {@link GetUserInitials}. */
1265
+ getUserInitials(user) {
1266
+ return this.GetUserInitials(user);
1267
+ }
1268
+ OnUserDialogResult(result) {
1269
+ this.ShowUserDialog = false;
1270
+ this.UserDialogData = null;
1086
1271
  if (result.action === 'save') {
1087
1272
  // Refresh the user list to show changes
1088
- this.loadInitialData();
1273
+ this.LoadInitialData();
1089
1274
  }
1090
1275
  }
1276
+ /** @deprecated Use {@link OnUserDialogResult}. */
1277
+ onUserDialogResult(result) {
1278
+ return this.OnUserDialogResult(result);
1279
+ }
1091
1280
  // Selection methods for bulk actions
1281
+ get IsAllSelected() {
1282
+ return this.FilteredUsers.length > 0 &&
1283
+ this.FilteredUsers.every(user => this.SelectedUserIds.has(user.ID));
1284
+ }
1285
+ /** @deprecated Use {@link IsAllSelected}. */
1092
1286
  get isAllSelected() {
1093
- return this.filteredUsers.length > 0 &&
1094
- this.filteredUsers.every(user => this.selectedUserIds.has(user.ID));
1287
+ return this.IsAllSelected;
1095
1288
  }
1289
+ get IsIndeterminate() {
1290
+ const selectedCount = this.FilteredUsers.filter(user => this.SelectedUserIds.has(user.ID)).length;
1291
+ return selectedCount > 0 && selectedCount < this.FilteredUsers.length;
1292
+ }
1293
+ /** @deprecated Use {@link IsIndeterminate}. */
1096
1294
  get isIndeterminate() {
1097
- const selectedCount = this.filteredUsers.filter(user => this.selectedUserIds.has(user.ID)).length;
1098
- return selectedCount > 0 && selectedCount < this.filteredUsers.length;
1295
+ return this.IsIndeterminate;
1296
+ }
1297
+ get HasSelection() {
1298
+ return this.SelectedUserIds.size > 0;
1099
1299
  }
1300
+ /** @deprecated Use {@link HasSelection}. */
1100
1301
  get hasSelection() {
1101
- return this.selectedUserIds.size > 0;
1302
+ return this.HasSelection;
1303
+ }
1304
+ get SelectedCount() {
1305
+ return this.SelectedUserIds.size;
1102
1306
  }
1307
+ /** @deprecated Use {@link SelectedCount}. */
1103
1308
  get selectedCount() {
1104
- return this.selectedUserIds.size;
1309
+ return this.SelectedCount;
1105
1310
  }
1106
- get hasActiveFilters() {
1107
- const filters = this.filters$.value;
1311
+ get HasActiveFilters() {
1312
+ const filters = this.Filters$.value;
1108
1313
  return filters.status !== 'all' || filters.role !== '';
1109
1314
  }
1110
- get activeFilterCount() {
1315
+ /** @deprecated Use {@link HasActiveFilters}. */
1316
+ get hasActiveFilters() {
1317
+ return this.HasActiveFilters;
1318
+ }
1319
+ get ActiveFilterCount() {
1111
1320
  let count = 0;
1112
- const filters = this.filters$.value;
1321
+ const filters = this.Filters$.value;
1113
1322
  if (filters.status !== 'all')
1114
1323
  count++;
1115
1324
  if (filters.role !== '')
1116
1325
  count++;
1117
1326
  return count;
1118
1327
  }
1119
- clearFilters() {
1120
- this.filters$.next({
1121
- ...this.filters$.value,
1328
+ /** @deprecated Use {@link ActiveFilterCount}. */
1329
+ get activeFilterCount() {
1330
+ return this.ActiveFilterCount;
1331
+ }
1332
+ ClearFilters() {
1333
+ this.Filters$.next({
1334
+ ...this.Filters$.value,
1122
1335
  status: 'all',
1123
1336
  role: ''
1124
1337
  });
1125
1338
  this.applyFilters();
1126
1339
  this.cdr.markForCheck();
1127
1340
  }
1341
+ /** @deprecated Use {@link ClearFilters}. */
1342
+ clearFilters() {
1343
+ return this.ClearFilters();
1344
+ }
1128
1345
  // -- Filter panel binding (mj-filter-panel inside the popover) -------------
1129
1346
  //
1130
1347
  // Only Role lives inside the popover — Status is exposed as visible chips
@@ -1136,7 +1353,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1136
1353
  * FilterFieldConfig[] describing the popover's Role field. Driven off
1137
1354
  * `roles[]` so the dropdown stays in sync as roles load.
1138
1355
  */
1139
- get filterFields() {
1356
+ get FilterFields() {
1140
1357
  return [
1141
1358
  {
1142
1359
  key: 'status',
@@ -1154,25 +1371,33 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1154
1371
  label: 'Role',
1155
1372
  icon: 'fa-solid fa-user-shield',
1156
1373
  placeholder: 'All Roles',
1157
- filterable: this.roles.length > 10,
1374
+ filterable: this.Roles.length > 10,
1158
1375
  options: [
1159
1376
  { text: 'All Roles', value: '' },
1160
- ...this.roles.map(r => ({ text: r.Name ?? '', value: r.ID }))
1377
+ ...this.Roles.map(r => ({ text: r.Name ?? '', value: r.ID }))
1161
1378
  ]
1162
1379
  }
1163
1380
  ];
1164
1381
  }
1382
+ /** @deprecated Use {@link FilterFields}. */
1383
+ get filterFields() {
1384
+ return this.FilterFields;
1385
+ }
1165
1386
  /** Current popover field values keyed by FilterFieldConfig.key. */
1387
+ get FilterValues() {
1388
+ return { status: this.Filters$.value.status, role: this.Filters$.value.role };
1389
+ }
1390
+ /** @deprecated Use {@link FilterValues}. */
1166
1391
  get filterValues() {
1167
- return { status: this.filters$.value.status, role: this.filters$.value.role };
1392
+ return this.FilterValues;
1168
1393
  }
1169
1394
  /** Total active filters (Status + Role) — drives the Filter button badge. */
1170
1395
  get TotalActiveFilterCount() {
1171
- const f = this.filters$.value;
1396
+ const f = this.Filters$.value;
1172
1397
  return (f.status !== 'all' ? 1 : 0) + (f.role !== '' ? 1 : 0);
1173
1398
  }
1174
1399
  /** Apply a value change from <mj-filter-panel>. */
1175
- onFilterPanelChange(values) {
1400
+ OnFilterPanelChange(values) {
1176
1401
  const partial = {};
1177
1402
  if ('status' in values) {
1178
1403
  partial.status = values['status'] || 'all';
@@ -1180,70 +1405,106 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1180
1405
  if ('role' in values) {
1181
1406
  partial.role = values['role'] ?? '';
1182
1407
  }
1183
- this.updateFilter(partial);
1408
+ this.UpdateFilter(partial);
1409
+ }
1410
+ /** @deprecated Use {@link OnFilterPanelChange}. */
1411
+ onFilterPanelChange(values) {
1412
+ return this.OnFilterPanelChange(values);
1184
1413
  }
1185
1414
  /** Clear all filters (Status + Role); search persists. */
1415
+ ClearAllAppliedFilters() {
1416
+ this.UpdateFilter({ status: 'all', role: '' });
1417
+ }
1418
+ /** @deprecated Use {@link ClearAllAppliedFilters}. */
1186
1419
  clearAllAppliedFilters() {
1187
- this.updateFilter({ status: 'all', role: '' });
1420
+ return this.ClearAllAppliedFilters();
1188
1421
  }
1189
1422
  /** True when search and/or panel filters are narrowing the list — gates the
1190
1423
  * no-results empty-state "Reset filters" CTA. */
1191
1424
  get IsListNarrowed() {
1192
- return this.filters$.value.search !== '' || this.TotalActiveFilterCount > 0;
1425
+ return this.Filters$.value.search !== '' || this.TotalActiveFilterCount > 0;
1193
1426
  }
1194
1427
  /** Reset everything narrowing the list (search + Status + Role) and refresh
1195
1428
  * immediately. Wired to the no-results empty-state CTA. Unlike
1196
1429
  * clearAllAppliedFilters(), this also clears the search box. */
1197
- resetAllFiltersAndSearch() {
1198
- this.filters$.next({ status: 'all', role: '', search: '' });
1430
+ ResetAllFiltersAndSearch() {
1431
+ this.Filters$.next({ status: 'all', role: '', search: '' });
1199
1432
  this.applyFilters();
1200
1433
  this.cdr.markForCheck();
1201
1434
  }
1202
- toggleSelectAll() {
1203
- if (this.isAllSelected) {
1435
+ /** @deprecated Use {@link ResetAllFiltersAndSearch}. */
1436
+ resetAllFiltersAndSearch() {
1437
+ return this.ResetAllFiltersAndSearch();
1438
+ }
1439
+ ToggleSelectAll() {
1440
+ if (this.IsAllSelected) {
1204
1441
  // Deselect all filtered users
1205
- this.filteredUsers.forEach(user => this.selectedUserIds.delete(user.ID));
1442
+ this.FilteredUsers.forEach(user => this.SelectedUserIds.delete(user.ID));
1206
1443
  }
1207
1444
  else {
1208
1445
  // Select all filtered users
1209
- this.filteredUsers.forEach(user => this.selectedUserIds.add(user.ID));
1446
+ this.FilteredUsers.forEach(user => this.SelectedUserIds.add(user.ID));
1210
1447
  }
1211
1448
  }
1212
- toggleUserSelection(userId, event) {
1449
+ /** @deprecated Use {@link ToggleSelectAll}. */
1450
+ toggleSelectAll() {
1451
+ return this.ToggleSelectAll();
1452
+ }
1453
+ ToggleUserSelection(userId, event) {
1213
1454
  if (event) {
1214
1455
  event.stopPropagation();
1215
1456
  }
1216
- if (this.selectedUserIds.has(userId)) {
1217
- this.selectedUserIds.delete(userId);
1457
+ if (this.SelectedUserIds.has(userId)) {
1458
+ this.SelectedUserIds.delete(userId);
1218
1459
  }
1219
1460
  else {
1220
- this.selectedUserIds.add(userId);
1461
+ this.SelectedUserIds.add(userId);
1221
1462
  }
1222
1463
  }
1464
+ /** @deprecated Use {@link ToggleUserSelection}. */
1465
+ toggleUserSelection(userId, event) {
1466
+ return this.ToggleUserSelection(userId, event);
1467
+ }
1468
+ IsUserSelected(userId) {
1469
+ return this.SelectedUserIds.has(userId);
1470
+ }
1471
+ /** @deprecated Use {@link IsUserSelected}. */
1223
1472
  isUserSelected(userId) {
1224
- return this.selectedUserIds.has(userId);
1473
+ return this.IsUserSelected(userId);
1225
1474
  }
1475
+ ClearSelection() {
1476
+ this.SelectedUserIds.clear();
1477
+ }
1478
+ /** @deprecated Use {@link ClearSelection}. */
1226
1479
  clearSelection() {
1227
- this.selectedUserIds.clear();
1480
+ return this.ClearSelection();
1228
1481
  }
1229
1482
  // Bulk action methods
1230
- confirmBulkAction(action) {
1231
- if (!this.hasSelection)
1483
+ ConfirmBulkAction(action) {
1484
+ if (!this.HasSelection)
1232
1485
  return;
1233
- this.bulkActionType = action;
1234
- this.showBulkActionConfirm = true;
1486
+ this.BulkActionType = action;
1487
+ this.ShowBulkActionConfirm = true;
1235
1488
  }
1489
+ /** @deprecated Use {@link ConfirmBulkAction}. */
1490
+ confirmBulkAction(action) {
1491
+ return this.ConfirmBulkAction(action);
1492
+ }
1493
+ CancelBulkAction() {
1494
+ this.ShowBulkActionConfirm = false;
1495
+ this.BulkActionType = null;
1496
+ }
1497
+ /** @deprecated Use {@link CancelBulkAction}. */
1236
1498
  cancelBulkAction() {
1237
- this.showBulkActionConfirm = false;
1238
- this.bulkActionType = null;
1499
+ return this.CancelBulkAction();
1239
1500
  }
1240
- async executeBulkAction() {
1241
- if (!this.bulkActionType || !this.hasSelection)
1501
+ async ExecuteBulkAction() {
1502
+ if (!this.BulkActionType || !this.HasSelection)
1242
1503
  return;
1243
1504
  try {
1244
1505
  this.isLoading = true;
1245
- const selectedUsers = this.users.filter(user => this.selectedUserIds.has(user.ID));
1246
- switch (this.bulkActionType) {
1506
+ const selectedUsers = this.Users.filter(user => this.SelectedUserIds.has(user.ID));
1507
+ switch (this.BulkActionType) {
1247
1508
  case 'enable':
1248
1509
  await this.bulkSetUserStatus(selectedUsers, true);
1249
1510
  break;
@@ -1254,10 +1515,10 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1254
1515
  await this.bulkDeleteUsers(selectedUsers);
1255
1516
  break;
1256
1517
  }
1257
- this.clearSelection();
1258
- this.showBulkActionConfirm = false;
1259
- this.bulkActionType = null;
1260
- await this.loadInitialData();
1518
+ this.ClearSelection();
1519
+ this.ShowBulkActionConfirm = false;
1520
+ this.BulkActionType = null;
1521
+ await this.LoadInitialData();
1261
1522
  }
1262
1523
  catch (error) {
1263
1524
  console.error('Bulk action failed:', error);
@@ -1273,6 +1534,10 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1273
1534
  });
1274
1535
  }
1275
1536
  }
1537
+ /** @deprecated Use {@link ExecuteBulkAction}. */
1538
+ async executeBulkAction() {
1539
+ return this.ExecuteBulkAction();
1540
+ }
1276
1541
  async bulkSetUserStatus(users, isActive) {
1277
1542
  if (users.length === 0)
1278
1543
  return;
@@ -1305,26 +1570,34 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1305
1570
  }
1306
1571
  }
1307
1572
  // Bulk role assignment
1308
- openBulkRoleAssign() {
1309
- if (!this.hasSelection)
1573
+ OpenBulkRoleAssign() {
1574
+ if (!this.HasSelection)
1310
1575
  return;
1311
- this.bulkRoleId = '';
1312
- this.showBulkRoleAssign = true;
1576
+ this.BulkRoleId = '';
1577
+ this.ShowBulkRoleAssign = true;
1578
+ }
1579
+ /** @deprecated Use {@link OpenBulkRoleAssign}. */
1580
+ openBulkRoleAssign() {
1581
+ return this.OpenBulkRoleAssign();
1313
1582
  }
1583
+ CancelBulkRoleAssign() {
1584
+ this.ShowBulkRoleAssign = false;
1585
+ this.BulkRoleId = '';
1586
+ }
1587
+ /** @deprecated Use {@link CancelBulkRoleAssign}. */
1314
1588
  cancelBulkRoleAssign() {
1315
- this.showBulkRoleAssign = false;
1316
- this.bulkRoleId = '';
1589
+ return this.CancelBulkRoleAssign();
1317
1590
  }
1318
- async executeBulkRoleAssign() {
1319
- if (!this.bulkRoleId || !this.hasSelection)
1591
+ async ExecuteBulkRoleAssign() {
1592
+ if (!this.BulkRoleId || !this.HasSelection)
1320
1593
  return;
1321
1594
  try {
1322
1595
  this.isLoading = true;
1323
- const selectedUserIds = Array.from(this.selectedUserIds);
1596
+ const selectedUserIds = Array.from(this.SelectedUserIds);
1324
1597
  // Collect only the users that don't already have the role
1325
1598
  const usersNeedingRole = selectedUserIds.filter(userId => {
1326
1599
  const existingRoles = this.userRoleMap.get(userId) || [];
1327
- return !existingRoles.includes(this.bulkRoleId);
1600
+ return !existingRoles.includes(this.BulkRoleId);
1328
1601
  });
1329
1602
  if (usersNeedingRole.length > 0) {
1330
1603
  const tg = await this.metadata.CreateTransactionGroup();
@@ -1337,13 +1610,15 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1337
1610
  // (issue #4282) lives in `@memberjunction/core-entities-server`, which no browser package
1338
1611
  // depends on, so it never registers here — it refuses on the server, during Submit().
1339
1612
  //
1340
- // And today that refusal reaches the user NOWHERE. `ExecuteTransactionGroup` discards the
1341
- // refused row's `Save()` return, so the row never enrols in the SERVER's group either; an
1342
- // all-refused batch submits an empty group, whose `Submit()` returns true for having
1343
- // nothing to do, and the screen closes with no message at all. Verified end to end against
1344
- // a live server: a non-Owner assigning a role they do not hold is correctly refused (no row
1345
- // is written) and is reported as success. Tracked as issue #4309 — fixing it belongs in the
1346
- // resolver, which is the only layer that still knows which row was refused and why.
1613
+ // That server refusal used to reach the user NOWHERE: `ExecuteTransactionGroup` discarded
1614
+ // the refused row's `Save()` return, so the row never enrolled in the SERVER's group
1615
+ // either; an all-refused batch submitted an empty group, whose `Submit()` returns true for
1616
+ // having nothing to do, and the screen closed reporting success having written nothing.
1617
+ // Issue #4309 fixed that in the resolver — the only layer that still knows which row was
1618
+ // refused and why. It now reports the refusal, `Submit()` returns FALSE, and
1619
+ // `GraphQLTransactionGroup.recordServerFailure` copies the server's reason onto each
1620
+ // item's `BaseEntity.LatestResult`. The `!await tg.Submit()` branch below reads it back;
1621
+ // that is the only place a server-side refusal surfaces on this screen.
1347
1622
  //
1348
1623
  // What this check DOES catch is a CLIENT-side refusal — a CheckPermissions denial or a
1349
1624
  // field-rule failure — which really does return false here, leaving that row unenrolled.
@@ -1351,15 +1626,22 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1351
1626
  // Submit() returns true for having nothing to do, so the screen reported success having
1352
1627
  // assigned nothing. That is the failure this guards; keep it.
1353
1628
  const refusals = [];
1629
+ const enrolled = [];
1354
1630
  for (const userId of usersNeedingRole) {
1355
1631
  const userRole = await this.metadata.GetEntityObject('MJ: User Roles');
1356
1632
  userRole.NewRecord();
1357
1633
  userRole.UserID = userId;
1358
- userRole.RoleID = this.bulkRoleId;
1634
+ userRole.RoleID = this.BulkRoleId;
1359
1635
  userRole.TransactionGroup = tg;
1360
1636
  if (!await userRole.Save()) {
1361
1637
  refusals.push(`${this.describeUser(userId)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1362
1638
  }
1639
+ else {
1640
+ // Kept so the SERVER's reason can be read back off it below. Without this the entity
1641
+ // goes out of scope at the end of the iteration and the reason #4309 puts on
1642
+ // LatestResult has nobody left to read it.
1643
+ enrolled.push({ Label: this.describeUser(userId), Entity: userRole });
1644
+ }
1363
1645
  }
1364
1646
  if (refusals.length > 0) {
1365
1647
  // Nothing was written: the refused rows never enrolled, and the rest are still only
@@ -1367,13 +1649,18 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1367
1649
  throw new Error(`Failed to assign roles — nothing was changed.\n${refusals.join('\n')}`);
1368
1650
  }
1369
1651
  if (!await tg.Submit()) {
1370
- throw new Error('Failed to assign roles — all changes have been rolled back');
1652
+ // Every row enrolled, so this is a SERVER-side refusal (or a rollback). Since #4309 the
1653
+ // server says which row and why, and that reason is now on each entity's LatestResult.
1654
+ const reasons = ServerRefusalReasons(enrolled);
1655
+ throw new Error(reasons.length > 0
1656
+ ? `Failed to assign roles — all changes have been rolled back.\n${reasons.join('\n')}`
1657
+ : 'Failed to assign roles — all changes have been rolled back');
1371
1658
  }
1372
1659
  }
1373
- this.clearSelection();
1374
- this.showBulkRoleAssign = false;
1375
- this.bulkRoleId = '';
1376
- await this.loadInitialData();
1660
+ this.ClearSelection();
1661
+ this.ShowBulkRoleAssign = false;
1662
+ this.BulkRoleId = '';
1663
+ await this.LoadInitialData();
1377
1664
  }
1378
1665
  catch (error) {
1379
1666
  console.error('Bulk role assignment failed:', error);
@@ -1389,9 +1676,13 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1389
1676
  });
1390
1677
  }
1391
1678
  }
1392
- getBulkActionMessage() {
1393
- const count = this.selectedCount;
1394
- switch (this.bulkActionType) {
1679
+ /** @deprecated Use {@link ExecuteBulkRoleAssign}. */
1680
+ async executeBulkRoleAssign() {
1681
+ return this.ExecuteBulkRoleAssign();
1682
+ }
1683
+ GetBulkActionMessage() {
1684
+ const count = this.SelectedCount;
1685
+ switch (this.BulkActionType) {
1395
1686
  case 'enable':
1396
1687
  return `Are you sure you want to enable ${count} user${count > 1 ? 's' : ''}?`;
1397
1688
  case 'disable':
@@ -1402,8 +1693,12 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1402
1693
  return '';
1403
1694
  }
1404
1695
  }
1405
- getBulkActionTitle() {
1406
- switch (this.bulkActionType) {
1696
+ /** @deprecated Use {@link GetBulkActionMessage}. */
1697
+ getBulkActionMessage() {
1698
+ return this.GetBulkActionMessage();
1699
+ }
1700
+ GetBulkActionTitle() {
1701
+ switch (this.BulkActionType) {
1407
1702
  case 'enable':
1408
1703
  return 'Enable Users';
1409
1704
  case 'disable':
@@ -1414,9 +1709,13 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1414
1709
  return 'Confirm Action';
1415
1710
  }
1416
1711
  }
1417
- getBulkActionButtonText() {
1418
- const count = this.selectedCount;
1419
- switch (this.bulkActionType) {
1712
+ /** @deprecated Use {@link GetBulkActionTitle}. */
1713
+ getBulkActionTitle() {
1714
+ return this.GetBulkActionTitle();
1715
+ }
1716
+ GetBulkActionButtonText() {
1717
+ const count = this.SelectedCount;
1718
+ switch (this.BulkActionType) {
1420
1719
  case 'enable':
1421
1720
  return `Enable ${count} User${count > 1 ? 's' : ''}`;
1422
1721
  case 'disable':
@@ -1427,8 +1726,12 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1427
1726
  return 'Confirm';
1428
1727
  }
1429
1728
  }
1729
+ /** @deprecated Use {@link GetBulkActionButtonText}. */
1730
+ getBulkActionButtonText() {
1731
+ return this.GetBulkActionButtonText();
1732
+ }
1430
1733
  // Expansion methods
1431
- toggleUserExpansion(userId) {
1734
+ ToggleUserExpansion(userId) {
1432
1735
  if (this.expandedUserIds.has(userId)) {
1433
1736
  this.expandedUserIds.delete(userId);
1434
1737
  }
@@ -1436,20 +1739,41 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1436
1739
  this.expandedUserIds.add(userId);
1437
1740
  }
1438
1741
  }
1439
- isUserExpanded(userId) {
1742
+ /** @deprecated Use {@link ToggleUserExpansion}. */
1743
+ toggleUserExpansion(userId) {
1744
+ return this.ToggleUserExpansion(userId);
1745
+ }
1746
+ IsUserExpanded(userId) {
1440
1747
  return this.expandedUserIds.has(userId);
1441
1748
  }
1749
+ /** @deprecated Use {@link IsUserExpanded}. */
1750
+ isUserExpanded(userId) {
1751
+ return this.IsUserExpanded(userId);
1752
+ }
1442
1753
  // Get roles for a specific user
1754
+ GetUserRoles(userId) {
1755
+ const roleIds = this.userRoleMap.get(userId);
1756
+ if (!roleIds || roleIds.length === 0) {
1757
+ return [];
1758
+ }
1759
+ // Normalize the user's role IDs into a Set rather than running a nested `.some(UUIDsEqual(...))`
1760
+ // per role: UUIDsEqual's `===` fast path misses whenever the IDs differ, so every miss allocated
1761
+ // two lowercased strings and the nested scan paid that roles x userRoles times. The template
1762
+ // calls this TWICE per user row (the @if and the @for), on every change-detection pass. Filtering
1763
+ // `this.Roles` still drives the result, so the rendered order is unchanged.
1764
+ const assignedIds = new Set(roleIds.map(id => NormalizeUUID(id)));
1765
+ return this.Roles.filter(role => assignedIds.has(NormalizeUUID(role.ID)));
1766
+ }
1767
+ /** @deprecated Use {@link GetUserRoles}. */
1443
1768
  getUserRoles(userId) {
1444
- const roleIds = this.userRoleMap.get(userId) || [];
1445
- return this.roles.filter(role => roleIds.some(id => UUIDsEqual(id, role.ID)));
1769
+ return this.GetUserRoles(userId);
1446
1770
  }
1447
1771
  /**
1448
1772
  * Names a user for an error message. Falls back to the raw ID rather than to a placeholder so a
1449
1773
  * refusal for a user who has dropped out of the loaded page is still traceable.
1450
1774
  */
1451
1775
  describeUser(userId) {
1452
- const user = this.users.find(u => UUIDsEqual(u.ID, userId));
1776
+ const user = this.Users.find(u => UUIDsEqual(u.ID, userId));
1453
1777
  return user?.Email ?? user?.Name ?? userId;
1454
1778
  }
1455
1779
  static ɵfac = function UserManagementComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || UserManagementComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone)); };
@@ -1529,5 +1853,5 @@ export { UserManagementComponent };
1529
1853
  type: Component,
1530
1854
  args: [{ standalone: false, selector: 'mj-user-management', template: "<!--\n Interior chrome \u2014 <mj-page-header-interior> is the body-level sibling of\n <mj-page-header>. Same slot conventions ([toolbar] / [meta] / [actions]);\n different visual shape (single-row card). See Section 10 of\n explorer-chrome-conventions.\n-->\n<mj-page-header-interior\n AriaLabel=\"Filter users\"\n Title=\"Users\"\n Subtitle=\"Manage user accounts, roles, and access\">\n <!-- X-of-Y filtered count earns its meta spot per chrome conventions \u00A72 \u2014\n tells the user how the search/filter is narrowing the data, signal at\n any scale. Partition badges (active/inactive/owners) stay dropped \u2014\n they 1:1 mirror the filter chips in [toolbar] below. -->\n <div meta>\n <mj-stat-badge\n [Count]=\"filteredUsers.length\"\n [Total]=\"users.length\"\n Label=\"users\">\n </mj-stat-badge>\n </div>\n <div toolbar>\n <!-- Concise-chrome control bar: search \u00B7 Filter. Status + Role live behind\n the one Filter button; applied filters show as removable chips below\n the card. On mobile the popover docks as a bottom sheet. -->\n <mj-page-search\n Placeholder=\"Search users by name or email...\"\n [Value]=\"filters$.value.search\"\n (ValueChange)=\"updateFilter({ search: $event })\">\n </mj-page-search>\n <mj-filter-popover\n Label=\"Filters\"\n Icon=\"fa-solid fa-filter\"\n [ActiveCount]=\"TotalActiveFilterCount\"\n [ShowClearAll]=\"TotalActiveFilterCount > 0\"\n (ClearAllRequested)=\"clearAllAppliedFilters()\">\n <mj-filter-panel\n [Fields]=\"filterFields\"\n [Values]=\"filterValues\"\n (ValuesChange)=\"onFilterPanelChange($event)\"\n (Reset)=\"clearAllAppliedFilters()\">\n </mj-filter-panel>\n </mj-filter-popover>\n </div>\n <div actions>\n <mj-refresh-button [Loading]=\"isLoading\" (Clicked)=\"refreshData()\"></mj-refresh-button>\n <button mjButton variant=\"secondary\" size=\"sm\" (click)=\"exportUsers()\" aria-label=\"Export users to file\">\n <i class=\"fa-solid fa-download\"></i> <span class=\"mj-action-label\">Export</span>\n </button>\n <button mjButton variant=\"primary\" size=\"sm\" (click)=\"createNewUser()\" aria-label=\"Add new user\">\n <i class=\"fa-solid fa-plus\"></i> <span class=\"mj-action-label\">Add User</span>\n </button>\n </div>\n</mj-page-header-interior>\n\n <!-- Bulk Action Toolbar \u2014 contextual band shown above the list when users are selected. -->\n @if (hasSelection) {\n <div class=\"bulk-action-toolbar\" role=\"toolbar\" aria-label=\"Bulk actions\">\n <div class=\"bulk-selection-info\">\n <i class=\"fa-solid fa-check-square\" aria-hidden=\"true\"></i>\n <span>{{ selectedCount }} user{{ selectedCount > 1 ? 's' : '' }} selected</span>\n <button mjButton variant=\"flat\" size=\"sm\" (click)=\"clearSelection()\" aria-label=\"Clear selection\">\n <i class=\"fa-solid fa-times\" aria-hidden=\"true\"></i>\n Clear\n </button>\n </div>\n <div class=\"bulk-action-buttons\">\n <button mjButton variant=\"secondary\" size=\"sm\" (click)=\"confirmBulkAction('enable')\" aria-label=\"Enable selected users\">\n <i class=\"fa-solid fa-toggle-on\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Enable</span>\n </button>\n <button mjButton variant=\"secondary\" size=\"sm\" (click)=\"confirmBulkAction('disable')\" aria-label=\"Disable selected users\">\n <i class=\"fa-solid fa-toggle-off\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Disable</span>\n </button>\n <button mjButton variant=\"secondary\" size=\"sm\" (click)=\"openBulkRoleAssign()\" aria-label=\"Assign role to selected users\">\n <i class=\"fa-solid fa-user-tag\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Assign Role</span>\n </button>\n <button mjButton variant=\"danger\" size=\"sm\" (click)=\"confirmBulkAction('delete')\" aria-label=\"Delete selected users\">\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Delete</span>\n </button>\n </div>\n </div>\n }\n\n <!-- Scrollable Content Section \u2014 owned by <mj-page-body-interior>. -->\n <mj-page-body-interior>\n <!-- Loading State -->\n @if (isLoading) {\n <div class=\"loading-container\" role=\"status\" aria-live=\"polite\" aria-busy=\"true\">\n <mj-loading text=\"Loading users...\" size=\"medium\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (error && !isLoading) {\n <div class=\"error-container\" role=\"alert\" aria-live=\"assertive\">\n <div class=\"error-content\">\n <i class=\"fa-solid fa-exclamation-triangle error-icon\" aria-hidden=\"true\"></i>\n <p class=\"error-message\">{{ error }}</p>\n <button class=\"retry-button\" (click)=\"loadInitialData()\" aria-label=\"Retry loading users\">\n <i class=\"fa-solid fa-refresh\" aria-hidden=\"true\"></i>\n Try Again\n </button>\n </div>\n </div>\n }\n\n <!-- Users List -->\n @if (!isLoading && !error) {\n <div class=\"users-list\" role=\"list\" aria-label=\"Users list\">\n <!-- List Header with Select All -->\n <div class=\"list-header\">\n <label class=\"select-all-label\">\n <input\n type=\"checkbox\"\n class=\"checkbox\"\n [checked]=\"isAllSelected\"\n [indeterminate]=\"isIndeterminate\"\n (change)=\"toggleSelectAll()\"\n aria-label=\"Select all users\"\n />\n <span class=\"select-all-text\">Select All ({{ filteredUsers.length }})</span>\n </label>\n @if (hasSelection) {\n <span class=\"selection-count\">{{ selectedCount }} selected</span>\n }\n </div>\n\n @for (user of filteredUsers; track user.ID) {\n <div\n class=\"user-card\"\n [class.expanded]=\"isUserExpanded(user.ID)\"\n [class.selected]=\"isUserSelected(user.ID)\"\n role=\"listitem\"\n >\n <!-- User Card Header - Always Visible -->\n <div\n class=\"user-header\"\n (click)=\"toggleUserExpansion(user.ID)\"\n role=\"button\"\n [attr.aria-expanded]=\"isUserExpanded(user.ID)\"\n tabindex=\"0\"\n (keydown.enter)=\"toggleUserExpansion(user.ID)\"\n >\n <!-- Selection Checkbox -->\n <div class=\"user-selection\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n class=\"checkbox\"\n [checked]=\"isUserSelected(user.ID)\"\n (change)=\"toggleUserSelection(user.ID, $event)\"\n [attr.aria-label]=\"'Select ' + user.Name\"\n />\n </div>\n\n <!-- User Info -->\n <div class=\"user-info\">\n <div class=\"user-avatar\" aria-hidden=\"true\">\n {{ getUserInitials(user) }}\n </div>\n <div class=\"user-details\">\n <h3 class=\"user-name\">{{ user.Name }}</h3>\n <p class=\"user-email\">{{ user.Email }}</p>\n </div>\n </div>\n\n <!-- User Meta -->\n <div class=\"user-meta\">\n <span class=\"status-badge\" [class]=\"getStatusClass(user)\">\n <i [class]=\"'fa-solid ' + getStatusIcon(user)\" aria-hidden=\"true\"></i>\n {{ user.IsActive ? 'Active' : 'Inactive' }}\n </span>\n <div class=\"user-actions desktop-only\" (click)=\"$event.stopPropagation()\">\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"editUser(user)\"\n [attr.aria-label]=\"'Edit ' + user.Name\"\n title=\"Edit\"\n >\n <i class=\"fa-solid fa-edit\" aria-hidden=\"true\"></i>\n </button>\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"toggleUserStatus(user)\"\n [attr.aria-label]=\"(user.IsActive ? 'Deactivate ' : 'Activate ') + user.Name\"\n [title]=\"user.IsActive ? 'Deactivate' : 'Activate'\"\n >\n <i [class]=\"user.IsActive ? 'fa-solid fa-toggle-on' : 'fa-solid fa-toggle-off'\" aria-hidden=\"true\"></i>\n </button>\n <button\n mjButton variant=\"danger\" size=\"sm\"\n (click)=\"confirmDeleteUser(user)\"\n [attr.aria-label]=\"'Delete ' + user.Name\"\n title=\"Delete\"\n >\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button class=\"expand-btn\" aria-label=\"Toggle user details\">\n <i class=\"fa-solid fa-chevron-down\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n\n <!-- User Card Content - Expanded -->\n @if (isUserExpanded(user.ID)) {\n <div class=\"user-content\">\n <!-- Mobile Actions Bar -->\n <div class=\"mobile-actions-bar\" (click)=\"$event.stopPropagation()\">\n <span class=\"status-badge\" [class]=\"getStatusClass(user)\">\n <i [class]=\"'fa-solid ' + getStatusIcon(user)\" aria-hidden=\"true\"></i>\n {{ user.IsActive ? 'Active' : 'Inactive' }}\n </span>\n <div class=\"mobile-action-buttons\">\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"editUser(user)\"\n [attr.aria-label]=\"'Edit ' + user.Name\"\n >\n <i class=\"fa-solid fa-edit\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">Edit</span>\n </button>\n <button\n mjButton variant=\"flat\" size=\"sm\"\n (click)=\"toggleUserStatus(user)\"\n [attr.aria-label]=\"(user.IsActive ? 'Deactivate ' : 'Activate ') + user.Name\"\n >\n <i [class]=\"user.IsActive ? 'fa-solid fa-toggle-on' : 'fa-solid fa-toggle-off'\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">{{ user.IsActive ? 'Disable' : 'Enable' }}</span>\n </button>\n <button\n mjButton variant=\"danger\" size=\"sm\"\n (click)=\"confirmDeleteUser(user)\"\n [attr.aria-label]=\"'Delete ' + user.Name\"\n >\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n <span class=\"btn-label\">Delete</span>\n </button>\n </div>\n </div>\n\n <!-- User Stats -->\n <div class=\"user-stats\">\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-user-tag\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Type:</span>\n <span class=\"stat-value\">\n <i [class]=\"'fa-solid ' + getUserTypeIcon(user)\" aria-hidden=\"true\"></i>\n {{ user.Type }}\n </span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-id-card\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Full Name:</span>\n <span class=\"stat-value\">{{ user.FirstName }} {{ user.LastName }}</span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-calendar\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Created:</span>\n <span class=\"stat-value\">{{ user.__mj_CreatedAt | date:'short' }}</span>\n </div>\n <div class=\"stat-item\">\n <i class=\"fa-solid fa-clock\" aria-hidden=\"true\"></i>\n <span class=\"stat-label\">Updated:</span>\n <span class=\"stat-value\">{{ user.__mj_UpdatedAt | date:'short' }}</span>\n </div>\n </div>\n\n <!-- User Roles Section -->\n <div class=\"roles-section\">\n <h4 class=\"section-title\">\n <i class=\"fa-solid fa-user-shield\" aria-hidden=\"true\"></i>\n Assigned Roles\n </h4>\n @if (getUserRoles(user.ID).length > 0) {\n <div class=\"roles-grid\">\n @for (role of getUserRoles(user.ID); track role.ID) {\n <div class=\"role-item\">\n <i class=\"fa-solid fa-shield\" aria-hidden=\"true\"></i>\n {{ role.Name }}\n </div>\n }\n </div>\n } @else {\n <p class=\"no-roles\">No roles assigned to this user</p>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @if (filteredUsers.length === 0) {\n <mj-empty-state\n [Variant]=\"IsListNarrowed ? 'no-results' : 'empty'\"\n Icon=\"fa-solid fa-users-slash\"\n Title=\"No users found\"\n Message=\"Try adjusting your filters or add a new user\"\n [ActionText]=\"IsListNarrowed ? 'Reset filters' : ''\"\n ActionIcon=\"fa-solid fa-rotate-left\"\n (Action)=\"resetAllFiltersAndSearch()\" />\n }\n </div>\n }\n </mj-page-body-interior>\n\n <!-- User Create/Edit Dialog -->\n <mj-user-dialog\n [data]=\"userDialogData\"\n [visible]=\"showUserDialog\"\n (result)=\"onUserDialogResult($event)\">\n </mj-user-dialog>\n\n <!-- Delete Confirmation Dialog -->\n @if (showDeleteConfirm && selectedUser) {\n <div\n class=\"modal-backdrop\"\n (click)=\"showDeleteConfirm = false\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"delete-dialog-title\"\n aria-describedby=\"delete-dialog-desc\"\n >\n <div class=\"modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3 class=\"modal-title\" id=\"delete-dialog-title\">\n <i class=\"fa-solid fa-exclamation-triangle text-danger\" aria-hidden=\"true\"></i>\n Confirm Delete\n </h3>\n <button\n class=\"modal-close\"\n (click)=\"showDeleteConfirm = false\"\n aria-label=\"Close dialog\"\n >\n <i class=\"fa-solid fa-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"modal-body\" id=\"delete-dialog-desc\">\n <p>Are you sure you want to delete user <strong>{{ selectedUser.Name }}</strong>?</p>\n <p class=\"text-muted\">This action cannot be undone.</p>\n </div>\n <div class=\"modal-footer\">\n <button mjButton variant=\"danger\" (click)=\"deleteUser()\">\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n Delete User\n </button>\n <button mjButton (click)=\"showDeleteConfirm = false\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Bulk Action Confirmation Dialog -->\n @if (showBulkActionConfirm) {\n <div\n class=\"modal-backdrop\"\n (click)=\"cancelBulkAction()\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"bulk-action-dialog-title\"\n aria-describedby=\"bulk-action-dialog-desc\"\n >\n <div class=\"modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3 class=\"modal-title\" id=\"bulk-action-dialog-title\">\n @if (bulkActionType === 'delete') {\n <i class=\"fa-solid fa-exclamation-triangle text-danger\" aria-hidden=\"true\"></i>\n } @else if (bulkActionType === 'enable') {\n <i class=\"fa-solid fa-toggle-on text-success\" aria-hidden=\"true\"></i>\n } @else {\n <i class=\"fa-solid fa-toggle-off text-warning\" aria-hidden=\"true\"></i>\n }\n {{ getBulkActionTitle() }}\n </h3>\n <button\n class=\"modal-close\"\n (click)=\"cancelBulkAction()\"\n aria-label=\"Close dialog\"\n >\n <i class=\"fa-solid fa-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"modal-body\" id=\"bulk-action-dialog-desc\">\n <p>{{ getBulkActionMessage() }}</p>\n @if (bulkActionType === 'delete') {\n <p class=\"text-muted\">This action cannot be undone.</p>\n }\n </div>\n <div class=\"modal-footer\">\n <button mjButton\n [variant]=\"bulkActionType === 'delete' ? 'danger' : 'primary'\"\n (click)=\"executeBulkAction()\"\n [disabled]=\"isLoading\"\n >\n @if (isLoading) {\n <i class=\"fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i>\n Processing...\n } @else {\n @if (bulkActionType === 'delete') {\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n } @else if (bulkActionType === 'enable') {\n <i class=\"fa-solid fa-toggle-on\" aria-hidden=\"true\"></i>\n } @else {\n <i class=\"fa-solid fa-toggle-off\" aria-hidden=\"true\"></i>\n }\n {{ getBulkActionButtonText() }}\n }\n </button>\n <button mjButton (click)=\"cancelBulkAction()\" [disabled]=\"isLoading\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Bulk Role Assignment Dialog -->\n @if (showBulkRoleAssign) {\n <div\n class=\"modal-backdrop\"\n (click)=\"cancelBulkRoleAssign()\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"bulk-role-dialog-title\"\n aria-describedby=\"bulk-role-dialog-desc\"\n >\n <div class=\"modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3 class=\"modal-title\" id=\"bulk-role-dialog-title\">\n <i class=\"fa-solid fa-user-tag\" aria-hidden=\"true\"></i>\n Assign Role to {{ selectedCount }} User{{ selectedCount > 1 ? 's' : '' }}\n </h3>\n <button\n class=\"modal-close\"\n (click)=\"cancelBulkRoleAssign()\"\n aria-label=\"Close dialog\"\n >\n <i class=\"fa-solid fa-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <div class=\"modal-body\" id=\"bulk-role-dialog-desc\">\n <p>Select a role to assign to the selected users:</p>\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"bulk-role-select\">Role</label>\n <select\n id=\"bulk-role-select\"\n class=\"field-select\"\n [(ngModel)]=\"bulkRoleId\"\n aria-label=\"Select role to assign\"\n >\n <option value=\"\">Select a role...</option>\n @for (role of roles; track role.ID) {\n <option [value]=\"role.ID\">{{ role.Name }}</option>\n }\n </select>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n mjButton variant=\"primary\"\n (click)=\"executeBulkRoleAssign()\"\n [disabled]=\"!bulkRoleId || isLoading\"\n >\n @if (isLoading) {\n <i class=\"fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i>\n Assigning...\n } @else {\n <i class=\"fa-solid fa-user-tag\" aria-hidden=\"true\"></i>\n Assign Role\n }\n </button>\n <button mjButton (click)=\"cancelBulkRoleAssign()\" [disabled]=\"isLoading\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n }\n", styles: ["/* =============================================================================\n Shared Admin Patterns - MJ Design System\n Common styles shared across all admin settings components.\n Component-specific CSS files override these values where needed.\n ============================================================================= */\n\n/* -----------------------------------------------------------------------------\n Host & Container\n ----------------------------------------------------------------------------- */\n:host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n/* -----------------------------------------------------------------------------\n Sticky Header\n ----------------------------------------------------------------------------- */\n.sticky-header {\n flex-shrink: 0;\n background: var(--mj-bg-page);\n border-bottom: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n}\n\n/* -----------------------------------------------------------------------------\n Scrollable Content\n ----------------------------------------------------------------------------- */\n.scrollable-content {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n -webkit-overflow-scrolling: touch;\n padding: 1rem;\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (min-width: 768px) {\n .scrollable-content {\n padding: 1.5rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .scrollable-content {\n padding: 2rem;\n }\n}\n\n@media (min-width: 1440px) {\n .scrollable-content {\n padding: 2rem 2.5rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Action Buttons (In Sticky Header)\n ----------------------------------------------------------------------------- */\n.action-buttons {\n flex-shrink: 0;\n display: flex;\n gap: 0.75rem;\n justify-content: flex-end;\n padding: 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .action-buttons {\n padding: 1rem 1.5rem;\n }\n}\n\n@media (max-width: 639px) {\n .action-buttons {\n justify-content: center;\n flex-wrap: wrap;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Statistics Cards Grid - Static Display (Non-interactive)\n ----------------------------------------------------------------------------- */\n.mj-grid-4 {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.5rem;\n padding: 0 1rem 0.75rem 1rem;\n background: var(--mj-bg-page);\n}\n\n@media (min-width: 768px) {\n .mj-grid-4 {\n grid-template-columns: repeat(4, 1fr);\n padding: 0 1.5rem 1rem 1.5rem;\n gap: 0.75rem;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-grid-4 {\n gap: 1rem;\n }\n}\n\n/* Static Card - No hover effects */\n.mj-card {\n background: var(--mj-bg-surface-card);\n border-radius: var(--mj-radius-lg);\n padding: 1rem;\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n gap: 1rem;\n border: 1px solid var(--mj-border-default);\n cursor: default;\n pointer-events: none;\n}\n\n@media (min-width: 768px) {\n .mj-card {\n padding: 1rem 1.25rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Stat Icon Containers\n ----------------------------------------------------------------------------- */\n.stat-icon {\n width: 48px;\n height: 48px;\n border-radius: var(--mj-radius-lg);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.25rem;\n flex-shrink: 0;\n}\n\n@media (min-width: 768px) {\n .stat-icon {\n width: 52px;\n height: 52px;\n font-size: 1.375rem;\n }\n}\n\n.stat-icon-total {\n background: var(--mj-color-accent-300);\n color: var(--mj-brand-primary);\n}\n\n.stat-icon-active {\n background: var(--mj-color-success-100);\n color: var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Stat Content Typography\n ----------------------------------------------------------------------------- */\n.stat-content {\n flex: 1;\n min-width: 0;\n}\n\n.stat-content .stat-value {\n font-size: 1.75rem;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 768px) {\n .stat-content .stat-value {\n font-size: 2rem;\n }\n}\n\n.stat-content .stat-label {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-top: 0.25rem;\n}\n\n@media (min-width: 768px) {\n .stat-content .stat-label {\n font-size: 0.8125rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Filters Section\n ----------------------------------------------------------------------------- */\n.filters-section {\n flex-shrink: 0;\n background: var(--mj-bg-surface-card);\n margin: 0 1rem 0.75rem 1rem;\n padding: 0.75rem;\n border-radius: var(--mj-radius-lg);\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .filters-section {\n margin: 0 1.5rem 1rem 1.5rem;\n padding: 1rem 1.25rem;\n }\n}\n\n.filters-row {\n display: flex;\n gap: 1rem;\n align-items: flex-end;\n flex-wrap: wrap;\n}\n\n@media (min-width: 768px) {\n .filters-row {\n gap: 1.5rem;\n }\n}\n\n.mj-filter-group {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.mj-filter-label {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n.mj-filter-buttons {\n display: flex;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-full);\n padding: 4px;\n gap: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Search Input\n ----------------------------------------------------------------------------- */\n.mj-search {\n position: relative;\n flex: 1;\n min-width: 200px;\n}\n\n@media (min-width: 640px) {\n .mj-search {\n min-width: 280px;\n }\n}\n\n@media (min-width: 1024px) {\n .mj-search {\n min-width: 400px;\n max-width: 600px;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search {\n min-width: 500px;\n max-width: 800px;\n }\n}\n\n.mj-search .mj-search-icon {\n position: absolute;\n left: 1rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-secondary);\n font-size: 1rem;\n pointer-events: none;\n transition: color 0.2s ease;\n}\n\n@media (min-width: 1024px) {\n .mj-search .mj-search-icon {\n left: 1.25rem;\n font-size: 1.125rem;\n }\n}\n\n.mj-search .mj-search-input {\n width: 100%;\n padding: 0.875rem 1rem 0.875rem 2.75rem;\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full);\n font-size: 1rem;\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n}\n\n@media (min-width: 1024px) {\n .mj-search .mj-search-input {\n padding: 1rem 1.25rem 1rem 3.25rem;\n font-size: 1.0625rem;\n }\n}\n\n@media (min-width: 1440px) {\n .mj-search .mj-search-input {\n padding: 1.125rem 1.5rem 1.125rem 3.5rem;\n }\n}\n\n.mj-search .mj-search-input::placeholder {\n color: var(--mj-text-secondary);\n}\n\n.mj-search .mj-search-input:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.mj-search .mj-search-input:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.2);\n background: var(--mj-bg-page);\n}\n\n.mj-search:focus-within .mj-search-icon {\n color: var(--mj-brand-primary);\n}\n\n/* -----------------------------------------------------------------------------\n Content Area\n ----------------------------------------------------------------------------- */\n.content-area {\n flex: 1 1 auto;\n overflow: visible;\n position: relative;\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-xl);\n box-shadow: var(--mj-shadow-sm);\n padding: 1rem;\n border: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .content-area {\n padding: 1.5rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Expand Button\n ----------------------------------------------------------------------------- */\n.expand-btn {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n transition: all 0.2s ease;\n border-radius: var(--mj-radius-full);\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.expand-btn:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.expand-btn i {\n transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* -----------------------------------------------------------------------------\n Status Badge\n ----------------------------------------------------------------------------- */\n.status-badge {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.status-badge.status-active {\n background: var(--mj-color-success-100);\n color: var(--mj-color-success-800);\n border: 1px solid var(--mj-status-success);\n}\n\n.status-badge.status-inactive {\n background: var(--mj-color-error-100);\n color: var(--mj-status-error);\n border: 1px solid var(--mj-status-error);\n}\n\n.status-badge i {\n font-size: 0.625rem;\n}\n\n/* -----------------------------------------------------------------------------\n Mobile Actions Bar\n ----------------------------------------------------------------------------- */\n.mobile-actions-bar {\n display: none;\n}\n\n.mobile-action-buttons {\n display: flex;\n gap: 0.5rem;\n}\n\n/* -----------------------------------------------------------------------------\n Empty State\n ----------------------------------------------------------------------------- */\n.empty-state {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.empty-state .empty-icon {\n font-size: 4rem;\n color: var(--mj-border-default);\n margin-bottom: 1.5rem;\n}\n\n.empty-state .empty-text {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.5rem 0;\n}\n\n.empty-state .empty-subtext {\n font-size: 1rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Loading State\n ----------------------------------------------------------------------------- */\n.loading-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 4rem 2rem;\n}\n\n/* -----------------------------------------------------------------------------\n Error State\n ----------------------------------------------------------------------------- */\n.error-container {\n text-align: center;\n padding: 4rem 2rem;\n}\n\n.error-container .error-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.error-container .error-icon {\n font-size: 3.5rem;\n color: var(--mj-status-error);\n margin-bottom: 1rem;\n}\n\n.error-container .error-message {\n font-size: 1.0625rem;\n color: var(--mj-text-primary);\n margin: 0 0 1.5rem 0;\n}\n\n/* -----------------------------------------------------------------------------\n Modal Dialog\n ----------------------------------------------------------------------------- */\n.modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 1rem;\n animation: fadeIn 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-dialog {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl);\n box-shadow: var(--mj-shadow-2xl);\n max-width: 500px;\n width: 100%;\n max-height: 90vh;\n overflow: hidden;\n animation: slideUp 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n.modal-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.modal-header .modal-title {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.375rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.modal-header .modal-close {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.modal-header .modal-close:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.modal-body {\n padding: 1.5rem;\n}\n\n.modal-body p {\n font-size: 1rem;\n color: var(--mj-text-primary);\n margin: 0 0 1rem 0;\n line-height: 1.5;\n}\n\n.modal-body p:last-child {\n margin-bottom: 0;\n}\n\n.modal-footer {\n display: flex;\n justify-content: flex-start;\n gap: 0.75rem;\n padding: 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (max-width: 639px) {\n .modal-dialog {\n width: 95%;\n max-height: 85vh;\n }\n\n .modal-header {\n padding: 1rem;\n }\n\n .modal-header .modal-title {\n font-size: 1.125rem;\n }\n\n .modal-body {\n padding: 1rem;\n }\n\n .modal-footer {\n padding: 1rem;\n flex-direction: column;\n }\n\n}\n\n/* The legacy \"MJ Button System\" rule block (.mj-btn / .mj-btn-primary /\n .mj-btn-secondary / .mj-btn-ghost / .mj-btn-danger / .mj-btn-sm /\n .mj-btn-icon-only / .mj-btn-icon-mobile) was removed 2026-05-19. These\n rules pre-dated the mjButton directive standardization (the directive\n uses BEM modifiers like .mj-btn--primary, single-dash names like\n .mj-btn-primary don't match it anyway). The base .mj-btn rule WAS active\n though \u2014 its component-scoped specificity outranked button.scss's global\n directive styles, so admin sub-page buttons rendered pill-shaped + 44px\n regardless of the variant/size requested via mjButton inputs. Removing the\n rule lets every sub-page fall back to the directive's intended look. */\n\n/* -----------------------------------------------------------------------------\n Filter Button (Mobile Only)\n ----------------------------------------------------------------------------- */\n.filter-button {\n display: none;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-page);\n color: var(--mj-text-primary);\n border-radius: var(--mj-radius-full);\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 44px;\n min-width: 44px;\n}\n\n.filter-button:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface);\n}\n\n.filter-button:active {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-button i {\n font-size: 1rem;\n}\n\n/* -----------------------------------------------------------------------------\n Filter Modal (Mobile Bottom Sheet)\n ----------------------------------------------------------------------------- */\n.filter-modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 1000;\n animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n padding: 0;\n}\n\n.filter-modal {\n background: var(--mj-bg-page);\n border-radius: var(--mj-radius-2xl) var(--mj-radius-2xl) 0 0;\n box-shadow: var(--mj-shadow-2xl);\n width: 100%;\n max-height: 80vh;\n overflow: hidden;\n animation: slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n display: flex;\n flex-direction: column;\n}\n\n.filter-modal-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.filter-modal-title {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.filter-modal-title i {\n color: var(--mj-brand-primary);\n font-size: 1.375rem;\n}\n\n.filter-modal-close {\n padding: 0.625rem;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 1.25rem;\n cursor: pointer;\n border-radius: var(--mj-radius-full);\n transition: all 0.2s ease;\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.filter-modal-close:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n}\n\n.filter-modal-body {\n padding: 1.5rem;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n flex: 1;\n}\n\n.filter-modal-footer {\n display: flex;\n gap: 0.75rem;\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n flex-shrink: 0;\n}\n\n.filter-modal-footer button {\n flex: 1;\n margin: 0;\n}\n\n/* Filter Options Container (Inside Modal) */\n.filter-options-container {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.filter-group {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.filter-group-label {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0;\n}\n\n.filter-group-options {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.filter-option {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.875rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border: 2px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n min-height: 56px;\n}\n\n.filter-option:hover {\n border-color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.filter-option.selected {\n background: var(--mj-color-accent-300);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.filter-option input[type=\"checkbox\"],\n.filter-option input[type=\"radio\"] {\n width: 20px;\n height: 20px;\n cursor: pointer;\n accent-color: var(--mj-brand-primary);\n flex-shrink: 0;\n}\n\n.filter-option-label {\n flex: 1;\n font-size: 1rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.filter-option.selected .filter-option-label {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n/* -----------------------------------------------------------------------------\n Utility Classes\n ----------------------------------------------------------------------------- */\n.text-danger {\n color: var(--mj-status-error);\n}\n\n.text-warning {\n color: var(--mj-color-warning-500);\n}\n\n.text-success {\n color: var(--mj-status-success);\n}\n\n/* -----------------------------------------------------------------------------\n Animations\n ----------------------------------------------------------------------------- */\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes slideUp {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n@keyframes slideDown {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes slideUpFromBottom {\n from {\n transform: translateY(100%);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Accessibility: Focus Indicators\n ----------------------------------------------------------------------------- */\n.mj-search-input:focus-visible,\nbutton:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Accessibility: Reduced Motion\n ----------------------------------------------------------------------------- */\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n", "/* =============================================================================\n User Management Component - Component-Specific Styles\n Shared patterns are in ../shared/styles/_admin-patterns.css\n ============================================================================= */\n\n/* -----------------------------------------------------------------------------\n Status chip group inside <mj-page-header-interior>'s [toolbar] slot. Sits\n alongside the search input. Tight spacing distinguishes the chips visually\n from the [actions] cluster on the right.\n ----------------------------------------------------------------------------- */\n.status-chips {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n flex-shrink: 0;\n}\n\n/* No bespoke .mj-btn rules here. Buttons inherit the mjButton directive's\n global styles from button.scss \u2014 see \"Button Styling\" in packages/Angular/CLAUDE.md. */\n\n/* :host already provides flex column / 100% height / overflow hidden via\n _admin-patterns.css. No bespoke container wrapper here \u2014 see Section 10 of\n explorer-chrome-conventions for the standardized sub-page layout contract. */\n\n/* -----------------------------------------------------------------------------\n Stats Grid Override - Hidden until 768px (unlike other admin components)\n ----------------------------------------------------------------------------- */\n.mj-grid-4 {\n display: none;\n}\n\n@media (min-width: 768px) {\n .mj-grid-4 {\n display: grid;\n }\n}\n\n/* Stat Icon Override - Stays 48px at 768px (others go to 52px) */\n@media (min-width: 768px) {\n .stat-icon {\n width: 48px;\n height: 48px;\n font-size: 1.25rem;\n }\n}\n\n/* Stat Icon Override - color-mix background instead of accent-300 */\n.stat-icon-total {\n background: color-mix(in srgb, var(--mj-brand-accent) 25%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.stat-icon-inactive {\n background: var(--mj-color-error-100);\n color: var(--mj-status-error);\n}\n\n.stat-icon-admin {\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-700);\n}\n\n/* -----------------------------------------------------------------------------\n Bulk Action Toolbar (Unique to user-management)\n ----------------------------------------------------------------------------- */\n.bulk-action-toolbar {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n padding: 0.75rem 1rem;\n background: var(--mj-brand-accent-subtle);\n border-bottom: 1px solid var(--mj-border-default);\n animation: slideDown 0.2s ease;\n}\n\n@media (min-width: 768px) {\n .bulk-action-toolbar {\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding: 1rem 1.5rem;\n }\n}\n\n.bulk-selection-info {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.bulk-selection-info i {\n font-size: 1.25rem;\n}\n\n.bulk-action-buttons {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n\n@media (max-width: 639px) {\n .bulk-action-buttons .btn-text {\n display: none;\n }\n\n .bulk-action-buttons .mj-btn-sm {\n padding: 0.5rem;\n min-width: 36px;\n }\n}\n\n/* -----------------------------------------------------------------------------\n User Card - Interactive\n ----------------------------------------------------------------------------- */\n.users-list {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.list-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.75rem 1rem;\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md);\n border: 1px solid var(--mj-border-default);\n margin-bottom: 0.5rem;\n}\n\n.select-all-label {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n cursor: pointer;\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.select-all-text {\n font-weight: 500;\n}\n\n.selection-count {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-accent) 25%, transparent);\n padding: 0.25rem 0.75rem;\n border-radius: var(--mj-radius-full);\n}\n\n.user-card {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-xl);\n overflow: hidden;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.user-card:hover {\n box-shadow: var(--mj-shadow-md);\n border-color: var(--mj-brand-primary);\n}\n\n.user-card.expanded {\n box-shadow: var(--mj-shadow-lg);\n border-color: var(--mj-brand-primary);\n}\n\n.user-card.selected {\n background: var(--mj-brand-accent-subtle);\n border-color: var(--mj-brand-primary);\n}\n\n.user-card.selected .user-header {\n background: transparent;\n}\n\n.user-card.selected:hover .user-header {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n}\n\n.user-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 1.25rem 1.5rem;\n cursor: pointer;\n background: var(--mj-bg-surface);\n transition: background-color 0.2s ease;\n}\n\n.user-header:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n@media (max-width: 639px) {\n .user-header {\n padding: 0.875rem 1rem;\n gap: 0.625rem;\n }\n}\n\n.user-selection {\n flex-shrink: 0;\n}\n\n.checkbox {\n width: 20px;\n height: 20px;\n cursor: pointer;\n accent-color: var(--mj-brand-primary);\n}\n\n.user-info {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex: 1;\n min-width: 0;\n}\n\n.user-avatar {\n width: 48px;\n height: 48px;\n border-radius: var(--mj-radius-full);\n background: linear-gradient(135deg, var(--mj-brand-primary) 0%, var(--mj-brand-primary-light) 100%);\n color: var(--mj-brand-on-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.875rem;\n font-weight: 600;\n flex-shrink: 0;\n}\n\n@media (max-width: 639px) {\n .user-avatar {\n width: 40px;\n height: 40px;\n font-size: 0.8125rem;\n }\n\n .user-details {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n }\n\n .user-name {\n font-size: 0.9375rem;\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .user-email {\n font-size: 0.8125rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n}\n\n.user-details {\n flex: 1;\n min-width: 0;\n}\n\n.user-name {\n font-size: 1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.user-email {\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n margin: 0.125rem 0 0 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.user-meta {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-shrink: 0;\n}\n\n@media (max-width: 767px) {\n .user-meta .status-badge,\n .user-meta .user-actions {\n display: none !important;\n }\n\n .user-meta {\n gap: 0;\n }\n}\n\n.desktop-only {\n display: none;\n}\n\n@media (min-width: 768px) {\n .desktop-only {\n display: flex;\n gap: 0.25rem;\n }\n}\n\n.user-actions {\n display: flex;\n gap: 0.25rem;\n}\n\n.user-card.expanded .expand-btn i {\n transform: rotate(180deg);\n}\n\n/* User Card Content - Expanded */\n.user-content {\n padding: 1.25rem 1.5rem;\n background: var(--mj-bg-page);\n border-top: 1px solid var(--mj-border-default);\n animation: slideDown 0.3s cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* Mobile Actions Bar - visible on mobile */\n.mobile-actions-bar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 1rem;\n margin-bottom: 1rem;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n@media (min-width: 768px) {\n .mobile-actions-bar {\n display: none;\n }\n}\n\n.mobile-action-buttons .btn-label {\n margin-left: 0.375rem;\n}\n\n/* User Stats in Expanded Content */\n.user-stats {\n display: flex;\n flex-wrap: wrap;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n.user-stats .stat-item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n}\n\n.user-stats .stat-item i {\n color: var(--mj-brand-primary);\n font-size: 0.875rem;\n}\n\n.user-stats .stat-label {\n color: var(--mj-text-secondary);\n}\n\n.user-stats .stat-value {\n color: var(--mj-text-primary);\n font-weight: 500;\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.user-stats .stat-value i {\n font-size: 0.75rem;\n}\n\n/* Roles Section */\n.roles-section {\n padding-top: 1rem;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.section-title {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n margin: 0 0 0.75rem 0;\n}\n\n.section-title i {\n color: var(--mj-brand-primary);\n font-size: 1.125rem;\n}\n\n.roles-grid {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.role-item {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.875rem;\n background: var(--mj-color-warning-50);\n color: var(--mj-color-warning-800);\n border: 1px solid var(--mj-color-warning-500);\n border-radius: var(--mj-radius-full);\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n\n.role-item i {\n font-size: 0.625rem;\n}\n\n.no-roles {\n font-size: 0.875rem;\n color: var(--mj-text-secondary);\n margin: 0;\n}\n\n/* -----------------------------------------------------------------------------\n Error State Override - includes retry button\n ----------------------------------------------------------------------------- */\n.error-container .retry-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.75rem 1.5rem;\n font-size: 0.875rem;\n font-weight: 600;\n border: none;\n border-radius: var(--mj-radius-full);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n background: var(--mj-brand-primary);\n color: var(--mj-brand-on-primary);\n box-shadow: var(--mj-shadow-sm);\n min-height: 44px;\n}\n\n.error-container .retry-button:hover {\n background: var(--mj-brand-primary-light);\n box-shadow: var(--mj-shadow-md);\n}\n\n.error-container .retry-button:active {\n background: var(--mj-color-brand-400);\n transform: scale(0.98);\n}\n\n/* -----------------------------------------------------------------------------\n Form Field in Modal\n ----------------------------------------------------------------------------- */\n.form-field {\n margin-bottom: 1rem;\n}\n\n.form-field .field-label {\n display: block;\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 0.5rem;\n}\n\n.form-field .field-select {\n width: 100%;\n padding: 0.75rem 1rem;\n border: 1px solid var(--mj-color-neutral-400);\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-page);\n font-size: 1rem;\n color: var(--mj-text-primary);\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.form-field .field-select:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n}\n\n/* Accessibility override for user-specific elements */\n.checkbox:focus-visible,\n.user-header:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n/* -----------------------------------------------------------------------------\n Large Desktop (1440px+)\n ----------------------------------------------------------------------------- */\n@media (min-width: 1440px) {\n .bulk-action-toolbar {\n padding: 1.25rem 2rem;\n }\n\n .mj-grid-4 {\n gap: 1.5rem;\n padding: 0 2rem 1rem 2rem;\n }\n\n .scrollable-content {\n padding: 2rem;\n }\n}\n\n/* -----------------------------------------------------------------------------\n Mobile (< 640px) - Compact adjustments\n ----------------------------------------------------------------------------- */\n@media (max-width: 639px) {\n .mj-grid-4 {\n grid-template-columns: repeat(2, 1fr);\n gap: 0.5rem;\n padding: 0 0.75rem 0.5rem 0.75rem;\n }\n\n .mj-card {\n padding: 0.75rem;\n gap: 0.75rem;\n }\n\n .stat-icon {\n width: 40px;\n height: 40px;\n font-size: 1.125rem;\n }\n\n .stat-content .stat-value {\n font-size: 1.5rem;\n }\n\n .stat-content .stat-label {\n font-size: 0.6875rem;\n }\n\n .scrollable-content {\n padding: 0.75rem;\n }\n\n .list-header {\n padding: 0.625rem 0.75rem;\n }\n}\n"] }]
1531
1855
  }], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }], null); })();
1532
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserManagementComponent, { className: "UserManagementComponent", filePath: "src/lib/user-management/user-management.component.ts", lineNumber: 42 }); })();
1856
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserManagementComponent, { className: "UserManagementComponent", filePath: "src/lib/user-management/user-management.component.ts", lineNumber: 43 }); })();
1533
1857
  //# sourceMappingURL=user-management.component.js.map