frontend: enhance room status display and meeting end actions in rooms list

This commit is contained in:
juancarmore 2025-08-29 17:24:55 +02:00
parent e20713288c
commit 2466acabce
6 changed files with 168 additions and 136 deletions

View File

@ -182,22 +182,36 @@
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef id="status-header">Status</th> <th mat-header-cell *matHeaderCellDef id="status-header">Status</th>
<td mat-cell *matCellDef="let room" id="status-cell-{{ room.roomId }}"> <td mat-cell *matCellDef="let room" id="status-cell-{{ room.roomId }}">
<div <div class="status-container" id="status-container-{{ room.roomId }}">
class="status-badge" <div
[style.color]="getStatusColor(room)" class="status-badge"
[matTooltip]="getStatusTooltip(room)"
id="status-badge-{{ room.roomId }}"
>
<mat-icon
class="status-icon"
[style.color]="getStatusColor(room)" [style.color]="getStatusColor(room)"
id="status-icon-{{ room.roomId }}" [matTooltip]="getStatusTooltip(room)"
id="status-badge-{{ room.roomId }}"
> >
{{ getStatusIcon(room) }} <mat-icon
</mat-icon> class="status-icon"
<span class="status-label" id="status-label-{{ room.roomId }}">{{ [style.color]="getStatusColor(room)"
getRoomStatus(room) id="status-icon-{{ room.roomId }}"
}}</span> >
{{ getStatusIcon(room) }}
</mat-icon>
<span class="status-label" id="status-label-{{ room.roomId }}">{{
getRoomStatus(room)
}}</span>
</div>
<!-- Meeting End Action Info Icon -->
@if (hasMeetingEndAction(room)) {
<mat-icon
class="meeting-end-info-icon"
[ngClass]="getMeetingEndActionClass(room)"
[matTooltip]="getMeetingEndActionTooltip(room)"
id="meeting-end-info-icon-{{ room.roomId }}"
>
info_outline
</mat-icon>
}
</div> </div>
</td> </td>
</ng-container> </ng-container>
@ -227,52 +241,34 @@
<td mat-cell *matCellDef="let room" id="auto-deletion-cell-{{ room.roomId }}"> <td mat-cell *matCellDef="let room" id="auto-deletion-cell-{{ room.roomId }}">
<div class="auto-deletion-content" id="auto-deletion-content-{{ room.roomId }}"> <div class="auto-deletion-content" id="auto-deletion-content-{{ room.roomId }}">
<div class="auto-deletion-info" id="auto-deletion-info-{{ room.roomId }}"> <div class="auto-deletion-info" id="auto-deletion-info-{{ room.roomId }}">
@if (hasAutoDeletion(room)) { <div
<div class="deletion-badge"
class="deletion-badge" [ngClass]="getAutoDeletionClass(room)"
[ngClass]="getAutoDeletionClass(room)" [matTooltip]="getAutoDeletionTooltip(room)"
[matTooltip]="getAutoDeletionTooltip(room)" id="deletion-badge-{{ room.roomId }}"
id="deletion-badge-{{ room.roomId }}" >
<mat-icon
class="deletion-icon material-symbols-outlined"
id="deletion-icon-{{ room.roomId }}"
>{{ getAutoDeletionIcon(room) }}</mat-icon
> >
<mat-icon <span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
class="deletion-icon material-symbols-outlined" getAutoDeletionStatus(room)
id="deletion-icon-{{ room.roomId }}" }}</span>
>{{ getAutoDeletionIcon(room) }}</mat-icon </div>
>
<span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
getAutoDeletionStatus(room)
}}</span>
</div>
@if (!room.markedForDeletion) { @if (hasAutoDeletion(room)) {
<div class="deletion-date-time" id="deletion-date-time-{{ room.roomId }}"> <div class="deletion-date-time" id="deletion-date-time-{{ room.roomId }}">
<div class="deletion-date" id="deletion-date-container-{{ room.roomId }}"> <div class="deletion-date" id="deletion-date-container-{{ room.roomId }}">
<span class="date" id="deletion-date-{{ room.roomId }}">{{ <span class="date" id="deletion-date-{{ room.roomId }}">{{
room.autoDeletionDate | date: 'mediumDate' room.autoDeletionDate | date: 'mediumDate'
}}</span> }}</span>
</div> </div>
<div class="deletion-time" id="deletion-time-container-{{ room.roomId }}"> <div class="deletion-time" id="deletion-time-container-{{ room.roomId }}">
<span class="time" id="deletion-time-{{ room.roomId }}">{{ <span class="time" id="deletion-time-{{ room.roomId }}">{{
room.autoDeletionDate | date: 'shortTime' room.autoDeletionDate | date: 'shortTime'
}}</span> }}</span>
</div>
</div> </div>
}
} @else {
<div
class="deletion-badge"
[ngClass]="getAutoDeletionClass(room)"
[matTooltip]="getAutoDeletionTooltip(room)"
id="deletion-badge-{{ room.roomId }}"
>
<mat-icon
class="deletion-icon material-symbols-outlined"
id="deletion-icon-{{ room.roomId }}"
>{{ getAutoDeletionIcon(room) }}</mat-icon
>
<span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
getAutoDeletionStatus(room)
}}</span>
</div> </div>
} }
</div> </div>
@ -333,7 +329,7 @@
<span>View Recordings</span> <span>View Recordings</span>
</button> </button>
@if (!room.markedForDeletion) { @if (canOpenRoom(room)) {
<button <button
mat-menu-item mat-menu-item
(click)="copyModeratorLink(room)" (click)="copyModeratorLink(room)"
@ -350,18 +346,18 @@
<mat-icon>content_copy</mat-icon> <mat-icon>content_copy</mat-icon>
<span>Copy Speaker Link</span> <span>Copy Speaker Link</span>
</button> </button>
<mat-divider id="actions-divider-{{ room.roomId }}"></mat-divider>
<button
mat-menu-item
(click)="deleteRoom(room)"
class="delete-action"
id="delete-room-btn-{{ room.roomId }}"
>
<mat-icon>delete</mat-icon>
<span>Delete room</span>
</button>
} }
<mat-divider id="actions-divider-{{ room.roomId }}"></mat-divider>
<button
mat-menu-item
(click)="deleteRoom(room)"
class="delete-action"
id="delete-room-btn-{{ room.roomId }}"
>
<mat-icon>delete</mat-icon>
<span>Delete room</span>
</button>
</mat-menu> </mat-menu>
</div> </div>
</td> </td>
@ -372,7 +368,6 @@
mat-row mat-row
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
[class.selected-row]="isRoomSelected(row)" [class.selected-row]="isRoomSelected(row)"
[class.marked-for-deletion]="row.markedForDeletion"
id="table-row-{{ row.roomId }}" id="table-row-{{ row.roomId }}"
></tr> ></tr>
</table> </table>

