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;
@@ -121,7 +248,17 @@ class TasService {
121
248
  }
122
249
  }
123
250
  // Session Management
124
- disconnectSession() {
251
+ disconnectSession(clearStorage = true) {
252
+ console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
253
+ // Call finishSession before disconnecting if we have a sessionId
254
+ const sessionIdToFinish = this.currentSessionId;
255
+ // Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
256
+ if (clearStorage) {
257
+ this.clearSessionState();
258
+ // Set a flag to indicate this session was intentionally disconnected
259
+ // This prevents reconnection on page reload
260
+ sessionStorage.setItem(this.DISCONNECTED_FLAG_KEY, 'true');
261
+ }
125
262
  if (this.session) {
126
263
  this.session.disconnect();
127
264
  this.session = null;
@@ -130,50 +267,261 @@ class TasService {
130
267
  this.subscribers = [];
131
268
  this.currentSessionId = null;
132
269
  this.currentToken = null;
270
+ this.proxyVideoSessionId = null;
271
+ this.proxyVideoToken = null;
133
272
  this.isMutedSubject.next(false); // Reset mute state
134
273
  this.viewModeSubject.next(ViewMode.FULLSCREEN);
135
274
  this.callStateSubject.next(CallState.DISCONNECTED);
275
+ // Call proxy finish after disconnecting (only if not already finishing)
276
+ if (sessionIdToFinish && !this.isFinishingSession) {
277
+ this.isFinishingSession = true;
278
+ this.finishProxyVideoSession({
279
+ sessionId: sessionIdToFinish,
280
+ businessRole: this.currentBusinessRole,
281
+ }).subscribe({
282
+ next: (response) => {
283
+ console.log('[TAS DEBUG] Session finished successfully:', response);
284
+ this.isFinishingSession = false;
285
+ },
286
+ error: (error) => {
287
+ console.error('[TAS DEBUG] Error finishing session:', error);
288
+ this.isFinishingSession = false;
289
+ },
290
+ });
291
+ }
136
292
  }
137
293
  isCallActive() {
138
294
  return this.callStateSubject.getValue() === CallState.CONNECTED;
139
295
  }
296
+ // State Persistence
297
+ saveSessionState(sessionId, token, viewMode, businessRole = TasBusinessRole.USER) {
298
+ const state = {
299
+ sessionId,
300
+ token,
301
+ viewMode,
302
+ businessRole,
303
+ };
304
+ sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
305
+ }
306
+ clearSessionState() {
307
+ sessionStorage.removeItem(this.STORAGE_KEY);
308
+ // Also clear the disconnected flag when clearing state
309
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
310
+ }
311
+ canRestoreSession() {
312
+ return !!sessionStorage.getItem(this.STORAGE_KEY);
313
+ }
314
+ restoreSession(containerId) {
315
+ const savedState = sessionStorage.getItem(this.STORAGE_KEY);
316
+ if (!savedState)
317
+ return;
318
+ // Check if this session was intentionally disconnected
319
+ // If so, don't restore it on page reload
320
+ const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
321
+ if (wasDisconnected === 'true') {
322
+ console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
323
+ this.clearSessionState();
324
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
325
+ return;
326
+ }
327
+ // Don't restore if we're already disconnected or if there's no active session
328
+ // This prevents restoring sessions that were properly disconnected
329
+ if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
330
+ console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
331
+ this.clearSessionState();
332
+ return;
333
+ }
334
+ try {
335
+ const state = JSON.parse(savedState);
336
+ if (state.sessionId && state.token) {
337
+ console.log('[TAS DEBUG] Restoring session from storage');
338
+ // Force PiP mode for restoration to ensure UI consistency
339
+ this.viewModeSubject.next(ViewMode.PIP);
340
+ if (state.businessRole) {
341
+ this.currentBusinessRole = state.businessRole;
342
+ }
343
+ this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
344
+ containerId, this.currentBusinessRole)
345
+ .then(() => {
346
+ console.log('[TAS DEBUG] Session restored successfully');
347
+ // Clear the disconnected flag if restoration succeeds
348
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
349
+ })
350
+ .catch((err) => {
351
+ console.error('[TAS DEBUG] Failed to restore session:', err);
352
+ this.clearSessionState(); // Clear bad state
353
+ });
354
+ }
355
+ }
356
+ catch (e) {
357
+ console.error('[TAS DEBUG] Error parsing saved session state', e);
358
+ this.clearSessionState();
359
+ }
360
+ }
140
361
  // API Methods
141
- createRoom(payload) {
142
- return this.httpClient.post("v2/room", { body: payload, headers: {} }).pipe(map((response) => response), catchError((error) => {
143
- console.error("TAS Service: createRoom failed", error);
362
+ /**
363
+ * PROXY circuit token: /v2/proxy/video/start
364
+ */
365
+ startProxyVideoSession(payload) {
366
+ return this.httpClient
367
+ .post('v2/proxy/video/start', {
368
+ body: payload,
369
+ headers: {},
370
+ })
371
+ .pipe(map((response) => response), catchError((error) => {
372
+ console.error('TAS Service: startProxyVideoSession failed', error);
373
+ throw error;
374
+ }));
375
+ }
376
+ /**
377
+ * PROXY circuit status: /v2/proxy/video/status
378
+ */
379
+ getProxyVideoStatus(payload) {
380
+ return this.httpClient
381
+ .post('v2/proxy/video/status', {
382
+ body: payload,
383
+ headers: {},
384
+ })
385
+ .pipe(map((response) => response), catchError((error) => {
386
+ console.error('TAS Service: getProxyVideoStatus failed', error);
387
+ throw error;
388
+ }));
389
+ }
390
+ /**
391
+ * PROXY circuit user modification: /v2/proxy/video/user/modify
392
+ */
393
+ modifyProxyVideoUser(payload) {
394
+ return this.httpClient
395
+ .patch('v2/proxy/video/user/modify', {
396
+ body: payload,
397
+ headers: {},
398
+ })
399
+ .pipe(catchError((error) => {
400
+ console.error('TAS Service: modifyProxyVideoUser failed', error);
144
401
  throw error;
145
402
  }));
146
403
  }
147
- generateToken(payload) {
148
- return this.httpClient.post("v2/room/token", { body: payload, headers: {} }).pipe(map((response) => response), catchError((error) => {
149
- console.error("TAS Service: generateToken failed", error);
404
+ finishProxyVideoSession(payload) {
405
+ return this.httpClient
406
+ .post('v2/proxy/video/finish', {
407
+ body: payload,
408
+ headers: {},
409
+ })
410
+ .pipe(map((response) => response), catchError((error) => {
411
+ console.error('TAS Service: finishProxyVideoSession failed', error);
150
412
  throw error;
151
413
  }));
152
414
  }
415
+ /**
416
+ * Start automatic status polling for the current session.
417
+ * Status is polled every STATUS_POLL_INTERVAL_MS milliseconds.
418
+ */
419
+ /**
420
+ * Stop automatic status polling.
421
+ */
422
+ /**
423
+ * Fetch status and process the response.
424
+ */
425
+ fetchAndProcessStatus(params) {
426
+ this.getProxyVideoStatus(params).subscribe({
427
+ next: (response) => {
428
+ this.processStatusResponse(response);
429
+ },
430
+ error: (err) => {
431
+ console.error('[TAS DEBUG] Status polling error:', err);
432
+ },
433
+ });
434
+ }
435
+ /**
436
+ * Process status response to update waiting room users and owner join status.
437
+ */
438
+ processStatusResponse(response) {
439
+ const content = response.content;
440
+ // Update videoCallId if available
441
+ if (content.videoCallId) {
442
+ this.currentVideoCallId = content.videoCallId;
443
+ }
444
+ // Update sessionId if available
445
+ if (content.sessionId) {
446
+ this.currentSessionId = content.sessionId;
447
+ this.proxyVideoSessionId = content.sessionId;
448
+ }
449
+ // Update joinable status
450
+ this.joinableSubject.next(content.joinable);
451
+ // Check if owner has joined
452
+ const ownerJoined = this.checkIfOwnerJoined(content.users);
453
+ this.ownerHasJoinedSubject.next(ownerJoined);
454
+ // Detect if owner left: was present, now not present
455
+ if (this.wasOwnerPresent && !ownerJoined) {
456
+ console.log('[TAS DEBUG] Owner has left the session');
457
+ this.ownerHasLeftSubject.next(true);
458
+ }
459
+ if (ownerJoined) {
460
+ this.wasOwnerPresent = true;
461
+ }
462
+ // Extract waiting room users (status === WAITING)
463
+ const waitingUsers = content.users
464
+ .filter((u) => u.status === 'WAITING')
465
+ .map((u) => ({
466
+ userId: u.userId,
467
+ name: `User ${u.userId}`,
468
+ status: RoomUserStatus.WAITING,
469
+ }));
470
+ this.waitingRoomUsersSubject.next(waitingUsers);
471
+ }
472
+ /**
473
+ * Check if at least one OWNER has joined the call.
474
+ */
475
+ checkIfOwnerJoined(users) {
476
+ return users.some((u) => u.rol === TasUserRole.OWNER && u.status === 'JOINED');
477
+ }
478
+ /**
479
+ * Admit a user from the waiting room by changing their status.
480
+ */
481
+ admitUserFromWaitingRoom(userId, videoCallId) {
482
+ return this.modifyProxyVideoUser({
483
+ userId,
484
+ videoCallId,
485
+ action: UserCallAction.WAITING_ROOM_LEAVE,
486
+ });
487
+ }
488
+ // Getters for current call context
489
+ get appointmentId() {
490
+ return this.currentAppointmentId;
491
+ }
492
+ get videoCallId() {
493
+ return this.currentVideoCallId;
494
+ }
153
495
  /**
154
496
  * Connects to a TokBox video session
155
497
  */
156
- connectSession(sessionId, token, publisherElement, subscriberElement) {
498
+ connectSession(sessionId, token, publisherElement, subscriberElement, businessRole = TasBusinessRole.USER) {
157
499
  this.callStateSubject.next(CallState.CONNECTING);
158
500
  this.currentSessionId = sessionId;
159
501
  this.currentToken = token;
502
+ this.currentBusinessRole = businessRole;
503
+ this.isFinishingSession = false; // Reset flag for new session
504
+ // Save initial state (defaulting to current view mode)
505
+ this.saveSessionState(sessionId, token, this.viewModeSubject.getValue(), businessRole);
506
+ // Clear the disconnected flag when starting a new connection
507
+ sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
160
508
  return new Promise((resolve, reject) => {
161
509
  if (!OT.checkSystemRequirements()) {
162
510
  this.callStateSubject.next(CallState.ERROR);
163
- reject(new Error("Browser not compatible with TokBox"));
511
+ reject(new Error('Browser not compatible with TokBox'));
164
512
  return;
165
513
  }
166
514
  this.session = OT.initSession(this.config.tokBoxApiKey, sessionId);
167
515
  // Handle new streams (remote participants joining)
168
- this.session.on("streamCreated", (event) => {
516
+ this.session.on('streamCreated', (event) => {
169
517
  var _a;
170
518
  const subscriber = (_a = this.session) === null || _a === void 0 ? void 0 : _a.subscribe(event.stream, subscriberElement, {
171
- insertMode: "append",
172
- width: "100%",
173
- height: "100%",
519
+ insertMode: 'append',
520
+ width: '100%',
521
+ height: '100%',
174
522
  }, (error) => {
175
523
  if (error) {
176
- console.error("Error subscribing to stream:", error);
524
+ console.error('Error subscribing to stream:', error);
177
525
  }
178
526
  });
179
527
  if (subscriber) {
@@ -181,30 +529,49 @@ class TasService {
181
529
  }
182
530
  });
183
531
  // Handle streams ending (remote participants leaving)
184
- this.session.on("streamDestroyed", (event) => {
532
+ this.session.on('streamDestroyed', (event) => {
185
533
  this.subscribers = this.subscribers.filter((sub) => { var _a; return ((_a = sub.stream) === null || _a === void 0 ? void 0 : _a.streamId) !== event.stream.streamId; });
186
534
  });
187
535
  // Handle session disconnection
188
- this.session.on("sessionDisconnected", () => {
536
+ this.session.on('sessionDisconnected', () => {
189
537
  this.callStateSubject.next(CallState.DISCONNECTED);
538
+ // Call finishSession when session is disconnected (e.g., by server)
539
+ // Only call if not already finishing (prevents duplicate calls)
540
+ const sessionIdToFinish = this.currentSessionId;
541
+ if (sessionIdToFinish && !this.isFinishingSession) {
542
+ this.isFinishingSession = true;
543
+ this.finishProxyVideoSession({
544
+ sessionId: sessionIdToFinish,
545
+ businessRole: this.currentBusinessRole,
546
+ }).subscribe({
547
+ next: (response) => {
548
+ console.log('[TAS DEBUG] Session finished on disconnect event:', response);
549
+ this.isFinishingSession = false;
550
+ },
551
+ error: (error) => {
552
+ console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
553
+ this.isFinishingSession = false;
554
+ },
555
+ });
556
+ }
190
557
  });
191
558
  // Connect to session
192
559
  this.session.connect(token, (error) => {
193
560
  if (error) {
194
- console.error("Error connecting to session:", error);
561
+ console.error('Error connecting to session:', error);
195
562
  this.callStateSubject.next(CallState.ERROR);
196
563
  reject(error);
197
564
  return;
198
565
  }
199
566
  // Initialize publisher (local video)
200
567
  this.publisher = OT.initPublisher(publisherElement, {
201
- insertMode: "append",
202
- width: "100%",
203
- height: "100%",
568
+ insertMode: 'append',
569
+ width: '100%',
570
+ height: '100%',
204
571
  }, (err) => {
205
572
  var _a;
206
573
  if (err) {
207
- console.error("Error initializing publisher:", err);
574
+ console.error('Error initializing publisher:', err);
208
575
  this.callStateSubject.next(CallState.ERROR);
209
576
  reject(err);
210
577
  return;
@@ -212,7 +579,7 @@ class TasService {
212
579
  // Publish to session
213
580
  (_a = this.session) === null || _a === void 0 ? void 0 : _a.publish(this.publisher, (pubErr) => {
214
581
  if (pubErr) {
215
- console.error("Error publishing stream:", pubErr);
582
+ console.error('Error publishing stream:', pubErr);
216
583
  this.callStateSubject.next(CallState.ERROR);
217
584
  reject(pubErr);
218
585
  }
@@ -273,12 +640,12 @@ class TasService {
273
640
  this.movePublisherTo('publisher-container');
274
641
  }
275
642
  }
276
- 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 });
277
- TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasService, providedIn: "root" });
278
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasService, decorators: [{
643
+ 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 });
644
+ TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, providedIn: 'root' });
645
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, decorators: [{
279
646
  type: Injectable,
280
647
  args: [{
281
- providedIn: "root",
648
+ providedIn: 'root',
282
649
  }]
283
650
  }], ctorParameters: function () {
284
651
  return [{ type: undefined, decorators: [{
@@ -290,14 +657,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
290
657
  }] }];
291
658
  } });
292
659
 
660
+ class TasAvatarComponent {
661
+ constructor() {
662
+ this.name = '';
663
+ this.size = 80;
664
+ }
665
+ get initials() {
666
+ if (!this.name)
667
+ return '';
668
+ return this.name
669
+ .split(' ')
670
+ .filter((n) => n.length > 0)
671
+ .map((n) => n[0])
672
+ .join('')
673
+ .toUpperCase()
674
+ .substring(0, 2);
675
+ }
676
+ get fontSize() {
677
+ return Math.round(this.size * 0.4);
678
+ }
679
+ }
680
+ TasAvatarComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasAvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
681
+ 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: `
682
+ <div
683
+ class="avatar"
684
+ [style.width.px]="size"
685
+ [style.height.px]="size"
686
+ [style.fontSize.px]="fontSize"
687
+ >
688
+ <span class="initials">{{ initials }}</span>
689
+ </div>
690
+ `, 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 });
691
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasAvatarComponent, decorators: [{
692
+ type: Component,
693
+ args: [{
694
+ selector: 'tas-avatar',
695
+ template: `
696
+ <div
697
+ class="avatar"
698
+ [style.width.px]="size"
699
+ [style.height.px]="size"
700
+ [style.fontSize.px]="fontSize"
701
+ >
702
+ <span class="initials">{{ initials }}</span>
703
+ </div>
704
+ `,
705
+ styles: [
706
+ `
707
+ .avatar {
708
+ display: flex;
709
+ align-items: center;
710
+ justify-content: center;
711
+ border-radius: 50%;
712
+ background-color: #ffffff;
713
+ color: #0072ac;
714
+ font-weight: 600;
715
+ font-family: inherit;
716
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
717
+ }
718
+
719
+ .initials {
720
+ text-transform: uppercase;
721
+ user-select: none;
722
+ }
723
+ `,
724
+ ],
725
+ changeDetection: ChangeDetectionStrategy.OnPush,
726
+ }]
727
+ }], propDecorators: { name: [{
728
+ type: Input
729
+ }], size: [{
730
+ type: Input
731
+ }] } });
732
+
293
733
  class TasVideocallComponent {
294
734
  constructor(activeModal, tasService) {
295
735
  this.activeModal = activeModal;
296
736
  this.tasService = tasService;
737
+ this.participantName = '';
738
+ this.tenant = '';
739
+ this.businessRole = TasBusinessRole.USER;
297
740
  this.isReturningFromPip = false;
298
741
  this.isPublisherSmall = true;
299
742
  this.callState = CallState.IDLE;
300
743
  this.isMuted = false;
744
+ this.waitingRoomUsers = [];
745
+ this.ownerHasJoined = false;
746
+ this.hasVideoStream = false;
747
+ this.dismissedUsers = [];
301
748
  this.subscriptions = new Subscription();
302
749
  }
303
750
  ngOnInit() {
@@ -309,6 +756,7 @@ class TasVideocallComponent {
309
756
  }
310
757
  ngOnDestroy() {
311
758
  this.subscriptions.unsubscribe();
759
+ this.tasService.resetPollingState();
312
760
  // Only disconnect if not in PiP mode (keep session alive for floating window)
313
761
  if (!this.tasService.isPipMode()) {
314
762
  this.tasService.disconnectSession();
@@ -335,25 +783,83 @@ class TasVideocallComponent {
335
783
  onDoubleClick() {
336
784
  this.toggleSwap();
337
785
  }
786
+ /**
787
+ * Check if current user can admit others (OWNER, BACKOFFICE, or MODERATOR)
788
+ */
789
+ get canAdmitUsers() {
790
+ return (this.businessRole === TasBusinessRole.BACKOFFICE ||
791
+ this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
792
+ this.businessRole === TasBusinessRole.MANAGER);
793
+ }
794
+ /**
795
+ * Admit a user from the waiting room
796
+ */
797
+ admitUser(userId) {
798
+ if (!this.videoCallId) {
799
+ console.error('Cannot admit user: videoCallId not set');
800
+ return;
801
+ }
802
+ this.tasService
803
+ .modifyProxyVideoUser({
804
+ userId,
805
+ videoCallId: this.videoCallId,
806
+ action: UserCallAction.WAITING_ROOM_LEAVE,
807
+ })
808
+ .subscribe({
809
+ next: () => {
810
+ // Remove user from waiting list after successful admit
811
+ this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
812
+ },
813
+ error: (err) => {
814
+ console.error('Error admitting user:', err);
815
+ },
816
+ });
817
+ }
818
+ /**
819
+ * Dismiss the waiting room notification for a user
820
+ */
821
+ dismissWaitingNotification(userId) {
822
+ this.dismissedUsers.push(userId);
823
+ this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
824
+ }
338
825
  // Private Methods
339
826
  setupSubscriptions() {
340
827
  // Call state subscription
341
- this.subscriptions.add(this.tasService.callState$.subscribe(state => {
828
+ this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
342
829
  this.callState = state;
343
830
  if (state === CallState.DISCONNECTED) {
344
831
  this.activeModal.close('hangup');
345
832
  }
833
+ // Track if we have an active video stream
834
+ this.hasVideoStream = state === CallState.CONNECTED;
346
835
  }));
347
836
  // View mode subscription
348
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
837
+ this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
349
838
  if (mode === ViewMode.PIP) {
350
839
  this.activeModal.close('pip');
351
840
  }
352
841
  }));
353
842
  // Mute state subscription
354
- this.subscriptions.add(this.tasService.isMuted$.subscribe(muted => {
843
+ this.subscriptions.add(this.tasService.isMuted$.subscribe((muted) => {
355
844
  this.isMuted = muted;
356
845
  }));
846
+ // Waiting room users subscription
847
+ this.subscriptions.add(this.tasService.waitingRoomUsers$.subscribe((users) => {
848
+ // Filter out dismissed users
849
+ this.waitingRoomUsers = users.filter((u) => !this.dismissedUsers.includes(u.userId));
850
+ }));
851
+ // Owner join status subscription
852
+ this.subscriptions.add(this.tasService.ownerHasJoined$.subscribe((joined) => {
853
+ this.ownerHasJoined = joined;
854
+ }));
855
+ // Owner left subscription - disconnect non-owners
856
+ this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
857
+ if (hasLeft && !this.canAdmitUsers) { // Non-owner user
858
+ console.log('[TAS DEBUG] Owner left, disconnecting user');
859
+ this.hangUp();
860
+ this.activeModal.close('owner_left');
861
+ }
862
+ }));
357
863
  }
358
864
  initializeCall() {
359
865
  if (this.isReturningFromPip) {
@@ -362,9 +868,22 @@ class TasVideocallComponent {
362
868
  }
363
869
  else if (this.sessionId && this.token) {
364
870
  // New call - connect to session
365
- this.tasService.connectSession(this.sessionId, this.token, 'publisher-container', 'subscriber-container').catch(err => {
871
+ this.tasService
872
+ .connectSession(this.sessionId, this.token, 'publisher-container', 'subscriber-container', this.businessRole)
873
+ .catch((err) => {
366
874
  console.error('Error connecting to video call:', err);
367
875
  });
876
+ // Start status polling with shorter interval (5s) to detect owner leaving/location requests
877
+ if (this.appointmentId) {
878
+ this.tasService.startStatusPolling({
879
+ appointmentId: this.appointmentId,
880
+ tenant: this.tenant,
881
+ businessRole: this.businessRole,
882
+ roomType: undefined,
883
+ entityId: undefined,
884
+ sessionId: this.sessionId
885
+ }, 5000);
886
+ }
368
887
  }
369
888
  }
370
889
  resetVideoPositions() {
@@ -390,8 +909,8 @@ class TasVideocallComponent {
390
909
  modifiers: [
391
910
  interact.modifiers.restrictRect({
392
911
  restriction: 'parent',
393
- endOnly: true
394
- })
912
+ endOnly: true,
913
+ }),
395
914
  ],
396
915
  autoScroll: true,
397
916
  listeners: {
@@ -402,8 +921,8 @@ class TasVideocallComponent {
402
921
  target.style.transform = `translate(${x}px, ${y}px)`;
403
922
  target.setAttribute('data-x', String(x));
404
923
  target.setAttribute('data-y', String(y));
405
- }
406
- }
924
+ },
925
+ },
407
926
  })
408
927
  .resizable({
409
928
  edges: { left: false, right: true, bottom: true, top: false },
@@ -419,26 +938,36 @@ class TasVideocallComponent {
419
938
  target.style.transform = `translate(${x}px, ${y}px)`;
420
939
  target.setAttribute('data-x', String(x));
421
940
  target.setAttribute('data-y', String(y));
422
- }
941
+ },
423
942
  },
424
943
  modifiers: [
425
944
  interact.modifiers.restrictEdges({ outer: 'parent' }),
426
945
  interact.modifiers.restrictSize({ min: { width: 150, height: 100 } }),
427
- interact.modifiers.aspectRatio({ ratio: 'preserve' })
946
+ interact.modifiers.aspectRatio({ ratio: 'preserve' }),
428
947
  ],
429
- inertia: true
948
+ inertia: true,
430
949
  });
431
950
  }
432
951
  }
433
- 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 });
434
- 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"] });
435
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasVideocallComponent, decorators: [{
952
+ 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 });
953
+ 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"] }] });
954
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
436
955
  type: Component,
437
- 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"] }]
956
+ 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"] }]
438
957
  }], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }]; }, propDecorators: { sessionId: [{
439
958
  type: Input
440
959
  }], token: [{
441
960
  type: Input
961
+ }], appointmentId: [{
962
+ type: Input
963
+ }], videoCallId: [{
964
+ type: Input
965
+ }], participantName: [{
966
+ type: Input
967
+ }], tenant: [{
968
+ type: Input
969
+ }], businessRole: [{
970
+ type: Input
442
971
  }], isReturningFromPip: [{
443
972
  type: Input
444
973
  }], publisherContainer: [{
@@ -451,129 +980,195 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
451
980
 
452
981
  var WaitingRoomState;
453
982
  (function (WaitingRoomState) {
454
- WaitingRoomState["IDLE"] = "IDLE";
455
- WaitingRoomState["CREATING_ROOM"] = "CREATING_ROOM";
456
- WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
983
+ WaitingRoomState["CHECKING_STATUS"] = "CHECKING_STATUS";
984
+ WaitingRoomState["WAITING_FOR_OWNER"] = "WAITING_FOR_OWNER";
457
985
  WaitingRoomState["READY"] = "READY";
986
+ WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
458
987
  WaitingRoomState["ERROR"] = "ERROR";
459
988
  })(WaitingRoomState || (WaitingRoomState = {}));
460
989
  class TasWaitingRoomComponent {
461
- constructor(activeModal, tasService, modalService) {
990
+ constructor(activeModal, tasService, modalService, cdr) {
462
991
  this.activeModal = activeModal;
463
992
  this.tasService = tasService;
464
993
  this.modalService = modalService;
465
- // Room configuration inputs
466
- this.appointmentId = 1;
467
- this.product = 'uell';
468
- this.tenantId = '';
469
- this.regularUserIds = [];
470
- this.moderatorUserIds = [];
471
- /** Optional: If provided, skips room creation and goes directly to getting a token */
472
- this.existingSessionId = '';
994
+ this.cdr = cdr;
995
+ // Status endpoint params
996
+ this.roomType = TasRoomType.TAS;
997
+ this.businessRole = TasBusinessRole.USER;
473
998
  // Component state
474
- this.state = WaitingRoomState.IDLE;
999
+ this.state = WaitingRoomState.CHECKING_STATUS;
475
1000
  this.WaitingRoomState = WaitingRoomState; // Expose enum to template
476
1001
  this.errorMessage = '';
477
- /** Whether we have an existing session (skip room creation) */
478
- this.hasExistingSession = false;
479
- // Session data
480
- this.sessionId = '';
1002
+ // Session data from status response
1003
+ this.resolvedSessionId = '';
1004
+ this.resolvedAppointmentId = null;
481
1005
  this.token = '';
482
- this.users = [];
1006
+ this.videoCallId = null;
1007
+ // Subscriptions
483
1008
  this.subscriptions = new Subscription();
484
1009
  this.videoCallModalRef = null;
485
1010
  }
1011
+ /** Whether current user is an owner */
1012
+ get isOwner() {
1013
+ var _a;
1014
+ return ((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.role) === TasUserRole.OWNER;
1015
+ }
486
1016
  ngOnInit() {
487
- this.buildUsersArray();
488
- this.setupViewModeSubscription();
489
- // Check if we have an existing session
490
- if (this.existingSessionId && this.existingSessionId.trim() !== '') {
491
- this.hasExistingSession = true;
492
- this.sessionId = this.existingSessionId;
493
- }
1017
+ console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
1018
+ this.checkStatus();
494
1019
  }
495
1020
  ngOnDestroy() {
1021
+ console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
496
1022
  this.subscriptions.unsubscribe();
1023
+ this.tasService.stopStatusPolling();
497
1024
  }
498
1025
  /**
499
- * Creates the room and fetches the token (new session flow)
1026
+ * Check status to get session info
500
1027
  */
501
- createRoom() {
502
- var _a;
503
- if (!this.tenantId || !((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.name)) {
504
- this.state = WaitingRoomState.ERROR;
505
- this.errorMessage = 'Missing configuration data (tenant or user)';
506
- return;
507
- }
508
- this.state = WaitingRoomState.CREATING_ROOM;
1028
+ checkStatus() {
1029
+ console.log('[TAS DEBUG] checkStatus called with:', {
1030
+ roomType: this.roomType,
1031
+ entityId: this.entityId,
1032
+ tenant: this.tenant,
1033
+ businessRole: this.businessRole,
1034
+ currentUser: this.currentUser,
1035
+ });
1036
+ this.state = WaitingRoomState.CHECKING_STATUS;
509
1037
  this.errorMessage = '';
510
- const body = {
511
- roomType: TasRoomType.TAS,
512
- type: TasSessionType.SPONTANEOUS,
513
- tenant: this.tenantId,
514
- appointmentId: this.appointmentId,
515
- users: this.users,
516
- product: this.product
1038
+ const statusParams = {
1039
+ roomType: this.roomType,
1040
+ entityId: this.entityId,
1041
+ tenant: this.tenant,
1042
+ businessRole: this.businessRole,
517
1043
  };
518
- this.subscriptions.add(this.tasService.createRoom(body).pipe(switchMap(response => {
519
- this.sessionId = response.content.sessionId;
520
- this.state = WaitingRoomState.GETTING_TOKEN;
521
- return this.tasService.generateToken({
522
- sessionId: this.sessionId,
523
- name: this.currentUser.name,
524
- lastname: this.currentUser.lastname,
525
- roleVC: this.currentUser.role
526
- });
527
- })).subscribe({
528
- next: (tokenResponse) => {
529
- this.token = tokenResponse.content.token;
530
- this.state = WaitingRoomState.READY;
1044
+ console.log('[TAS DEBUG] Calling getProxyVideoStatus...');
1045
+ this.subscriptions.add(this.tasService.getProxyVideoStatus(statusParams).subscribe({
1046
+ next: (response) => {
1047
+ const content = response.content;
1048
+ // Store session info from response
1049
+ this.resolvedSessionId = content.sessionId;
1050
+ this.resolvedAppointmentId = content.appointmentId;
1051
+ this.videoCallId = content.videoCallId;
1052
+ console.log('[TAS DEBUG] Status response:', content);
1053
+ // Start polling for status updates
1054
+ this.tasService.startStatusPolling(statusParams);
1055
+ // Subscribe to joinable status
1056
+ this.subscriptions.add(this.tasService.joinable$.subscribe((joinable) => {
1057
+ this.handleJoinableChange(joinable);
1058
+ }));
531
1059
  },
532
1060
  error: (err) => {
533
- console.error('Error creating room or getting token:', err);
1061
+ console.error('[TAS DEBUG] Status check failed:', err);
534
1062
  this.state = WaitingRoomState.ERROR;
535
- this.errorMessage = 'Error creating room. Please try again.';
536
- }
1063
+ this.errorMessage = 'Error checking session status. Please try again.';
1064
+ },
537
1065
  }));
538
1066
  }
539
1067
  /**
540
- * Gets a token for an existing session (existing session flow)
1068
+ * Handle changes to joinable status
541
1069
  */
542
- getTokenForExistingSession() {
543
- var _a;
544
- if (!this.sessionId || !((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.name)) {
1070
+ handleJoinableChange(joinable) {
1071
+ console.log('[TAS DEBUG] handleJoinableChange called', {
1072
+ joinable,
1073
+ currentState: this.state,
1074
+ isOwner: this.isOwner,
1075
+ isBackoffice: this.isBackoffice,
1076
+ resolvedSessionId: this.resolvedSessionId,
1077
+ });
1078
+ // Don't update state if already getting token, ready, or in error
1079
+ if (this.state === WaitingRoomState.GETTING_TOKEN ||
1080
+ this.state === WaitingRoomState.READY ||
1081
+ this.state === WaitingRoomState.ERROR) {
1082
+ console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
1083
+ return;
1084
+ }
1085
+ if (this.isOwner || this.isBackoffice) {
1086
+ console.log('[TAS DEBUG] User is owner/backoffice, calling getTokenForOwner');
1087
+ // Owner/Backoffice: call /start to get token first, then show join button
1088
+ if (this.state === WaitingRoomState.CHECKING_STATUS) {
1089
+ this.getTokenForOwner();
1090
+ }
1091
+ }
1092
+ else {
1093
+ // Non-owner: wait until joinable is true
1094
+ if (joinable) {
1095
+ console.log('[TAS DEBUG] Non-owner: joinable is true, getting token');
1096
+ this.getTokenForOwner(); // Also get token when joinable
1097
+ }
1098
+ else {
1099
+ console.log('[TAS DEBUG] Non-owner: waiting for owner');
1100
+ this.state = WaitingRoomState.WAITING_FOR_OWNER;
1101
+ this.cdr.detectChanges();
1102
+ }
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Check if user has owner/backoffice role
1107
+ */
1108
+ get isBackoffice() {
1109
+ return (this.businessRole === TasBusinessRole.BACKOFFICE ||
1110
+ this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
1111
+ this.businessRole === TasBusinessRole.MANAGER);
1112
+ }
1113
+ /**
1114
+ * Get token for owner/backoffice - call /start endpoint
1115
+ */
1116
+ getTokenForOwner() {
1117
+ if (!this.resolvedSessionId) {
545
1118
  this.state = WaitingRoomState.ERROR;
546
- this.errorMessage = 'Missing session ID or user data';
1119
+ this.errorMessage = 'Session ID not available';
1120
+ this.tasService.stopStatusPolling();
1121
+ this.cdr.detectChanges();
547
1122
  return;
548
1123
  }
549
1124
  this.state = WaitingRoomState.GETTING_TOKEN;
550
1125
  this.errorMessage = '';
551
- this.subscriptions.add(this.tasService.generateToken({
552
- sessionId: this.sessionId,
1126
+ console.log('[TAS DEBUG] Calling /start for session:', this.resolvedSessionId);
1127
+ this.subscriptions.add(this.tasService
1128
+ .startProxyVideoSession({
1129
+ sessionId: this.resolvedSessionId,
553
1130
  name: this.currentUser.name,
554
1131
  lastname: this.currentUser.lastname,
555
- roleVC: this.currentUser.role
556
- }).subscribe({
1132
+ })
1133
+ .subscribe({
557
1134
  next: (tokenResponse) => {
1135
+ var _a;
1136
+ console.log('[TAS DEBUG] Token response:', tokenResponse);
1137
+ // Handle case where HTTP adapter returns error in next instead of error handler
1138
+ if (!((_a = tokenResponse === null || tokenResponse === void 0 ? void 0 : tokenResponse.content) === null || _a === void 0 ? void 0 : _a.token)) {
1139
+ console.error('[TAS DEBUG] Invalid token response:', tokenResponse);
1140
+ this.state = WaitingRoomState.ERROR;
1141
+ this.errorMessage = (tokenResponse === null || tokenResponse === void 0 ? void 0 : tokenResponse.message) || 'Error al iniciar la sesión. Respuesta inválida.';
1142
+ this.tasService.stopStatusPolling();
1143
+ this.cdr.detectChanges();
1144
+ return;
1145
+ }
1146
+ console.log('[TAS DEBUG] Token obtained successfully');
558
1147
  this.token = tokenResponse.content.token;
559
1148
  this.state = WaitingRoomState.READY;
1149
+ this.cdr.detectChanges();
560
1150
  },
561
1151
  error: (err) => {
562
- console.error('Error getting token:', err);
1152
+ var _a;
1153
+ console.error('[TAS DEBUG] /start request failed:', err);
563
1154
  this.state = WaitingRoomState.ERROR;
564
- this.errorMessage = 'Error getting session token. Please try again.';
565
- }
1155
+ this.errorMessage = ((_a = err === null || err === void 0 ? void 0 : err.error) === null || _a === void 0 ? void 0 : _a.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
1156
+ this.tasService.stopStatusPolling();
1157
+ console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
1158
+ this.cdr.detectChanges();
1159
+ },
566
1160
  }));
567
1161
  }
568
1162
  /**
569
- * Joins the video call session
1163
+ * Join the session - token already obtained
570
1164
  */
571
1165
  joinSession() {
572
- if (!this.sessionId || !this.token) {
573
- this.errorMessage = 'Cannot join session. Incomplete data.';
1166
+ if (!this.resolvedSessionId || !this.token) {
1167
+ this.errorMessage = 'Session not ready';
574
1168
  return;
575
1169
  }
576
1170
  // Close waiting room and open video call
1171
+ this.tasService.stopStatusPolling();
577
1172
  this.activeModal.close('joining');
578
1173
  this.openVideoCallModal();
579
1174
  }
@@ -581,85 +1176,53 @@ class TasWaitingRoomComponent {
581
1176
  * Closes the waiting room
582
1177
  */
583
1178
  cancel() {
1179
+ this.tasService.stopStatusPolling();
584
1180
  this.activeModal.dismiss('cancel');
585
1181
  }
586
1182
  /**
587
1183
  * Retry after an error
588
1184
  */
589
1185
  retry() {
590
- this.state = WaitingRoomState.IDLE;
1186
+ this.state = WaitingRoomState.CHECKING_STATUS;
591
1187
  this.errorMessage = '';
592
1188
  this.token = '';
593
- // Only reset sessionId if we don't have an existing one
594
- if (!this.hasExistingSession) {
595
- this.sessionId = '';
596
- }
597
- }
598
- // Private Methods
599
- buildUsersArray() {
600
- this.users = [];
601
- // Add owners from input
602
- this.ownerUserIds.forEach(id => {
603
- this.users.push({ userExternalId: id, rol: TasUserRole.OWNER });
604
- });
605
- // Add regular users from input
606
- this.regularUserIds.forEach(id => {
607
- this.users.push({ userExternalId: id, rol: TasUserRole.USER });
608
- });
609
- // Add moderators from input
610
- this.moderatorUserIds.forEach(id => {
611
- this.users.push({ userExternalId: id, rol: TasUserRole.MODERATOR });
612
- });
613
- }
614
- setupViewModeSubscription() {
615
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
616
- // Re-open video call modal when returning from PiP mode
617
- if (mode === ViewMode.FULLSCREEN &&
618
- this.tasService.isCallActive() &&
619
- !this.videoCallModalRef) {
620
- const sessionId = this.tasService.sessionId;
621
- const token = this.tasService.token;
622
- if (sessionId && token) {
623
- this.openVideoCallModal(true);
624
- }
625
- }
626
- }));
1189
+ this.checkStatus();
627
1190
  }
628
- openVideoCallModal(isReturningFromPip = false) {
1191
+ openVideoCallModal() {
629
1192
  this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
630
1193
  size: 'xl',
631
1194
  windowClass: 'tas-video-modal',
632
1195
  backdrop: 'static',
633
- keyboard: false
1196
+ keyboard: false,
1197
+ });
1198
+ this.videoCallModalRef.componentInstance.sessionId = this.resolvedSessionId;
1199
+ this.videoCallModalRef.componentInstance.token = this.token;
1200
+ this.videoCallModalRef.componentInstance.appointmentId = this.resolvedAppointmentId;
1201
+ this.videoCallModalRef.componentInstance.videoCallId = this.videoCallId;
1202
+ this.videoCallModalRef.componentInstance.tenant = this.tenant;
1203
+ this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
1204
+ this.videoCallModalRef.componentInstance.isReturningFromPip = false;
1205
+ this.videoCallModalRef.result.then(() => {
1206
+ this.videoCallModalRef = null;
1207
+ }, () => {
1208
+ this.videoCallModalRef = null;
634
1209
  });
635
- const sessionIdToUse = this.sessionId || this.tasService.sessionId;
636
- const tokenToUse = this.token || this.tasService.token;
637
- this.videoCallModalRef.componentInstance.sessionId = sessionIdToUse;
638
- this.videoCallModalRef.componentInstance.token = tokenToUse;
639
- this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
640
- this.videoCallModalRef.result.then(() => { this.videoCallModalRef = null; }, () => { this.videoCallModalRef = null; });
641
1210
  }
642
1211
  }
643
- 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 });
644
- 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"] }] });
645
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
1212
+ 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 });
1213
+ 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"] }] });
1214
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
646
1215
  type: Component,
647
- 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"] }]
648
- }], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: i1.NgbModal }]; }, propDecorators: { appointmentId: [{
649
- type: Input
650
- }], product: [{
651
- type: Input
652
- }], tenantId: [{
653
- type: Input
654
- }], currentUser: [{
1216
+ 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"] }]
1217
+ }], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
655
1218
  type: Input
656
- }], ownerUserIds: [{
1219
+ }], entityId: [{
657
1220
  type: Input
658
- }], regularUserIds: [{
1221
+ }], tenant: [{
659
1222
  type: Input
660
- }], moderatorUserIds: [{
1223
+ }], businessRole: [{
661
1224
  type: Input
662
- }], existingSessionId: [{
1225
+ }], currentUser: [{
663
1226
  type: Input
664
1227
  }] } });
665
1228
 
@@ -667,117 +1230,214 @@ class TasButtonComponent {
667
1230
  constructor(modalService, tasService) {
668
1231
  this.modalService = modalService;
669
1232
  this.tasService = tasService;
670
- this.appointmentId = 1;
671
- this.product = "uell";
672
- this.tenantId = "";
673
- this.regularUserIds = [];
674
- this.moderatorUserIds = [];
675
- /** Optional: If provided, skips room creation and goes directly to getting a token */
676
- this.existingSessionId = "";
677
- /** Optional: Custom button text */
678
- this.buttonText = "Iniciar TAS";
1233
+ // Status endpoint params
1234
+ this.roomType = TasRoomType.TAS;
1235
+ this.businessRole = TasBusinessRole.USER;
679
1236
  this.isLoading = false;
1237
+ this.buttonText = 'Iniciar TAS';
1238
+ // Status check state
1239
+ this.isCheckingStatus = false;
1240
+ this.isStatusError = false;
1241
+ this.statusErrorMessage = '';
680
1242
  this.subscriptions = new Subscription();
681
1243
  this.currentModalRef = null;
682
1244
  this.videoCallModalRef = null;
1245
+ this.statusPollingInterval = null;
1246
+ this.STATUS_POLL_INTERVAL_MS = 30000; // 30 seconds
683
1247
  }
684
- ngOnInit() {
685
- if (!this.ownerUserIds || this.ownerUserIds.length !== 1) {
686
- throw new Error('tas-btn: ownerUserIds input is required and must contain exactly one user');
1248
+ /** Whether user is backoffice (or admin/manager) */
1249
+ get isBackoffice() {
1250
+ return (this.businessRole === TasBusinessRole.BACKOFFICE ||
1251
+ this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
1252
+ this.businessRole === TasBusinessRole.MANAGER);
1253
+ }
1254
+ /** Whether the button should be visible */
1255
+ get isVisible() {
1256
+ // Backoffice: always show (disabled if error)
1257
+ // Other roles: hide if status error
1258
+ if (this.isBackoffice) {
1259
+ return true;
687
1260
  }
1261
+ return !this.isStatusError;
1262
+ }
1263
+ /** Whether the button should be disabled */
1264
+ get isDisabled() {
1265
+ return this.isLoading || this.isCheckingStatus || this.isStatusError;
1266
+ }
1267
+ ngOnInit() {
688
1268
  // Subscribe to viewMode to handle PiP return
689
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
690
- // Reopen video call modal when returning from PiP
691
- if (mode === ViewMode.FULLSCREEN &&
692
- this.tasService.isCallActive() &&
693
- !this.videoCallModalRef) {
694
- const sessionId = this.tasService.sessionId;
695
- const token = this.tasService.token;
696
- if (sessionId && token) {
697
- this.openVideoCallModal(true);
698
- }
699
- }
1269
+ this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
700
1270
  // When entering PiP, clear the videoCallModalRef since modal will close
701
1271
  if (mode === ViewMode.PIP) {
702
1272
  this.videoCallModalRef = null;
703
1273
  }
704
1274
  }));
1275
+ // Start status checking
1276
+ this.startStatusPolling();
705
1277
  }
706
1278
  ngOnDestroy() {
707
1279
  this.subscriptions.unsubscribe();
1280
+ this.stopStatusPolling();
1281
+ }
1282
+ /**
1283
+ * Start polling status every 30 seconds
1284
+ */
1285
+ startStatusPolling() {
1286
+ // Initial status check
1287
+ this.checkStatus();
1288
+ // Set up periodic polling
1289
+ this.statusPollingInterval = setInterval(() => {
1290
+ this.checkStatus();
1291
+ }, this.STATUS_POLL_INTERVAL_MS);
1292
+ }
1293
+ /**
1294
+ * Stop status polling
1295
+ */
1296
+ stopStatusPolling() {
1297
+ if (this.statusPollingInterval) {
1298
+ clearInterval(this.statusPollingInterval);
1299
+ this.statusPollingInterval = null;
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Check status endpoint to determine if button should be enabled
1304
+ */
1305
+ checkStatus() {
1306
+ // Skip if required inputs are not available
1307
+ if (!this.tenant || !this.entityId) {
1308
+ console.log('[TAS DEBUG] checkStatus skipped - missing required inputs');
1309
+ return;
1310
+ }
1311
+ this.isCheckingStatus = true;
1312
+ this.statusErrorMessage = '';
1313
+ console.log('[TAS DEBUG] checkStatus called with:', {
1314
+ roomType: this.roomType,
1315
+ entityId: this.entityId,
1316
+ tenant: this.tenant,
1317
+ businessRole: this.businessRole,
1318
+ });
1319
+ this.subscriptions.add(this.tasService.getProxyVideoStatus({
1320
+ roomType: this.roomType,
1321
+ entityId: this.entityId,
1322
+ tenant: this.tenant,
1323
+ businessRole: this.businessRole,
1324
+ }).subscribe({
1325
+ next: (response) => {
1326
+ var _a;
1327
+ // Check if response is actually an error (some HTTP adapters return errors in next)
1328
+ // Also check for undefined/null or missing content
1329
+ const isErrorResponse = !response ||
1330
+ !response.content ||
1331
+ (response === null || response === void 0 ? void 0 : response.ok) === false ||
1332
+ (response === null || response === void 0 ? void 0 : response.status) >= 400 ||
1333
+ (response === null || response === void 0 ? void 0 : response.error) ||
1334
+ (response === null || response === void 0 ? void 0 : response.name) === 'HttpErrorResponse';
1335
+ if (isErrorResponse) {
1336
+ console.error('[TAS DEBUG] Status check returned error in response:', response);
1337
+ this.isCheckingStatus = false;
1338
+ this.isStatusError = true;
1339
+ this.statusErrorMessage = ((_a = response === null || response === void 0 ? void 0 : response.error) === null || _a === void 0 ? void 0 : _a.message) || (response === null || response === void 0 ? void 0 : response.message) || 'Error checking status';
1340
+ }
1341
+ else {
1342
+ console.log('[TAS DEBUG] Status check successful:', response);
1343
+ this.isCheckingStatus = false;
1344
+ this.isStatusError = false;
1345
+ this.statusErrorMessage = '';
1346
+ }
1347
+ },
1348
+ error: (err) => {
1349
+ var _a;
1350
+ console.error('[TAS DEBUG] Status check failed:', err);
1351
+ this.isCheckingStatus = false;
1352
+ this.isStatusError = true;
1353
+ this.statusErrorMessage = ((_a = err === null || err === void 0 ? void 0 : err.error) === null || _a === void 0 ? void 0 : _a.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Error checking status';
1354
+ },
1355
+ }));
708
1356
  }
709
1357
  onClick() {
710
1358
  var _a;
711
- if (!this.tenantId || !((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.name)) {
712
- console.error("Tenant ID or current user not available");
1359
+ console.log('[TAS DEBUG] onClick called');
1360
+ console.log('[TAS DEBUG] Inputs:', {
1361
+ tenant: this.tenant,
1362
+ entityId: this.entityId,
1363
+ roomType: this.roomType,
1364
+ businessRole: this.businessRole,
1365
+ currentUser: this.currentUser,
1366
+ });
1367
+ if (!this.tenant || !((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.name)) {
1368
+ console.error('[TAS DEBUG] Tenant or current user not available');
713
1369
  return;
714
1370
  }
1371
+ if (!this.entityId) {
1372
+ console.error('[TAS DEBUG] entityId is required');
1373
+ return;
1374
+ }
1375
+ console.log('[TAS DEBUG] Validation passed, opening waiting room modal');
715
1376
  this.openWaitingRoomModal();
716
1377
  }
717
1378
  openWaitingRoomModal() {
718
1379
  this.currentModalRef = this.modalService.open(TasWaitingRoomComponent, {
719
- size: "lg",
720
- windowClass: "tas-waiting-room-modal",
721
- backdrop: "static",
1380
+ size: 'lg',
1381
+ windowClass: 'tas-waiting-room-modal',
1382
+ backdrop: 'static',
722
1383
  keyboard: false,
723
- centered: true
1384
+ centered: true,
724
1385
  });
725
1386
  // Pass all necessary inputs to the waiting room component
726
- this.currentModalRef.componentInstance.appointmentId = this.appointmentId;
727
- this.currentModalRef.componentInstance.product = this.product;
728
- this.currentModalRef.componentInstance.tenantId = this.tenantId;
1387
+ this.currentModalRef.componentInstance.roomType = this.roomType;
1388
+ this.currentModalRef.componentInstance.entityId = this.entityId;
1389
+ this.currentModalRef.componentInstance.tenant = this.tenant;
1390
+ this.currentModalRef.componentInstance.businessRole = this.businessRole;
729
1391
  this.currentModalRef.componentInstance.currentUser = this.currentUser;
730
- this.currentModalRef.componentInstance.ownerUserIds = this.ownerUserIds;
731
- this.currentModalRef.componentInstance.regularUserIds = this.regularUserIds;
732
- this.currentModalRef.componentInstance.moderatorUserIds = this.moderatorUserIds;
733
- // Pass existing session ID if provided
734
- this.currentModalRef.componentInstance.existingSessionId = this.existingSessionId;
735
- this.currentModalRef.result.then(() => { this.currentModalRef = null; }, () => { this.currentModalRef = null; });
1392
+ this.currentModalRef.result.then(() => {
1393
+ this.currentModalRef = null;
1394
+ }, () => {
1395
+ this.currentModalRef = null;
1396
+ });
736
1397
  }
737
1398
  openVideoCallModal(isReturningFromPip = false) {
738
1399
  this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
739
1400
  size: 'xl',
740
1401
  windowClass: 'tas-video-modal',
741
1402
  backdrop: 'static',
742
- keyboard: false
1403
+ keyboard: false,
743
1404
  });
744
1405
  this.videoCallModalRef.componentInstance.sessionId = this.tasService.sessionId;
745
1406
  this.videoCallModalRef.componentInstance.token = this.tasService.token;
1407
+ this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
746
1408
  this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
747
- this.videoCallModalRef.result.then(() => { this.videoCallModalRef = null; }, () => { this.videoCallModalRef = null; });
1409
+ this.videoCallModalRef.result.then(() => {
1410
+ this.videoCallModalRef = null;
1411
+ }, () => {
1412
+ this.videoCallModalRef = null;
1413
+ });
748
1414
  }
749
1415
  }
750
- 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 });
751
- 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"] }] });
752
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasButtonComponent, decorators: [{
1416
+ 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 });
1417
+ 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"] }] });
1418
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
753
1419
  type: Component,
754
- 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"] }]
755
- }], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: { appointmentId: [{
756
- type: Input
757
- }], product: [{
758
- type: Input
759
- }], tenantId: [{
1420
+ 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"] }]
1421
+ }], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: { roomType: [{
760
1422
  type: Input
761
- }], currentUser: [{
762
- type: Input
763
- }], ownerUserIds: [{
1423
+ }], entityId: [{
764
1424
  type: Input
765
- }], regularUserIds: [{
1425
+ }], tenant: [{
766
1426
  type: Input
767
- }], moderatorUserIds: [{
1427
+ }], businessRole: [{
768
1428
  type: Input
769
- }], existingSessionId: [{
770
- type: Input
771
- }], buttonText: [{
1429
+ }], currentUser: [{
772
1430
  type: Input
773
1431
  }] } });
774
1432
 
775
1433
  class TasFloatingCallComponent {
776
- constructor(tasService) {
1434
+ constructor(tasService, modalService) {
777
1435
  this.tasService = tasService;
1436
+ this.modalService = modalService;
778
1437
  this.isVisible = false;
779
1438
  this.isMuted = false;
780
1439
  this.subscriptions = new Subscription();
1440
+ this.videoCallModalRef = null;
781
1441
  // Margin from screen edges (in pixels)
782
1442
  this.PIP_MARGIN = 20;
783
1443
  }
@@ -790,6 +1450,7 @@ class TasFloatingCallComponent {
790
1450
  }
791
1451
  // Public Methods
792
1452
  onExpand() {
1453
+ this.openVideoCallModal(true);
793
1454
  this.tasService.exitPipMode();
794
1455
  }
795
1456
  onHangUp() {
@@ -801,23 +1462,45 @@ class TasFloatingCallComponent {
801
1462
  // Private Methods
802
1463
  setupSubscriptions() {
803
1464
  // Call state subscription
804
- this.subscriptions.add(this.tasService.callState$.subscribe(state => {
1465
+ this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
805
1466
  if (state === CallState.DISCONNECTED) {
806
1467
  this.isVisible = false;
807
1468
  }
808
1469
  }));
809
1470
  // View mode subscription
810
- this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
1471
+ this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
811
1472
  this.isVisible = mode === ViewMode.PIP && this.tasService.isCallActive();
812
1473
  if (this.isVisible) {
813
1474
  setTimeout(() => this.initInteract(), 100);
1475
+ // Clear modal ref if we enter PiP mode (modal closes itself)
1476
+ this.videoCallModalRef = null;
814
1477
  }
815
1478
  }));
816
1479
  // Mute state subscription
817
- this.subscriptions.add(this.tasService.isMuted$.subscribe(muted => {
1480
+ this.subscriptions.add(this.tasService.isMuted$.subscribe((muted) => {
818
1481
  this.isMuted = muted;
819
1482
  }));
820
1483
  }
1484
+ openVideoCallModal(isReturningFromPip = false) {
1485
+ if (this.videoCallModalRef) {
1486
+ return;
1487
+ }
1488
+ this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
1489
+ size: 'xl',
1490
+ windowClass: 'tas-video-modal',
1491
+ backdrop: 'static',
1492
+ keyboard: false,
1493
+ });
1494
+ this.videoCallModalRef.componentInstance.sessionId = this.tasService.sessionId;
1495
+ this.videoCallModalRef.componentInstance.token = this.tasService.token;
1496
+ this.videoCallModalRef.componentInstance.businessRole = this.tasService.businessRole;
1497
+ this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
1498
+ this.videoCallModalRef.result.then(() => {
1499
+ this.videoCallModalRef = null;
1500
+ }, () => {
1501
+ this.videoCallModalRef = null;
1502
+ });
1503
+ }
821
1504
  initInteract() {
822
1505
  interact('.tas-floating-container').unset();
823
1506
  // Create restriction area with margin
@@ -828,17 +1511,15 @@ class TasFloatingCallComponent {
828
1511
  left: margin,
829
1512
  top: margin,
830
1513
  right: window.innerWidth - margin,
831
- bottom: window.innerHeight - margin
1514
+ bottom: window.innerHeight - margin,
832
1515
  };
833
1516
  },
834
- elementRect: { left: 0, right: 1, top: 0, bottom: 1 }
1517
+ elementRect: { left: 0, right: 1, top: 0, bottom: 1 },
835
1518
  };
836
1519
  interact('.tas-floating-container')
837
1520
  .draggable({
838
1521
  inertia: true,
839
- modifiers: [
840
- interact.modifiers.restrict(restrictToBodyWithMargin)
841
- ],
1522
+ modifiers: [interact.modifiers.restrict(restrictToBodyWithMargin)],
842
1523
  autoScroll: false,
843
1524
  listeners: {
844
1525
  move: (event) => {
@@ -848,8 +1529,8 @@ class TasFloatingCallComponent {
848
1529
  target.style.transform = `translate(${x}px, ${y}px)`;
849
1530
  target.setAttribute('data-x', String(x));
850
1531
  target.setAttribute('data-y', String(y));
851
- }
852
- }
1532
+ },
1533
+ },
853
1534
  })
854
1535
  .resizable({
855
1536
  edges: { left: false, right: true, bottom: true, top: false },
@@ -867,7 +1548,7 @@ class TasFloatingCallComponent {
867
1548
  target.style.transform = `translate(${x}px, ${y}px)`;
868
1549
  target.setAttribute('data-x', String(x));
869
1550
  target.setAttribute('data-y', String(y));
870
- }
1551
+ },
871
1552
  },
872
1553
  modifiers: [
873
1554
  interact.modifiers.restrictEdges({
@@ -875,25 +1556,25 @@ class TasFloatingCallComponent {
875
1556
  left: margin,
876
1557
  top: margin,
877
1558
  right: window.innerWidth - margin,
878
- bottom: window.innerHeight - margin
879
- }
1559
+ bottom: window.innerHeight - margin,
1560
+ },
880
1561
  }),
881
1562
  interact.modifiers.restrictSize({
882
1563
  min: { width: 200, height: 130 },
883
- max: { width: 500, height: 350 }
1564
+ max: { width: 500, height: 350 },
884
1565
  }),
885
- interact.modifiers.aspectRatio({ ratio: 'preserve' })
1566
+ interact.modifiers.aspectRatio({ ratio: 'preserve' }),
886
1567
  ],
887
- inertia: true
1568
+ inertia: true,
888
1569
  });
889
1570
  }
890
1571
  }
891
- TasFloatingCallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasFloatingCallComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
892
- 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"] });
893
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasFloatingCallComponent, decorators: [{
1572
+ 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 });
1573
+ 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"] });
1574
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFloatingCallComponent, decorators: [{
894
1575
  type: Component,
895
- 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"] }]
896
- }], ctorParameters: function () { return [{ type: TasService }]; } });
1576
+ 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"] }]
1577
+ }], ctorParameters: function () { return [{ type: TasService }, { type: i1.NgbModal }]; } });
897
1578
 
