diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.html index 924a4ccc..d1aaef8e 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.html @@ -84,8 +84,9 @@ - +
+
@@ -120,6 +121,115 @@
+ + + + + + + + people + Room Members + {{ roomMembers().length }} + + +
+ @if (loadingMembers()) { +
+ +

Loading members...

+
+ } @else if (roomMembers().length === 0) { +
+ person_off +

No members yet

+

This room doesn't have any members assigned.

+
+ } @else { + +
+
+

Members ({{ roomMembers().length }})

+
+ @for (member of roomMembers(); track member.memberId) { +
+
+
+ {{ member.name.substring(0, 2).toUpperCase() }} +
+
+

{{ member.name }}

+

{{ member.memberId }}

+
+
+
+
+ + {{ + member.memberId.startsWith('ext-') + ? 'person_outline' + : 'verified_user' + }} + + {{ + member.memberId.startsWith('ext-') ? 'External' : 'Registered' + }} +
+
+ + {{ member.baseRole }} + +
+
+
+ + + +
+
+ } +
+ } +
+
+ + + + + video_library + Recordings + {{ recordings().length }} + + +
+ + +
+
+
+
} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.scss index 04c7cdc8..3ab5cd2e 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.scss +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.scss @@ -1,5 +1,8 @@ @use '../../../../../../../../src/assets/styles/design-tokens' as *; +.ov-page-container { + overflow: hidden; +} .page-header { @include ov-page-header; @@ -41,18 +44,6 @@ text-transform: uppercase; width: fit-content; - &.active { - background-color: rgba(76, 175, 80, 0.1); - color: var(--ov-meet-color-success); - border: 1px solid rgba(76, 175, 80, 0.3); - } - - &.closed { - background-color: rgba(158, 158, 158, 0.1); - color: var(--ov-meet-text-secondary); - border: 1px solid rgba(158, 158, 158, 0.3); - } - .status-dot { width: 8px; height: 8px; @@ -109,11 +100,7 @@ margin-top: var(--ov-meet-spacing-lg); .info-card { - @include ov-card; - - mat-card-content { - padding: 0; - } + background-color: var(--ov-meet-surface-container); .info-grid { display: grid; @@ -132,6 +119,7 @@ display: flex; flex-direction: column; gap: var(--ov-meet-spacing-xs); + margin: auto; .info-label { font-size: var(--ov-meet-font-size-xs); @@ -140,6 +128,8 @@ text-transform: uppercase; letter-spacing: 0.05em; margin: 0; + text-align: center; + margin-bottom: var(--ov-meet-spacing-xs); } .info-value { @@ -180,6 +170,231 @@ } } } + + // Tabs card + .tabs-card { + @include ov-card; + overflow: hidden; + box-shadow: none; + &:hover { + box-shadow: none; + } + + ::ng-deep { + .mat-mdc-tab-group { + .mat-mdc-tab-labels { + border-bottom: 1px solid var(--ov-meet-border-color); + } + + .mat-mdc-tab { + min-width: 160px; + + .tab-icon { + @include ov-icon(sm); + margin-right: var(--ov-meet-spacing-xs); + } + + .tab-count { + margin-left: var(--ov-meet-spacing-xs); + background-color: var(--ov-meet-background-elevated); + color: var(--ov-meet-text-secondary); + padding: 2px 8px; + border-radius: var(--ov-meet-radius-sm); + font-size: var(--ov-meet-font-size-xs); + font-weight: var(--ov-meet-font-weight-semibold); + } + + &.mat-mdc-tab-active .tab-count { + background-color: var(--ov-meet-color-primary); + color: white; + } + } + } + } + + .tab-content { + padding: var(--ov-meet-spacing-lg); + min-height: 400px; + + &.recordings-tab { + padding: 0; + } + + .loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--ov-meet-spacing-md); + padding: var(--ov-meet-spacing-xxl); + + p { + color: var(--ov-meet-text-secondary); + margin: 0; + } + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--ov-meet-spacing-md); + padding: var(--ov-meet-spacing-xxl); + + mat-icon { + @include ov-icon(xxl); + color: var(--ov-meet-text-hint); + } + + h3 { + margin: 0; + color: var(--ov-meet-text-primary); + font-size: var(--ov-meet-font-size-lg); + } + + p { + margin: 0; + color: var(--ov-meet-text-secondary); + font-size: var(--ov-meet-font-size-sm); + } + } + } + } + + // Members list + .members-list { + .members-header { + margin-bottom: var(--ov-meet-spacing-md); + padding-bottom: var(--ov-meet-spacing-sm); + border-bottom: 1px solid var(--ov-meet-border-color); + + h3 { + margin: 0; + font-size: var(--ov-meet-font-size-md); + font-weight: var(--ov-meet-font-weight-semibold); + color: var(--ov-meet-text-primary); + } + } + + .member-item { + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-md); + padding: var(--ov-meet-spacing-md); + border: 1px solid var(--ov-meet-border-color); + border-radius: var(--ov-meet-radius-md); + margin-bottom: var(--ov-meet-spacing-sm); + transition: all 0.2s ease; + + &:hover { + background-color: var(--ov-meet-background-hover); + border-color: var(--ov-meet-color-primary); + } + + .member-info { + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-sm); + flex: 1; + + .member-avatar { + width: 40px; + height: 40px; + border-radius: var(--ov-meet-radius-circle); + background: linear-gradient(135deg, var(--ov-meet-color-primary), var(--ov-meet-color-accent)); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--ov-meet-font-size-sm); + font-weight: var(--ov-meet-font-weight-bold); + flex-shrink: 0; + } + + .member-details { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + + .member-name { + margin: 0; + font-size: var(--ov-meet-font-size-sm); + font-weight: var(--ov-meet-font-weight-semibold); + color: var(--ov-meet-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .member-id { + margin: 0; + font-size: var(--ov-meet-font-size-xs); + color: var(--ov-meet-text-secondary); + font-family: 'Courier New', monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + .member-meta { + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-md); + flex-wrap: wrap; + + .member-type { + display: flex; + align-items: center; + gap: var(--ov-meet-spacing-xs); + padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm); + background-color: var(--ov-meet-background-elevated); + border-radius: var(--ov-meet-radius-sm); + font-size: var(--ov-meet-font-size-xs); + color: var(--ov-meet-text-secondary); + + .meta-icon { + @include ov-icon(xs); + } + } + + .member-role { + .role-badge { + display: inline-block; + padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm); + background-color: var(--ov-meet-background-elevated); + border-radius: var(--ov-meet-radius-sm); + font-size: var(--ov-meet-font-size-xs); + font-weight: var(--ov-meet-font-weight-semibold); + color: var(--ov-meet-text-secondary); + text-transform: capitalize; + + &.moderator { + background-color: rgba(33, 150, 243, 0.1); + color: var(--ov-meet-color-primary); + border: 1px solid rgba(33, 150, 243, 0.3); + } + } + } + } + + .member-actions { + display: flex; + gap: var(--ov-meet-spacing-xs); + + button { + @include ov-theme-transition; + + mat-icon { + @include ov-icon(sm); + } + } + } + } + } } @keyframes pulse { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.ts index d2804da5..8abf3a61 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-detail/room-detail.component.ts @@ -6,13 +6,18 @@ import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { MeetRoom, MeetRoomStatus } from '@openvidu-meet/typings'; +import { MeetRecordingInfo, MeetRoom, MeetRoomMember, MeetRoomStatus } from '@openvidu-meet/typings'; import { ILogger, LoggerService } from 'openvidu-components-angular'; import { BreadcrumbComponent, BreadcrumbItem } from '../../../../shared/components/breadcrumb/breadcrumb.component'; import { NavigationService } from '../../../../shared/services/navigation.service'; import { NotificationService } from '../../../../shared/services/notification.service'; +import { RecordingListsComponent } from '../../../recordings/components/recording-lists/recording-lists.component'; +import { RecordingTableAction, RecordingTableFilter } from '../../../recordings/models/recording-list.model'; +import { RecordingService } from '../../../recordings/services/recording.service'; +import { RoomMemberService } from '../../../room-members/services/room-member.service'; import { DeleteRoomDialogComponent } from '../../components/delete-room-dialog/delete-room-dialog.component'; import { RoomService } from '../../services/room.service'; import { RoomUiUtils } from '../../utils/ui'; @@ -26,8 +31,10 @@ import { RoomUiUtils } from '../../utils/ui'; MatTooltipModule, MatProgressSpinnerModule, MatChipsModule, + MatTabsModule, RouterModule, - BreadcrumbComponent + BreadcrumbComponent, + RecordingListsComponent ], templateUrl: './room-detail.component.html', styleUrl: './room-detail.component.scss' @@ -36,6 +43,20 @@ export class RoomDetailComponent implements OnInit { room = signal(undefined); isLoading = signal(true); breadcrumbItems = signal([]); + + // Room Members tab + roomMembers = signal([]); + loadingMembers = signal(false); + + // Recordings tab + recordings = signal([]); + loadingRecordings = signal(false); + hasMoreRecordings = false; + private nextRecordingsPageToken?: string; + + // Tab management + selectedTabIndex = signal(0); + protected log: ILogger; MeetRoomStatus = MeetRoomStatus; @@ -45,6 +66,8 @@ export class RoomDetailComponent implements OnInit { private route: ActivatedRoute, private router: Router, private roomService: RoomService, + private roomMemberService: RoomMemberService, + private recordingService: RecordingService, private notificationService: NotificationService, protected navigationService: NavigationService, private clipboard: Clipboard, @@ -81,6 +104,12 @@ export class RoomDetailComponent implements OnInit { label: room.roomName } ]); + + // Load initial data for tabs + await Promise.all([ + this.loadRoomMembers(roomId), + this.loadRecordings(roomId) + ]); } catch (error) { this.log.e('Error loading room details:', error); this.notificationService.showSnackbar('Failed to load room details'); @@ -90,6 +119,87 @@ export class RoomDetailComponent implements OnInit { } } + private async loadRoomMembers(roomId: string) { + try { + this.loadingMembers.set(true); + const response = await this.roomMemberService.listRoomMembers(roomId, { + maxItems: 100, + sortField: 'membershipDate', + sortOrder: 'desc' + }); + this.roomMembers.set(response.members); + } catch (error) { + this.log.e('Error loading room members:', error); + this.notificationService.showSnackbar('Failed to load room members'); + } finally { + this.loadingMembers.set(false); + } + } + + private async loadRecordings(roomId: string, refresh = false) { + try { + this.loadingRecordings.set(true); + const response = await this.recordingService.listRecordings({ + roomId, + maxItems: 50, + nextPageToken: !refresh ? this.nextRecordingsPageToken : undefined, + sortField: 'startDate', + sortOrder: 'desc' + }); + + if (!refresh) { + const currentRecordings = this.recordings(); + this.recordings.set([...currentRecordings, ...response.recordings]); + } else { + this.recordings.set(response.recordings); + } + + this.nextRecordingsPageToken = response.pagination.nextPageToken; + this.hasMoreRecordings = response.pagination.isTruncated; + } catch (error) { + this.log.e('Error loading recordings:', error); + this.notificationService.showSnackbar('Failed to load recordings'); + } finally { + this.loadingRecordings.set(false); + } + } + + async onRecordingAction(action: RecordingTableAction) { + const recording = action.recordings[0]; + + switch (action.action) { + case 'play': + await this.recordingService.playRecording(recording.recordingId); + break; + case 'download': + this.recordingService.downloadRecording(recording); + break; + case 'shareLink': + this.recordingService.openShareRecordingDialog(recording.recordingId); + break; + case 'delete': + case 'bulkDelete': + // Handle delete - can be implemented later + break; + } + } + + async loadMoreRecordings(filters: RecordingTableFilter) { + if (!this.hasMoreRecordings || this.loadingRecordings()) return; + const roomId = this.room()?.roomId; + if (roomId) { + await this.loadRecordings(roomId); + } + } + + async refreshRecordings(filters: RecordingTableFilter) { + const roomId = this.room()?.roomId; + if (roomId) { + this.nextRecordingsPageToken = undefined; + await this.loadRecordings(roomId, true); + } + } + async joinRoom() { const room = this.room(); if (!room) return;