@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.
- package/dist/lib/account-info/account-info.component.d.ts +1 -1
- package/dist/lib/account-info/account-info.component.js +2 -2
- package/dist/lib/account-info/account-info.component.js.map +1 -1
- package/dist/lib/application-management/application-dialog/application-dialog.component.d.ts +93 -10
- package/dist/lib/application-management/application-dialog/application-dialog.component.d.ts.map +1 -1
- package/dist/lib/application-management/application-dialog/application-dialog.component.js +232 -80
- package/dist/lib/application-management/application-dialog/application-dialog.component.js.map +1 -1
- package/dist/lib/application-management/application-management.component.d.ts +94 -11
- package/dist/lib/application-management/application-management.component.d.ts.map +1 -1
- package/dist/lib/application-management/application-management.component.js +231 -67
- package/dist/lib/application-management/application-management.component.js.map +1 -1
- package/dist/lib/application-settings/application-settings.component.d.ts +4 -4
- package/dist/lib/application-settings/application-settings.component.js +10 -10
- package/dist/lib/application-settings/application-settings.component.js.map +1 -1
- package/dist/lib/entity-permissions/entity-permissions.component.d.ts +88 -10
- package/dist/lib/entity-permissions/entity-permissions.component.d.ts.map +1 -1
- package/dist/lib/entity-permissions/entity-permissions.component.js +219 -63
- package/dist/lib/entity-permissions/entity-permissions.component.js.map +1 -1
- package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.d.ts +44 -6
- package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.d.ts.map +1 -1
- package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.js +117 -43
- package/dist/lib/entity-permissions/permission-dialog/permission-dialog.component.js.map +1 -1
- package/dist/lib/notification-preferences/notification-preferences.component.d.ts +25 -3
- package/dist/lib/notification-preferences/notification-preferences.component.d.ts.map +1 -1
- package/dist/lib/notification-preferences/notification-preferences.component.js +62 -18
- package/dist/lib/notification-preferences/notification-preferences.component.js.map +1 -1
- package/dist/lib/role-management/role-dialog/role-dialog.component.d.ts +32 -4
- package/dist/lib/role-management/role-dialog/role-dialog.component.d.ts.map +1 -1
- package/dist/lib/role-management/role-dialog/role-dialog.component.js +84 -30
- package/dist/lib/role-management/role-dialog/role-dialog.component.js.map +1 -1
- package/dist/lib/role-management/role-management-agent-context.d.ts +12 -4
- package/dist/lib/role-management/role-management-agent-context.d.ts.map +1 -1
- package/dist/lib/role-management/role-management-agent-context.js +26 -10
- package/dist/lib/role-management/role-management-agent-context.js.map +1 -1
- package/dist/lib/role-management/role-management.component.d.ts +95 -11
- package/dist/lib/role-management/role-management.component.d.ts.map +1 -1
- package/dist/lib/role-management/role-management.component.js +259 -91
- package/dist/lib/role-management/role-management.component.js.map +1 -1
- package/dist/lib/settings/settings.component.d.ts +87 -11
- package/dist/lib/settings/settings.component.d.ts.map +1 -1
- package/dist/lib/settings/settings.component.js +211 -65
- package/dist/lib/settings/settings.component.js.map +1 -1
- package/dist/lib/shared/settings-card.component.d.ts +24 -4
- package/dist/lib/shared/settings-card.component.d.ts.map +1 -1
- package/dist/lib/shared/settings-card.component.js +46 -6
- package/dist/lib/shared/settings-card.component.js.map +1 -1
- package/dist/lib/sql-logging/sql-logging.component.d.ts +172 -18
- package/dist/lib/sql-logging/sql-logging.component.d.ts.map +1 -1
- package/dist/lib/sql-logging/sql-logging.component.js +320 -108
- package/dist/lib/sql-logging/sql-logging.component.js.map +1 -1
- package/dist/lib/user-app-config/user-app-config-content.component.js +3 -3
- package/dist/lib/user-app-config/user-app-config-content.component.js.map +1 -1
- package/dist/lib/user-app-config/user-app-config-reorder.d.ts +4 -0
- package/dist/lib/user-app-config/user-app-config-reorder.d.ts.map +1 -1
- package/dist/lib/user-app-config/user-app-config-reorder.js +11 -3
- package/dist/lib/user-app-config/user-app-config-reorder.js.map +1 -1
- package/dist/lib/user-management/transaction-group-refusals.d.ts +45 -0
- package/dist/lib/user-management/transaction-group-refusals.d.ts.map +1 -0
- package/dist/lib/user-management/transaction-group-refusals.js +49 -0
- package/dist/lib/user-management/transaction-group-refusals.js.map +1 -0
- package/dist/lib/user-management/user-dialog/user-dialog.component.d.ts +44 -6
- package/dist/lib/user-management/user-dialog/user-dialog.component.d.ts.map +1 -1
- package/dist/lib/user-management/user-dialog/user-dialog.component.js +134 -45
- package/dist/lib/user-management/user-dialog/user-dialog.component.js.map +1 -1
- package/dist/lib/user-management/user-management-agent-context.d.ts +10 -0
- package/dist/lib/user-management/user-management-agent-context.d.ts.map +1 -1
- package/dist/lib/user-management/user-management-agent-context.js +26 -6
- package/dist/lib/user-management/user-management-agent-context.js.map +1 -1
- package/dist/lib/user-management/user-management.component.d.ts +174 -16
- package/dist/lib/user-management/user-management.component.d.ts.map +1 -1
- package/dist/lib/user-management/user-management.component.js +501 -177
- package/dist/lib/user-management/user-management.component.js.map +1 -1
- package/dist/lib/user-profile-settings/user-profile-settings.component.d.ts +97 -15
- package/dist/lib/user-profile-settings/user-profile-settings.component.d.ts.map +1 -1
- package/dist/lib/user-profile-settings/user-profile-settings.component.js +280 -116
- package/dist/lib/user-profile-settings/user-profile-settings.component.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
602
|
-
const roleFilter = filters.role ? this.
|
|
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.
|
|
605
|
-
FilteredUserCount: this.
|
|
606
|
-
ActiveUserCount: this.
|
|
607
|
-
InactiveUserCount: this.
|
|
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.
|
|
613
|
-
SelectedUserName: this.
|
|
614
|
-
VisibleUserNames: this.
|
|
615
|
-
VisibleUserEmails: this.
|
|
616
|
-
AvailableRoleNames: this.
|
|
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,
|
|
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 (!
|
|
815
|
+
if (!IsValidUserStatusFilter(status)) {
|
|
687
816
|
return { Success: false, ErrorMessage: `Invalid status "${String(status)}". Expected one of: all, active, inactive.` };
|
|
688
817
|
}
|
|
689
|
-
this.
|
|
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.
|
|
826
|
+
this.UpdateFilter({ role: '' });
|
|
698
827
|
this.publishAgentContext();
|
|
699
828
|
return { Success: true };
|
|
700
829
|
}
|
|
701
|
-
const resolved =
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
727
|
-
this.
|
|
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 =
|
|
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.
|
|
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.
|
|
906
|
+
this.ResetAllFiltersAndSearch();
|
|
778
907
|
this.publishAgentContext();
|
|
779
908
|
return { Success: true };
|
|
780
909
|
}
|
|
781
910
|
async handleRefreshTool() {
|
|
782
911
|
try {
|
|
783
|
-
await this.
|
|
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.
|
|
921
|
+
if (this.FilteredUsers.length === 0) {
|
|
793
922
|
return { Success: false, ErrorMessage: 'No users to export.' };
|
|
794
923
|
}
|
|
795
|
-
this.
|
|
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
|
|
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.
|
|
814
|
-
this.
|
|
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.
|
|
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.
|
|
878
|
-
let filtered = [...this.
|
|
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.
|
|
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.
|
|
904
|
-
totalUsers: this.
|
|
905
|
-
activeUsers: this.
|
|
906
|
-
inactiveUsers: this.
|
|
907
|
-
adminUsers: this.
|
|
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.
|
|
1049
|
+
return this.OnStatusFilterChange(status);
|
|
913
1050
|
}
|
|
914
|
-
|
|
915
|
-
this.
|
|
916
|
-
...this.
|
|
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.
|
|
929
|
-
this.showEditDialog = true;
|
|
1074
|
+
return this.SelectUser(user);
|
|
930
1075
|
}
|
|
931
1076
|
createNewUser() {
|
|
932
|
-
this.
|
|
1077
|
+
this.UserDialogData = {
|
|
933
1078
|
mode: 'create',
|
|
934
|
-
availableRoles: this.
|
|
1079
|
+
availableRoles: this.Roles
|
|
935
1080
|
};
|
|
936
|
-
this.
|
|
1081
|
+
this.ShowUserDialog = true;
|
|
937
1082
|
}
|
|
938
|
-
|
|
939
|
-
this.
|
|
1083
|
+
EditUser(user) {
|
|
1084
|
+
this.UserDialogData = {
|
|
940
1085
|
user: user,
|
|
941
1086
|
mode: 'edit',
|
|
942
|
-
availableRoles: this.
|
|
1087
|
+
availableRoles: this.Roles
|
|
943
1088
|
};
|
|
944
|
-
this.
|
|
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.
|
|
948
|
-
this.showDeleteConfirm = true;
|
|
1101
|
+
return this.ConfirmDeleteUser(user);
|
|
949
1102
|
}
|
|
950
|
-
async
|
|
951
|
-
if (!this.
|
|
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.
|
|
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.
|
|
961
|
-
this.
|
|
962
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
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.
|
|
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.
|
|
1229
|
+
return this.RefreshData();
|
|
1061
1230
|
}
|
|
1062
|
-
|
|
1231
|
+
GetStatusIcon(user) {
|
|
1063
1232
|
return user.IsActive ? 'fa-check-circle' : 'fa-times-circle';
|
|
1064
1233
|
}
|
|
1065
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1098
|
-
|
|
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.
|
|
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.
|
|
1309
|
+
return this.SelectedCount;
|
|
1105
1310
|
}
|
|
1106
|
-
get
|
|
1107
|
-
const filters = this.
|
|
1311
|
+
get HasActiveFilters() {
|
|
1312
|
+
const filters = this.Filters$.value;
|
|
1108
1313
|
return filters.status !== 'all' || filters.role !== '';
|
|
1109
1314
|
}
|
|
1110
|
-
|
|
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.
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
|
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.
|
|
1374
|
+
filterable: this.Roles.length > 10,
|
|
1158
1375
|
options: [
|
|
1159
1376
|
{ text: 'All Roles', value: '' },
|
|
1160
|
-
...this.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1198
|
-
this.
|
|
1430
|
+
ResetAllFiltersAndSearch() {
|
|
1431
|
+
this.Filters$.next({ status: 'all', role: '', search: '' });
|
|
1199
1432
|
this.applyFilters();
|
|
1200
1433
|
this.cdr.markForCheck();
|
|
1201
1434
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
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.
|
|
1442
|
+
this.FilteredUsers.forEach(user => this.SelectedUserIds.delete(user.ID));
|
|
1206
1443
|
}
|
|
1207
1444
|
else {
|
|
1208
1445
|
// Select all filtered users
|
|
1209
|
-
this.
|
|
1446
|
+
this.FilteredUsers.forEach(user => this.SelectedUserIds.add(user.ID));
|
|
1210
1447
|
}
|
|
1211
1448
|
}
|
|
1212
|
-
|
|
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.
|
|
1217
|
-
this.
|
|
1457
|
+
if (this.SelectedUserIds.has(userId)) {
|
|
1458
|
+
this.SelectedUserIds.delete(userId);
|
|
1218
1459
|
}
|
|
1219
1460
|
else {
|
|
1220
|
-
this.
|
|
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.
|
|
1473
|
+
return this.IsUserSelected(userId);
|
|
1225
1474
|
}
|
|
1475
|
+
ClearSelection() {
|
|
1476
|
+
this.SelectedUserIds.clear();
|
|
1477
|
+
}
|
|
1478
|
+
/** @deprecated Use {@link ClearSelection}. */
|
|
1226
1479
|
clearSelection() {
|
|
1227
|
-
this.
|
|
1480
|
+
return this.ClearSelection();
|
|
1228
1481
|
}
|
|
1229
1482
|
// Bulk action methods
|
|
1230
|
-
|
|
1231
|
-
if (!this.
|
|
1483
|
+
ConfirmBulkAction(action) {
|
|
1484
|
+
if (!this.HasSelection)
|
|
1232
1485
|
return;
|
|
1233
|
-
this.
|
|
1234
|
-
this.
|
|
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.
|
|
1238
|
-
this.bulkActionType = null;
|
|
1499
|
+
return this.CancelBulkAction();
|
|
1239
1500
|
}
|
|
1240
|
-
async
|
|
1241
|
-
if (!this.
|
|
1501
|
+
async ExecuteBulkAction() {
|
|
1502
|
+
if (!this.BulkActionType || !this.HasSelection)
|
|
1242
1503
|
return;
|
|
1243
1504
|
try {
|
|
1244
1505
|
this.isLoading = true;
|
|
1245
|
-
const selectedUsers = this.
|
|
1246
|
-
switch (this.
|
|
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.
|
|
1258
|
-
this.
|
|
1259
|
-
this.
|
|
1260
|
-
await this.
|
|
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
|
-
|
|
1309
|
-
if (!this.
|
|
1573
|
+
OpenBulkRoleAssign() {
|
|
1574
|
+
if (!this.HasSelection)
|
|
1310
1575
|
return;
|
|
1311
|
-
this.
|
|
1312
|
-
this.
|
|
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.
|
|
1316
|
-
this.bulkRoleId = '';
|
|
1589
|
+
return this.CancelBulkRoleAssign();
|
|
1317
1590
|
}
|
|
1318
|
-
async
|
|
1319
|
-
if (!this.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
1341
|
-
// refused row's `Save()` return, so the row never
|
|
1342
|
-
// all-refused batch
|
|
1343
|
-
// nothing to do, and the screen
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1374
|
-
this.
|
|
1375
|
-
this.
|
|
1376
|
-
await this.
|
|
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
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|