898
1579
  class TasUellSdkModule {
899
1580
  /**
@@ -931,40 +1612,40 @@ class TasUellSdkModule {
931
1612
  providers: [
932
1613
  { provide: TAS_CONFIG, useValue: options.config },
933
1614
  { provide: TAS_HTTP_CLIENT, useClass: options.httpClient },
934
- TasService
935
- ]
1615
+ TasService,
1616
+ ],
936
1617
  };
937
1618
  }
938
1619
  }
939
- TasUellSdkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
940
- TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, declarations: [TasButtonComponent,
1620
+ TasUellSdkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1621
+ TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, declarations: [TasButtonComponent,
941
1622
  TasVideocallComponent,
942
1623
  TasFloatingCallComponent,
943
- TasWaitingRoomComponent], imports: [CommonModule], exports: [TasButtonComponent,
1624
+ TasWaitingRoomComponent,
1625
+ TasAvatarComponent], imports: [CommonModule, FormsModule], exports: [TasButtonComponent,
944
1626
  TasVideocallComponent,
945
1627
  TasFloatingCallComponent,
946
- TasWaitingRoomComponent] });
947
- TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, imports: [[
948
- CommonModule,
949
- ]] });
950
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, decorators: [{
1628
+ TasWaitingRoomComponent,
1629
+ TasAvatarComponent] });
1630
+ TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, imports: [[CommonModule, FormsModule]] });
1631
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, decorators: [{
951
1632
  type: NgModule,
952
1633
  args: [{
953
1634
  declarations: [
954
1635
  TasButtonComponent,
955
1636
  TasVideocallComponent,
956
1637
  TasFloatingCallComponent,
957
- TasWaitingRoomComponent
958
- ],
959
- imports: [
960
- CommonModule,
1638
+ TasWaitingRoomComponent,
1639
+ TasAvatarComponent,
961
1640
  ],
1641
+ imports: [CommonModule, FormsModule],
962
1642
  exports: [
963
1643
  TasButtonComponent,
964
1644
  TasVideocallComponent,
965
1645
  TasFloatingCallComponent,
966
- TasWaitingRoomComponent
967
- ]
1646
+ TasWaitingRoomComponent,
1647
+ TasAvatarComponent,
1648
+ ],
968
1649
  }]
969
1650
  }] });
970
1651
 
@@ -976,5 +1657,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
976
1657
  * Generated bundle index. Do not edit.
977
1658
  */
978
1659
 
979
- export { CallState, TAS_CONFIG, TAS_HTTP_CLIENT, TasButtonComponent, TasFloatingCallComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, ViewMode, WaitingRoomState };
1660
+ export { CallState, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
980
1661
  //# sourceMappingURL=tas-uell-sdk.mjs.map