tas-uell-sdk 0.0.4 → 0.0.6

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 (28) hide show
  1. package/README.md +94 -53
  2. package/esm2020/lib/components/tas-avatar/tas-avatar.component.mjs +75 -0
  3. package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +156 -63
  4. package/esm2020/lib/components/tas-floating-call/tas-floating-call.component.mjs +48 -23
  5. package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +109 -18
  6. package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +163 -131
  7. package/esm2020/lib/config/tas.config.mjs +1 -1
  8. package/esm2020/lib/interfaces/tas.interfaces.mjs +39 -2
  9. package/esm2020/lib/services/tas.service.mjs +363 -34
  10. package/esm2020/lib/tas-uell-sdk.module.mjs +20 -18
  11. package/esm2020/public-api.mjs +2 -1
  12. package/fesm2015/tas-uell-sdk.mjs +951 -270
  13. package/fesm2015/tas-uell-sdk.mjs.map +1 -1
  14. package/fesm2020/tas-uell-sdk.mjs +946 -268
  15. package/fesm2020/tas-uell-sdk.mjs.map +1 -1
  16. package/lib/components/tas-avatar/tas-avatar.component.d.ts +9 -0
  17. package/lib/components/tas-btn/tas-btn.component.d.ts +33 -15
  18. package/lib/components/tas-floating-call/tas-floating-call.component.d.ts +5 -1
  19. package/lib/components/tas-videocall/tas-videocall.component.d.ts +23 -2
  20. package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +30 -26
  21. package/lib/config/tas.config.d.ts +4 -0
  22. package/lib/interfaces/tas.interfaces.d.ts +103 -35
  23. package/lib/services/tas.service.d.ts +86 -9
  24. package/lib/tas-uell-sdk.module.d.ts +4 -2
  25. package/package.json +1 -1
  26. package/public-api.d.ts +1 -0
  27. package/src/lib/styles/tas-global.scss +27 -28
  28. package/INSTALL_AND_TEST.md +0 -427
@@ -1,12 +1,13 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, Inject, Component, Input, ViewChild, NgModule } from '@angular/core';
2
+ import { InjectionToken, Injectable, Inject, Component, ChangeDetectionStrategy, Input, ViewChild, NgModule } from '@angular/core';
3
3
  import { BehaviorSubject, Subscription } from 'rxjs';
4
- import { map, catchError, switchMap } from 'rxjs/operators';
4
+ import { map, catchError } from 'rxjs/operators';
5
5
  import * as OT from '@opentok/client';
6
6
  import interact from 'interactjs';
7
7
  import * as i1 from '@ng-bootstrap/ng-bootstrap';
8
8
  import * as i3 from '@angular/common';
9
9
  import { CommonModule } from '@angular/common';
10
+ import { FormsModule } from '@angular/forms';
10
11
 
11
12
  /**
12
13
  * Injection token for TAS configuration
@@ -17,11 +18,13 @@ const TAS_CONFIG = new InjectionToken('TAS_CONFIG');
17
18
  */
18
19
  const TAS_HTTP_CLIENT = new InjectionToken('TAS_HTTP_CLIENT');
19
20
 
21
+ // Enums
20
22
  var TasRoomType;
21
23
  (function (TasRoomType) {
22
24
  TasRoomType["TAS"] = "TAS";
23
25
  TasRoomType["JM"] = "JM";
24
26
  TasRoomType["WEBINAR"] = "WEBINAR";
27
+ TasRoomType["WELLNESS_MANAGER"] = "WELLNESS_MANAGER";
25
28
  })(TasRoomType || (TasRoomType = {}));
26
29
  var TasSessionType;
27
30
  (function (TasSessionType) {
@@ -34,7 +37,42 @@ var TasUserRole;
34
37
  TasUserRole["USER"] = "USER";
35
38
  TasUserRole["MODERATOR"] = "MODERATOR";
36
39
  })(TasUserRole || (TasUserRole = {}));
37
- // Enums for TAS Service state management
40
+ var TasBusinessRole;
41
+ (function (TasBusinessRole) {
42
+ TasBusinessRole["ADMIN_MANAGER"] = "ADMIN_MANAGER";
43
+ TasBusinessRole["MANAGER"] = "MANAGER";
44
+ TasBusinessRole["BACKOFFICE"] = "BACKOFFICE";
45
+ TasBusinessRole["USER"] = "USER";
46
+ })(TasBusinessRole || (TasBusinessRole = {}));
47
+ var VideoSessionStatus;
48
+ (function (VideoSessionStatus) {
49
+ VideoSessionStatus["ACTIVE"] = "ACTIVE";
50
+ VideoSessionStatus["INACTIVE"] = "INACTIVE";
51
+ VideoSessionStatus["PENDING"] = "PENDING";
52
+ VideoSessionStatus["SCHEDULED"] = "SCHEDULED";
53
+ })(VideoSessionStatus || (VideoSessionStatus = {}));
54
+ var UserStatus;
55
+ (function (UserStatus) {
56
+ UserStatus["ACTIVE"] = "ACTIVE";
57
+ UserStatus["INACTIVE"] = "INACTIVE";
58
+ UserStatus["ASSIGNED"] = "ASSIGNED";
59
+ })(UserStatus || (UserStatus = {}));
60
+ var UserCallAction;
61
+ (function (UserCallAction) {
62
+ UserCallAction["WAITING_ROOM_ENTER"] = "WAITING_ROOM_ENTER";
63
+ UserCallAction["WAITING_ROOM_LEAVE"] = "WAITING_ROOM_LEAVE";
64
+ UserCallAction["BAN"] = "BAN";
65
+ UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
66
+ UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
67
+ UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
68
+ })(UserCallAction || (UserCallAction = {}));
69
+ var RoomUserStatus;
70
+ (function (RoomUserStatus) {
71
+ RoomUserStatus["ASSIGNED"] = "ASSIGNED";
72
+ RoomUserStatus["WAITING"] = "WAITING";
73
+ RoomUserStatus["JOINED"] = "JOINED";
74
+ RoomUserStatus["FINISHED"] = "FINISHED";
75
+ })(RoomUserStatus || (RoomUserStatus = {}));
38
76
  var CallState;
