-
+
+@if (isInitializing && showInitialLoader) {
+
+
- } @else {
-
-
- To be implemented: user list with pagination, search, and actions (edit/delete)
+
+}
+
+@if (!isInitializing) {
+
+
+
- }
-
+
+
+
+
+
+
+}
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.scss
index 26c7a759..826a9ace 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.scss
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.scss
@@ -1,115 +1,12 @@
@use '../../../../../../../../src/assets/styles/design-tokens';
-.form-field-header {
- position: relative;
+// ─── Page Content ─────────────────────────────────────────────────────────────
+
+.page-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--ov-meet-spacing-lg);
}
-.admin-auth-form {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: var(--ov-meet-spacing-md, 16px);
- align-items: start;
- @media (max-width: 768px) {
- grid-template-columns: 1fr;
- gap: var(--ov-meet-spacing-sm, 12px);
- }
- .user-id-field,
- .password-field {
- width: 100%;
-
- ::ng-deep .mat-mdc-form-field {
- width: 100%;
- }
-
- ::ng-deep .input-btn {
- padding: var(--ov-meet-spacing-sm);
- }
- }
-
- .user-id-field {
- ::ng-deep {
- .mat-mdc-form-field {
- .mat-mdc-input-element:disabled {
- color: var(--ov-meet-text-secondary, rgba(0, 0, 0, 0.6));
- }
- }
- }
- }
-
- .password-field {
- ::ng-deep {
- .mat-mdc-form-field {
- .mat-mdc-floating-label {
- &.mdc-floating-label--required::after {
- content: ' *';
- color: var(--ov-meet-error, #f44336);
- }
- }
- }
- }
- }
-}
-
-.form-section {
- margin-bottom: var(--ov-meet-spacing-lg, 24px);
-
- .form-field-header {
- margin-bottom: var(--ov-meet-spacing-sm, 12px);
-
- h3 {
- margin: 0 0 var(--ov-meet-spacing-xs, 8px) 0;
- font-weight: 600;
- color: var(--ov-meet-text-primary, rgba(0, 0, 0, 0.87));
- }
- }
-
- .field-description {
- margin: 0 0 var(--ov-meet-spacing-md, 16px) 0;
- color: var(--ov-meet-text-secondary, rgba(0, 0, 0, 0.6));
- font-size: 14px;
- line-height: 1.4;
- }
-}
-
-.mat-mdc-form-field {
- width: 100%;
-
- &.textarea-field {
- ::ng-deep .mat-mdc-text-field-wrapper {
- min-height: 120px;
- }
- }
-
- ::ng-deep .mat-mdc-text-field-wrapper {
- &.mdc-text-field--disabled {
- background-color: var(--ov-meet-surface-variant);
- }
- border-radius: var(--ov-meet-border-radius-sm);
- }
-
- ::ng-deep .mdc-notched-outline__leading,
- ::ng-deep .mdc-notched-outline__notch,
- ::ng-deep .mdc-notched-outline__trailing {
- border-color: var(--ov-meet-border-color);
- }
-}
-
-.mat-mdc-card-actions {
- padding: var(--ov-meet-spacing-lg) var(--ov-meet-spacing-xl);
- gap: var(--ov-meet-spacing-sm);
- border-top: 1px solid var(--ov-meet-border-color);
- margin: auto;
-
- @include design-tokens.ov-mobile-down {
- flex-direction: column;
-
- .mat-mdc-button,
- .mat-mdc-raised-button,
- .mat-mdc-stroked-button {
- width: 100%;
- margin: var(--ov-meet-spacing-xs) 0;
- }
- }
-}
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.ts
index d2fb74cb..672f2504 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/pages/users/users.component.ts
@@ -1,13 +1,200 @@
-import { Component, signal } from '@angular/core';
+import { Component, inject, OnInit, signal } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MeetUserDTO, MeetUserFilters, MeetUserRole, SortOrder } from '@openvidu-meet/typings';
+import { ILogger, LoggerService } from 'openvidu-components-angular';
+import { NavigationService } from '../../../../shared/services/navigation.service';
+import { NotificationService } from '../../../../shared/services/notification.service';
+import { AuthService } from '../../../auth/services/auth.service';
+import { ResetPasswordDialogComponent } from '../../components/reset-password-dialog/reset-password-dialog.component';
+import { UsersListsComponent, UserTableAction, UserTableFilter } from '../../components/users-lists/users-lists.component';
+import { UserService } from '../../services/user.service';
@Component({
selector: 'ov-users',
- imports: [MatProgressSpinnerModule, MatIconModule],
+ imports: [
+ MatButtonModule,
+ MatIconModule,
+ MatProgressSpinnerModule,
+ UsersListsComponent
+ ],
templateUrl: './users.component.html',
styleUrl: './users.component.scss'
})
-export class UsersComponent {
- isLoading = signal(false);
+export class UsersComponent implements OnInit {
+ private userService = inject(UserService);
+ private authService = inject(AuthService);
+ private notificationService = inject(NotificationService);
+ private navigationService = inject(NavigationService);
+ private dialog = inject(MatDialog);
+ private loggerService = inject(LoggerService);
+ protected log: ILogger;
+
+ users = signal
([]);
+ currentUserId = signal('');
+
+ isInitializing = true;
+ showInitialLoader = false;
+ isLoading = false;
+
+ hasMoreUsers = false;
+ private nextPageToken?: string;
+
+ initialFilters = signal({
+ nameFilter: '',
+ roleFilter: '',
+ sortField: 'registrationDate',
+ sortOrder: SortOrder.DESC
+ });
+
+ constructor() {
+ this.log = this.loggerService.get('OpenVidu Meet - UsersComponent');
+ }
+
+ async ngOnInit() {
+ const delayLoader = setTimeout(() => {
+ this.showInitialLoader = true;
+ }, 200);
+
+ this.currentUserId.set((await this.authService.getUserId()) ?? '');
+ await this.loadUsers(this.initialFilters());
+
+ clearTimeout(delayLoader);
+ this.showInitialLoader = false;
+ this.isInitializing = false;
+ }
+
+ async onUserAction(action: UserTableAction) {
+ switch (action.action) {
+ case 'create':
+ await this.onCreateUser();
+ break;
+ case 'resetPassword':
+ this.onResetPassword(action.users[0]);
+ break;
+ case 'delete':
+ this.onDeleteUser(action.users[0]);
+ break;
+ case 'bulkDelete':
+ this.onBulkDeleteUsers(action.users);
+ break;
+ }
+ }
+
+ async refreshUsers(filters: UserTableFilter) {
+ this.nextPageToken = undefined;
+ await this.loadUsers(filters, true);
+ }
+
+ async loadMoreUsers(filters: UserTableFilter) {
+ await this.loadUsers(filters);
+ }
+
+ // ─── Actions ──────────────────────────────────────────────────────────────
+
+ private async onCreateUser() {
+ await this.navigationService.navigateTo('/users/new');
+ }
+
+ private onResetPassword(user: MeetUserDTO) {
+ this.dialog.open(ResetPasswordDialogComponent, {
+ width: '520px',
+ data: { user },
+ panelClass: 'ov-meet-dialog'
+ });
+ }
+
+ private onDeleteUser(user: MeetUserDTO) {
+ this.notificationService.showDialog({
+ title: 'Delete User',
+ icon: 'delete_forever',
+ message: `Are you sure you want to permanently delete user ${user.name} (${user.userId})? This action cannot be undone.`,
+ confirmText: 'Delete',
+ cancelText: 'Cancel',
+ confirmCallback: async () => {
+ try {
+ await this.userService.deleteUser(user.userId);
+ this.users.set(this.users().filter((u) => u.userId !== user.userId));
+ this.notificationService.showSnackbar(`User "${user.name}" deleted successfully`);
+ } catch (error) {
+ this.log.e('Error deleting user:', error);
+ this.notificationService.showSnackbar('Failed to delete user');
+ }
+ }
+ });
+ }
+
+ private onBulkDeleteUsers(usersToDelete: MeetUserDTO[]) {
+ const count = usersToDelete.length;
+ this.notificationService.showDialog({
+ title: 'Delete Users',
+ icon: 'delete_forever',
+ message: `Are you sure you want to permanently delete ${count} user${count > 1 ? 's' : ''}? This action cannot be undone.`,
+ confirmText: 'Delete',
+ cancelText: 'Cancel',
+ confirmCallback: async () => {
+ const failed: string[] = [];
+ for (const user of usersToDelete) {
+ try {
+ await this.userService.deleteUser(user.userId);
+ } catch (error) {
+ this.log.e('Error deleting user:', user.userId, error);
+ failed.push(user.name);
+ }
+ }
+ const deletedIds = new Set(usersToDelete.map((u) => u.userId));
+ this.users.set(this.users().filter((u) => !deletedIds.has(u.userId)));
+ if (failed.length > 0) {
+ this.notificationService.showSnackbar(`Failed to delete: ${failed.join(', ')}`);
+ } else {
+ this.notificationService.showSnackbar(`${count} user${count > 1 ? 's' : ''} deleted successfully`);
+ }
+ }
+ });
+ }
+
+ // ─── Data loading ─────────────────────────────────────────────────────────
+
+ private async loadUsers(filters: UserTableFilter, refresh = false) {
+ const delayLoader = setTimeout(() => {
+ this.isLoading = true;
+ }, 200);
+
+ try {
+ const userFilters: MeetUserFilters = {
+ maxItems: 50,
+ nextPageToken: !refresh ? this.nextPageToken : undefined,
+ sortField: filters.sortField,
+ sortOrder: filters.sortOrder
+ };
+
+ if (filters.nameFilter) {
+ userFilters.name = filters.nameFilter;
+ }
+
+ if (filters.roleFilter) {
+ userFilters.role = filters.roleFilter as MeetUserRole;
+ }
+
+ const response = await this.userService.listUsers(userFilters);
+
+ if (!refresh) {
+ this.users.set([...this.users(), ...response.users]);
+ } else {
+ this.users.set(response.users);
+ }
+
+ this.nextPageToken = response.pagination.nextPageToken;
+ this.hasMoreUsers = response.pagination.isTruncated;
+ } catch (error) {
+ this.log.e('Error loading users:', error);
+ this.notificationService.showSnackbar('Failed to load users');
+ } finally {
+ clearTimeout(delayLoader);
+ this.isLoading = false;
+ }
+ }
}
+
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/routes/users.routes.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/routes/users.routes.ts
index 426c3c12..acd2aae6 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/routes/users.routes.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/users/routes/users.routes.ts
@@ -21,5 +21,11 @@ export const usersConsoleRoutes: DomainRouteConfig[] = [
iconClass: 'ov-users material-symbols-outlined',
order: 4
}
+ },
+ {
+ route: {
+ path: 'users/new',
+ loadComponent: () => import('../pages/create-user/create-user.component').then((m) => m.CreateUserComponent)
+ }
}
];