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