View File

@ -109,16 +109,6 @@
@extend .actions-cell; @extend .actions-cell;
} }
} }
.mat-mdc-row {
&.marked-for-deletion {
background-color: rgba(244, 67, 54, 0.05);
&:hover {
background-color: rgba(244, 67, 54, 0.1);
}
}
}
} }
.room-info { .room-info {
@ -133,6 +123,13 @@
} }
} }
.status-container {
display: flex;
align-items: center;
gap: var(--ov-meet-spacing-sm);
width: 100%;
}
.status-badge { .status-badge {
@extend .ov-status-badge; @extend .ov-status-badge;
padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm); padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm);
@ -145,6 +142,18 @@
} }
} }
.meeting-end-info-icon {
@include ov-icon(sm);
&.meeting-end-close {
color: var(--ov-meet-color-warning);
}
&.meeting-end-delete {
color: var(--ov-meet-color-error);
}
}
.date-info { .date-info {
@extend .ov-date-info; @extend .ov-date-info;
} }
@ -183,15 +192,7 @@
} }
} }
.auto-deletion-pending { .auto-deletion-disabled {
color: var(--ov-meet-color-warn);
.deletion-icon {
color: var(--ov-meet-color-warn);
}
}
.auto-deletion-not-scheduled {
color: var(--ov-meet-text-hint); color: var(--ov-meet-text-hint);
.deletion-icon { .deletion-icon {

View File

@ -24,8 +24,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { MeetRoom } from '@lib/typings/ce'; import { MeetingEndAction, MeetRoom, MeetRoomStatus } from '@lib/typings/ce';
import { debounceTime, distinctUntilChanged } from 'rxjs';
export interface RoomTableAction { export interface RoomTableAction {
rooms: MeetRoom[]; rooms: MeetRoom[];
@ -128,8 +127,9 @@ export class RoomsListsComponent implements OnInit, OnChanges {
// Status options // Status options
statusOptions = [ statusOptions = [
{ value: '', label: 'All statuses' }, { value: '', label: 'All statuses' },
{ value: 'active', label: 'Active' }, { value: 'open', label: 'Open' },
{ value: 'inactive', label: 'Inactive' } { value: 'active_meeting', label: 'Active Meeting' },
{ value: 'closed', label: 'Closed' }
]; ];
constructor() {} constructor() {}
@ -228,8 +228,8 @@ export class RoomsListsComponent implements OnInit, OnChanges {
return this.selectedRooms().has(room.roomId); return this.selectedRooms().has(room.roomId);
} }
canSelectRoom(room: MeetRoom): boolean { canSelectRoom(_room: MeetRoom): boolean {
return !room.markedForDeletion; // Only active rooms can be selected return true;
} }
getSelectedRooms(): MeetRoom[] { getSelectedRooms(): MeetRoom[] {
@ -308,72 +308,106 @@ export class RoomsListsComponent implements OnInit, OnChanges {
this.statusFilterControl.setValue(''); this.statusFilterControl.setValue('');
} }
// ===== STATUS UTILITY METHODS =====
getRoomStatus(room: MeetRoom): string {
return room.markedForDeletion ? 'INACTIVE' : 'ACTIVE';
}
// ===== PERMISSION AND CAPABILITY METHODS ===== // ===== PERMISSION AND CAPABILITY METHODS =====
canOpenRoom(room: MeetRoom): boolean { canOpenRoom(room: MeetRoom): boolean {
return !room.markedForDeletion; return room.status !== MeetRoomStatus.CLOSED;
} }
canEditRoom(room: MeetRoom): boolean { canEditRoom(_room: MeetRoom): boolean {
return !room.markedForDeletion; return true;
} }
// ===== UI HELPER METHODS ===== // ===== UI HELPER METHODS =====
getStatusIcon(room: MeetRoom): string { // ===== STATUS =====
return room.markedForDeletion ? 'delete_outline' : 'check_circle';
getRoomStatus(room: MeetRoom): string {
return room.status.toUpperCase().replace(/_/g, ' ');
} }
getStatusColor(room: MeetRoom): string { getStatusIcon(room: MeetRoom): string {
if (room.markedForDeletion) { switch (room.status) {
return 'var(--ov-meet-color-error)'; case MeetRoomStatus.OPEN:
return 'meeting_room';
case MeetRoomStatus.ACTIVE_MEETING:
return 'videocam';
case MeetRoomStatus.CLOSED:
return 'lock';
} }
return 'var(--ov-meet-color-success)';
} }
getStatusTooltip(room: MeetRoom): string { getStatusTooltip(room: MeetRoom): string {
return room.markedForDeletion switch (room.status) {
? 'Room is inactive and marked for deletion' case MeetRoomStatus.OPEN:
: 'Room is active and accepting participants'; return 'Room is open and ready to accept participants';
case MeetRoomStatus.ACTIVE_MEETING:
return 'A meeting is currently ongoing in this room';
case MeetRoomStatus.CLOSED:
return 'Room is closed and not accepting participants';
}
} }
getStatusColor(room: MeetRoom): string {
switch (room.status) {
case MeetRoomStatus.OPEN:
return 'var(--ov-meet-color-success)';
case MeetRoomStatus.ACTIVE_MEETING:
return 'var(--ov-meet-color-primary)';
case MeetRoomStatus.CLOSED:
return 'var(--ov-meet-color-warning)';
}
}
// ===== MEETING END ACTION INFO =====
hasMeetingEndAction(room: MeetRoom): boolean {
return room.status === MeetRoomStatus.ACTIVE_MEETING && room.meetingEndAction !== MeetingEndAction.NONE;
}
getMeetingEndActionTooltip(room: MeetRoom): string {
switch (room.meetingEndAction) {
case MeetingEndAction.CLOSE:
return 'The room will be closed when the meeting ends';
case MeetingEndAction.DELETE:
return 'The room and its recordings will be deleted when the meeting ends';
default:
return '';
}
}
getMeetingEndActionClass(room: MeetRoom): string {
switch (room.meetingEndAction) {
case MeetingEndAction.CLOSE:
return 'meeting-end-close';
case MeetingEndAction.DELETE:
return 'meeting-end-delete';
default:
return '';
}
}
// ===== AUTO-DELETION =====
hasAutoDeletion(room: MeetRoom): boolean { hasAutoDeletion(room: MeetRoom): boolean {
return !!room.autoDeletionDate; return !!room.autoDeletionDate;
} }
getAutoDeletionStatus(room: MeetRoom): string { getAutoDeletionStatus(room: MeetRoom): string {
if (room.markedForDeletion) { return room.autoDeletionDate ? 'SCHEDULED' : 'DISABLED';
return 'Immediate';
}
return room.autoDeletionDate ? 'Scheduled' : 'Disabled';
} }
getAutoDeletionIcon(room: MeetRoom): string { getAutoDeletionIcon(room: MeetRoom): string {
if (room.markedForDeletion) {
return 'acute';
}
return room.autoDeletionDate ? 'auto_delete' : 'close'; return room.autoDeletionDate ? 'auto_delete' : 'close';
} }
getAutoDeletionClass(room: MeetRoom): string {
if (room.markedForDeletion) {
return 'auto-deletion-pending';
}
return room.autoDeletionDate ? 'auto-deletion-scheduled' : 'auto-deletion-not-scheduled';
}
getAutoDeletionTooltip(room: MeetRoom): string { getAutoDeletionTooltip(room: MeetRoom): string {
if (room.markedForDeletion) {
return 'Deletes when last participant leaves';
}
return room.autoDeletionDate return room.autoDeletionDate
? 'Auto-deletion scheduled' ? 'Auto-deletion scheduled'
: 'No auto-deletion. Room remains until manually deleted'; : 'No auto-deletion. Room remains until manually deleted';
} }
getAutoDeletionClass(room: MeetRoom): string {
return room.autoDeletionDate ? 'auto-deletion-scheduled' : 'auto-deletion-disabled';
}
} }

View File

@ -30,7 +30,7 @@
<div class="stat-content"> <div class="stat-content">
<div class="stat-number">{{ stats.totalRooms }}</div> <div class="stat-number">{{ stats.totalRooms }}</div>
<div class="stat-label">Total Rooms</div> <div class="stat-label">Total Rooms</div>
<div class="stat-detail">{{ stats.activeRooms }} active</div> <div class="stat-detail">{{ stats.activeRooms }} with active meetings</div>
</div> </div>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>

View File

@ -5,7 +5,7 @@ import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list'; import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { NavigationService, RecordingService, RoomService } from '@lib/services'; import { NavigationService, RecordingService, RoomService } from '@lib/services';
import { MeetRecordingStatus, MeetRoom } from '@lib/typings/ce'; import { MeetRecordingStatus, MeetRoom, MeetRoomStatus } from '@lib/typings/ce';
interface OverviewStats { interface OverviewStats {
totalRooms: number; totalRooms: number;
@ -56,7 +56,7 @@ export class OverviewComponent implements OnInit {
this.stats = { this.stats = {
totalRooms: rooms.length, totalRooms: rooms.length,
activeRooms: rooms.filter((room: MeetRoom) => !room.markedForDeletion).length, activeRooms: rooms.filter((room: MeetRoom) => room.status === MeetRoomStatus.ACTIVE_MEETING).length,
totalRecordings: recordings.length, totalRecordings: recordings.length,
playableRecordings: recordings.filter((recording) => recording.status === MeetRecordingStatus.COMPLETE) playableRecordings: recordings.filter((recording) => recording.status === MeetRecordingStatus.COMPLETE)
.length, .length,

View File

@ -16,7 +16,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { RoomsListsComponent, RoomTableAction } from '@lib/components'; import { RoomsListsComponent, RoomTableAction } from '@lib/components';
import { NavigationService, NotificationService, RoomService } from '@lib/services'; import { NavigationService, NotificationService, RoomService } from '@lib/services';
import { MeetRoom, MeetRoomFilters } from '@lib/typings/ce'; import { MeetingEndAction, MeetRoom, MeetRoomFilters } from '@lib/typings/ce';
import { ILogger, LoggerService } from 'openvidu-components-angular'; import { ILogger, LoggerService } from 'openvidu-components-angular';
@Component({ @Component({
@ -273,7 +273,9 @@ export class RoomsComponent implements OnInit {
// If the room is marked for deletion, we don't remove it from the list immediately // If the room is marked for deletion, we don't remove it from the list immediately
const currentRooms = this.rooms(); const currentRooms = this.rooms();
this.rooms.set( this.rooms.set(
currentRooms.map((r) => (r.roomId === roomId ? { ...r, markedForDeletion: true } : r)) currentRooms.map((r) =>
r.roomId === roomId ? { ...r, meetingEndAction: MeetingEndAction.DELETE } : r
)
); );
// this.dataSource.data = this.rooms(); // this.dataSource.data = this.rooms();
this.notificationService.showSnackbar('Room marked for deletion'); this.notificationService.showSnackbar('Room marked for deletion');
@ -331,7 +333,7 @@ export class RoomsComponent implements OnInit {
// We don't remove them from the list immediately // We don't remove them from the list immediately
this.rooms.set( this.rooms.set(
currentRooms.map((r) => currentRooms.map((r) =>
roomIds.includes(r.roomId) ? { ...r, markedForDeletion: true } : r roomIds.includes(r.roomId) ? { ...r, meetingEndAction: MeetingEndAction.DELETE } : r
) )
); );
this.notificationService.showSnackbar('All rooms marked for deletion'); this.notificationService.showSnackbar('All rooms marked for deletion');