@memberjunction/ng-explorer-settings 6.1.3 → 6.2.0-edge.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/dist/lib/application-management/application-dialog/application-dialog.component.d.ts +1 -0
  2. package/dist/lib/application-management/application-dialog/application-dialog.component.d.ts.map +1 -1
  3. package/dist/lib/application-management/application-dialog/application-dialog.component.js +12 -6
  4. package/dist/lib/application-management/application-dialog/application-dialog.component.js.map +1 -1
  5. package/dist/lib/application-management/application-management.component.d.ts +2 -1
  6. package/dist/lib/application-management/application-management.component.d.ts.map +1 -1
  7. package/dist/lib/application-management/application-management.component.js +2 -2
  8. package/dist/lib/application-management/application-management.component.js.map +1 -1
  9. package/dist/lib/user-management/transaction-group-refusals.d.ts +43 -0
  10. package/dist/lib/user-management/transaction-group-refusals.d.ts.map +1 -0
  11. package/dist/lib/user-management/transaction-group-refusals.js +45 -0
  12. package/dist/lib/user-management/transaction-group-refusals.js.map +1 -0
  13. package/dist/lib/user-management/user-dialog/user-dialog.component.d.ts.map +1 -1
  14. package/dist/lib/user-management/user-dialog/user-dialog.component.js +17 -2
  15. package/dist/lib/user-management/user-dialog/user-dialog.component.js.map +1 -1
  16. package/dist/lib/user-management/user-management.component.d.ts.map +1 -1
  17. package/dist/lib/user-management/user-management.component.js +24 -9
  18. package/dist/lib/user-management/user-management.component.js.map +1 -1
  19. package/package.json +20 -20
@@ -3,6 +3,7 @@ import { FormBuilder, Validators } from '@angular/forms';
3
3
  import { RunView } from '@memberjunction/core';
4
4
  import { UUIDsEqual } from '@memberjunction/global';
5
5
  import { BaseAngularComponent } from '@memberjunction/ng-base-types';
6
+ import { serverRefusalReasons } from '../transaction-group-refusals';
6
7
  import * as i0 from "@angular/core";
7
8
  import * as i1 from "@angular/forms";
8
9
  import * as i2 from "@memberjunction/ng-ui-components";
@@ -429,11 +430,17 @@ export class UserDialogComponent extends BaseAngularComponent {
429
430
  // same reason as `UserManagementComponent.executeBulkRoleAssign`, where the transaction-group
430
431
  // semantics — and why the server-side #4282 guard is NOT what this catches — are set out in full.
431
432
  const refusals = [];
433
+ // Enrolled rows are kept so the SERVER's reason can be read back off them below; without
434
+ // that, the reason #4309 puts on LatestResult has nobody left to read it.
435
+ const enrolled = [];
432
436
  for (const userRole of rolesToRemove) {
433
437
  userRole.TransactionGroup = tg;
434
438
  if (!await userRole.Delete()) {
435
439
  refusals.push(`Remove ${this.describeRole(userRole.RoleID)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
436
440
  }
441
+ else {
442
+ enrolled.push({ label: `Remove ${this.describeRole(userRole.RoleID)}`, entity: userRole });
443
+ }
437
444
  }
438
445
  for (const roleId of rolesToAdd) {
439
446
  const userRole = await this.metadata.GetEntityObject('MJ: User Roles');
@@ -444,6 +451,9 @@ export class UserDialogComponent extends BaseAngularComponent {
444
451
  if (!await userRole.Save()) {
445
452
  refusals.push(`Add ${this.describeRole(roleId)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
446
453
  }
454
+ else {
455
+ enrolled.push({ label: `Add ${this.describeRole(roleId)}`, entity: userRole });
456
+ }
447
457
  }
448
458
  if (refusals.length > 0) {
449
459
  // Nothing was written: the refused rows never enrolled, and the rest are still only queued
@@ -451,7 +461,12 @@ export class UserDialogComponent extends BaseAngularComponent {
451
461
  throw new Error(`Failed to update user roles — no role changes were made.\n${refusals.join('\n')}`);
452
462
  }
453
463
  if (!await tg.Submit()) {
454
- throw new Error('Failed to update user roles — all changes have been rolled back');
464
+ // Every row enrolled, so this is a SERVER-side refusal (or a rollback). Since #4309 the
465
+ // server says which row and why, and that reason is now on each entity's LatestResult.
466
+ const reasons = serverRefusalReasons(enrolled);
467
+ throw new Error(reasons.length > 0
468
+ ? `Failed to update user roles — all changes have been rolled back.\n${reasons.join('\n')}`
469
+ : 'Failed to update user roles — all changes have been rolled back');
455
470
  }
456
471
  }
457
472
  catch (error) {
@@ -494,5 +509,5 @@ export class UserDialogComponent extends BaseAngularComponent {
494
509
  type: HostListener,
495
510
  args: ['document:keydown.escape', ['$event']]
496
511
  }] }); })();