39
77
  (function (CallState) {
40
78
  CallState["IDLE"] = "IDLE";
@@ -65,6 +103,77 @@ class TasService {
65
103
  // Session info for PiP mode restoration
66
104
  this.currentSessionId = null;
67
105
  this.currentToken = null;
106
+ this.STORAGE_KEY = 'tas_session_state';
107
+ this.DISCONNECTED_FLAG_KEY = 'tas_session_disconnected';
108
+ this.isFinishingSession = false;
109
+ // Proxy-video circuit state
110
+ this.proxyVideoSessionId = null;
111
+ this.proxyVideoToken = null;
112
+ this.currentBusinessRole = TasBusinessRole.USER;
113
+ // Waiting room and status polling
114
+ this.waitingRoomUsersSubject = new BehaviorSubject([]);
115
+ this.waitingRoomUsers$ = this.waitingRoomUsersSubject.asObservable();
116
+ this.ownerHasJoinedSubject = new BehaviorSubject(false);
117
+ this.ownerHasJoined$ = this.ownerHasJoinedSubject.asObservable();
118
+ // Observable that emits true when owner was present but then left
119
+ this.ownerHasLeftSubject = new BehaviorSubject(false);
120
+ this.ownerHasLeft$ = this.ownerHasLeftSubject.asObservable();
121
+ this.joinableSubject = new BehaviorSubject(false);
122
+ this.joinable$ = this.joinableSubject.asObservable();
123
+ this.statusPollingInterval = null;
124
+ this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
125
+ this.wasOwnerPresent = false;
126
+ // Current call context
127
+ this.currentAppointmentId = null;
128
+ this.currentVideoCallId = null;
129
+ this.currentTenant = null;
130
+ }
131
+ // ... (Getters and other methods remain unchanged)
132
+ /**
133
+ * Start automatic status polling for the current session.
134
+ * Status is polled every intervalMs (default 30s).
135
+ */
136
+ startStatusPolling(params, intervalMs = this.DEFAULT_POLL_INTERVAL_MS) {
137
+ this.stopStatusPolling(); // Clear any existing polling
138
+ // Store context for polling
139
+ if (params.sessionId) {
140
+ this.currentSessionId = params.sessionId;
141
+ }
142
+ if (params.appointmentId) {
143
+ this.currentAppointmentId = params.appointmentId;
144
+ }
145
+ if (params.tenant) {
146
+ this.currentTenant = params.tenant;
147
+ }
148
+ console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
149
+ // Initial status fetch
150
+ this.fetchAndProcessStatus(params);
151
+ // Set up periodic polling
152
+ this.statusPollingInterval = setInterval(() => {
153
+ this.fetchAndProcessStatus(params);
154
+ }, intervalMs);
155
+ }
156
+ /**
157
+ * Stop automatic status polling.
158
+ */
159
+ stopStatusPolling() {
160
+ if (this.statusPollingInterval) {
161
+ clearInterval(this.statusPollingInterval);
162
+ this.statusPollingInterval = null;
163
+ }
164
+ // Do NOT reset waiting room state here, as other components might observe it
165
+ }
166
+ /**
167
+ * Reset polling state (waiting room, owner status, etc)
168
+ * Call this when completely exiting the flow
169
+ */
170
+ resetPollingState() {
171
+ this.stopStatusPolling();
172
+ this.waitingRoomUsersSubject.next([]);
173
+ this.ownerHasJoinedSubject.next(false);
174
+ this.ownerHasLeftSubject.next(false);
175
+ this.joinableSubject.next(false);
176
+ this.wasOwnerPresent = false;
68
177
  }
69
178
  // Getters
70
179
  get currentSession() {
@@ -82,18 +191,36 @@ class TasService {
82
191
  get token() {
83
192
  return this.currentToken;
84
193
  }
194
+ get proxySessionId() {
195
+ return this.proxyVideoSessionId;
196
+ }
197
+ get proxyToken() {
198
+ return this.proxyVideoToken;
199
+ }
200
+ get businessRole() {
201
+ return this.currentBusinessRole;
202
+ }
85
203
  get isMuted() {
86
204
  return this.isMutedSubject.getValue();
87
205
  }
88
206
  // View Mode Methods
89
207
  setViewMode(mode) {
90
208
  this.viewModeSubject.next(mode);
209
+ if (this.currentSessionId && this.currentToken) {
210
+ this.saveSessionState(this.currentSessionId, this.currentToken, mode, this.currentBusinessRole);
211
+ }
91
212
  }
92
213
  enterPipMode() {
93
214
  this.viewModeSubject.next(ViewMode.PIP);
215
+ if (this.currentSessionId && this.currentToken) {
216
+ this.saveSessionState(this.currentSessionId, this.currentToken, ViewMode.PIP, this.currentBusinessRole);
217
+ }
94
218
  }
95
219
  exitPipMode() {
96
220
  this.viewModeSubject.next(ViewMode.FULLSCREEN);
221
+ if (this.currentSessionId && this.currentToken) {
222
+ this.saveSessionState(this.currentSessionId, this.currentToken, ViewMode.FULLSCREEN, this.currentBusinessRole);
223
+ }
97
224
  }
98
225
  isPipMode() {
99
226
  return this.viewModeSubject.getValue() === ViewMode.PIP;
@@ -120,7 +247,17 @@ class TasService {
120
247
  }
121
248
  }
122
249
  // Session Management
123
- disconnectSession() {
250
+ disconnectSession(clearStorage = true) {
251
+ console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
252
+ // Call finishSession before disconnecting if we have a sessionId
253
+ const sessionIdToFinish = this.currentSessionId;
254
+ // Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
255
+ if (clearStorage) {
256
+ this.clearSessionState();
257
+ // Set a flag to indicate this session was intentionally disconnected
258
+ // This prevents reconnection on page reload
259
+ sessionStorage.setItem(this.DISCONNECTED_FLAG_KEY, 'true');
260
+ }
124
261
  if (this.session) {
125
262
  this.session.disconnect();
126
263
  this.session = null;
@@ -129,49 +266,260 @@ class TasService {
129
266
  this.subscribers = [];
130
267
  this.currentSessionId = null;
131
268
  this.currentToken = null;
269
+ this.proxyVideoSessionId = null;
270
+ this.proxyVideoToken = null;
132
271
  this.isMutedSubject.next(false); // Reset mute state
133
272
  this.viewModeSubject.next(ViewMode.FULLSCREEN);
134
273
  this.callStateSubject.next(CallState.DISCONNECTED);
274
+ // Call proxy finish after disconnecting (only if not already finishing)
275
+ if (sessionIdToFinish && !this.isFinishingSession) {
276
+ this.isFinishingSession = true;
277
+ this.finishProxyVideoSession({
278
+ sessionId: sessionIdToFinish,
279
+ businessRole: this.currentBusinessRole,
280
+ }).subscribe({
281
+ next: (response) => {
282
+ console.log('[TAS DEBUG] Session finished successfully:', response);
283
+ this.isFinishingSession = false;
284
+ },
285
+ error: (error) => {
286
+ console.error('[TAS DEBUG] Error finishing session:', error);
287
+ this.isFinishingSession = false;
288
+ },
289
+ });
290
+ }
135
291
  }
136
292
  isCallActive() {
137
293
  return this.callStateSubject.getValue() === CallState.CONNECTED;
138
294
  }
295
+ // State Persistence
296
+ saveSessionState(sessionId, token, viewMode, businessRole = TasBusinessRole.USER) {
297
+ const state = {
298
+ sessionId,
299
+ token,
300
+ viewMode,
301
+ businessRole,
302
+ };
303
+ sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
304
+ }
305
+ clearSessionState() {
306
+ sessionStorage.removeItem(this.STORAGE_KEY);
307
+ // Also clear the disconnected flag when clearing state
308
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
309
+ }
310
+ canRestoreSession() {
311
+ return !!sessionStorage.getItem(this.STORAGE_KEY);
312
+ }
313
+ restoreSession(containerId) {
314
+ const savedState = sessionStorage.getItem(this.STORAGE_KEY);
315
+ if (!savedState)
316
+ return;
317
+ // Check if this session was intentionally disconnected
318
+ // If so, don't restore it on page reload
319
+ const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
320
+ if (wasDisconnected === 'true') {
321
+ console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
322
+ this.clearSessionState();
323
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
324
+ return;
325
+ }
326
+ // Don't restore if we're already disconnected or if there's no active session
327
+ // This prevents restoring sessions that were properly disconnected
328
+ if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
329
+ console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
330
+ this.clearSessionState();
331
+ return;
332
+ }
333
+ try {
334
+ const state = JSON.parse(savedState);
335
+ if (state.sessionId && state.token) {
336
+ console.log('[TAS DEBUG] Restoring session from storage');
337
+ // Force PiP mode for restoration to ensure UI consistency
338
+ this.viewModeSubject.next(ViewMode.PIP);
339
+ if (state.businessRole) {
340
+ this.currentBusinessRole = state.businessRole;
341
+ }
342
+ this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
343
+ containerId, this.currentBusinessRole)
344
+ .then(() => {
345
+ console.log('[TAS DEBUG] Session restored successfully');
346
+ // Clear the disconnected flag if restoration succeeds
347
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
348
+ })
349
+ .catch((err) => {
350
+ console.error('[TAS DEBUG] Failed to restore session:', err);
351
+ this.clearSessionState(); // Clear bad state
352
+ });
353
+ }
354
+ }
355
+ catch (e) {
356
+ console.error('[TAS DEBUG] Error parsing saved session state', e);
357
+ this.clearSessionState();
358
+ }
359
+ }
139
360
  // API Methods
140
- createRoom(payload) {
141
- return this.httpClient.post("v2/room", { body: payload, headers: {} }).pipe(map((response) => response), catchError((error) => {
142
- console.error("TAS Service: createRoom failed", error);
361
+ /**
362
+ * PROXY circuit token: /v2/proxy/video/start
363
+ */
364
+ startProxyVideoSession(payload) {
365
+ return this.httpClient
366
+ .post('v2/proxy/video/start', {
367
+ body: payload,
368
+ headers: {},
369
+ })
370
+ .pipe(map((response) => response), catchError((error) => {
371
+ console.error('TAS Service: startProxyVideoSession failed', error);
143
372
  throw error;
144
373
  }));
145
374
  }
146
- generateToken(payload) {
147
- return this.httpClient.post("v2/room/token", { body: payload, headers: {} }).pipe(map((response) => response), catchError((error) => {
148
- console.error("TAS Service: generateToken failed", error);
375
+ /**
376
+ * PROXY circuit status: /v2/proxy/video/status
377
+ */
378
+ getProxyVideoStatus(payload) {
379
+ return this.httpClient
380
+ .post('v2/proxy/video/status', {
381
+ body: payload,
382
+ headers: {},
383
+ })
384
+ .pipe(map((response) => response), catchError((error) => {
385
+ console.error('TAS Service: getProxyVideoStatus failed', error);
149
386
  throw error;
150
387
  }));
151
388
  }
389
+ /**
390
+ * PROXY circuit user modification: /v2/proxy/video/user/modify
391
+ */
392
+ modifyProxyVideoUser(payload) {
393
+ return this.httpClient
394
+ .patch('v2/proxy/video/user/modify', {
395
+ body: payload,
396
+ headers: {},
397
+ })
398
+ .pipe(catchError((error) => {
399
+ console.error('TAS Service: modifyProxyVideoUser failed', error);
400
+ throw error;
401
+ }));
402
+ }
403
+ finishProxyVideoSession(payload) {
404
+ return this.httpClient
405
+ .post('v2/proxy/video/finish', {
406
+ body: payload,
407
+ headers: {},
408
+ })
409
+ .pipe(map((response) => response), catchError((error) => {
410
+ console.error('TAS Service: finishProxyVideoSession failed', error);
411
+ throw error;
412
+ }));
413
+ }
414
+ /**
415
+ * Start automatic status polling for the current session.
416
+ * Status is polled every STATUS_POLL_INTERVAL_MS milliseconds.
417
+ */
418
+ /**
419
+ * Stop automatic status polling.
420
+ */
421
+ /**
422
+ * Fetch status and process the response.
423
+ */
424
+ fetchAndProcessStatus(params) {
425
+ this.getProxyVideoStatus(params).subscribe({
426
+ next: (response) => {
427
+ this.processStatusResponse(response);
428
+ },
429
+ error: (err) => {
430
+ console.error('[TAS DEBUG] Status polling error:', err);
431
+ },
432
+ });
433
+ }
434
+ /**
435
+ * Process status response to update waiting room users and owner join status.
436
+ */
437
+ processStatusResponse(response) {
438
+ const content = response.content;
439
+ // Update videoCallId if available
440
+ if (content.videoCallId) {
441
+ this.currentVideoCallId = content.videoCallId;
442
+ }
443
+ // Update sessionId if available
444
+ if (content.sessionId) {
445
+ this.currentSessionId = content.sessionId;
446
+ this.proxyVideoSessionId = content.sessionId;
447
+ }
448
+ // Update joinable status
449
+ this.joinableSubject.next(content.joinable);
450
+ // Check if owner has joined
451
+ const ownerJoined = this.checkIfOwnerJoined(content.users);
452
+ this.ownerHasJoinedSubject.next(ownerJoined);
453
+ // Detect if owner left: was present, now not present
454
+ if (this.wasOwnerPresent && !ownerJoined) {
455
+ console.log('[TAS DEBUG] Owner has left the session');
456
+ this.ownerHasLeftSubject.next(true);
457
+ }
458
+ if (ownerJoined) {
459
+ this.wasOwnerPresent = true;
460
+ }
461
+ // Extract waiting room users (status === WAITING)
462
+ const waitingUsers = content.users
463
+ .filter((u) => u.status === 'WAITING')
464
+ .map((u) => ({
465
+ userId: u.userId,
466
+ name: `User ${u.userId}`,
467
+ status: RoomUserStatus.WAITING,
468
+ }));
469
+ this.waitingRoomUsersSubject.next(waitingUsers);
470
+ }
471
+ /**
472
+ * Check if at least one OWNER has joined the call.
473
+ */
474
+ checkIfOwnerJoined(users) {
475
+ return users.some((u) => u.rol === TasUserRole.OWNER && u.status === 'JOINED');
476
+ }
477
+ /**
478
+ * Admit a user from the waiting room by changing their status.
479
+ */
480
+ admitUserFromWaitingRoom(userId, videoCallId) {
481
+ return this.modifyProxyVideoUser({
482
+ userId,
483
+ videoCallId,
484
+ action: UserCallAction.WAITING_ROOM_LEAVE,
485
+ });
486
+ }
487
+ // Getters for current call context
488
+ get appointmentId() {
489
+ return this.currentAppointmentId;
490
+ }
491
+ get videoCallId() {
492
+ return this.currentVideoCallId;
493
+ }
152
494
  /**
153
495
  * Connects to a TokBox video session
154
496
  */
155
- connectSession(sessionId, token, publisherElement, subscriberElement) {
497
+ connectSession(sessionId, token, publisherElement, subscriberElement, businessRole = TasBusinessRole.USER) {
156
498
  this.callStateSubject.next(CallState.CONNECTING);
157
499
  this.currentSessionId = sessionId;
158
500
  this.currentToken = token;
501
+ this.currentBusinessRole = businessRole;
502
+ this.isFinishingSession = false; // Reset flag for new session
503
+ // Save initial state (defaulting to current view mode)
504
+ this.saveSessionState(sessionId, token, this.viewModeSubject.getValue(), businessRole);
505
+ // Clear the disconnected flag when starting a new connection
506
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
159
507
  return new Promise((resolve, reject) => {
160
508
  if (!OT.checkSystemRequirements()) {
161
509
  this.callStateSubject.next(CallState.ERROR);
162
- reject(new Error("Browser not compatible with TokBox"));
510
+ reject(new Error('Browser not compatible with TokBox'));
163
511
  return;
164
512
  }
165
513
  this.session = OT.initSession(this.config.tokBoxApiKey, sessionId);
166
514
  // Handle new streams (remote participants joining)
167
- this.session.on("streamCreated", (event) => {
515
+ this.session.on('streamCreated', (event) => {
168
516
  const subscriber = this.session?.subscribe(event.stream, subscriberElement, {
169
- insertMode: "append",
170
- width: "100%",
171
- height: "100%",
517
+ insertMode: 'append',
518
+ width: '100%',
519
+ height: '100%',
172
520
  }, (error) => {
173
521
  if (error) {
174
- console.error("Error subscribing to stream:", error);
522
+ console.error('Error subscribing to stream:', error);
175
523
  }
176
524
  });
177
525
  if (subscriber) {
@@ -179,29 +527,48 @@ class TasService {
179
527
  }
180
528
  });
181
529
  // Handle streams ending (remote participants leaving)
182
- this.session.on("streamDestroyed", (event) => {
530
+ this.session.on('streamDestroyed', (event) => {
183
531
  this.subscribers = this.subscribers.filter((sub) => sub.stream?.streamId !== event.stream.streamId);
184
532
  });
185
533
  // Handle session disconnection
186
- this.session.on("sessionDisconnected", () => {
534
+ this.session.on('sessionDisconnected', () => {
187
535
  this.callStateSubject.next(CallState.DISCONNECTED);
536
+ // Call finishSession when session is disconnected (e.g., by server)
537
+ // Only call if not already finishing (prevents duplicate calls)
538
+ const sessionIdToFinish = this.currentSessionId;
539
+ if (sessionIdToFinish && !this.isFinishingSession) {
540
+ this.isFinishingSession = true;
541
+ this.finishProxyVideoSession({
542
+ sessionId: sessionIdToFinish,
543
+ businessRole: this.currentBusinessRole,
544
+ }).subscribe({
545
+ next: (response) => {
546
+ console.log('[TAS DEBUG] Session finished on disconnect event:', response);
547
+ this.isFinishingSession = false;
548
+ },
549
+ error: (error) => {
550
+ console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
551
+ this.isFinishingSession = false;
552
+ },
553
+ });
554
+ }
188
555
  });
189
556
  // Connect to session
190
557
  this.session.connect(token, (error) => {
191
558
  if (error) {
192
- console.error("Error connecting to session:", error);
559
+ console.error('Error connecting to session:', error);
193
560
  this.callStateSubject.next(CallState.ERROR);
194
561
  reject(error);
195
562
  return;
196
563
  }
197
564
  // Initialize publisher (local video)
198
565
  this.publisher = OT.initPublisher(publisherElement, {
199
- insertMode: "append",
200
- width: "100%",
201
- height: "100%",
566
+ insertMode: 'append',
567
+ width: '100%',
568
+ height: '100%',
202
569
  }, (err) => {
203
570
  if (err) {
204
- console.error("Error initializing publisher:", err);
571
+ console.error('Error initializing publisher:', err);
205
572
  this.callStateSubject.next(CallState.ERROR);
206
573
  reject(err);
207
574
  return;
@@ -209,7 +576,7 @@ class TasService {
209
576
  // Publish to session
210
577
  this.session?.publish(this.publisher, (pubErr) => {
211
578
  if (pubErr) {
212
- console.error("Error publishing stream:", pubErr);
579
+ console.error('Error publishing stream:', pubErr);
213
580
  this.callStateSubject.next(CallState.ERROR);
214
581
  reject(pubErr);
215
582
  }
@@ -269,12 +636,12 @@ class TasService {
269
636
  this.movePublisherTo('publisher-container');
270
637
  }
271
638
  }
272
- TasService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasService, deps: [{ token: TAS_HTTP_CLIENT }, { token: TAS_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
273
- TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasService, providedIn: "root" });
274
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasService, decorators: [{
639
+ TasService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, deps: [{ token: TAS_HTTP_CLIENT }, { token: TAS_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
640
+ TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, providedIn: 'root' });
641
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, decorators: [{
275
642
  type: Injectable,
276
643
  args: [{
277
- providedIn: "root",
644
+ providedIn: 'root',
278
645
  }]
279
646
  }], ctorParameters: function () { return [{ type: undefined, decorators: [{
280
647
  type: Inject,
@@ -284,14 +651,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
284
651
  args: [TAS_CONFIG]
285
652
  }] }]; } });
286
653
 
654
+ class TasAvatarComponent {
655
+ constructor() {
656
+ this.name = '';
657
+ this.size = 80;
658
+ }
659
+ get initials() {
660
+ if (!this.name)
661
+ return '';
662
+ return this.name
663
+ .split(' ')
664
+ .filter((n) => n.length > 0)
665
+ .map((n) => n[0])
666
+ .join('')
667
+ .toUpperCase()
668
+ .substring(0, 2);
669
+ }
670
+ get fontSize() {
671
+ return Math.round(this.size * 0.4);
672
+ }
673
+ }
674
+ TasAvatarComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasAvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
675
+ TasAvatarComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasAvatarComponent, selector: "tas-avatar", inputs: { name: "name", size: "size" }, ngImport: i0, template: `
676
+ <div
677
+ class="avatar"
678
+ [style.width.px]="size"
679
+ [style.height.px]="size"
680
+ [style.fontSize.px]="fontSize"
681
+ >
682
+ <span class="initials">{{ initials }}</span>
683
+ </div>
684
+ `, isInline: true, styles: [".avatar{display:flex;align-items:center;justify-content:center;border-radius:50%;background-color:#fff;color:#0072ac;font-weight:600;font-family:inherit;box-shadow:0 4px 12px #00000026}.initials{text-transform:uppercase;-webkit-user-select:none;user-select:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
685
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasAvatarComponent, decorators: [{
686
+ type: Component,
687
+ args: [{
688
+ selector: 'tas-avatar',
689
+ template: `
690
+ <div
691
+ class="avatar"
692
+ [style.width.px]="size"
693
+ [style.height.px]="size"
694
+ [style.fontSize.px]="fontSize"
695
+ >
696
+ <span class="initials">{{ initials }}</span>
697
+ </div>
698
+ `,
699
+ styles: [
700
+ `
701
+ .avatar {
702
+ display: flex;
703
+ align-items: center;
704
+ justify-content: center;
705
+ border-radius: 50%;
706
+ background-color: #ffffff;
707
+ color: #0072ac;
708
+ font-weight: 600;
709
+ font-family: inherit;
710
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
711
+ }
712
+
713
+ .initials {
714
+ text-transform: uppercase;
715
+ user-select: none;
716
+ }
717
+ `,
718
+ ],
719
+ changeDetection: ChangeDetectionStrategy.OnPush,
720
+ }]
721
+ }], propDecorators: { name: [{
722
+ type: Input
723
+ }], size: [{
724
+ type: Input
725
+ }] } });
726
+
287
727
  class TasVideocallComponent {
288
728
  constructor(activeModal, tasService) {
289
729
  this.activeModal = activeModal;
290
730
  this.tasService = tasService;
731
+ this.participantName = '';
732
+ this.tenant = '';
733
+ this.businessRole = TasBusinessRole.USER;
291
734
  this.isReturningFromPip = false;
292
735
  this.isPublisherSmall = true;
293
736
  this.callState = CallState.IDLE;
294
737
  this.isMuted = false;
738
+ this.waitingRoomUsers = [];
739
+ this.ownerHasJoined = false;
740
+ this.hasVideoStream = false;
741
+ this.dismissedUsers = [];
295
742
  this.subscriptions = new Subscription();
296
743
  }
297
744
  ngOnInit() {
@@ -303,6 +750,7 @@ class TasVideocallComponent {
303
750
  }
304
751
  ngOnDestroy() {
305
752
  this.subscriptions.unsubscribe();
753
+ this.tasService.resetPollingState();
306
754
  // Only disconnect if not in PiP mode (keep session alive for floating window)
307
755
  if (!this.tasService.isPipMode()) {
308
756
  this.tasService.disconnectSession();
@@ -329,25 +777,83 @@ class TasVideocallComponent {
329
777
  onDoubleClick() {
330
778
  this.toggleSwap();
331
779
  }
780
+ /**
781
+ * Check if current user can admit others (OWNER, BACKOFFICE, or MODERATOR)
782
+ */
783
+ get canAdmitUsers() {
784
+ return (this.businessRole === TasBusinessRole.BACKOFFICE ||
785
+ this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
786
+ this.businessRole === TasBusinessRole.MANAGER);
787
+ }
788
+ /**
789
+ * Admit a user from the waiting room
790
+ */
791
+ admitUser(userId) {
792
+ if (!this.videoCallId) {
793
+ console.error('Cannot admit user: videoCallId not set');
794
+ return;
795
+ }
796
+ this.tasService
797
+ .modifyProxyVideoUser({
798
+ userId,
799
+ videoCallId: this.videoCallId,
800
+ action: UserCallAction.WAITING_ROOM_LEAVE,
801
+ })
802
+ .subscribe({
803
+ next: () => {
804
+ // Remove user from waiting list after successful admit
805
+ this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
806
+ },
807
+ error: (err) => {
808
+ console.error('Error admitting user:', err);
809
+ },
810
+ });
811
+ }
812
+ /**
813
+ * Dismiss the waiting room notification for a user
814
+ */
815
+ dismissWaitingNotification(userId) {
816
+ this.dismissedUsers.push(userId);
817
+ this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
818
+ }
332
819
  // Private Methods
333
820
  setupSubscriptions() {
334
821
  // Call state subscription
335
- this.subscriptions.add(this.tasService.callState$.subscribe(state => {
822
+ this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
336
823
  this.callState = state;
337
824
  if (state === CallState.DISCONNECTED) {
338
825
  this.activeModal.close('hangup');
339
826
  }
827
+ // Track if we have an active video stream
828
+ this.hasVideoStream = state === CallState.CONNECTED;
340
829
  }));
341
830
  // View mode subscription
342
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
831
+ this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
343
832
  if (mode === ViewMode.PIP) {
344
833
  this.activeModal.close('pip');
345
834
  }
346
835
  }));
347
836
  // Mute state subscription
348
- this.subscriptions.add(this.tasService.isMuted$.subscribe(muted => {
837
+ this.subscriptions.add(this.tasService.isMuted$.subscribe((muted) => {
349
838
  this.isMuted = muted;
350
839
  }));
840
+ // Waiting room users subscription
841
+ this.subscriptions.add(this.tasService.waitingRoomUsers$.subscribe((users) => {
842
+ // Filter out dismissed users
843
+ this.waitingRoomUsers = users.filter((u) => !this.dismissedUsers.includes(u.userId));
844
+ }));
845
+ // Owner join status subscription
846
+ this.subscriptions.add(this.tasService.ownerHasJoined$.subscribe((joined) => {
847
+ this.ownerHasJoined = joined;
848
+ }));
849
+ // Owner left subscription - disconnect non-owners
850
+ this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
851
+ if (hasLeft && !this.canAdmitUsers) { // Non-owner user
852
+ console.log('[TAS DEBUG] Owner left, disconnecting user');
853
+ this.hangUp();
854
+ this.activeModal.close('owner_left');
855
+ }
856
+ }));
351
857
  }
352
858
  initializeCall() {
353
859
  if (this.isReturningFromPip) {
@@ -356,9 +862,22 @@ class TasVideocallComponent {
356
862
  }
357
863
  else if (this.sessionId && this.token) {
358
864
  // New call - connect to session
359
- this.tasService.connectSession(this.sessionId, this.token, 'publisher-container', 'subscriber-container').catch(err => {
865
+ this.tasService
866
+ .connectSession(this.sessionId, this.token, 'publisher-container', 'subscriber-container', this.businessRole)
867
+ .catch((err) => {
360
868
  console.error('Error connecting to video call:', err);
361
869
  });
870
+ // Start status polling with shorter interval (5s) to detect owner leaving/location requests
871
+ if (this.appointmentId) {
872
+ this.tasService.startStatusPolling({
873
+ appointmentId: this.appointmentId,
874
+ tenant: this.tenant,
875
+ businessRole: this.businessRole,
876
+ roomType: undefined,
877
+ entityId: undefined,
878
+ sessionId: this.sessionId
879
+ }, 5000);
880
+ }
362
881
  }
363
882
  }
364
883
  resetVideoPositions() {
@@ -383,8 +902,8 @@ class TasVideocallComponent {
383
902
  modifiers: [
384
903
  interact.modifiers.restrictRect({
385
904
  restriction: 'parent',
386
- endOnly: true
387
- })
905
+ endOnly: true,
906
+ }),
388
907
  ],
389
908
  autoScroll: true,
390
909
  listeners: {
@@ -395,8 +914,8 @@ class TasVideocallComponent {
395
914
  target.style.transform = `translate(${x}px, ${y}px)`;
396
915
  target.setAttribute('data-x', String(x));
397
916
  target.setAttribute('data-y', String(y));
398
- }
399
- }
917
+ },
918
+ },
400
919
  })
401
920
  .resizable({
402
921
  edges: { left: false, right: true, bottom: true, top: false },
@@ -412,26 +931,36 @@ class TasVideocallComponent {
412
931
  target.style.transform = `translate(${x}px, ${y}px)`;
413
932
  target.setAttribute('data-x', String(x));
414
933
  target.setAttribute('data-y', String(y));
415
- }
934
+ },
416
935
  },
417
936
  modifiers: [
418
937
  interact.modifiers.restrictEdges({ outer: 'parent' }),
419
938
  interact.modifiers.restrictSize({ min: { width: 150, height: 100 } }),
420
- interact.modifiers.aspectRatio({ ratio: 'preserve' })
939
+ interact.modifiers.aspectRatio({ ratio: 'preserve' }),
421
940
  ],
422
- inertia: true
941
+ inertia: true,
423
942
  });
424
943
  }
425
944
  }
426
- TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
427
- TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-container\">\n\t<div id=\"subscriber-container\" \n\t\t[class.subscriber-view]=\"isPublisherSmall\" \n\t\t[class.publisher-view]=\"!isPublisherSmall\"\n\t\t#subscriberContainer\n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\n\t<div id=\"publisher-container\" \n\t\t[class.publisher-view]=\"isPublisherSmall\" \n\t\t[class.subscriber-view]=\"!isPublisherSmall\"\n\t\t#publisherContainer \n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\t\n\t<div class=\"controls-container\">\n\t\t<button class=\"btn swap-btn\" (click)=\"toggleSwap()\" title=\"Swap view\">\n\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t</button>\n\t\t<button class=\"btn pip-btn\" (click)=\"minimize()\" title=\"Minimize (Picture in Picture)\">\n\t\t\t<i class=\"fa fa-compress\"></i>\n\t\t</button>\n\t\t<button class=\"btn mute-btn\" [class.muted]=\"isMuted\" (click)=\"toggleMute()\" [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\">\n\t\t\t<i class=\"fa\" [class.fa-microphone]=\"!isMuted\" [class.fa-microphone-slash]=\"isMuted\"></i>\n\t\t</button>\n\t\t<button class=\"btn hangup-btn\" (click)=\"hangUp()\" title=\"Hang up\">\n\t\t\t<i class=\"fa fa-phone\" style=\"transform: rotate(135deg);\"></i>\n\t\t</button>\n\t</div>\n</div>\n\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;background-color:#000;overflow:hidden}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#333}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:20px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#00000080;padding:15px 25px;border-radius:50px;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.tas-videocall-container .controls-container .hangup-btn,.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;border:none;box-shadow:0 4px 6px #0000004d;transition:all .2s ease}.tas-videocall-container .controls-container .hangup-btn i,.tas-videocall-container .controls-container .swap-btn i,.tas-videocall-container .controls-container .pip-btn i,.tas-videocall-container .controls-container .mute-btn i{color:#fff}.tas-videocall-container .controls-container .hangup-btn{background:#dc3545}.tas-videocall-container .controls-container .hangup-btn:hover{background:#c82333;transform:scale(1.05)}.tas-videocall-container .controls-container .swap-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .swap-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .pip-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .pip-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .mute-btn.muted:hover{background:#e67e22}\n"] });
428
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasVideocallComponent, decorators: [{
945
+ TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
946
+ TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n</div>\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
947
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
429
948
  type: Component,
430
- args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-container\">\n\t<div id=\"subscriber-container\" \n\t\t[class.subscriber-view]=\"isPublisherSmall\" \n\t\t[class.publisher-view]=\"!isPublisherSmall\"\n\t\t#subscriberContainer\n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\n\t<div id=\"publisher-container\" \n\t\t[class.publisher-view]=\"isPublisherSmall\" \n\t\t[class.subscriber-view]=\"!isPublisherSmall\"\n\t\t#publisherContainer \n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\t\n\t<div class=\"controls-container\">\n\t\t<button class=\"btn swap-btn\" (click)=\"toggleSwap()\" title=\"Swap view\">\n\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t</button>\n\t\t<button class=\"btn pip-btn\" (click)=\"minimize()\" title=\"Minimize (Picture in Picture)\">\n\t\t\t<i class=\"fa fa-compress\"></i>\n\t\t</button>\n\t\t<button class=\"btn mute-btn\" [class.muted]=\"isMuted\" (click)=\"toggleMute()\" [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\">\n\t\t\t<i class=\"fa\" [class.fa-microphone]=\"!isMuted\" [class.fa-microphone-slash]=\"isMuted\"></i>\n\t\t</button>\n\t\t<button class=\"btn hangup-btn\" (click)=\"hangUp()\" title=\"Hang up\">\n\t\t\t<i class=\"fa fa-phone\" style=\"transform: rotate(135deg);\"></i>\n\t\t</button>\n\t</div>\n</div>\n\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;background-color:#000;overflow:hidden}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#333}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:20px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#00000080;padding:15px 25px;border-radius:50px;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.tas-videocall-container .controls-container .hangup-btn,.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;border:none;box-shadow:0 4px 6px #0000004d;transition:all .2s ease}.tas-videocall-container .controls-container .hangup-btn i,.tas-videocall-container .controls-container .swap-btn i,.tas-videocall-container .controls-container .pip-btn i,.tas-videocall-container .controls-container .mute-btn i{color:#fff}.tas-videocall-container .controls-container .hangup-btn{background:#dc3545}.tas-videocall-container .controls-container .hangup-btn:hover{background:#c82333;transform:scale(1.05)}.tas-videocall-container .controls-container .swap-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .swap-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .pip-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .pip-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .mute-btn.muted:hover{background:#e67e22}\n"] }]
949
+ args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n</div>\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}\n"] }]
431
950
  }], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }]; }, propDecorators: { sessionId: [{
432
951
  type: Input
433
952
  }], token: [{
434
953
  type: Input
954
+ }], appointmentId: [{
955
+ type: Input
956
+ }], videoCallId: [{
957
+ type: Input
958
+ }], participantName: [{
959
+ type: Input
960
+ }], tenant: [{
961
+ type: Input
962
+ }], businessRole: [{
963
+ type: Input
435
964
  }], isReturningFromPip: [{
436
965
  type: Input
437
966
  }], publisherContainer: [{
@@ -444,127 +973,192 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
444
973
 
445
974
  var WaitingRoomState;
446
975
  (function (WaitingRoomState) {
447
- WaitingRoomState["IDLE"] = "IDLE";
448
- WaitingRoomState["CREATING_ROOM"] = "CREATING_ROOM";
449
- WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
976
+ WaitingRoomState["CHECKING_STATUS"] = "CHECKING_STATUS";
977
+ WaitingRoomState["WAITING_FOR_OWNER"] = "WAITING_FOR_OWNER";
450
978
  WaitingRoomState["READY"] = "READY";
979
+ WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
451
980
  WaitingRoomState["ERROR"] = "ERROR";
452
981
  })(WaitingRoomState || (WaitingRoomState = {}));
453
982
  class TasWaitingRoomComponent {
454
- constructor(activeModal, tasService, modalService) {
983
+ constructor(activeModal, tasService, modalService, cdr) {
455
984
  this.activeModal = activeModal;
456
985
  this.tasService = tasService;
457
986
  this.modalService = modalService;
458
- // Room configuration inputs
459
- this.appointmentId = 1;
460
- this.product = 'uell';
461
- this.tenantId = '';
462
- this.regularUserIds = [];
463
- this.moderatorUserIds = [];
464
- /** Optional: If provided, skips room creation and goes directly to getting a token */
465
- this.existingSessionId = '';
987
+ this.cdr = cdr;
988
+ // Status endpoint params
989
+ this.roomType = TasRoomType.TAS;
990
+ this.businessRole = TasBusinessRole.USER;
466
991
  // Component state
467
- this.state = WaitingRoomState.IDLE;
992
+ this.state = WaitingRoomState.CHECKING_STATUS;
468
993
  this.WaitingRoomState = WaitingRoomState; // Expose enum to template
469
994
  this.errorMessage = '';
470
- /** Whether we have an existing session (skip room creation) */
471
- this.hasExistingSession = false;
472
- // Session data
473
- this.sessionId = '';
995
+ // Session data from status response
996
+ this.resolvedSessionId = '';
997
+ this.resolvedAppointmentId = null;
474
998
  this.token = '';
475
- this.users = [];
999
+ this.videoCallId = null;
1000
+ // Subscriptions
476
1001
  this.subscriptions = new Subscription();
477
1002
  this.videoCallModalRef = null;
478
1003
  }
1004
+ /** Whether current user is an owner */
1005
+ get isOwner() {
1006
+ return this.currentUser?.role === TasUserRole.OWNER;
1007
+ }
479
1008
  ngOnInit() {
480
- this.buildUsersArray();
481
- this.setupViewModeSubscription();
482
- // Check if we have an existing session
483
- if (this.existingSessionId && this.existingSessionId.trim() !== '') {
484
- this.hasExistingSession = true;
485
- this.sessionId = this.existingSessionId;
486
- }
1009
+ console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
1010
+ this.checkStatus();
487
1011
  }
488
1012
  ngOnDestroy() {
1013
+ console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
489
1014
  this.subscriptions.unsubscribe();
1015
+ this.tasService.stopStatusPolling();
490
1016
  }
491
1017
  /**
492
- * Creates the room and fetches the token (new session flow)
1018
+ * Check status to get session info
493
1019
  */
494
- createRoom() {
495
- if (!this.tenantId || !this.currentUser?.name) {
496
- this.state = WaitingRoomState.ERROR;
497
- this.errorMessage = 'Missing configuration data (tenant or user)';
498
- return;
499
- }
500
- this.state = WaitingRoomState.CREATING_ROOM;
1020
+ checkStatus() {
1021
+ console.log('[TAS DEBUG] checkStatus called with:', {
1022
+ roomType: this.roomType,
1023
+ entityId: this.entityId,
1024
+ tenant: this.tenant,
1025
+ businessRole: this.businessRole,
1026
+ currentUser: this.currentUser,
1027
+ });
1028
+ this.state = WaitingRoomState.CHECKING_STATUS;
501
1029
  this.errorMessage = '';
502
- const body = {
503
- roomType: TasRoomType.TAS,
504
- type: TasSessionType.SPONTANEOUS,
505
- tenant: this.tenantId,
506
- appointmentId: this.appointmentId,
507
- users: this.users,
508
- product: this.product
1030
+ const statusParams = {
1031
+ roomType: this.roomType,
1032
+ entityId: this.entityId,
1033
+ tenant: this.tenant,
1034
+ businessRole: this.businessRole,
509
1035
  };
510
- this.subscriptions.add(this.tasService.createRoom(body).pipe(switchMap(response => {
511
- this.sessionId = response.content.sessionId;
512
- this.state = WaitingRoomState.GETTING_TOKEN;
513
- return this.tasService.generateToken({
514
- sessionId: this.sessionId,
515
- name: this.currentUser.name,
516
- lastname: this.currentUser.lastname,
517
- roleVC: this.currentUser.role
518
- });
519
- })).subscribe({
520
- next: (tokenResponse) => {
521
- this.token = tokenResponse.content.token;
522
- this.state = WaitingRoomState.READY;
1036
+ console.log('[TAS DEBUG] Calling getProxyVideoStatus...');
1037
+ this.subscriptions.add(this.tasService.getProxyVideoStatus(statusParams).subscribe({
1038
+ next: (response) => {
1039
+ const content = response.content;
1040
+ // Store session info from response
1041
+ this.resolvedSessionId = content.sessionId;
1042
+ this.resolvedAppointmentId = content.appointmentId;
1043
+ this.videoCallId = content.videoCallId;
1044
+ console.log('[TAS DEBUG] Status response:', content);
1045
+ // Start polling for status updates
1046
+ this.tasService.startStatusPolling(statusParams);
1047
+ // Subscribe to joinable status
1048
+ this.subscriptions.add(this.tasService.joinable$.subscribe((joinable) => {
1049
+ this.handleJoinableChange(joinable);
1050
+ }));
523
1051
  },
524
1052
  error: (err) => {
525
- console.error('Error creating room or getting token:', err);
1053
+ console.error('[TAS DEBUG] Status check failed:', err);
526
1054
  this.state = WaitingRoomState.ERROR;
527
- this.errorMessage = 'Error creating room. Please try again.';
528
- }
1055
+ this.errorMessage = 'Error checking session status. Please try again.';
1056
+ },
529
1057
  }));
530
1058
  }
531
1059
  /**
532
- * Gets a token for an existing session (existing session flow)
1060
+ * Handle changes to joinable status
1061
+ */
1062
+ handleJoinableChange(joinable) {
1063
+ console.log('[TAS DEBUG] handleJoinableChange called', {
1064
+ joinable,
1065
+ currentState: this.state,
1066
+ isOwner: this.isOwner,
1067
+ isBackoffice: this.isBackoffice,
1068
+ resolvedSessionId: this.resolvedSessionId,
1069
+ });
1070
+ // Don't update state if already getting token, ready, or in error
1071
+ if (this.state === WaitingRoomState.GETTING_TOKEN ||
1072
+ this.state === WaitingRoomState.READY ||
1073
+ this.state === WaitingRoomState.ERROR) {
1074
+ console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
1075
+ return;
1076
+ }
1077
+ if (this.isOwner || this.isBackoffice) {
1078
+ console.log('[TAS DEBUG] User is owner/backoffice, calling getTokenForOwner');
1079
+ // Owner/Backoffice: call /start to get token first, then show join button
1080
+ if (this.state === WaitingRoomState.CHECKING_STATUS) {
1081
+ this.getTokenForOwner();
1082
+ }
1083
+ }
1084
+ else {
1085
+ // Non-owner: wait until joinable is true
1086
+ if (joinable) {
1087
+ console.log('[TAS DEBUG] Non-owner: joinable is true, getting token');
1088
+ this.getTokenForOwner(); // Also get token when joinable
1089
+ }
1090
+ else {
1091
+ console.log('[TAS DEBUG] Non-owner: waiting for owner');
1092
+ this.state = WaitingRoomState.WAITING_FOR_OWNER;
1093
+ this.cdr.detectChanges();
1094
+ }
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Check if user has owner/backoffice role
1099
+ */
1100
+ get isBackoffice() {
1101
+ return (this.businessRole === TasBusinessRole.BACKOFFICE ||
1102
+ this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
1103
+ this.businessRole === TasBusinessRole.MANAGER);
1104
+ }
1105
+ /**
1106
+ * Get token for owner/backoffice - call /start endpoint
533
1107
  */
534
- getTokenForExistingSession() {
535
- if (!this.sessionId || !this.currentUser?.name) {
1108
+ getTokenForOwner() {
1109
+ if (!this.resolvedSessionId) {
536
1110
  this.state = WaitingRoomState.ERROR;
537
- this.errorMessage = 'Missing session ID or user data';
1111
+ this.errorMessage = 'Session ID not available';
1112
+ this.tasService.stopStatusPolling();
1113
+ this.cdr.detectChanges();
538
1114
  return;
539
1115
  }
540
1116
  this.state = WaitingRoomState.GETTING_TOKEN;
541
1117
  this.errorMessage = '';
542
- this.subscriptions.add(this.tasService.generateToken({
543
- sessionId: this.sessionId,
1118
+ console.log('[TAS DEBUG] Calling /start for session:', this.resolvedSessionId);
1119
+ this.subscriptions.add(this.tasService
1120
+ .startProxyVideoSession({
1121
+ sessionId: this.resolvedSessionId,
544
1122
  name: this.currentUser.name,
545
1123
  lastname: this.currentUser.lastname,
546
- roleVC: this.currentUser.role
547
- }).subscribe({
1124
+ })
1125
+ .subscribe({
548
1126
  next: (tokenResponse) => {
1127
+ console.log('[TAS DEBUG] Token response:', tokenResponse);
1128
+ // Handle case where HTTP adapter returns error in next instead of error handler
1129
+ if (!tokenResponse?.content?.token) {
1130
+ console.error('[TAS DEBUG] Invalid token response:', tokenResponse);
1131
+ this.state = WaitingRoomState.ERROR;
1132
+ this.errorMessage = tokenResponse?.message || 'Error al iniciar la sesión. Respuesta inválida.';
1133
+ this.tasService.stopStatusPolling();
1134
+ this.cdr.detectChanges();
1135
+ return;
1136
+ }
1137
+ console.log('[TAS DEBUG] Token obtained successfully');
549
1138
  this.token = tokenResponse.content.token;
550
1139
  this.state = WaitingRoomState.READY;
1140
+ this.cdr.detectChanges();
551
1141
  },
552
1142
  error: (err) => {
553
- console.error('Error getting token:', err);
1143
+ console.error('[TAS DEBUG] /start request failed:', err);
554
1144
  this.state = WaitingRoomState.ERROR;
555
- this.errorMessage = 'Error getting session token. Please try again.';
556
- }
1145
+ this.errorMessage = err?.error?.message || err?.message || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
1146
+ this.tasService.stopStatusPolling();
1147
+ console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
1148
+ this.cdr.detectChanges();
1149
+ },
557
1150
  }));
558
1151
  }
559
1152
  /**
560
- * Joins the video call session
1153
+ * Join the session - token already obtained
561
1154
  */
562
1155
  joinSession() {
563
- if (!this.sessionId || !this.token) {
564
- this.errorMessage = 'Cannot join session. Incomplete data.';
1156
+ if (!this.resolvedSessionId || !this.token) {
1157
+ this.errorMessage = 'Session not ready';
565
1158
  return;
566
1159
  }
567
1160
  // Close waiting room and open video call
1161
+ this.tasService.stopStatusPolling();
568
1162
  this.activeModal.close('joining');
569
1163
  this.openVideoCallModal();
570
1164
  }
@@ -572,85 +1166,53 @@ class TasWaitingRoomComponent {
572
1166
  * Closes the waiting room
573
1167
  */
574
1168
  cancel() {
1169
+ this.tasService.stopStatusPolling();
575
1170
  this.activeModal.dismiss('cancel');
576
1171
  }
577
1172
  /**
578
1173
  * Retry after an error
579
1174
  */
580
1175
  retry() {
581
- this.state = WaitingRoomState.IDLE;
1176
+ this.state = WaitingRoomState.CHECKING_STATUS;
582
1177
  this.errorMessage = '';
583
1178
  this.token = '';
584
- // Only reset sessionId if we don't have an existing one
585
- if (!this.hasExistingSession) {
586
- this.sessionId = '';
587
- }
588
- }
589
- // Private Methods
590
- buildUsersArray() {
591
- this.users = [];
592
- // Add owners from input
593
- this.ownerUserIds.forEach(id => {
594
- this.users.push({ userExternalId: id, rol: TasUserRole.OWNER });
595
- });
596
- // Add regular users from input
597
- this.regularUserIds.forEach(id => {
598
- this.users.push({ userExternalId: id, rol: TasUserRole.USER });
599
- });
600
- // Add moderators from input
601
- this.moderatorUserIds.forEach(id => {
602
- this.users.push({ userExternalId: id, rol: TasUserRole.MODERATOR });
603
- });
604
- }
605
- setupViewModeSubscription() {
606
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
607
- // Re-open video call modal when returning from PiP mode
608
- if (mode === ViewMode.FULLSCREEN &&
609
- this.tasService.isCallActive() &&
610
- !this.videoCallModalRef) {
611
- const sessionId = this.tasService.sessionId;
612
- const token = this.tasService.token;
613
- if (sessionId && token) {
614
- this.openVideoCallModal(true);
615
- }
616
- }
617
- }));
1179
+ this.checkStatus();
618
1180
  }
619
- openVideoCallModal(isReturningFromPip = false) {
1181
+ openVideoCallModal() {
620
1182
  this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
621
1183
  size: 'xl',
622
1184
  windowClass: 'tas-video-modal',
623
1185
  backdrop: 'static',
624
- keyboard: false
1186
+ keyboard: false,
1187
+ });
1188
+ this.videoCallModalRef.componentInstance.sessionId = this.resolvedSessionId;
1189
+ this.videoCallModalRef.componentInstance.token = this.token;
1190
+ this.videoCallModalRef.componentInstance.appointmentId = this.resolvedAppointmentId;
1191
+ this.videoCallModalRef.componentInstance.videoCallId = this.videoCallId;
1192
+ this.videoCallModalRef.componentInstance.tenant = this.tenant;
1193
+ this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
1194
+ this.videoCallModalRef.componentInstance.isReturningFromPip = false;
1195
+ this.videoCallModalRef.result.then(() => {
1196
+ this.videoCallModalRef = null;
1197
+ }, () => {
1198
+ this.videoCallModalRef = null;
625
1199
  });
626
- const sessionIdToUse = this.sessionId || this.tasService.sessionId;
627
- const tokenToUse = this.token || this.tasService.token;
628
- this.videoCallModalRef.componentInstance.sessionId = sessionIdToUse;
629
- this.videoCallModalRef.componentInstance.token = tokenToUse;
630
- this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
631
- this.videoCallModalRef.result.then(() => { this.videoCallModalRef = null; }, () => { this.videoCallModalRef = null; });
632
1200
  }
633
1201
  }
634
- TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasWaitingRoomComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: i1.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
635
- TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { appointmentId: "appointmentId", product: "product", tenantId: "tenantId", currentUser: "currentUser", ownerUserIds: "ownerUserIds", regularUserIds: "regularUserIds", moderatorUserIds: "moderatorUserIds", existingSessionId: "existingSessionId" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n\t<!-- Header -->\n\t<div class=\"waiting-room-header\">\n\t\t<div class=\"header-icon\">\n\t\t\t<i class=\"fa fa-video-camera\"></i>\n\t\t</div>\n\t\t<h2 class=\"header-title\">Waiting Room</h2>\n\t\t<p class=\"header-subtitle\">Prepare for your video call</p>\n\t\t<button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n\t\t\t<span aria-hidden=\"true\">&times;</span>\n\t\t</button>\n\t</div>\n\n\t<!-- Content -->\n\t<div class=\"waiting-room-content\">\n\t\t<!-- IDLE State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.IDLE && !hasExistingSession\">\n\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tCreate a new video call room\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tPress the button to begin\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t(click)=\"createRoom()\">\n\t\t\t\t<i class=\"fa fa-plus\"></i>\n\t\t\t\tCreate Room\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- IDLE State - Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.IDLE && hasExistingSession\">\n\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tJoin existing video call room\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tPress the button to get access\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t(click)=\"getTokenForExistingSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Room\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- CREATING_ROOM State (only for new sessions) -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CREATING_ROOM\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tCreating video call room...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Creating</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && !hasExistingSession\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tPreparing room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && hasExistingSession\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tGetting room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Connecting</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- READY State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && !hasExistingSession\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tRoom is ready!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- READY State - Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && hasExistingSession\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tReady to join!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Connected</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- ERROR State -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n\t\t\t<div class=\"state-icon error\">\n\t\t\t\t<i class=\"fa fa-exclamation-triangle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message error\">\n\t\t\t\tAn error occurred\n\t\t\t</p>\n\t\t\t<p class=\"error-details\" *ngIf=\"errorMessage\">\n\t\t\t\t{{ errorMessage }}\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn retry-btn\"\n\t\t\t\t(click)=\"retry()\">\n\t\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t\t\tRetry\n\t\t\t</button>\n\t\t</div>\n\t</div>\n\n\t<!-- Footer -->\n\t<div class=\"waiting-room-footer\">\n\t\t<button \n\t\t\ttype=\"button\" \n\t\t\tclass=\"btn cancel-btn\"\n\t\t\t(click)=\"cancel()\">\n\t\t\tCancel\n\t\t</button>\n\t</div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:32px 40px 24px;text-align:center;border-bottom:1px solid #e9ecef}.waiting-room-header .header-icon{width:72px;height:72px;margin:0 auto 16px;background:linear-gradient(135deg,#1da4b1 0%,#0077b3 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #1da4b140}.waiting-room-header .header-icon i{font-size:28px;color:#fff}.waiting-room-header .header-title{margin:0 0 8px;font-size:20px;font-weight:700;line-height:28px;color:#212529}.waiting-room-header .header-subtitle{margin:0;font-size:14px;color:#6c757d;font-weight:400}.waiting-room-header .close-btn{position:absolute;top:16px;right:16px;width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:360px;width:100%}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
636
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
1202
+ TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: i1.NgbModal }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
1203
+ TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n <!-- Header -->\n <div class=\"waiting-room-header\">\n <h2 class=\"header-title\">Iniciar turno</h2>\n <button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n <span aria-hidden=\"true\">&times;</span>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"waiting-room-content\">\n <!-- CHECKING_STATUS State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CHECKING_STATUS\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Verificando estado de la sesi\u00F3n...</p>\n </div>\n\n <!-- WAITING_FOR_OWNER State (Non-owner waiting) -->\n <div class=\"state-container waiting-for-owner\" *ngIf=\"state === WaitingRoomState.WAITING_FOR_OWNER\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message waiting-title\">Medicina laboral te va a admitir en unos instantes...</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla hasta que inicie la consulta.</p>\n </div>\n\n <!-- READY State (Owner can join) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY\">\n <div class=\"state-icon ready\">\n <i class=\"fa fa-check-circle\"></i>\n </div>\n <p class=\"state-message success\">\u00A1La sala est\u00E1 lista!</p>\n <p class=\"state-submessage\">Pod\u00E9s unirte a la videollamada cuando quieras</p>\n <button type=\"button\" class=\"btn action-btn join-btn\" (click)=\"joinSession()\">\n <i class=\"fa fa-sign-in\"></i>\n Unirse a la llamada\n </button>\n </div>\n\n <!-- GETTING_TOKEN State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Conectando...</p>\n </div>\n\n <!-- ERROR State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n <p class=\"error-details\" *ngIf=\"errorMessage\">\n {{ errorMessage }}\n </p>\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:20px 40px;border-bottom:1px solid #e9ecef;display:flex;align-items:center;justify-content:space-between}.waiting-room-header .header-title{margin:0;font-size:18px;font-weight:600;line-height:24px;color:#212529}.waiting-room-header .close-btn{width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px;display:flex;align-items:center;justify-content:center}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-title{font-size:16px;font-weight:600;color:#212529;margin-bottom:8px}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.mode-tabs{display:flex;justify-content:center;gap:8px;margin-bottom:24px;padding:4px;background:#f8f9fa;border-radius:8px}.mode-tab{flex:1;padding:10px 16px;font-size:13px;font-weight:600;border:none;border-radius:6px;background:transparent;color:#6c757d;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px}.mode-tab i{font-size:14px}.mode-tab:hover{color:#212529}.mode-tab.active{background:#ffffff;color:#1da4b1;box-shadow:0 2px 8px #00000014}.mode-content{animation:fadeIn .3s ease}.session-input-container{display:flex;flex-direction:column;gap:16px;margin-top:8px}.session-input{width:100%;padding:14px 16px;font-size:14px;border:2px solid #e9ecef;border-radius:8px;background:#ffffff;color:#212529;transition:all .2s ease;text-align:center;font-family:Monaco,Consolas,monospace;letter-spacing:.5px}.session-input::placeholder{color:#6c757d;font-family:inherit;letter-spacing:normal}.session-input:focus{outline:none;border-color:#1da4b1;box-shadow:0 0 0 3px #1da4b126}.session-input:hover:not(:focus){border-color:#6c757d}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
1204
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
637
1205
  type: Component,
638
- args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n\t<!-- Header -->\n\t<div class=\"waiting-room-header\">\n\t\t<div class=\"header-icon\">\n\t\t\t<i class=\"fa fa-video-camera\"></i>\n\t\t</div>\n\t\t<h2 class=\"header-title\">Waiting Room</h2>\n\t\t<p class=\"header-subtitle\">Prepare for your video call</p>\n\t\t<button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n\t\t\t<span aria-hidden=\"true\">&times;</span>\n\t\t</button>\n\t</div>\n\n\t<!-- Content -->\n\t<div class=\"waiting-room-content\">\n\t\t<!-- IDLE State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.IDLE && !hasExistingSession\">\n\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tCreate a new video call room\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tPress the button to begin\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t(click)=\"createRoom()\">\n\t\t\t\t<i class=\"fa fa-plus\"></i>\n\t\t\t\tCreate Room\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- IDLE State - Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.IDLE && hasExistingSession\">\n\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tJoin existing video call room\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tPress the button to get access\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t(click)=\"getTokenForExistingSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Room\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- CREATING_ROOM State (only for new sessions) -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CREATING_ROOM\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tCreating video call room...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Creating</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && !hasExistingSession\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tPreparing room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && hasExistingSession\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tGetting room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Connecting</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- READY State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && !hasExistingSession\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tRoom is ready!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- READY State - Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && hasExistingSession\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tReady to join!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Connected</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- ERROR State -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n\t\t\t<div class=\"state-icon error\">\n\t\t\t\t<i class=\"fa fa-exclamation-triangle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message error\">\n\t\t\t\tAn error occurred\n\t\t\t</p>\n\t\t\t<p class=\"error-details\" *ngIf=\"errorMessage\">\n\t\t\t\t{{ errorMessage }}\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn retry-btn\"\n\t\t\t\t(click)=\"retry()\">\n\t\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t\t\tRetry\n\t\t\t</button>\n\t\t</div>\n\t</div>\n\n\t<!-- Footer -->\n\t<div class=\"waiting-room-footer\">\n\t\t<button \n\t\t\ttype=\"button\" \n\t\t\tclass=\"btn cancel-btn\"\n\t\t\t(click)=\"cancel()\">\n\t\t\tCancel\n\t\t</button>\n\t</div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:32px 40px 24px;text-align:center;border-bottom:1px solid #e9ecef}.waiting-room-header .header-icon{width:72px;height:72px;margin:0 auto 16px;background:linear-gradient(135deg,#1da4b1 0%,#0077b3 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #1da4b140}.waiting-room-header .header-icon i{font-size:28px;color:#fff}.waiting-room-header .header-title{margin:0 0 8px;font-size:20px;font-weight:700;line-height:28px;color:#212529}.waiting-room-header .header-subtitle{margin:0;font-size:14px;color:#6c757d;font-weight:400}.waiting-room-header .close-btn{position:absolute;top:16px;right:16px;width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:360px;width:100%}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"] }]
639
- }], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: i1.NgbModal }]; }, propDecorators: { appointmentId: [{
640
- type: Input
641
- }], product: [{
642
- type: Input
643
- }], tenantId: [{
644
- type: Input
645
- }], currentUser: [{
1206
+ args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n <!-- Header -->\n <div class=\"waiting-room-header\">\n <h2 class=\"header-title\">Iniciar turno</h2>\n <button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n <span aria-hidden=\"true\">&times;</span>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"waiting-room-content\">\n <!-- CHECKING_STATUS State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CHECKING_STATUS\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Verificando estado de la sesi\u00F3n...</p>\n </div>\n\n <!-- WAITING_FOR_OWNER State (Non-owner waiting) -->\n <div class=\"state-container waiting-for-owner\" *ngIf=\"state === WaitingRoomState.WAITING_FOR_OWNER\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message waiting-title\">Medicina laboral te va a admitir en unos instantes...</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla hasta que inicie la consulta.</p>\n </div>\n\n <!-- READY State (Owner can join) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY\">\n <div class=\"state-icon ready\">\n <i class=\"fa fa-check-circle\"></i>\n </div>\n <p class=\"state-message success\">\u00A1La sala est\u00E1 lista!</p>\n <p class=\"state-submessage\">Pod\u00E9s unirte a la videollamada cuando quieras</p>\n <button type=\"button\" class=\"btn action-btn join-btn\" (click)=\"joinSession()\">\n <i class=\"fa fa-sign-in\"></i>\n Unirse a la llamada\n </button>\n </div>\n\n <!-- GETTING_TOKEN State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Conectando...</p>\n </div>\n\n <!-- ERROR State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n <p class=\"error-details\" *ngIf=\"errorMessage\">\n {{ errorMessage }}\n </p>\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:20px 40px;border-bottom:1px solid #e9ecef;display:flex;align-items:center;justify-content:space-between}.waiting-room-header .header-title{margin:0;font-size:18px;font-weight:600;line-height:24px;color:#212529}.waiting-room-header .close-btn{width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px;display:flex;align-items:center;justify-content:center}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-title{font-size:16px;font-weight:600;color:#212529;margin-bottom:8px}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.mode-tabs{display:flex;justify-content:center;gap:8px;margin-bottom:24px;padding:4px;background:#f8f9fa;border-radius:8px}.mode-tab{flex:1;padding:10px 16px;font-size:13px;font-weight:600;border:none;border-radius:6px;background:transparent;color:#6c757d;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px}.mode-tab i{font-size:14px}.mode-tab:hover{color:#212529}.mode-tab.active{background:#ffffff;color:#1da4b1;box-shadow:0 2px 8px #00000014}.mode-content{animation:fadeIn .3s ease}.session-input-container{display:flex;flex-direction:column;gap:16px;margin-top:8px}.session-input{width:100%;padding:14px 16px;font-size:14px;border:2px solid #e9ecef;border-radius:8px;background:#ffffff;color:#212529;transition:all .2s ease;text-align:center;font-family:Monaco,Consolas,monospace;letter-spacing:.5px}.session-input::placeholder{color:#6c757d;font-family:inherit;letter-spacing:normal}.session-input:focus{outline:none;border-color:#1da4b1;box-shadow:0 0 0 3px #1da4b126}.session-input:hover:not(:focus){border-color:#6c757d}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"] }]
1207
+ }], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
646
1208
  type: Input
647
- }], ownerUserIds: [{
1209
+ }], entityId: [{
648
1210
  type: Input
649
- }], regularUserIds: [{
1211
+ }], tenant: [{
650
1212
  type: Input
651
- }], moderatorUserIds: [{
1213
+ }], businessRole: [{
652
1214
  type: Input
653
- }], existingSessionId: [{
1215
+ }], currentUser: [{
654
1216
  type: Input
655
1217
  }] } });
656
1218
 
@@ -658,116 +1220,211 @@ class TasButtonComponent {
658
1220
  constructor(modalService, tasService) {
659
1221
  this.modalService = modalService;
660
1222
  this.tasService = tasService;
661
- this.appointmentId = 1;
662
- this.product = "uell";
663
- this.tenantId = "";
664
- this.regularUserIds = [];
665
- this.moderatorUserIds = [];
666
- /** Optional: If provided, skips room creation and goes directly to getting a token */
667
- this.existingSessionId = "";
668
- /** Optional: Custom button text */
669
- this.buttonText = "Iniciar TAS";
1223
+ // Status endpoint params
1224
+ this.roomType = TasRoomType.TAS;
1225
+ this.businessRole = TasBusinessRole.USER;
670
1226
  this.isLoading = false;
1227
+ this.buttonText = 'Iniciar TAS';
1228
+ // Status check state
1229
+ this.isCheckingStatus = false;
1230
+ this.isStatusError = false;
1231
+ this.statusErrorMessage = '';
671
1232
  this.subscriptions = new Subscription();
672
1233
  this.currentModalRef = null;
673
1234
  this.videoCallModalRef = null;
1235
+ this.statusPollingInterval = null;
1236
+ this.STATUS_POLL_INTERVAL_MS = 30000; // 30 seconds
674
1237
  }
675
- ngOnInit() {
676
- if (!this.ownerUserIds || this.ownerUserIds.length !== 1) {
677
- throw new Error('tas-btn: ownerUserIds input is required and must contain exactly one user');
1238
+ /** Whether user is backoffice (or admin/manager) */
1239
+ get isBackoffice() {
1240
+ return (this.businessRole === TasBusinessRole.BACKOFFICE ||
1241
+ this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
1242
+ this.businessRole === TasBusinessRole.MANAGER);
1243
+ }
1244
+ /** Whether the button should be visible */
1245
+ get isVisible() {
1246
+ // Backoffice: always show (disabled if error)
1247
+ // Other roles: hide if status error
1248
+ if (this.isBackoffice) {
1249
+ return true;
678
1250
  }
1251
+ return !this.isStatusError;
1252
+ }
1253
+ /** Whether the button should be disabled */
1254
+ get isDisabled() {
1255
+ return this.isLoading || this.isCheckingStatus || this.isStatusError;
1256
+ }
1257
+ ngOnInit() {
679
1258
  // Subscribe to viewMode to handle PiP return
680
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
681
- // Reopen video call modal when returning from PiP
682
- if (mode === ViewMode.FULLSCREEN &&
683
- this.tasService.isCallActive() &&
684
- !this.videoCallModalRef) {
685
- const sessionId = this.tasService.sessionId;
686
- const token = this.tasService.token;
687
- if (sessionId && token) {
688
- this.openVideoCallModal(true);
689
- }
690
- }
1259
+ this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
691
1260
  // When entering PiP, clear the videoCallModalRef since modal will close
692
1261
  if (mode === ViewMode.PIP) {
693
1262
  this.videoCallModalRef = null;
694
1263
  }
695
1264
  }));
1265
+ // Start status checking
1266
+ this.startStatusPolling();
696
1267
  }
697
1268
  ngOnDestroy() {
698
1269
  this.subscriptions.unsubscribe();
1270
+ this.stopStatusPolling();
1271
+ }
1272
+ /**
1273
+ * Start polling status every 30 seconds
1274
+ */
1275
+ startStatusPolling() {
1276
+ // Initial status check
1277
+ this.checkStatus();
1278
+ // Set up periodic polling
1279
+ this.statusPollingInterval = setInterval(() => {
1280
+ this.checkStatus();
1281
+ }, this.STATUS_POLL_INTERVAL_MS);
1282
+ }
1283
+ /**
1284
+ * Stop status polling
1285
+ */
1286
+ stopStatusPolling() {
1287
+ if (this.statusPollingInterval) {
1288
+ clearInterval(this.statusPollingInterval);
1289
+ this.statusPollingInterval = null;
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Check status endpoint to determine if button should be enabled
1294
+ */
1295
+ checkStatus() {
1296
+ // Skip if required inputs are not available
1297
+ if (!this.tenant || !this.entityId) {
1298
+ console.log('[TAS DEBUG] checkStatus skipped - missing required inputs');
1299
+ return;
1300
+ }
1301
+ this.isCheckingStatus = true;
1302
+ this.statusErrorMessage = '';
1303
+ console.log('[TAS DEBUG] checkStatus called with:', {
1304
+ roomType: this.roomType,
1305
+ entityId: this.entityId,
1306
+ tenant: this.tenant,
1307
+ businessRole: this.businessRole,
1308
+ });
1309
+ this.subscriptions.add(this.tasService.getProxyVideoStatus({
1310
+ roomType: this.roomType,
1311
+ entityId: this.entityId,
1312
+ tenant: this.tenant,
1313
+ businessRole: this.businessRole,
1314
+ }).subscribe({
1315
+ next: (response) => {
1316
+ // Check if response is actually an error (some HTTP adapters return errors in next)
1317
+ // Also check for undefined/null or missing content
1318
+ const isErrorResponse = !response ||
1319
+ !response.content ||
1320
+ response?.ok === false ||
1321
+ response?.status >= 400 ||
1322
+ response?.error ||
1323
+ response?.name === 'HttpErrorResponse';
1324
+ if (isErrorResponse) {
1325
+ console.error('[TAS DEBUG] Status check returned error in response:', response);
1326
+ this.isCheckingStatus = false;
1327
+ this.isStatusError = true;
1328
+ this.statusErrorMessage = response?.error?.message || response?.message || 'Error checking status';
1329
+ }
1330
+ else {
1331
+ console.log('[TAS DEBUG] Status check successful:', response);
1332
+ this.isCheckingStatus = false;
1333
+ this.isStatusError = false;
1334
+ this.statusErrorMessage = '';
1335
+ }
1336
+ },
1337
+ error: (err) => {
1338
+ console.error('[TAS DEBUG] Status check failed:', err);
1339
+ this.isCheckingStatus = false;
1340
+ this.isStatusError = true;
1341
+ this.statusErrorMessage = err?.error?.message || err?.message || 'Error checking status';
1342
+ },
1343
+ }));
699
1344
  }
700
1345
  onClick() {
701
- if (!this.tenantId || !this.currentUser?.name) {
702
- console.error("Tenant ID or current user not available");
1346
+ console.log('[TAS DEBUG] onClick called');
1347
+ console.log('[TAS DEBUG] Inputs:', {
1348
+ tenant: this.tenant,
1349
+ entityId: this.entityId,
1350
+ roomType: this.roomType,
1351
+ businessRole: this.businessRole,
1352
+ currentUser: this.currentUser,
1353
+ });
1354
+ if (!this.tenant || !this.currentUser?.name) {
1355
+ console.error('[TAS DEBUG] Tenant or current user not available');
1356
+ return;
1357
+ }
1358
+ if (!this.entityId) {
1359
+ console.error('[TAS DEBUG] entityId is required');
703
1360
  return;
704
1361
  }
1362
+ console.log('[TAS DEBUG] Validation passed, opening waiting room modal');
705
1363
  this.openWaitingRoomModal();
706
1364
  }
707
1365
  openWaitingRoomModal() {
708
1366
  this.currentModalRef = this.modalService.open(TasWaitingRoomComponent, {
709
- size: "lg",
710
- windowClass: "tas-waiting-room-modal",
711
- backdrop: "static",
1367
+ size: 'lg',
1368
+ windowClass: 'tas-waiting-room-modal',
1369
+ backdrop: 'static',
712
1370
  keyboard: false,
713
- centered: true
1371
+ centered: true,
714
1372
  });
715
1373
  // Pass all necessary inputs to the waiting room component
716
- this.currentModalRef.componentInstance.appointmentId = this.appointmentId;
717
- this.currentModalRef.componentInstance.product = this.product;
718
- this.currentModalRef.componentInstance.tenantId = this.tenantId;
1374
+ this.currentModalRef.componentInstance.roomType = this.roomType;
1375
+ this.currentModalRef.componentInstance.entityId = this.entityId;
1376
+ this.currentModalRef.componentInstance.tenant = this.tenant;
1377
+ this.currentModalRef.componentInstance.businessRole = this.businessRole;
719
1378
  this.currentModalRef.componentInstance.currentUser = this.currentUser;
720
- this.currentModalRef.componentInstance.ownerUserIds = this.ownerUserIds;
721
- this.currentModalRef.componentInstance.regularUserIds = this.regularUserIds;
722
- this.currentModalRef.componentInstance.moderatorUserIds = this.moderatorUserIds;
723
- // Pass existing session ID if provided
724
- this.currentModalRef.componentInstance.existingSessionId = this.existingSessionId;
725
- this.currentModalRef.result.then(() => { this.currentModalRef = null; }, () => { this.currentModalRef = null; });
1379
+ this.currentModalRef.result.then(() => {
1380
+ this.currentModalRef = null;
1381
+ }, () => {
1382
+ this.currentModalRef = null;
1383
+ });
726
1384
  }
727
1385
  openVideoCallModal(isReturningFromPip = false) {
728
1386
  this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
729
1387
  size: 'xl',
730
1388
  windowClass: 'tas-video-modal',
731
1389
  backdrop: 'static',
732
- keyboard: false
1390
+ keyboard: false,
733
1391
  });
734
1392
  this.videoCallModalRef.componentInstance.sessionId = this.tasService.sessionId;
735
1393
  this.videoCallModalRef.componentInstance.token = this.tasService.token;
1394
+ this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
736
1395
  this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
737
- this.videoCallModalRef.result.then(() => { this.videoCallModalRef = null; }, () => { this.videoCallModalRef = null; });
1396
+ this.videoCallModalRef.result.then(() => {
1397
+ this.videoCallModalRef = null;
1398
+ }, () => {
1399
+ this.videoCallModalRef = null;
1400
+ });
738
1401
  }
739
1402
  }
740
- TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasButtonComponent, deps: [{ token: i1.NgbModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
741
- TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: TasButtonComponent, selector: "tas-btn", inputs: { appointmentId: "appointmentId", product: "product", tenantId: "tenantId", currentUser: "currentUser", ownerUserIds: "ownerUserIds", regularUserIds: "regularUserIds", moderatorUserIds: "moderatorUserIds", existingSessionId: "existingSessionId", buttonText: "buttonText" }, ngImport: i0, template: "<button\n\ttype=\"button\"\n\tclass=\"btn btn-primary tas-btn\"\n\t(click)=\"onClick()\"\n\t[disabled]=\"isLoading\"\n>\n\t<i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n\t<span *ngIf=\"!isLoading\"> {{ buttonText }}</span>\n\t<span *ngIf=\"isLoading\"> Processing...</span>\n</button>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}\n"], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
742
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasButtonComponent, decorators: [{
1403
+ TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, deps: [{ token: i1.NgbModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
1404
+ TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasButtonComponent, selector: "tas-btn", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<button\n *ngIf=\"isVisible\"\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n>\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">Iniciar TAS</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n</button>\n\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}\n"], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
1405
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
743
1406
  type: Component,
744
- args: [{ selector: "tas-btn", template: "<button\n\ttype=\"button\"\n\tclass=\"btn btn-primary tas-btn\"\n\t(click)=\"onClick()\"\n\t[disabled]=\"isLoading\"\n>\n\t<i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n\t<span *ngIf=\"!isLoading\"> {{ buttonText }}</span>\n\t<span *ngIf=\"isLoading\"> Processing...</span>\n</button>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}\n"] }]
745
- }], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: { appointmentId: [{
746
- type: Input
747
- }], product: [{
748
- type: Input
749
- }], tenantId: [{
750
- type: Input
751
- }], currentUser: [{
752
- type: Input
753
- }], ownerUserIds: [{
1407
+ args: [{ selector: 'tas-btn', template: "<button\n *ngIf=\"isVisible\"\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n>\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">Iniciar TAS</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n</button>\n\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}\n"] }]
1408
+ }], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: { roomType: [{
754
1409
  type: Input
755
- }], regularUserIds: [{
1410
+ }], entityId: [{
756
1411
  type: Input
757
- }], moderatorUserIds: [{
1412
+ }], tenant: [{
758
1413
  type: Input
759
- }], existingSessionId: [{
1414
+ }], businessRole: [{
760
1415
  type: Input
761
- }], buttonText: [{
1416
+ }], currentUser: [{
762
1417
  type: Input
763
1418
  }] } });
764
1419
 
765
1420
  class TasFloatingCallComponent {
766
- constructor(tasService) {
1421
+ constructor(tasService, modalService) {
767
1422
  this.tasService = tasService;
1423
+ this.modalService = modalService;
768
1424
  this.isVisible = false;
769
1425
  this.isMuted = false;
770
1426
  this.subscriptions = new Subscription();
1427
+ this.videoCallModalRef = null;
771
1428
  // Margin from screen edges (in pixels)
772
1429
  this.PIP_MARGIN = 20;
773
1430
  }
@@ -780,6 +1437,7 @@ class TasFloatingCallComponent {
780
1437
  }
781
1438
  // Public Methods
782
1439
  onExpand() {
1440
+ this.openVideoCallModal(true);
783
1441
  this.tasService.exitPipMode();
784
1442
  }
785
1443
  onHangUp() {
@@ -791,23 +1449,45 @@ class TasFloatingCallComponent {
791
1449
  // Private Methods
792
1450
  setupSubscriptions() {
793
1451
  // Call state subscription
794
- this.subscriptions.add(this.tasService.callState$.subscribe(state => {
1452
+ this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
795
1453
  if (state === CallState.DISCONNECTED) {
796
1454
  this.isVisible = false;
797
1455
  }
798
1456
  }));
799
1457
  // View mode subscription
800
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
1458
+ this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
801
1459
  this.isVisible = mode === ViewMode.PIP && this.tasService.isCallActive();
802
1460
  if (this.isVisible) {
803
1461
  setTimeout(() => this.initInteract(), 100);
1462
+ // Clear modal ref if we enter PiP mode (modal closes itself)
1463
+ this.videoCallModalRef = null;
804
1464
  }
805
1465
  }));
806
1466
  // Mute state subscription
807
- this.subscriptions.add(this.tasService.isMuted$.subscribe(muted => {
1467
+ this.subscriptions.add(this.tasService.isMuted$.subscribe((muted) => {
808
1468
  this.isMuted = muted;
809
1469
  }));
810
1470
  }
1471
+ openVideoCallModal(isReturningFromPip = false) {
1472
+ if (this.videoCallModalRef) {
1473
+ return;
1474
+ }
1475
+ this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
1476
+ size: 'xl',
1477
+ windowClass: 'tas-video-modal',
1478
+ backdrop: 'static',
1479
+ keyboard: false,
1480
+ });
1481
+ this.videoCallModalRef.componentInstance.sessionId = this.tasService.sessionId;
1482
+ this.videoCallModalRef.componentInstance.token = this.tasService.token;
1483
+ this.videoCallModalRef.componentInstance.businessRole = this.tasService.businessRole;
1484
+ this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
1485
+ this.videoCallModalRef.result.then(() => {
1486
+ this.videoCallModalRef = null;
1487
+ }, () => {
1488
+ this.videoCallModalRef = null;
1489
+ });
1490
+ }
811
1491
  initInteract() {
812
1492
  interact('.tas-floating-container').unset();
813
1493
  // Create restriction area with margin
@@ -818,17 +1498,15 @@ class TasFloatingCallComponent {
818
1498
  left: margin,
819
1499
  top: margin,
820
1500
  right: window.innerWidth - margin,
821
- bottom: window.innerHeight - margin
1501
+ bottom: window.innerHeight - margin,
822
1502
  };
823
1503
  },
824
- elementRect: { left: 0, right: 1, top: 0, bottom: 1 }
1504
+ elementRect: { left: 0, right: 1, top: 0, bottom: 1 },
825
1505
  };
826
1506
  interact('.tas-floating-container')
827
1507
  .draggable({
828
1508
  inertia: true,
829
- modifiers: [
830
- interact.modifiers.restrict(restrictToBodyWithMargin)
831
- ],
1509
+ modifiers: [interact.modifiers.restrict(restrictToBodyWithMargin)],
832
1510
  autoScroll: false,
833
1511
  listeners: {
834
1512
  move: (event) => {
@@ -838,8 +1516,8 @@ class TasFloatingCallComponent {
838
1516
  target.style.transform = `translate(${x}px, ${y}px)`;
839
1517
  target.setAttribute('data-x', String(x));
840
1518
  target.setAttribute('data-y', String(y));
841
- }
842
- }
1519
+ },
1520
+ },
843
1521
  })
844
1522
  .resizable({
845
1523
  edges: { left: false, right: true, bottom: true, top: false },
@@ -857,7 +1535,7 @@ class TasFloatingCallComponent {
857
1535
  target.style.transform = `translate(${x}px, ${y}px)`;
858
1536
  target.setAttribute('data-x', String(x));
859
1537
  target.setAttribute('data-y', String(y));
860
- }
1538
+ },
861
1539
  },
862
1540
  modifiers: [
863
1541
  interact.modifiers.restrictEdges({
@@ -865,25 +1543,25 @@ class TasFloatingCallComponent {
865
1543
  left: margin,
866
1544
  top: margin,
867
1545
  right: window.innerWidth - margin,
868
- bottom: window.innerHeight - margin
869
- }
1546
+ bottom: window.innerHeight - margin,
1547
+ },
870
1548
  }),
871
1549
  interact.modifiers.restrictSize({
872
1550
  min: { width: 200, height: 130 },
873
- max: { width: 500, height: 350 }
1551
+ max: { width: 500, height: 350 },
874
1552
  }),
875
- interact.modifiers.aspectRatio({ ratio: 'preserve' })
1553
+ interact.modifiers.aspectRatio({ ratio: 'preserve' }),
876
1554
  ],
877
- inertia: true
1555
+ inertia: true,
878
1556
  });
879
1557
  }
880
1558
  }
881
- TasFloatingCallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasFloatingCallComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
882
- TasFloatingCallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: TasFloatingCallComponent, selector: "tas-floating-call", ngImport: i0, template: "<div class=\"tas-floating-container\" [class.visible]=\"isVisible\">\n\t<!-- Video content area - shows main video only -->\n\t<div class=\"floating-content\">\n\t\t<!-- Main video container (subscriber if available, otherwise publisher) -->\n\t\t<div id=\"pip-main-video\" class=\"pip-main-video\"></div>\n\n\t\t<!-- Bottom controls -->\n\t\t<div class=\"floating-controls\">\n\t\t\t<button class=\"action-btn expand-btn\" (click)=\"onExpand()\" title=\"Expand to fullscreen\">\n\t\t\t\t<i class=\"fa fa-expand\"></i>\n\t\t\t</button>\n\t\t\t<button class=\"action-btn mute-btn\" [class.muted]=\"isMuted\" (click)=\"toggleMute()\" [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\">\n\t\t\t\t<i class=\"fa\" [class.fa-microphone]=\"!isMuted\" [class.fa-microphone-slash]=\"isMuted\"></i>\n\t\t\t</button>\n\t\t\t<button class=\"action-btn hangup-btn\" (click)=\"onHangUp()\" title=\"Hang up call\">\n\t\t\t\t<i class=\"fa fa-phone\" style=\"transform: rotate(135deg);\"></i>\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</div>\n\n", styles: [".tas-floating-container{position:fixed;bottom:20px;right:20px;width:280px;height:180px;background:#000;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none;transition:opacity .3s ease,visibility .3s ease,box-shadow .2s ease;opacity:0;visibility:hidden;pointer-events:none}.tas-floating-container.visible{opacity:1;visibility:visible;pointer-events:auto}.tas-floating-container:hover{box-shadow:0 8px 32px #00000080,0 0 0 2px #ffffff4d}.floating-content{position:relative;width:100%;height:100%;overflow:hidden}.pip-main-video{position:absolute;top:0;left:0;width:100%;height:100%;background:#000}.pip-main-video ::ng-deep video{width:100%;height:100%;object-fit:cover}.pip-main-video ::ng-deep .OT_subscriber,.pip-main-video ::ng-deep .OT_publisher{width:100%!important;height:100%!important}.pip-main-video ::ng-deep .OT_edge-bar-item,.pip-main-video ::ng-deep .OT_mute,.pip-main-video ::ng-deep .OT_audio-level-meter,.pip-main-video ::ng-deep .OT_bar,.pip-main-video ::ng-deep .OT_name{display:none!important}.floating-controls{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:12px;padding:6px 14px;background:rgba(0,0,0,.7);border-radius:24px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}.tas-floating-container:hover .floating-controls{opacity:1;visibility:visible}.action-btn{width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s ease}.action-btn.expand-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.expand-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn.muted{background:#f39c12;color:#fff}.action-btn.mute-btn.muted:hover{background:#e67e22}.action-btn.hangup-btn{background:#dc3545;color:#fff}.action-btn.hangup-btn:hover{background:#c82333;transform:scale(1.1)}\n"] });
883
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasFloatingCallComponent, decorators: [{
1559
+ TasFloatingCallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFloatingCallComponent, deps: [{ token: TasService }, { token: i1.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
1560
+ TasFloatingCallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasFloatingCallComponent, selector: "tas-floating-call", ngImport: i0, template: "<div class=\"tas-floating-container\" [class.visible]=\"isVisible\">\n <!-- Video content area - shows main video only -->\n <div class=\"floating-content\">\n <!-- Main video container (subscriber if available, otherwise publisher) -->\n <div id=\"pip-main-video\" class=\"pip-main-video\"></div>\n\n <!-- Bottom controls -->\n <div class=\"floating-controls\">\n <button\n class=\"action-btn expand-btn\"\n (click)=\"onExpand()\"\n title=\"Expand to fullscreen\"\n >\n <i class=\"fa fa-expand\"></i>\n </button>\n <button\n class=\"action-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button class=\"action-btn hangup-btn\" (click)=\"onHangUp()\" title=\"Hang up call\">\n <i class=\"fa fa-phone\" style=\"transform: rotate(135deg)\"></i>\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-floating-container{position:fixed;bottom:20px;right:20px;width:280px;height:180px;background:#000;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none;transition:opacity .3s ease,visibility .3s ease,box-shadow .2s ease;opacity:0;visibility:hidden;pointer-events:none}.tas-floating-container.visible{opacity:1;visibility:visible;pointer-events:auto}.tas-floating-container:hover{box-shadow:0 8px 32px #00000080,0 0 0 2px #ffffff4d}.floating-content{position:relative;width:100%;height:100%;overflow:hidden}.pip-main-video{position:absolute;top:0;left:0;width:100%;height:100%;background:#000}.pip-main-video ::ng-deep video{width:100%;height:100%;object-fit:cover}.pip-main-video ::ng-deep .OT_subscriber,.pip-main-video ::ng-deep .OT_publisher{width:100%!important;height:100%!important}.pip-main-video ::ng-deep .OT_edge-bar-item,.pip-main-video ::ng-deep .OT_mute,.pip-main-video ::ng-deep .OT_audio-level-meter,.pip-main-video ::ng-deep .OT_bar,.pip-main-video ::ng-deep .OT_name{display:none!important}.floating-controls{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:12px;padding:6px 14px;background:rgba(0,0,0,.7);border-radius:24px;backdrop-filter:blur(8px);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}.tas-floating-container:hover .floating-controls{opacity:1;visibility:visible}.action-btn{width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s ease}.action-btn.expand-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.expand-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn.muted{background:#f39c12;color:#fff}.action-btn.mute-btn.muted:hover{background:#e67e22}.action-btn.hangup-btn{background:#dc3545;color:#fff}.action-btn.hangup-btn:hover{background:#c82333;transform:scale(1.1)}\n"] });
1561
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFloatingCallComponent, decorators: [{
884
1562
  type: Component,
885
- args: [{ selector: 'tas-floating-call', template: "<div class=\"tas-floating-container\" [class.visible]=\"isVisible\">\n\t<!-- Video content area - shows main video only -->\n\t<div class=\"floating-content\">\n\t\t<!-- Main video container (subscriber if available, otherwise publisher) -->\n\t\t<div id=\"pip-main-video\" class=\"pip-main-video\"></div>\n\n\t\t<!-- Bottom controls -->\n\t\t<div class=\"floating-controls\">\n\t\t\t<button class=\"action-btn expand-btn\" (click)=\"onExpand()\" title=\"Expand to fullscreen\">\n\t\t\t\t<i class=\"fa fa-expand\"></i>\n\t\t\t</button>\n\t\t\t<button class=\"action-btn mute-btn\" [class.muted]=\"isMuted\" (click)=\"toggleMute()\" [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\">\n\t\t\t\t<i class=\"fa\" [class.fa-microphone]=\"!isMuted\" [class.fa-microphone-slash]=\"isMuted\"></i>\n\t\t\t</button>\n\t\t\t<button class=\"action-btn hangup-btn\" (click)=\"onHangUp()\" title=\"Hang up call\">\n\t\t\t\t<i class=\"fa fa-phone\" style=\"transform: rotate(135deg);\"></i>\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</div>\n\n", styles: [".tas-floating-container{position:fixed;bottom:20px;right:20px;width:280px;height:180px;background:#000;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none;transition:opacity .3s ease,visibility .3s ease,box-shadow .2s ease;opacity:0;visibility:hidden;pointer-events:none}.tas-floating-container.visible{opacity:1;visibility:visible;pointer-events:auto}.tas-floating-container:hover{box-shadow:0 8px 32px #00000080,0 0 0 2px #ffffff4d}.floating-content{position:relative;width:100%;height:100%;overflow:hidden}.pip-main-video{position:absolute;top:0;left:0;width:100%;height:100%;background:#000}.pip-main-video ::ng-deep video{width:100%;height:100%;object-fit:cover}.pip-main-video ::ng-deep .OT_subscriber,.pip-main-video ::ng-deep .OT_publisher{width:100%!important;height:100%!important}.pip-main-video ::ng-deep .OT_edge-bar-item,.pip-main-video ::ng-deep .OT_mute,.pip-main-video ::ng-deep .OT_audio-level-meter,.pip-main-video ::ng-deep .OT_bar,.pip-main-video ::ng-deep .OT_name{display:none!important}.floating-controls{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:12px;padding:6px 14px;background:rgba(0,0,0,.7);border-radius:24px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}.tas-floating-container:hover .floating-controls{opacity:1;visibility:visible}.action-btn{width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s ease}.action-btn.expand-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.expand-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn.muted{background:#f39c12;color:#fff}.action-btn.mute-btn.muted:hover{background:#e67e22}.action-btn.hangup-btn{background:#dc3545;color:#fff}.action-btn.hangup-btn:hover{background:#c82333;transform:scale(1.1)}\n"] }]
886
- }], ctorParameters: function () { return [{ type: TasService }]; } });
1563
+ args: [{ selector: 'tas-floating-call', template: "<div class=\"tas-floating-container\" [class.visible]=\"isVisible\">\n <!-- Video content area - shows main video only -->\n <div class=\"floating-content\">\n <!-- Main video container (subscriber if available, otherwise publisher) -->\n <div id=\"pip-main-video\" class=\"pip-main-video\"></div>\n\n <!-- Bottom controls -->\n <div class=\"floating-controls\">\n <button\n class=\"action-btn expand-btn\"\n (click)=\"onExpand()\"\n title=\"Expand to fullscreen\"\n >\n <i class=\"fa fa-expand\"></i>\n </button>\n <button\n class=\"action-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button class=\"action-btn hangup-btn\" (click)=\"onHangUp()\" title=\"Hang up call\">\n <i class=\"fa fa-phone\" style=\"transform: rotate(135deg)\"></i>\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-floating-container{position:fixed;bottom:20px;right:20px;width:280px;height:180px;background:#000;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none;transition:opacity .3s ease,visibility .3s ease,box-shadow .2s ease;opacity:0;visibility:hidden;pointer-events:none}.tas-floating-container.visible{opacity:1;visibility:visible;pointer-events:auto}.tas-floating-container:hover{box-shadow:0 8px 32px #00000080,0 0 0 2px #ffffff4d}.floating-content{position:relative;width:100%;height:100%;overflow:hidden}.pip-main-video{position:absolute;top:0;left:0;width:100%;height:100%;background:#000}.pip-main-video ::ng-deep video{width:100%;height:100%;object-fit:cover}.pip-main-video ::ng-deep .OT_subscriber,.pip-main-video ::ng-deep .OT_publisher{width:100%!important;height:100%!important}.pip-main-video ::ng-deep .OT_edge-bar-item,.pip-main-video ::ng-deep .OT_mute,.pip-main-video ::ng-deep .OT_audio-level-meter,.pip-main-video ::ng-deep .OT_bar,.pip-main-video ::ng-deep .OT_name{display:none!important}.floating-controls{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:12px;padding:6px 14px;background:rgba(0,0,0,.7);border-radius:24px;backdrop-filter:blur(8px);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}.tas-floating-container:hover .floating-controls{opacity:1;visibility:visible}.action-btn{width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s ease}.action-btn.expand-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.expand-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn.muted{background:#f39c12;color:#fff}.action-btn.mute-btn.muted:hover{background:#e67e22}.action-btn.hangup-btn{background:#dc3545;color:#fff}.action-btn.hangup-btn:hover{background:#c82333;transform:scale(1.1)}\n"] }]
1564
+ }], ctorParameters: function () { return [{ type: TasService }, { type: i1.NgbModal }]; } });
887
1565
 
888
1566
  class TasUellSdkModule {
889
1567
  /**
@@ -921,40 +1599,40 @@ class TasUellSdkModule {
921
1599
  providers: [
922
1600
  { provide: TAS_CONFIG, useValue: options.config },
923
1601
  { provide: TAS_HTTP_CLIENT, useClass: options.httpClient },
924
- TasService
925
- ]
1602
+ TasService,
1603
+ ],
926
1604
  };
927
1605
  }
928
1606
  }
929
- TasUellSdkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
930
- TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, declarations: [TasButtonComponent,
1607
+ TasUellSdkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1608
+ TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, declarations: [TasButtonComponent,
931
1609
  TasVideocallComponent,
932
1610
  TasFloatingCallComponent,
933
- TasWaitingRoomComponent], imports: [CommonModule], exports: [TasButtonComponent,
1611
+ TasWaitingRoomComponent,
1612
+ TasAvatarComponent], imports: [CommonModule, FormsModule], exports: [TasButtonComponent,
934
1613
  TasVideocallComponent,
935
1614
  TasFloatingCallComponent,
936
- TasWaitingRoomComponent] });
937
- TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, imports: [[
938
- CommonModule,
939
- ]] });
940
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, decorators: [{
1615
+ TasWaitingRoomComponent,
1616
+ TasAvatarComponent] });
1617
+ TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, imports: [[CommonModule, FormsModule]] });
1618
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, decorators: [{
941
1619
  type: NgModule,
942
1620
  args: [{
943
1621
  declarations: [
944
1622
  TasButtonComponent,
945
1623
  TasVideocallComponent,
946
1624
  TasFloatingCallComponent,
947
- TasWaitingRoomComponent
948
- ],
949
- imports: [
950
- CommonModule,
1625
+ TasWaitingRoomComponent,
1626
+ TasAvatarComponent,
951
1627
  ],
1628
+ imports: [CommonModule, FormsModule],
952
1629
  exports: [
953
1630
  TasButtonComponent,
954
1631
  TasVideocallComponent,
955
1632
  TasFloatingCallComponent,
956
- TasWaitingRoomComponent
957
- ]
1633
+ TasWaitingRoomComponent,
1634
+ TasAvatarComponent,
1635
+ ],
958
1636
  }]
959
1637
  }] });
960
1638
 
@@ -966,5 +1644,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
966
1644
  * Generated bundle index. Do not edit.
967
1645
  */
968
1646
 
969
- export { CallState, TAS_CONFIG, TAS_HTTP_CLIENT, TasButtonComponent, TasFloatingCallComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, ViewMode, WaitingRoomState };
1647
+ export { CallState, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
970
1648
  //# sourceMappingURL=tas-uell-sdk.mjs.map