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