497
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserDialogComponent, { className: "UserDialogComponent", filePath: "src/lib/user-management/user-dialog/user-dialog.component.ts", lineNumber: 27 }); })();
512
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserDialogComponent, { className: "UserDialogComponent", filePath: "src/lib/user-management/user-dialog/user-dialog.component.ts", lineNumber: 28 }); })();
498
513
  //# sourceMappingURL=user-dialog.component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-dialog.component.js","sourceRoot":"","sources":["../../../../src/lib/user-management/user-dialog/user-dialog.component.ts","../../../../src/lib/user-management/user-dialog/user-dialog.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, inject, HostListener, ViewEncapsulation, ChangeDetectorRef, NgZone } from '@angular/core';\n\nimport { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';\nimport { Metadata, RunView } from '@memberjunction/core';\nimport { MJUserEntity, MJRoleEntity, MJUserRoleEntity } from '@memberjunction/core-entities';\n\nimport { UUIDsEqual } from '@memberjunction/global';\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\nexport interface UserDialogData {\n user?: MJUserEntity;\n mode: 'create' | 'edit';\n availableRoles: MJRoleEntity[];\n}\n\nexport interface UserDialogResult {\n action: 'save' | 'cancel';\n user?: MJUserEntity;\n}\n\n@Component({\n standalone: false,\n selector: 'mj-user-dialog',\n encapsulation: ViewEncapsulation.None,\n templateUrl: './user-dialog.component.html',\n styleUrls: ['./user-dialog.component.css']\n})\nexport class UserDialogComponent extends BaseAngularComponent implements OnInit, OnDestroy, OnChanges {\n @Input() data: UserDialogData | null = null;\n @Input() visible = false;\n @Output() result = new EventEmitter<UserDialogResult>();\n\n private fb = inject(FormBuilder);\n private cdr = inject(ChangeDetectorRef);\n private ngZone = inject(NgZone);\n private get metadata() { return this.ProviderToUse; }\n public userForm: FormGroup;\n public isLoading = false;\n public error: string | null = null;\n public selectedRoleIds = new Set<string>();\n public existingUserRoles: MJUserRoleEntity[] = [];\n\n constructor() {\n super();\n this.userForm = this.fb.group({\n name: ['', [Validators.required, Validators.email]],\n firstName: [''],\n lastName: [''],\n email: ['', [Validators.required, Validators.email]],\n title: [''],\n type: ['User', Validators.required],\n isActive: [true]\n });\n }\n\n ngOnInit(): void {\n // Initial setup\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n // Always clear state when data changes to prevent persistence bugs\n if (changes['data']) {\n this.selectedRoleIds.clear();\n this.existingUserRoles = [];\n \n if (this.data?.user && this.isEditMode) {\n this.loadUserData();\n } else {\n this.resetForm();\n }\n }\n \n // Reset form when dialog becomes visible and not in edit mode\n if (changes['visible'] && this.visible && !this.isEditMode) {\n this.resetForm();\n }\n }\n\n ngOnDestroy(): void {\n // Cleanup if needed\n }\n\n private resetForm(): void {\n this.userForm.reset({\n name: '',\n firstName: '',\n lastName: '',\n email: '',\n title: '',\n type: 'User',\n isActive: true\n });\n this.selectedRoleIds.clear();\n this.error = null;\n }\n\n @HostListener('document:keydown.escape', ['$event'])\n onEscapeKey(event: Event): void {\n if (this.visible) {\n this.onCancel();\n }\n }\n\n public get windowTitle(): string {\n return this.isEditMode ? 'Edit User' : 'Create New User';\n }\n\n public get isEditMode(): boolean {\n return this.data?.mode === 'edit';\n }\n\n private async loadUserData(): Promise<void> {\n if (!this.data?.user) return;\n\n const user = this.data.user;\n this.userForm.patchValue({\n name: user.Name,\n firstName: user.FirstName,\n lastName: user.LastName,\n email: user.Email,\n title: user.Title,\n type: user.Type,\n isActive: user.IsActive\n });\n\n // Load existing user roles\n await this.loadExistingUserRoles(user.ID);\n }\n\n private async loadExistingUserRoles(userId: string): Promise<void> {\n try {\n const rv = RunView.FromMetadataProvider(this.ProviderToUse);\n const result = await rv.RunView<MJUserRoleEntity>({\n EntityName: 'MJ: User Roles',\n ExtraFilter: `UserID='${userId}'`,\n ResultType: 'entity_object'\n });\n\n if (result.Success && result.Results) {\n this.existingUserRoles = result.Results;\n // Pre-select existing roles\n for (const userRole of this.existingUserRoles) {\n this.selectedRoleIds.add(userRole.RoleID);\n }\n }\n } catch (error) {\n console.warn('Failed to load existing user roles:', error);\n }\n }\n\n public onRoleToggle(roleId: string, event: Event): void {\n event.stopPropagation();\n const checkbox = event.target as HTMLInputElement;\n if (checkbox.checked) {\n this.selectedRoleIds.add(roleId);\n } else {\n this.selectedRoleIds.delete(roleId);\n }\n }\n\n public toggleRole(roleId: string): void {\n if (this.selectedRoleIds.has(roleId)) {\n this.selectedRoleIds.delete(roleId);\n } else {\n this.selectedRoleIds.add(roleId);\n }\n }\n\n public async onSubmit(): Promise<void> {\n if (this.userForm.invalid) {\n this.markFormGroupTouched(this.userForm);\n return;\n }\n\n this.isLoading = true;\n this.error = null;\n\n try {\n let user: MJUserEntity;\n\n if (this.isEditMode && this.data?.user) {\n // Edit existing user\n user = this.data.user;\n } else {\n // Create new user\n user = await this.metadata.GetEntityObject<MJUserEntity>('MJ: Users');\n user.NewRecord();\n }\n\n // Update user properties\n const formValue = this.userForm.value;\n user.Name = formValue.name;\n user.FirstName = formValue.firstName;\n user.LastName = formValue.lastName;\n user.Email = formValue.email;\n user.Title = formValue.title;\n user.Type = formValue.type;\n user.IsActive = formValue.isActive;\n\n // Save user\n const saveResult = await user.Save();\n if (!saveResult) {\n throw new Error(user.LatestResult?.Message || 'Failed to save user');\n }\n\n // Handle role assignments\n await this.updateUserRoles(user.ID);\n\n this.result.emit({ action: 'save', user });\n\n } catch (error: unknown) {\n console.error('Error saving user:', error);\n this.ngZone.run(() => {\n this.error = error instanceof Error ? error.message : 'An unexpected error occurred';\n this.cdr.markForCheck();\n });\n } finally {\n this.ngZone.run(() => {\n this.isLoading = false;\n this.cdr.markForCheck();\n });\n }\n }\n\n private async updateUserRoles(userId: string): Promise<void> {\n try {\n // Get current role IDs from existing UserRole entities\n const existingRoleIds = new Set(this.existingUserRoles.map(ur => ur.RoleID));\n\n // Determine roles to add and remove\n const rolesToAdd = Array.from(this.selectedRoleIds).filter(roleId => !existingRoleIds.has(roleId));\n const rolesToRemove = this.existingUserRoles.filter(userRole => !this.selectedRoleIds.has(userRole.RoleID));\n\n if (rolesToAdd.length === 0 && rolesToRemove.length === 0) {\n return;\n }\n\n // Batch all role deletes and adds into one transactional GraphQL call\n const tg = await this.metadata.CreateTransactionGroup();\n\n // Each Delete()/Save() only ENROLS the row in the group; the write is deferred to Submit().\n // A false return therefore means the row was refused CLIENT-side and never enrolled — so\n // submitting anyway writes a PARTIAL change, or an empty one whose Submit() returns true for\n // having nothing to do, and either way the dialog closes reporting success. Same rule and\n // same reason as `UserManagementComponent.executeBulkRoleAssign`, where the transaction-group\n // semantics — and why the server-side #4282 guard is NOT what this catches — are set out in full.\n const refusals: string[] = [];\n\n for (const userRole of rolesToRemove) {\n userRole.TransactionGroup = tg;\n if (!await userRole.Delete()) {\n refusals.push(`Remove ${this.describeRole(userRole.RoleID)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);\n }\n }\n\n for (const roleId of rolesToAdd) {\n const userRole = await this.metadata.GetEntityObject<MJUserRoleEntity>('MJ: User Roles');\n userRole.NewRecord();\n userRole.UserID = userId;\n userRole.RoleID = roleId;\n userRole.TransactionGroup = tg;\n if (!await userRole.Save()) {\n refusals.push(`Add ${this.describeRole(roleId)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);\n }\n }\n\n if (refusals.length > 0) {\n // Nothing was written: the refused rows never enrolled, and the rest are still only queued\n // because Submit() is not reached.\n throw new Error(`Failed to update user roles — no role changes were made.\\n${refusals.join('\\n')}`);\n }\n\n if (!await tg.Submit()) {\n throw new Error('Failed to update user roles — all changes have been rolled back');\n }\n } catch (error) {\n console.error('Error updating user roles:', error);\n throw error;\n }\n }\n\n /** Names a role for a refusal message; falls back to the ID when the catalog has no match. */\n private describeRole(roleId: string): string {\n return this.data?.availableRoles.find(r => UUIDsEqual(r.ID, roleId))?.Name ?? roleId;\n }\n\n public onCancel(): void {\n this.result.emit({ action: 'cancel' });\n }\n\n private markFormGroupTouched(formGroup: FormGroup): void {\n Object.keys(formGroup.controls).forEach(key => {\n const control = formGroup.get(key);\n control?.markAsTouched();\n });\n }\n}","<!-- MD3 Modal Dialog -->\n@if (visible) {\n <div class=\"modal-backdrop\" (click)=\"onCancel()\">\n <div class=\"modal-dialog\" (click)=\"$event.stopPropagation()\">\n <!-- Modal Header with MJ Blue -->\n <div class=\"modal-header\">\n <div class=\"dialog-header\">\n <h2 class=\"dialog-title\">\n <i class=\"fa-solid fa-user\"></i>\n {{ isEditMode ? 'Edit User' : 'Create New User' }}\n </h2>\n <p class=\"dialog-subtitle\">\n {{ isEditMode ? 'Update user information and role assignments' : 'Add a new user to the system' }}\n </p>\n </div>\n <button type=\"button\" class=\"modal-close\" (click)=\"onCancel()\" aria-label=\"Close dialog\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n <!-- Modal Body -->\n <div class=\"modal-body\">\n <form [formGroup]=\"userForm\" (ngSubmit)=\"onSubmit()\">\n @if (error) {\n <div class=\"content-section\">\n <mj-alert Variant=\"error\" [Message]=\"error\" />\n </div>\n }\n <!-- Basic Information Section -->\n <div class=\"content-section\">\n <div class=\"form-section\">\n <div class=\"section-header\">\n <h3 class=\"section-title\">\n <i class=\"fa-solid fa-id-card\"></i>\n Basic Information\n </h3>\n <p class=\"section-description\">Enter the user's personal and contact details</p>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-field\">\n <label class=\"field-label required\" for=\"name\">Username/Email</label>\n <input\n id=\"name\"\n type=\"email\"\n class=\"field-input\"\n formControlName=\"name\"\n placeholder=\"john@company.com\"\n [class.error]=\"userForm.get('name')?.invalid && userForm.get('name')?.touched\"\n />\n @if (userForm.get('name')?.invalid && userForm.get('name')?.touched) {\n <div class=\"field-error\">\n <i class=\"fa-solid fa-exclamation-circle\"></i>\n @if (userForm.get('name')?.errors?.['required']) {\n Username/Email is required\n }\n @if (userForm.get('name')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"form-row\">\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"firstName\">First Name</label>\n <input\n id=\"firstName\"\n type=\"text\"\n class=\"field-input\"\n formControlName=\"firstName\"\n placeholder=\"John\"\n />\n </div>\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"lastName\">Last Name</label>\n <input\n id=\"lastName\"\n type=\"text\"\n class=\"field-input\"\n formControlName=\"lastName\"\n placeholder=\"Doe\"\n />\n </div>\n </div>\n <div class=\"form-field\">\n <label class=\"field-label required\" for=\"email\">Email Address</label>\n <input\n id=\"email\"\n type=\"email\"\n class=\"field-input\"\n formControlName=\"email\"\n placeholder=\"john@company.com\"\n [class.error]=\"userForm.get('email')?.invalid && userForm.get('email')?.touched\"\n />\n @if (userForm.get('email')?.invalid && userForm.get('email')?.touched) {\n <div class=\"field-error\">\n <i class=\"fa-solid fa-exclamation-circle\"></i>\n @if (userForm.get('email')?.errors?.['required']) {\n Email address is required\n }\n @if (userForm.get('email')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"title\">Job Title</label>\n <input\n id=\"title\"\n type=\"text\"\n class=\"field-input\"\n formControlName=\"title\"\n placeholder=\"Software Engineer\"\n />\n </div>\n </div>\n </div>\n </div>\n <!-- User Settings Section -->\n <div class=\"content-section\">\n <div class=\"form-section\">\n <div class=\"section-header\">\n <h3 class=\"section-title\">\n <i class=\"fa-solid fa-cog\"></i>\n User Settings\n </h3>\n <p class=\"section-description\">Configure user type and account status</p>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-row\">\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"type\">User Type</label>\n <select id=\"type\" class=\"field-select\" formControlName=\"type\">\n <option value=\"User\">Standard User</option>\n <option value=\"Owner\">System Owner</option>\n </select>\n </div>\n <div class=\"form-field\">\n <div class=\"checkbox-field\">\n <input\n id=\"isActive\"\n type=\"checkbox\"\n class=\"checkbox-input\"\n formControlName=\"isActive\"\n />\n <label class=\"checkbox-label\" for=\"isActive\">\n <div class=\"checkbox-indicator\"></div>\n <div>\n <strong>Active Account</strong>\n <div style=\"font-size: 12px; color: var(--md-on-surface-variant); margin-top: 2px;\">\n User can log in and access the system\n </div>\n </div>\n </label>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <!-- Role Assignment Section -->\n <div class=\"content-section\">\n <div class=\"form-section\">\n <div class=\"section-header\">\n <h3 class=\"section-title\">\n <i class=\"fa-solid fa-user-tag\"></i>\n Role Assignment\n </h3>\n <p class=\"section-description\">Select the roles to assign to this user</p>\n </div>\n <div class=\"roles-grid\">\n @for (role of data?.availableRoles; track role.ID) {\n <div class=\"role-card\"\n [class.selected]=\"selectedRoleIds.has(role.ID)\"\n (click)=\"toggleRole(role.ID)\">\n <div class=\"role-content\">\n <div class=\"checkbox-field\">\n <input\n type=\"checkbox\"\n class=\"checkbox-input\"\n [id]=\"'role-' + role.ID\"\n [checked]=\"selectedRoleIds.has(role.ID)\"\n (change)=\"onRoleToggle(role.ID, $event)\"\n />\n <label class=\"checkbox-label\" [for]=\"'role-' + role.ID\">\n <div class=\"checkbox-indicator\"></div>\n </label>\n </div>\n <div class=\"role-info\">\n <h4 class=\"role-name\">{{ role.Name }}</h4>\n @if (role.Description) {\n <p class=\"role-description\">{{ role.Description }}</p>\n }\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n <!-- Modal Footer -->\n <div class=\"modal-footer\">\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"userForm.invalid || isLoading\"\n >\n @if (isLoading) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n Saving...\n } @else {\n <i class=\"fa-solid fa-save\"></i>\n {{ isEditMode ? 'Update User' : 'Create User' }}\n }\n </button>\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancel()\">\n <i class=\"fa-solid fa-times\"></i>\n Cancel\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n}\n"]}
1
+ {"version":3,"file":"user-dialog.component.js","sourceRoot":"","sources":["../../../../src/lib/user-management/user-dialog/user-dialog.component.ts","../../../../src/lib/user-management/user-dialog/user-dialog.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, inject, HostListener, ViewEncapsulation, ChangeDetectorRef, NgZone } from '@angular/core';\n\nimport { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';\nimport { Metadata, RunView } from '@memberjunction/core';\nimport { MJUserEntity, MJRoleEntity, MJUserRoleEntity } from '@memberjunction/core-entities';\n\nimport { UUIDsEqual } from '@memberjunction/global';\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\nimport { EnrolledRow, serverRefusalReasons } from '../transaction-group-refusals';\nexport interface UserDialogData {\n user?: MJUserEntity;\n mode: 'create' | 'edit';\n availableRoles: MJRoleEntity[];\n}\n\nexport interface UserDialogResult {\n action: 'save' | 'cancel';\n user?: MJUserEntity;\n}\n\n@Component({\n standalone: false,\n selector: 'mj-user-dialog',\n encapsulation: ViewEncapsulation.None,\n templateUrl: './user-dialog.component.html',\n styleUrls: ['./user-dialog.component.css']\n})\nexport class UserDialogComponent extends BaseAngularComponent implements OnInit, OnDestroy, OnChanges {\n @Input() data: UserDialogData | null = null;\n @Input() visible = false;\n @Output() result = new EventEmitter<UserDialogResult>();\n\n private fb = inject(FormBuilder);\n private cdr = inject(ChangeDetectorRef);\n private ngZone = inject(NgZone);\n private get metadata() { return this.ProviderToUse; }\n public userForm: FormGroup;\n public isLoading = false;\n public error: string | null = null;\n public selectedRoleIds = new Set<string>();\n public existingUserRoles: MJUserRoleEntity[] = [];\n\n constructor() {\n super();\n this.userForm = this.fb.group({\n name: ['', [Validators.required, Validators.email]],\n firstName: [''],\n lastName: [''],\n email: ['', [Validators.required, Validators.email]],\n title: [''],\n type: ['User', Validators.required],\n isActive: [true]\n });\n }\n\n ngOnInit(): void {\n // Initial setup\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n // Always clear state when data changes to prevent persistence bugs\n if (changes['data']) {\n this.selectedRoleIds.clear();\n this.existingUserRoles = [];\n \n if (this.data?.user && this.isEditMode) {\n this.loadUserData();\n } else {\n this.resetForm();\n }\n }\n \n // Reset form when dialog becomes visible and not in edit mode\n if (changes['visible'] && this.visible && !this.isEditMode) {\n this.resetForm();\n }\n }\n\n ngOnDestroy(): void {\n // Cleanup if needed\n }\n\n private resetForm(): void {\n this.userForm.reset({\n name: '',\n firstName: '',\n lastName: '',\n email: '',\n title: '',\n type: 'User',\n isActive: true\n });\n this.selectedRoleIds.clear();\n this.error = null;\n }\n\n @HostListener('document:keydown.escape', ['$event'])\n onEscapeKey(event: Event): void {\n if (this.visible) {\n this.onCancel();\n }\n }\n\n public get windowTitle(): string {\n return this.isEditMode ? 'Edit User' : 'Create New User';\n }\n\n public get isEditMode(): boolean {\n return this.data?.mode === 'edit';\n }\n\n private async loadUserData(): Promise<void> {\n if (!this.data?.user) return;\n\n const user = this.data.user;\n this.userForm.patchValue({\n name: user.Name,\n firstName: user.FirstName,\n lastName: user.LastName,\n email: user.Email,\n title: user.Title,\n type: user.Type,\n isActive: user.IsActive\n });\n\n // Load existing user roles\n await this.loadExistingUserRoles(user.ID);\n }\n\n private async loadExistingUserRoles(userId: string): Promise<void> {\n try {\n const rv = RunView.FromMetadataProvider(this.ProviderToUse);\n const result = await rv.RunView<MJUserRoleEntity>({\n EntityName: 'MJ: User Roles',\n ExtraFilter: `UserID='${userId}'`,\n ResultType: 'entity_object'\n });\n\n if (result.Success && result.Results) {\n this.existingUserRoles = result.Results;\n // Pre-select existing roles\n for (const userRole of this.existingUserRoles) {\n this.selectedRoleIds.add(userRole.RoleID);\n }\n }\n } catch (error) {\n console.warn('Failed to load existing user roles:', error);\n }\n }\n\n public onRoleToggle(roleId: string, event: Event): void {\n event.stopPropagation();\n const checkbox = event.target as HTMLInputElement;\n if (checkbox.checked) {\n this.selectedRoleIds.add(roleId);\n } else {\n this.selectedRoleIds.delete(roleId);\n }\n }\n\n public toggleRole(roleId: string): void {\n if (this.selectedRoleIds.has(roleId)) {\n this.selectedRoleIds.delete(roleId);\n } else {\n this.selectedRoleIds.add(roleId);\n }\n }\n\n public async onSubmit(): Promise<void> {\n if (this.userForm.invalid) {\n this.markFormGroupTouched(this.userForm);\n return;\n }\n\n this.isLoading = true;\n this.error = null;\n\n try {\n let user: MJUserEntity;\n\n if (this.isEditMode && this.data?.user) {\n // Edit existing user\n user = this.data.user;\n } else {\n // Create new user\n user = await this.metadata.GetEntityObject<MJUserEntity>('MJ: Users');\n user.NewRecord();\n }\n\n // Update user properties\n const formValue = this.userForm.value;\n user.Name = formValue.name;\n user.FirstName = formValue.firstName;\n user.LastName = formValue.lastName;\n user.Email = formValue.email;\n user.Title = formValue.title;\n user.Type = formValue.type;\n user.IsActive = formValue.isActive;\n\n // Save user\n const saveResult = await user.Save();\n if (!saveResult) {\n throw new Error(user.LatestResult?.Message || 'Failed to save user');\n }\n\n // Handle role assignments\n await this.updateUserRoles(user.ID);\n\n this.result.emit({ action: 'save', user });\n\n } catch (error: unknown) {\n console.error('Error saving user:', error);\n this.ngZone.run(() => {\n this.error = error instanceof Error ? error.message : 'An unexpected error occurred';\n this.cdr.markForCheck();\n });\n } finally {\n this.ngZone.run(() => {\n this.isLoading = false;\n this.cdr.markForCheck();\n });\n }\n }\n\n private async updateUserRoles(userId: string): Promise<void> {\n try {\n // Get current role IDs from existing UserRole entities\n const existingRoleIds = new Set(this.existingUserRoles.map(ur => ur.RoleID));\n\n // Determine roles to add and remove\n const rolesToAdd = Array.from(this.selectedRoleIds).filter(roleId => !existingRoleIds.has(roleId));\n const rolesToRemove = this.existingUserRoles.filter(userRole => !this.selectedRoleIds.has(userRole.RoleID));\n\n if (rolesToAdd.length === 0 && rolesToRemove.length === 0) {\n return;\n }\n\n // Batch all role deletes and adds into one transactional GraphQL call\n const tg = await this.metadata.CreateTransactionGroup();\n\n // Each Delete()/Save() only ENROLS the row in the group; the write is deferred to Submit().\n // A false return therefore means the row was refused CLIENT-side and never enrolled — so\n // submitting anyway writes a PARTIAL change, or an empty one whose Submit() returns true for\n // having nothing to do, and either way the dialog closes reporting success. Same rule and\n // same reason as `UserManagementComponent.executeBulkRoleAssign`, where the transaction-group\n // semantics — and why the server-side #4282 guard is NOT what this catches — are set out in full.\n const refusals: string[] = [];\n // Enrolled rows are kept so the SERVER's reason can be read back off them below; without\n // that, the reason #4309 puts on LatestResult has nobody left to read it.\n const enrolled: EnrolledRow[] = [];\n\n for (const userRole of rolesToRemove) {\n userRole.TransactionGroup = tg;\n if (!await userRole.Delete()) {\n refusals.push(`Remove ${this.describeRole(userRole.RoleID)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);\n }\n else {\n enrolled.push({ label: `Remove ${this.describeRole(userRole.RoleID)}`, entity: userRole });\n }\n }\n\n for (const roleId of rolesToAdd) {\n const userRole = await this.metadata.GetEntityObject<MJUserRoleEntity>('MJ: User Roles');\n userRole.NewRecord();\n userRole.UserID = userId;\n userRole.RoleID = roleId;\n userRole.TransactionGroup = tg;\n if (!await userRole.Save()) {\n refusals.push(`Add ${this.describeRole(roleId)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);\n }\n else {\n enrolled.push({ label: `Add ${this.describeRole(roleId)}`, entity: userRole });\n }\n }\n\n if (refusals.length > 0) {\n // Nothing was written: the refused rows never enrolled, and the rest are still only queued\n // because Submit() is not reached.\n throw new Error(`Failed to update user roles — no role changes were made.\\n${refusals.join('\\n')}`);\n }\n\n if (!await tg.Submit()) {\n // Every row enrolled, so this is a SERVER-side refusal (or a rollback). Since #4309 the\n // server says which row and why, and that reason is now on each entity's LatestResult.\n const reasons = serverRefusalReasons(enrolled);\n throw new Error(reasons.length > 0\n ? `Failed to update user roles — all changes have been rolled back.\\n${reasons.join('\\n')}`\n : 'Failed to update user roles — all changes have been rolled back');\n }\n } catch (error) {\n console.error('Error updating user roles:', error);\n throw error;\n }\n }\n\n /** Names a role for a refusal message; falls back to the ID when the catalog has no match. */\n private describeRole(roleId: string): string {\n return this.data?.availableRoles.find(r => UUIDsEqual(r.ID, roleId))?.Name ?? roleId;\n }\n\n public onCancel(): void {\n this.result.emit({ action: 'cancel' });\n }\n\n private markFormGroupTouched(formGroup: FormGroup): void {\n Object.keys(formGroup.controls).forEach(key => {\n const control = formGroup.get(key);\n control?.markAsTouched();\n });\n }\n}","<!-- MD3 Modal Dialog -->\n@if (visible) {\n <div class=\"modal-backdrop\" (click)=\"onCancel()\">\n <div class=\"modal-dialog\" (click)=\"$event.stopPropagation()\">\n <!-- Modal Header with MJ Blue -->\n <div class=\"modal-header\">\n <div class=\"dialog-header\">\n <h2 class=\"dialog-title\">\n <i class=\"fa-solid fa-user\"></i>\n {{ isEditMode ? 'Edit User' : 'Create New User' }}\n </h2>\n <p class=\"dialog-subtitle\">\n {{ isEditMode ? 'Update user information and role assignments' : 'Add a new user to the system' }}\n </p>\n </div>\n <button type=\"button\" class=\"modal-close\" (click)=\"onCancel()\" aria-label=\"Close dialog\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n <!-- Modal Body -->\n <div class=\"modal-body\">\n <form [formGroup]=\"userForm\" (ngSubmit)=\"onSubmit()\">\n @if (error) {\n <div class=\"content-section\">\n <mj-alert Variant=\"error\" [Message]=\"error\" />\n </div>\n }\n <!-- Basic Information Section -->\n <div class=\"content-section\">\n <div class=\"form-section\">\n <div class=\"section-header\">\n <h3 class=\"section-title\">\n <i class=\"fa-solid fa-id-card\"></i>\n Basic Information\n </h3>\n <p class=\"section-description\">Enter the user's personal and contact details</p>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-field\">\n <label class=\"field-label required\" for=\"name\">Username/Email</label>\n <input\n id=\"name\"\n type=\"email\"\n class=\"field-input\"\n formControlName=\"name\"\n placeholder=\"john@company.com\"\n [class.error]=\"userForm.get('name')?.invalid && userForm.get('name')?.touched\"\n />\n @if (userForm.get('name')?.invalid && userForm.get('name')?.touched) {\n <div class=\"field-error\">\n <i class=\"fa-solid fa-exclamation-circle\"></i>\n @if (userForm.get('name')?.errors?.['required']) {\n Username/Email is required\n }\n @if (userForm.get('name')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"form-row\">\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"firstName\">First Name</label>\n <input\n id=\"firstName\"\n type=\"text\"\n class=\"field-input\"\n formControlName=\"firstName\"\n placeholder=\"John\"\n />\n </div>\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"lastName\">Last Name</label>\n <input\n id=\"lastName\"\n type=\"text\"\n class=\"field-input\"\n formControlName=\"lastName\"\n placeholder=\"Doe\"\n />\n </div>\n </div>\n <div class=\"form-field\">\n <label class=\"field-label required\" for=\"email\">Email Address</label>\n <input\n id=\"email\"\n type=\"email\"\n class=\"field-input\"\n formControlName=\"email\"\n placeholder=\"john@company.com\"\n [class.error]=\"userForm.get('email')?.invalid && userForm.get('email')?.touched\"\n />\n @if (userForm.get('email')?.invalid && userForm.get('email')?.touched) {\n <div class=\"field-error\">\n <i class=\"fa-solid fa-exclamation-circle\"></i>\n @if (userForm.get('email')?.errors?.['required']) {\n Email address is required\n }\n @if (userForm.get('email')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"title\">Job Title</label>\n <input\n id=\"title\"\n type=\"text\"\n class=\"field-input\"\n formControlName=\"title\"\n placeholder=\"Software Engineer\"\n />\n </div>\n </div>\n </div>\n </div>\n <!-- User Settings Section -->\n <div class=\"content-section\">\n <div class=\"form-section\">\n <div class=\"section-header\">\n <h3 class=\"section-title\">\n <i class=\"fa-solid fa-cog\"></i>\n User Settings\n </h3>\n <p class=\"section-description\">Configure user type and account status</p>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-row\">\n <div class=\"form-field\">\n <label class=\"field-label\" for=\"type\">User Type</label>\n <select id=\"type\" class=\"field-select\" formControlName=\"type\">\n <option value=\"User\">Standard User</option>\n <option value=\"Owner\">System Owner</option>\n </select>\n </div>\n <div class=\"form-field\">\n <div class=\"checkbox-field\">\n <input\n id=\"isActive\"\n type=\"checkbox\"\n class=\"checkbox-input\"\n formControlName=\"isActive\"\n />\n <label class=\"checkbox-label\" for=\"isActive\">\n <div class=\"checkbox-indicator\"></div>\n <div>\n <strong>Active Account</strong>\n <div style=\"font-size: 12px; color: var(--md-on-surface-variant); margin-top: 2px;\">\n User can log in and access the system\n </div>\n </div>\n </label>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <!-- Role Assignment Section -->\n <div class=\"content-section\">\n <div class=\"form-section\">\n <div class=\"section-header\">\n <h3 class=\"section-title\">\n <i class=\"fa-solid fa-user-tag\"></i>\n Role Assignment\n </h3>\n <p class=\"section-description\">Select the roles to assign to this user</p>\n </div>\n <div class=\"roles-grid\">\n @for (role of data?.availableRoles; track role.ID) {\n <div class=\"role-card\"\n [class.selected]=\"selectedRoleIds.has(role.ID)\"\n (click)=\"toggleRole(role.ID)\">\n <div class=\"role-content\">\n <div class=\"checkbox-field\">\n <input\n type=\"checkbox\"\n class=\"checkbox-input\"\n [id]=\"'role-' + role.ID\"\n [checked]=\"selectedRoleIds.has(role.ID)\"\n (change)=\"onRoleToggle(role.ID, $event)\"\n />\n <label class=\"checkbox-label\" [for]=\"'role-' + role.ID\">\n <div class=\"checkbox-indicator\"></div>\n </label>\n </div>\n <div class=\"role-info\">\n <h4 class=\"role-name\">{{ role.Name }}</h4>\n @if (role.Description) {\n <p class=\"role-description\">{{ role.Description }}</p>\n }\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n <!-- Modal Footer -->\n <div class=\"modal-footer\">\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"userForm.invalid || isLoading\"\n >\n @if (isLoading) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n Saving...\n } @else {\n <i class=\"fa-solid fa-save\"></i>\n {{ isEditMode ? 'Update User' : 'Create User' }}\n }\n </button>\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancel()\">\n <i class=\"fa-solid fa-times\"></i>\n Cancel\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"user-management.component.d.ts","sourceRoot":"","sources":["../../../src/lib/user-management/user-management.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAGhD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAoB,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC3G,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;;AAavF,UAAU,SAAS;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAOa,uBAAwB,SAAQ,aAAc,YAAW,SAAS;IA0DjE,OAAO,CAAC,GAAG;IAAqB,OAAO,CAAC,MAAM;IAvDnD,KAAK,EAAE,YAAY,EAAE,CAAM;IAC3B,aAAa,EAAE,YAAY,EAAE,CAAM;IACnC,KAAK,EAAE,YAAY,EAAE,CAAM;IAC3B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAQ;IACzC,SAAS,UAAS;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IAG5B,eAAe,cAAqB;IAGpC,cAAc,UAAS;IACvB,cAAc,EAAE,cAAc,GAAG,IAAI,CAAQ;IAG7C,qBAAqB,UAAS;IAC9B,cAAc,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAQ;IAC9D,kBAAkB,UAAS;IAC3B,UAAU,EAAE,MAAM,CAAM;IAGxB,KAAK,EAAE,SAAS,CAKrB;IAGK,QAAQ,iCAIZ;IAGI,gBAAgB,UAAS;IACzB,cAAc,UAAS;IACvB,iBAAiB,UAAS;IAGjC,OAAO,CAAC,eAAe,CAAqB;IAG5C,OAAO,CAAC,WAAW,CAA+B;IAG3C,UAAU;;;;MAIf;IAEF,UAAmB,QAAQ,gBAAuB;IAClD,OAAO,KAAK,QAAQ,GAAiC;gBACjC,GAAG,EAAE,iBAAiB,EAAU,MAAM,EAAE,MAAM;IAI5D,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,SAAS,CAAC,aAAa,IAAI,IAAI;IAK/B,SAAS,CAAC,QAAQ,IAAI,IAAI;IAoB1B,qEAAqE;IACrE,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,kBAAkB,CAA4B;IAEtD;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAsB3B;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IA2DhC,OAAO,CAAC,4BAA4B;IAUpC,OAAO,CAAC,sBAAsB;IAiB9B,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,8BAA8B;IAYtC,mGAAmG;IACnG,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,sBAAsB;YAMhB,iBAAiB;IAU/B,OAAO,CAAC,gBAAgB;IAQf,WAAW,IAAI,IAAI;IAMf,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;YAiC/B,SAAS;YAUT,SAAS;YAWT,aAAa;IAU3B,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,uBAAuB;IAa/B,OAAO,CAAC,YAAY;IAmCpB,OAAO,CAAC,cAAc;IAUf,oBAAoB,CAAC,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,IAAI;IAIjE,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAcnD,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAKpC,aAAa,IAAI,IAAI;IAQrB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IASlC,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAKrC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3B,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BzD,WAAW,IAAI,IAAI;IA8C1B,OAAO,CAAC,SAAS;IAOV,WAAW,IAAI,IAAI;IAInB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAIzC,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAI1C,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAW3C,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAM3C,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAWzD,IAAW,aAAa,IAAI,OAAO,CAGlC;IAED,IAAW,eAAe,IAAI,OAAO,CAGpC;IAED,IAAW,YAAY,IAAI,OAAO,CAEjC;IAED,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED,IAAW,gBAAgB,IAAI,OAAO,CAGrC;IAED,IAAW,iBAAiB,IAAI,MAAM,CAMrC;IAEM,YAAY,IAAI,IAAI;IAkB3B;;;OAGG;IACH,IAAW,YAAY,IAAI,iBAAiB,EAAE,CAyB7C;IAED,mEAAmE;IACnE,IAAW,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEjD;IAED,6EAA6E;IAC7E,IAAW,sBAAsB,IAAI,MAAM,CAG1C;IAED,mDAAmD;IAC5C,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAWjE,0DAA0D;IACnD,sBAAsB,IAAI,IAAI;IAIrC;sDACkD;IAClD,IAAW,cAAc,IAAI,OAAO,CAEnC;IAED;;qEAEiE;IAC1D,wBAAwB,IAAI,IAAI;IAMhC,eAAe,IAAI,IAAI;IAUvB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI;IAWxD,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIvC,cAAc,IAAI,IAAI;IAKtB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI;IAMhE,gBAAgB,IAAI,IAAI;IAKlB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;YAqCjC,iBAAiB;YAmBjB,eAAe;IAetB,kBAAkB,IAAI,IAAI;IAM1B,oBAAoB,IAAI,IAAI;IAKtB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6E5C,oBAAoB,IAAI,MAAM;IAc9B,kBAAkB,IAAI,MAAM;IAa5B,uBAAuB,IAAI,MAAM;IAejC,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQzC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKvC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE;IAKnD;;;OAGG;IACH,OAAO,CAAC,YAAY;yCA/gCT,uBAAuB;2CAAvB,uBAAuB;CAmhCnC"}
1
+ {"version":3,"file":"user-management.component.d.ts","sourceRoot":"","sources":["../../../src/lib/user-management/user-management.component.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';
10
10
  import { RunView, CompositeKey } from '@memberjunction/core';
11
11
  import { BaseDashboard } from '@memberjunction/ng-shared';
12
12
  import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
13
+ import { serverRefusalReasons } from './transaction-group-refusals';
13
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";
@@ -1337,13 +1338,15 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1337
1338
  // (issue #4282) lives in `@memberjunction/core-entities-server`, which no browser package
1338
1339
  // depends on, so it never registers here — it refuses on the server, during Submit().
1339
1340
  //
1340
- // And today that refusal reaches the user NOWHERE. `ExecuteTransactionGroup` discards the
1341
- // refused row's `Save()` return, so the row never enrols in the SERVER's group either; an
1342
- // all-refused batch submits an empty group, whose `Submit()` returns true for having
1343
- // nothing to do, and the screen closes with no message at all. Verified end to end against
1344
- // a live server: a non-Owner assigning a role they do not hold is correctly refused (no row
1345
- // is written) and is reported as success. Tracked as issue #4309 — fixing it belongs in the
1346
- // resolver, which is the only layer that still knows which row was refused and why.
1341
+ // That server refusal used to reach the user NOWHERE: `ExecuteTransactionGroup` discarded
1342
+ // the refused row's `Save()` return, so the row never enrolled in the SERVER's group
1343
+ // either; an all-refused batch submitted an empty group, whose `Submit()` returns true for
1344
+ // having nothing to do, and the screen closed reporting success having written nothing.
1345
+ // Issue #4309 fixed that in the resolver — the only layer that still knows which row was
1346
+ // refused and why. It now reports the refusal, `Submit()` returns FALSE, and
1347
+ // `GraphQLTransactionGroup.recordServerFailure` copies the server's reason onto each
1348
+ // item's `BaseEntity.LatestResult`. The `!await tg.Submit()` branch below reads it back;
1349
+ // that is the only place a server-side refusal surfaces on this screen.
1347
1350
  //
1348
1351
  // What this check DOES catch is a CLIENT-side refusal — a CheckPermissions denial or a
1349
1352
  // field-rule failure — which really does return false here, leaving that row unenrolled.
@@ -1351,6 +1354,7 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1351
1354
  // Submit() returns true for having nothing to do, so the screen reported success having
1352
1355
  // assigned nothing. That is the failure this guards; keep it.
1353
1356
  const refusals = [];
1357
+ const enrolled = [];
1354
1358
  for (const userId of usersNeedingRole) {
1355
1359
  const userRole = await this.metadata.GetEntityObject('MJ: User Roles');
1356
1360
  userRole.NewRecord();
@@ -1360,6 +1364,12 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1360
1364
  if (!await userRole.Save()) {
1361
1365
  refusals.push(`${this.describeUser(userId)}: ${userRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1362
1366
  }
1367
+ else {
1368
+ // Kept so the SERVER's reason can be read back off it below. Without this the entity
1369
+ // goes out of scope at the end of the iteration and the reason #4309 puts on
1370
+ // LatestResult has nobody left to read it.
1371
+ enrolled.push({ label: this.describeUser(userId), entity: userRole });
1372
+ }
1363
1373
  }
1364
1374
  if (refusals.length > 0) {
1365
1375
  // Nothing was written: the refused rows never enrolled, and the rest are still only
@@ -1367,7 +1377,12 @@ let UserManagementComponent = class UserManagementComponent extends BaseDashboar
1367
1377
  throw new Error(`Failed to assign roles — nothing was changed.\n${refusals.join('\n')}`);
1368
1378
  }
1369
1379
  if (!await tg.Submit()) {
1370
- throw new Error('Failed to assign roles — all changes have been rolled back');
1380
+ // Every row enrolled, so this is a SERVER-side refusal (or a rollback). Since #4309 the
1381
+ // server says which row and why, and that reason is now on each entity's LatestResult.
1382
+ const reasons = serverRefusalReasons(enrolled);
1383
+ throw new Error(reasons.length > 0
1384
+ ? `Failed to assign roles — all changes have been rolled back.\n${reasons.join('\n')}`
1385
+ : 'Failed to assign roles — all changes have been rolled back');
1371
1386
  }
1372
1387
  }
1373
1388
  this.clearSelection();
@@ -1529,5 +1544,5 @@ export { UserManagementComponent };
1529
1544
  type: Component,
1530
1545
  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
1546
  }], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }], null); })();
1532
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserManagementComponent, { className: "UserManagementComponent", filePath: "src/lib/user-management/user-management.component.ts", lineNumber: 42 }); })();
1547
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserManagementComponent, { className: "UserManagementComponent", filePath: "src/lib/user-management/user-management.component.ts", lineNumber: 43 }); })();
1533
1548
  //# sourceMappingURL=user-management.component.js.map