tas-uell-sdk 0.0.5 → 0.1.0
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.
- package/README.md +161 -51
- package/esm2020/lib/components/tas-avatar/tas-avatar.component.mjs +75 -0
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +146 -61
- package/esm2020/lib/components/tas-floating-call/tas-floating-call.component.mjs +48 -23
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +109 -0
- package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +217 -20
- package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +226 -160
- package/esm2020/lib/config/tas.config.mjs +1 -1
- package/esm2020/lib/interfaces/tas.interfaces.mjs +45 -2
- package/esm2020/lib/services/geolocation.service.mjs +56 -0
- package/esm2020/lib/services/tas.service.mjs +400 -34
- package/esm2020/lib/tas-uell-sdk.module.mjs +25 -21
- package/esm2020/public-api.mjs +4 -1
- package/fesm2015/tas-uell-sdk.mjs +1323 -302
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +1307 -300
- package/fesm2020/tas-uell-sdk.mjs.map +1 -1
- package/lib/components/tas-avatar/tas-avatar.component.d.ts +9 -0
- package/lib/components/tas-btn/tas-btn.component.d.ts +35 -15
- package/lib/components/tas-floating-call/tas-floating-call.component.d.ts +5 -1
- package/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.d.ts +33 -0
- package/lib/components/tas-videocall/tas-videocall.component.d.ts +49 -3
- package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +50 -35
- package/lib/config/tas.config.d.ts +7 -0
- package/lib/interfaces/tas.interfaces.d.ts +127 -35
- package/lib/services/geolocation.service.d.ts +24 -0
- package/lib/services/tas.service.d.ts +98 -9
- package/lib/tas-uell-sdk.module.d.ts +6 -3
- package/package.json +1 -1
- package/public-api.d.ts +3 -0
- package/src/lib/styles/tas-global.scss +27 -28
- package/INSTALL_AND_TEST.md +0 -427
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Inject, Component, Input, ViewChild, NgModule } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, Inject, Component, ChangeDetectionStrategy, Input, ViewChild, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
4
|
-
import { map, catchError
|
|
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
|
-
import
|
|
8
|
+
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
9
|
+
import * as i4 from '@angular/common';
|
|
9
10
|
import { CommonModule } from '@angular/common';
|
|
10
|
-
import * as i4 from '@angular/forms';
|
|
11
11
|
import { FormsModule } from '@angular/forms';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -19,11 +19,13 @@ const TAS_CONFIG = new InjectionToken('TAS_CONFIG');
|
|
|
19
19
|
*/
|
|
20
20
|
const TAS_HTTP_CLIENT = new InjectionToken('TAS_HTTP_CLIENT');
|
|
21
21
|
|
|
22
|
+
// Enums
|
|
22
23
|
var TasRoomType;
|
|
23
24
|
(function (TasRoomType) {
|
|
24
25
|
TasRoomType["TAS"] = "TAS";
|
|
25
26
|
TasRoomType["JM"] = "JM";
|
|
26
27
|
TasRoomType["WEBINAR"] = "WEBINAR";
|
|
28
|
+
TasRoomType["WELLNESS_MANAGER"] = "WELLNESS_MANAGER";
|
|
27
29
|
})(TasRoomType || (TasRoomType = {}));
|
|
28
30
|
var TasSessionType;
|
|
29
31
|
(function (TasSessionType) {
|
|
@@ -36,7 +38,42 @@ var TasUserRole;
|
|
|
36
38
|
TasUserRole["USER"] = "USER";
|
|
37
39
|
TasUserRole["MODERATOR"] = "MODERATOR";
|
|
38
40
|
})(TasUserRole || (TasUserRole = {}));
|
|
39
|
-
|
|
41
|
+
var TasBusinessRole;
|
|
42
|
+
(function (TasBusinessRole) {
|
|
43
|
+
TasBusinessRole["ADMIN_MANAGER"] = "ADMIN_MANAGER";
|
|
44
|
+
TasBusinessRole["MANAGER"] = "MANAGER";
|
|
45
|
+
TasBusinessRole["BACKOFFICE"] = "BACKOFFICE";
|
|
46
|
+
TasBusinessRole["USER"] = "USER";
|
|
47
|
+
})(TasBusinessRole || (TasBusinessRole = {}));
|
|
48
|
+
var VideoSessionStatus;
|
|
49
|
+
(function (VideoSessionStatus) {
|
|
50
|
+
VideoSessionStatus["ACTIVE"] = "ACTIVE";
|
|
51
|
+
VideoSessionStatus["INACTIVE"] = "INACTIVE";
|
|
52
|
+
VideoSessionStatus["PENDING"] = "PENDING";
|
|
53
|
+
VideoSessionStatus["SCHEDULED"] = "SCHEDULED";
|
|
54
|
+
})(VideoSessionStatus || (VideoSessionStatus = {}));
|
|
55
|
+
var UserStatus;
|
|
56
|
+
(function (UserStatus) {
|
|
57
|
+
UserStatus["ACTIVE"] = "ACTIVE";
|
|
58
|
+
UserStatus["INACTIVE"] = "INACTIVE";
|
|
59
|
+
UserStatus["ASSIGNED"] = "ASSIGNED";
|
|
60
|
+
})(UserStatus || (UserStatus = {}));
|
|
61
|
+
var UserCallAction;
|
|
62
|
+
(function (UserCallAction) {
|
|
63
|
+
UserCallAction["WAITING_ROOM_ENTER"] = "WAITING_ROOM_ENTER";
|
|
64
|
+
UserCallAction["WAITING_ROOM_LEAVE"] = "WAITING_ROOM_LEAVE";
|
|
65
|
+
UserCallAction["BAN"] = "BAN";
|
|
66
|
+
UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
|
|
67
|
+
UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
|
|
68
|
+
UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
|
|
69
|
+
})(UserCallAction || (UserCallAction = {}));
|
|
70
|
+
var RoomUserStatus;
|
|
71
|
+
(function (RoomUserStatus) {
|
|
72
|
+
RoomUserStatus["ASSIGNED"] = "ASSIGNED";
|
|
73
|
+
RoomUserStatus["WAITING"] = "WAITING";
|
|
74
|
+
RoomUserStatus["JOINED"] = "JOINED";
|
|
75
|
+
RoomUserStatus["FINISHED"] = "FINISHED";
|
|
76
|
+
})(RoomUserStatus || (RoomUserStatus = {}));
|
|
40
77
|
var CallState;
|
|
41
78
|
(function (CallState) {
|
|
42
79
|
CallState["IDLE"] = "IDLE";
|
|
@@ -50,6 +87,12 @@ var ViewMode;
|
|
|
50
87
|
ViewMode["FULLSCREEN"] = "FULLSCREEN";
|
|
51
88
|
ViewMode["PIP"] = "PIP";
|
|
52
89
|
})(ViewMode || (ViewMode = {}));
|
|
90
|
+
// Appointment types
|
|
91
|
+
var AppointmentStatus;
|
|
92
|
+
(function (AppointmentStatus) {
|
|
93
|
+
AppointmentStatus["CONFIRMED"] = "CONFIRMED";
|
|
94
|
+
AppointmentStatus["CANCELLED"] = "CANCELLED";
|
|
95
|
+
})(AppointmentStatus || (AppointmentStatus = {}));
|
|
53
96
|
|
|
54
97
|
class TasService {
|
|
55
98
|
constructor(httpClient, config) {
|
|
@@ -67,6 +110,86 @@ class TasService {
|
|
|
67
110
|
// Session info for PiP mode restoration
|
|
68
111
|
this.currentSessionId = null;
|
|
69
112
|
this.currentToken = null;
|
|
113
|
+
this.STORAGE_KEY = 'tas_session_state';
|
|
114
|
+
this.DISCONNECTED_FLAG_KEY = 'tas_session_disconnected';
|
|
115
|
+
this.isFinishingSession = false;
|
|
116
|
+
// Proxy-video circuit state
|
|
117
|
+
this.proxyVideoSessionId = null;
|
|
118
|
+
this.proxyVideoToken = null;
|
|
119
|
+
this.currentBusinessRole = TasBusinessRole.USER;
|
|
120
|
+
// Waiting room and status polling
|
|
121
|
+
this.waitingRoomUsersSubject = new BehaviorSubject([]);
|
|
122
|
+
this.waitingRoomUsers$ = this.waitingRoomUsersSubject.asObservable();
|
|
123
|
+
this.ownerHasJoinedSubject = new BehaviorSubject(false);
|
|
124
|
+
this.ownerHasJoined$ = this.ownerHasJoinedSubject.asObservable();
|
|
125
|
+
// Observable that emits true when owner was present but then left
|
|
126
|
+
this.ownerHasLeftSubject = new BehaviorSubject(false);
|
|
127
|
+
this.ownerHasLeft$ = this.ownerHasLeftSubject.asObservable();
|
|
128
|
+
this.joinableSubject = new BehaviorSubject(false);
|
|
129
|
+
this.joinable$ = this.joinableSubject.asObservable();
|
|
130
|
+
// Observable for when backend requests geolocation activation
|
|
131
|
+
this.activateGeoSubject = new BehaviorSubject(false);
|
|
132
|
+
this.activateGeo$ = this.activateGeoSubject.asObservable();
|
|
133
|
+
// Observable for when geo request is active (owner waiting for user response)
|
|
134
|
+
this.geoRequestActiveSubject = new BehaviorSubject(false);
|
|
135
|
+
this.geoRequestActive$ = this.geoRequestActiveSubject.asObservable();
|
|
136
|
+
// Observable for when all geo has been granted
|
|
137
|
+
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
138
|
+
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
139
|
+
this.statusPollingInterval = null;
|
|
140
|
+
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
141
|
+
this.wasOwnerPresent = false;
|
|
142
|
+
// Current call context
|
|
143
|
+
this.currentAppointmentId = null;
|
|
144
|
+
this.currentVideoCallId = null;
|
|
145
|
+
this.currentTenant = null;
|
|
146
|
+
}
|
|
147
|
+
// ... (Getters and other methods remain unchanged)
|
|
148
|
+
/**
|
|
149
|
+
* Start automatic status polling for the current session.
|
|
150
|
+
* Status is polled every intervalMs (default 30s).
|
|
151
|
+
*/
|
|
152
|
+
startStatusPolling(params, intervalMs = this.DEFAULT_POLL_INTERVAL_MS) {
|
|
153
|
+
this.stopStatusPolling(); // Clear any existing polling
|
|
154
|
+
// Store context for polling
|
|
155
|
+
if (params.sessionId) {
|
|
156
|
+
this.currentSessionId = params.sessionId;
|
|
157
|
+
}
|
|
158
|
+
if (params.appointmentId) {
|
|
159
|
+
this.currentAppointmentId = params.appointmentId;
|
|
160
|
+
}
|
|
161
|
+
if (params.tenant) {
|
|
162
|
+
this.currentTenant = params.tenant;
|
|
163
|
+
}
|
|
164
|
+
console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
|
|
165
|
+
// Initial status fetch
|
|
166
|
+
this.fetchAndProcessStatus(params);
|
|
167
|
+
// Set up periodic polling
|
|
168
|
+
this.statusPollingInterval = setInterval(() => {
|
|
169
|
+
this.fetchAndProcessStatus(params);
|
|
170
|
+
}, intervalMs);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Stop automatic status polling.
|
|
174
|
+
*/
|
|
175
|
+
stopStatusPolling() {
|
|
176
|
+
if (this.statusPollingInterval) {
|
|
177
|
+
clearInterval(this.statusPollingInterval);
|
|
178
|
+
this.statusPollingInterval = null;
|
|
179
|
+
}
|
|
180
|
+
// Do NOT reset waiting room state here, as other components might observe it
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Reset polling state (waiting room, owner status, etc)
|
|
184
|
+
* Call this when completely exiting the flow
|
|
185
|
+
*/
|
|
186
|
+
resetPollingState() {
|
|
187
|
+
this.stopStatusPolling();
|
|
188
|
+
this.waitingRoomUsersSubject.next([]);
|
|
189
|
+
this.ownerHasJoinedSubject.next(false);
|
|
190
|
+
this.ownerHasLeftSubject.next(false);
|
|
191
|
+
this.joinableSubject.next(false);
|
|
192
|
+
this.wasOwnerPresent = false;
|
|
70
193
|
}
|
|
71
194
|
// Getters
|
|
72
195
|
get currentSession() {
|
|
@@ -84,18 +207,36 @@ class TasService {
|
|
|
84
207
|
get token() {
|
|
85
208
|
return this.currentToken;
|
|
86
209
|
}
|
|
210
|
+
get proxySessionId() {
|
|
211
|
+
return this.proxyVideoSessionId;
|
|
212
|
+
}
|
|
213
|
+
get proxyToken() {
|
|
214
|
+
return this.proxyVideoToken;
|
|
215
|
+
}
|
|
216
|
+
get businessRole() {
|
|
217
|
+
return this.currentBusinessRole;
|
|
218
|
+
}
|
|
87
219
|
get isMuted() {
|
|
88
220
|
return this.isMutedSubject.getValue();
|
|
89
221
|
}
|
|
90
222
|
// View Mode Methods
|
|
91
223
|
setViewMode(mode) {
|
|
92
224
|
this.viewModeSubject.next(mode);
|
|
225
|
+
if (this.currentSessionId && this.currentToken) {
|
|
226
|
+
this.saveSessionState(this.currentSessionId, this.currentToken, mode, this.currentBusinessRole);
|
|
227
|
+
}
|
|
93
228
|
}
|
|
94
229
|
enterPipMode() {
|
|
95
230
|
this.viewModeSubject.next(ViewMode.PIP);
|
|
231
|
+
if (this.currentSessionId && this.currentToken) {
|
|
232
|
+
this.saveSessionState(this.currentSessionId, this.currentToken, ViewMode.PIP, this.currentBusinessRole);
|
|
233
|
+
}
|
|
96
234
|
}
|
|
97
235
|
exitPipMode() {
|
|
98
236
|
this.viewModeSubject.next(ViewMode.FULLSCREEN);
|
|
237
|
+
if (this.currentSessionId && this.currentToken) {
|
|
238
|
+
this.saveSessionState(this.currentSessionId, this.currentToken, ViewMode.FULLSCREEN, this.currentBusinessRole);
|
|
239
|
+
}
|
|
99
240
|
}
|
|
100
241
|
isPipMode() {
|
|
101
242
|
return this.viewModeSubject.getValue() === ViewMode.PIP;
|
|
@@ -122,7 +263,17 @@ class TasService {
|
|
|
122
263
|
}
|
|
123
264
|
}
|
|
124
265
|
// Session Management
|
|
125
|
-
disconnectSession() {
|
|
266
|
+
disconnectSession(clearStorage = true) {
|
|
267
|
+
console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
|
|
268
|
+
// Call finishSession before disconnecting if we have a sessionId
|
|
269
|
+
const sessionIdToFinish = this.currentSessionId;
|
|
270
|
+
// Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
|
|
271
|
+
if (clearStorage) {
|
|
272
|
+
this.clearSessionState();
|
|
273
|
+
// Set a flag to indicate this session was intentionally disconnected
|
|
274
|
+
// This prevents reconnection on page reload
|
|
275
|
+
sessionStorage.setItem(this.DISCONNECTED_FLAG_KEY, 'true');
|
|
276
|
+
}
|
|
126
277
|
if (this.session) {
|
|
127
278
|
this.session.disconnect();
|
|
128
279
|
this.session = null;
|
|
@@ -131,49 +282,288 @@ class TasService {
|
|
|
131
282
|
this.subscribers = [];
|
|
132
283
|
this.currentSessionId = null;
|
|
133
284
|
this.currentToken = null;
|
|
285
|
+
this.proxyVideoSessionId = null;
|
|
286
|
+
this.proxyVideoToken = null;
|
|
134
287
|
this.isMutedSubject.next(false); // Reset mute state
|
|
135
288
|
this.viewModeSubject.next(ViewMode.FULLSCREEN);
|
|
136
289
|
this.callStateSubject.next(CallState.DISCONNECTED);
|
|
290
|
+
// Call proxy finish after disconnecting (only if not already finishing)
|
|
291
|
+
if (sessionIdToFinish && !this.isFinishingSession) {
|
|
292
|
+
this.isFinishingSession = true;
|
|
293
|
+
this.finishProxyVideoSession({
|
|
294
|
+
sessionId: sessionIdToFinish,
|
|
295
|
+
businessRole: this.currentBusinessRole,
|
|
296
|
+
}).subscribe({
|
|
297
|
+
next: (response) => {
|
|
298
|
+
console.log('[TAS DEBUG] Session finished successfully:', response);
|
|
299
|
+
this.isFinishingSession = false;
|
|
300
|
+
},
|
|
301
|
+
error: (error) => {
|
|
302
|
+
console.error('[TAS DEBUG] Error finishing session:', error);
|
|
303
|
+
this.isFinishingSession = false;
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
137
307
|
}
|
|
138
308
|
isCallActive() {
|
|
139
309
|
return this.callStateSubject.getValue() === CallState.CONNECTED;
|
|
140
310
|
}
|
|
311
|
+
// State Persistence
|
|
312
|
+
saveSessionState(sessionId, token, viewMode, businessRole = TasBusinessRole.USER) {
|
|
313
|
+
const state = {
|
|
314
|
+
sessionId,
|
|
315
|
+
token,
|
|
316
|
+
viewMode,
|
|
317
|
+
businessRole,
|
|
318
|
+
};
|
|
319
|
+
sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
|
320
|
+
}
|
|
321
|
+
clearSessionState() {
|
|
322
|
+
sessionStorage.removeItem(this.STORAGE_KEY);
|
|
323
|
+
// Also clear the disconnected flag when clearing state
|
|
324
|
+
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
325
|
+
}
|
|
326
|
+
canRestoreSession() {
|
|
327
|
+
return !!sessionStorage.getItem(this.STORAGE_KEY);
|
|
328
|
+
}
|
|
329
|
+
restoreSession(containerId) {
|
|
330
|
+
const savedState = sessionStorage.getItem(this.STORAGE_KEY);
|
|
331
|
+
if (!savedState)
|
|
332
|
+
return;
|
|
333
|
+
// Check if this session was intentionally disconnected
|
|
334
|
+
// If so, don't restore it on page reload
|
|
335
|
+
const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
|
|
336
|
+
if (wasDisconnected === 'true') {
|
|
337
|
+
console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
|
|
338
|
+
this.clearSessionState();
|
|
339
|
+
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Don't restore if we're already disconnected or if there's no active session
|
|
343
|
+
// This prevents restoring sessions that were properly disconnected
|
|
344
|
+
if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
|
|
345
|
+
console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
|
|
346
|
+
this.clearSessionState();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const state = JSON.parse(savedState);
|
|
351
|
+
if (state.sessionId && state.token) {
|
|
352
|
+
console.log('[TAS DEBUG] Restoring session from storage');
|
|
353
|
+
// Force PiP mode for restoration to ensure UI consistency
|
|
354
|
+
this.viewModeSubject.next(ViewMode.PIP);
|
|
355
|
+
if (state.businessRole) {
|
|
356
|
+
this.currentBusinessRole = state.businessRole;
|
|
357
|
+
}
|
|
358
|
+
this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
|
|
359
|
+
containerId, this.currentBusinessRole)
|
|
360
|
+
.then(() => {
|
|
361
|
+
console.log('[TAS DEBUG] Session restored successfully');
|
|
362
|
+
// Clear the disconnected flag if restoration succeeds
|
|
363
|
+
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
364
|
+
})
|
|
365
|
+
.catch((err) => {
|
|
366
|
+
console.error('[TAS DEBUG] Failed to restore session:', err);
|
|
367
|
+
this.clearSessionState(); // Clear bad state
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
console.error('[TAS DEBUG] Error parsing saved session state', e);
|
|
373
|
+
this.clearSessionState();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
141
376
|
// API Methods
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
377
|
+
/**
|
|
378
|
+
* PROXY circuit token: /v2/proxy/video/start
|
|
379
|
+
*/
|
|
380
|
+
startProxyVideoSession(payload) {
|
|
381
|
+
return this.httpClient
|
|
382
|
+
.post('v2/proxy/video/start', {
|
|
383
|
+
body: payload,
|
|
384
|
+
headers: {},
|
|
385
|
+
})
|
|
386
|
+
.pipe(map((response) => response), catchError((error) => {
|
|
387
|
+
console.error('TAS Service: startProxyVideoSession failed', error);
|
|
145
388
|
throw error;
|
|
146
389
|
}));
|
|
147
390
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
391
|
+
/**
|
|
392
|
+
* PROXY circuit status: /v2/proxy/video/status
|
|
393
|
+
*/
|
|
394
|
+
getProxyVideoStatus(payload) {
|
|
395
|
+
return this.httpClient
|
|
396
|
+
.post('v2/proxy/video/status', {
|
|
397
|
+
body: payload,
|
|
398
|
+
headers: {},
|
|
399
|
+
})
|
|
400
|
+
.pipe(map((response) => response), catchError((error) => {
|
|
401
|
+
console.error('TAS Service: getProxyVideoStatus failed', error);
|
|
151
402
|
throw error;
|
|
152
403
|
}));
|
|
153
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* PROXY circuit user modification: /v2/proxy/video/user/modify
|
|
407
|
+
*/
|
|
408
|
+
modifyProxyVideoUser(payload) {
|
|
409
|
+
return this.httpClient
|
|
410
|
+
.patch('v2/proxy/video/user/modify', {
|
|
411
|
+
body: payload,
|
|
412
|
+
headers: {},
|
|
413
|
+
})
|
|
414
|
+
.pipe(catchError((error) => {
|
|
415
|
+
console.error('TAS Service: modifyProxyVideoUser failed', error);
|
|
416
|
+
throw error;
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
finishProxyVideoSession(payload) {
|
|
420
|
+
return this.httpClient
|
|
421
|
+
.post('v2/proxy/video/finish', {
|
|
422
|
+
body: payload,
|
|
423
|
+
headers: {},
|
|
424
|
+
})
|
|
425
|
+
.pipe(map((response) => response), catchError((error) => {
|
|
426
|
+
console.error('TAS Service: finishProxyVideoSession failed', error);
|
|
427
|
+
throw error;
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get user appointments within a date range.
|
|
432
|
+
* @param params Date range for querying appointments
|
|
433
|
+
* @returns Observable of appointment array
|
|
434
|
+
*/
|
|
435
|
+
getAppointments(params) {
|
|
436
|
+
return this.httpClient
|
|
437
|
+
.get(`v2/proxy/appointment/agendas/user/appointments?fromDate=${params.fromDate}&toDate=${params.toDate}`, { headers: {} })
|
|
438
|
+
.pipe(catchError((error) => {
|
|
439
|
+
console.error('TAS Service: getAppointments failed', error);
|
|
440
|
+
throw error;
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Start automatic status polling for the current session.
|
|
445
|
+
* Status is polled every STATUS_POLL_INTERVAL_MS milliseconds.
|
|
446
|
+
*/
|
|
447
|
+
/**
|
|
448
|
+
* Stop automatic status polling.
|
|
449
|
+
*/
|
|
450
|
+
/**
|
|
451
|
+
* Fetch status and process the response.
|
|
452
|
+
*/
|
|
453
|
+
fetchAndProcessStatus(params) {
|
|
454
|
+
this.getProxyVideoStatus(params).subscribe({
|
|
455
|
+
next: (response) => {
|
|
456
|
+
this.processStatusResponse(response);
|
|
457
|
+
},
|
|
458
|
+
error: (err) => {
|
|
459
|
+
console.error('[TAS DEBUG] Status polling error:', err);
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Process status response to update waiting room users and owner join status.
|
|
465
|
+
*/
|
|
466
|
+
processStatusResponse(response) {
|
|
467
|
+
const content = response.content;
|
|
468
|
+
// Update videoCallId if available
|
|
469
|
+
if (content.videoCallId) {
|
|
470
|
+
this.currentVideoCallId = content.videoCallId;
|
|
471
|
+
}
|
|
472
|
+
// Update sessionId if available
|
|
473
|
+
if (content.sessionId) {
|
|
474
|
+
this.currentSessionId = content.sessionId;
|
|
475
|
+
this.proxyVideoSessionId = content.sessionId;
|
|
476
|
+
}
|
|
477
|
+
// Update joinable status
|
|
478
|
+
this.joinableSubject.next(content.joinable);
|
|
479
|
+
// Update activateGeo status
|
|
480
|
+
if (content.activateGeo !== undefined) {
|
|
481
|
+
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
482
|
+
this.activateGeoSubject.next(content.activateGeo);
|
|
483
|
+
}
|
|
484
|
+
// Update geoRequestActive status (owner waiting for user geo response)
|
|
485
|
+
if (content.geoRequestActive !== undefined) {
|
|
486
|
+
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
487
|
+
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
488
|
+
}
|
|
489
|
+
// Update allGeoGranted status (all users responded with geo)
|
|
490
|
+
if (content.allGeoGranted !== undefined) {
|
|
491
|
+
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
492
|
+
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
493
|
+
}
|
|
494
|
+
// Check if owner has joined
|
|
495
|
+
const ownerJoined = this.checkIfOwnerJoined(content.users);
|
|
496
|
+
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
497
|
+
// Detect if owner left: was present, now not present
|
|
498
|
+
if (this.wasOwnerPresent && !ownerJoined) {
|
|
499
|
+
console.log('[TAS DEBUG] Owner has left the session');
|
|
500
|
+
this.ownerHasLeftSubject.next(true);
|
|
501
|
+
}
|
|
502
|
+
if (ownerJoined) {
|
|
503
|
+
this.wasOwnerPresent = true;
|
|
504
|
+
}
|
|
505
|
+
// Extract waiting room users (status === WAITING)
|
|
506
|
+
const waitingUsers = content.users
|
|
507
|
+
.filter((u) => u.status === 'WAITING')
|
|
508
|
+
.map((u) => ({
|
|
509
|
+
userId: u.userId,
|
|
510
|
+
name: `User ${u.userId}`,
|
|
511
|
+
status: RoomUserStatus.WAITING,
|
|
512
|
+
}));
|
|
513
|
+
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Check if at least one OWNER has joined the call.
|
|
517
|
+
*/
|
|
518
|
+
checkIfOwnerJoined(users) {
|
|
519
|
+
return users.some((u) => u.rol === TasUserRole.OWNER && u.status === 'JOINED');
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Admit a user from the waiting room by changing their status.
|
|
523
|
+
*/
|
|
524
|
+
admitUserFromWaitingRoom(userId, videoCallId) {
|
|
525
|
+
return this.modifyProxyVideoUser({
|
|
526
|
+
userId,
|
|
527
|
+
videoCallId,
|
|
528
|
+
action: UserCallAction.WAITING_ROOM_LEAVE,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
// Getters for current call context
|
|
532
|
+
get appointmentId() {
|
|
533
|
+
return this.currentAppointmentId;
|
|
534
|
+
}
|
|
535
|
+
get videoCallId() {
|
|
536
|
+
return this.currentVideoCallId;
|
|
537
|
+
}
|
|
154
538
|
/**
|
|
155
539
|
* Connects to a TokBox video session
|
|
156
540
|
*/
|
|
157
|
-
connectSession(sessionId, token, publisherElement, subscriberElement) {
|
|
541
|
+
connectSession(sessionId, token, publisherElement, subscriberElement, businessRole = TasBusinessRole.USER) {
|
|
158
542
|
this.callStateSubject.next(CallState.CONNECTING);
|
|
159
543
|
this.currentSessionId = sessionId;
|
|
160
544
|
this.currentToken = token;
|
|
545
|
+
this.currentBusinessRole = businessRole;
|
|
546
|
+
this.isFinishingSession = false; // Reset flag for new session
|
|
547
|
+
// Save initial state (defaulting to current view mode)
|
|
548
|
+
this.saveSessionState(sessionId, token, this.viewModeSubject.getValue(), businessRole);
|
|
549
|
+
// Clear the disconnected flag when starting a new connection
|
|
550
|
+
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
161
551
|
return new Promise((resolve, reject) => {
|
|
162
552
|
if (!OT.checkSystemRequirements()) {
|
|
163
553
|
this.callStateSubject.next(CallState.ERROR);
|
|
164
|
-
reject(new Error(
|
|
554
|
+
reject(new Error('Browser not compatible with TokBox'));
|
|
165
555
|
return;
|
|
166
556
|
}
|
|
167
557
|
this.session = OT.initSession(this.config.tokBoxApiKey, sessionId);
|
|
168
558
|
// Handle new streams (remote participants joining)
|
|
169
|
-
this.session.on(
|
|
559
|
+
this.session.on('streamCreated', (event) => {
|
|
170
560
|
const subscriber = this.session?.subscribe(event.stream, subscriberElement, {
|
|
171
|
-
insertMode:
|
|
172
|
-
width:
|
|
173
|
-
height:
|
|
561
|
+
insertMode: 'append',
|
|
562
|
+
width: '100%',
|
|
563
|
+
height: '100%',
|
|
174
564
|
}, (error) => {
|
|
175
565
|
if (error) {
|
|
176
|
-
console.error(
|
|
566
|
+
console.error('Error subscribing to stream:', error);
|
|
177
567
|
}
|
|
178
568
|
});
|
|
179
569
|
if (subscriber) {
|
|
@@ -181,29 +571,48 @@ class TasService {
|
|
|
181
571
|
}
|
|
182
572
|
});
|
|
183
573
|
// Handle streams ending (remote participants leaving)
|
|
184
|
-
this.session.on(
|
|
574
|
+
this.session.on('streamDestroyed', (event) => {
|
|
185
575
|
this.subscribers = this.subscribers.filter((sub) => sub.stream?.streamId !== event.stream.streamId);
|
|
186
576
|
});
|
|
187
577
|
// Handle session disconnection
|
|
188
|
-
this.session.on(
|
|
578
|
+
this.session.on('sessionDisconnected', () => {
|
|
189
579
|
this.callStateSubject.next(CallState.DISCONNECTED);
|
|
580
|
+
// Call finishSession when session is disconnected (e.g., by server)
|
|
581
|
+
// Only call if not already finishing (prevents duplicate calls)
|
|
582
|
+
const sessionIdToFinish = this.currentSessionId;
|
|
583
|
+
if (sessionIdToFinish && !this.isFinishingSession) {
|
|
584
|
+
this.isFinishingSession = true;
|
|
585
|
+
this.finishProxyVideoSession({
|
|
586
|
+
sessionId: sessionIdToFinish,
|
|
587
|
+
businessRole: this.currentBusinessRole,
|
|
588
|
+
}).subscribe({
|
|
589
|
+
next: (response) => {
|
|
590
|
+
console.log('[TAS DEBUG] Session finished on disconnect event:', response);
|
|
591
|
+
this.isFinishingSession = false;
|
|
592
|
+
},
|
|
593
|
+
error: (error) => {
|
|
594
|
+
console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
|
|
595
|
+
this.isFinishingSession = false;
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
190
599
|
});
|
|
191
600
|
// Connect to session
|
|
192
601
|
this.session.connect(token, (error) => {
|
|
193
602
|
if (error) {
|
|
194
|
-
console.error(
|
|
603
|
+
console.error('Error connecting to session:', error);
|
|
195
604
|
this.callStateSubject.next(CallState.ERROR);
|
|
196
605
|
reject(error);
|
|
197
606
|
return;
|
|
198
607
|
}
|
|
199
608
|
// Initialize publisher (local video)
|
|
200
609
|
this.publisher = OT.initPublisher(publisherElement, {
|
|
201
|
-
insertMode:
|
|
202
|
-
width:
|
|
203
|
-
height:
|
|
610
|
+
insertMode: 'append',
|
|
611
|
+
width: '100%',
|
|
612
|
+
height: '100%',
|
|
204
613
|
}, (err) => {
|
|
205
614
|
if (err) {
|
|
206
|
-
console.error(
|
|
615
|
+
console.error('Error initializing publisher:', err);
|
|
207
616
|
this.callStateSubject.next(CallState.ERROR);
|
|
208
617
|
reject(err);
|
|
209
618
|
return;
|
|
@@ -211,7 +620,7 @@ class TasService {
|
|
|
211
620
|
// Publish to session
|
|
212
621
|
this.session?.publish(this.publisher, (pubErr) => {
|
|
213
622
|
if (pubErr) {
|
|
214
|
-
console.error(
|
|
623
|
+
console.error('Error publishing stream:', pubErr);
|
|
215
624
|
this.callStateSubject.next(CallState.ERROR);
|
|
216
625
|
reject(pubErr);
|
|
217
626
|
}
|
|
@@ -271,12 +680,12 @@ class TasService {
|
|
|
271
680
|
this.movePublisherTo('publisher-container');
|
|
272
681
|
}
|
|
273
682
|
}
|
|
274
|
-
TasService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.
|
|
275
|
-
TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.
|
|
276
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.
|
|
683
|
+
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 });
|
|
684
|
+
TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, providedIn: 'root' });
|
|
685
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, decorators: [{
|
|
277
686
|
type: Injectable,
|
|
278
687
|
args: [{
|
|
279
|
-
providedIn:
|
|
688
|
+
providedIn: 'root',
|
|
280
689
|
}]
|
|
281
690
|
}], ctorParameters: function () { return [{ type: undefined, decorators: [{
|
|
282
691
|
type: Inject,
|
|
@@ -286,14 +695,155 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
|
|
|
286
695
|
args: [TAS_CONFIG]
|
|
287
696
|
}] }]; } });
|
|
288
697
|
|
|
698
|
+
class GeolocationService {
|
|
699
|
+
constructor() {
|
|
700
|
+
this.cachedPosition = null;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Request current geolocation position using Web Geolocation API.
|
|
704
|
+
* Works in both browser and Capacitor environments.
|
|
705
|
+
* @returns Promise with {latitude, longitude} or null if denied/unavailable
|
|
706
|
+
*/
|
|
707
|
+
async getCurrentPosition() {
|
|
708
|
+
if (!navigator.geolocation) {
|
|
709
|
+
console.warn('[GeolocationService] Geolocation not supported');
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return new Promise((resolve) => {
|
|
713
|
+
navigator.geolocation.getCurrentPosition((position) => {
|
|
714
|
+
const geoPosition = {
|
|
715
|
+
latitude: position.coords.latitude,
|
|
716
|
+
longitude: position.coords.longitude,
|
|
717
|
+
};
|
|
718
|
+
this.cachedPosition = geoPosition;
|
|
719
|
+
resolve(geoPosition);
|
|
720
|
+
}, (error) => {
|
|
721
|
+
console.warn('[GeolocationService] Geolocation error:', error.message);
|
|
722
|
+
resolve(null);
|
|
723
|
+
}, {
|
|
724
|
+
enableHighAccuracy: false,
|
|
725
|
+
timeout: 30000,
|
|
726
|
+
maximumAge: 60000,
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Get the cached position from the last successful request.
|
|
732
|
+
*/
|
|
733
|
+
getCachedPosition() {
|
|
734
|
+
return this.cachedPosition;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Clear the cached position.
|
|
738
|
+
*/
|
|
739
|
+
clearCache() {
|
|
740
|
+
this.cachedPosition = null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
GeolocationService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: GeolocationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
744
|
+
GeolocationService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: GeolocationService, providedIn: 'root' });
|
|
745
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: GeolocationService, decorators: [{
|
|
746
|
+
type: Injectable,
|
|
747
|
+
args: [{
|
|
748
|
+
providedIn: 'root',
|
|
749
|
+
}]
|
|
750
|
+
}] });
|
|
751
|
+
|
|
752
|
+
class TasAvatarComponent {
|
|
753
|
+
constructor() {
|
|
754
|
+
this.name = '';
|
|
755
|
+
this.size = 80;
|
|
756
|
+
}
|
|
757
|
+
get initials() {
|
|
758
|
+
if (!this.name)
|
|
759
|
+
return '';
|
|
760
|
+
return this.name
|
|
761
|
+
.split(' ')
|
|
762
|
+
.filter((n) => n.length > 0)
|
|
763
|
+
.map((n) => n[0])
|
|
764
|
+
.join('')
|
|
765
|
+
.toUpperCase()
|
|
766
|
+
.substring(0, 2);
|
|
767
|
+
}
|
|
768
|
+
get fontSize() {
|
|
769
|
+
return Math.round(this.size * 0.4);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
TasAvatarComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasAvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
773
|
+
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: `
|
|
774
|
+
<div
|
|
775
|
+
class="avatar"
|
|
776
|
+
[style.width.px]="size"
|
|
777
|
+
[style.height.px]="size"
|
|
778
|
+
[style.fontSize.px]="fontSize"
|
|
779
|
+
>
|
|
780
|
+
<span class="initials">{{ initials }}</span>
|
|
781
|
+
</div>
|
|
782
|
+
`, 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 });
|
|
783
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasAvatarComponent, decorators: [{
|
|
784
|
+
type: Component,
|
|
785
|
+
args: [{
|
|
786
|
+
selector: 'tas-avatar',
|
|
787
|
+
template: `
|
|
788
|
+
<div
|
|
789
|
+
class="avatar"
|
|
790
|
+
[style.width.px]="size"
|
|
791
|
+
[style.height.px]="size"
|
|
792
|
+
[style.fontSize.px]="fontSize"
|
|
793
|
+
>
|
|
794
|
+
<span class="initials">{{ initials }}</span>
|
|
795
|
+
</div>
|
|
796
|
+
`,
|
|
797
|
+
styles: [
|
|
798
|
+
`
|
|
799
|
+
.avatar {
|
|
800
|
+
display: flex;
|
|
801
|
+
align-items: center;
|
|
802
|
+
justify-content: center;
|
|
803
|
+
border-radius: 50%;
|
|
804
|
+
background-color: #ffffff;
|
|
805
|
+
color: #0072ac;
|
|
806
|
+
font-weight: 600;
|
|
807
|
+
font-family: inherit;
|
|
808
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.initials {
|
|
812
|
+
text-transform: uppercase;
|
|
813
|
+
user-select: none;
|
|
814
|
+
}
|
|
815
|
+
`,
|
|
816
|
+
],
|
|
817
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
818
|
+
}]
|
|
819
|
+
}], propDecorators: { name: [{
|
|
820
|
+
type: Input
|
|
821
|
+
}], size: [{
|
|
822
|
+
type: Input
|
|
823
|
+
}] } });
|
|
824
|
+
|
|
289
825
|
class TasVideocallComponent {
|
|
290
|
-
constructor(activeModal, tasService) {
|
|
826
|
+
constructor(activeModal, tasService, geolocationService) {
|
|
291
827
|
this.activeModal = activeModal;
|
|
292
828
|
this.tasService = tasService;
|
|
829
|
+
this.geolocationService = geolocationService;
|
|
830
|
+
this.participantName = '';
|
|
831
|
+
this.tenant = '';
|
|
832
|
+
this.businessRole = TasBusinessRole.USER;
|
|
293
833
|
this.isReturningFromPip = false;
|
|
294
834
|
this.isPublisherSmall = true;
|
|
295
835
|
this.callState = CallState.IDLE;
|
|
296
836
|
this.isMuted = false;
|
|
837
|
+
this.waitingRoomUsers = [];
|
|
838
|
+
this.ownerHasJoined = false;
|
|
839
|
+
this.hasVideoStream = false;
|
|
840
|
+
this.dismissedUsers = [];
|
|
841
|
+
this.showLocationPanel = true; // Show by default for owners, will be updated based on user's location status
|
|
842
|
+
this.userHasLocation = false; // Tracks if the user has shared their location
|
|
843
|
+
this.geoLocationStatus = 'unknown';
|
|
844
|
+
// Geo panel states for owner
|
|
845
|
+
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
846
|
+
this.allGeoGranted = false; // All users responded with geo
|
|
297
847
|
this.subscriptions = new Subscription();
|
|
298
848
|
}
|
|
299
849
|
ngOnInit() {
|
|
@@ -305,6 +855,7 @@ class TasVideocallComponent {
|
|
|
305
855
|
}
|
|
306
856
|
ngOnDestroy() {
|
|
307
857
|
this.subscriptions.unsubscribe();
|
|
858
|
+
this.tasService.resetPollingState();
|
|
308
859
|
// Only disconnect if not in PiP mode (keep session alive for floating window)
|
|
309
860
|
if (!this.tasService.isPipMode()) {
|
|
310
861
|
this.tasService.disconnectSession();
|
|
@@ -331,25 +882,179 @@ class TasVideocallComponent {
|
|
|
331
882
|
onDoubleClick() {
|
|
332
883
|
this.toggleSwap();
|
|
333
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Check if current user can admit others (OWNER, BACKOFFICE, or MODERATOR)
|
|
887
|
+
*/
|
|
888
|
+
get canAdmitUsers() {
|
|
889
|
+
return (this.businessRole === TasBusinessRole.BACKOFFICE ||
|
|
890
|
+
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
891
|
+
this.businessRole === TasBusinessRole.MANAGER);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Admit a user from the waiting room
|
|
895
|
+
*/
|
|
896
|
+
admitUser(userId) {
|
|
897
|
+
if (!this.videoCallId) {
|
|
898
|
+
console.error('Cannot admit user: videoCallId not set');
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
this.tasService
|
|
902
|
+
.modifyProxyVideoUser({
|
|
903
|
+
userId,
|
|
904
|
+
videoCallId: this.videoCallId,
|
|
905
|
+
action: UserCallAction.WAITING_ROOM_LEAVE,
|
|
906
|
+
})
|
|
907
|
+
.subscribe({
|
|
908
|
+
next: () => {
|
|
909
|
+
// Remove user from waiting list after successful admit
|
|
910
|
+
this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
|
|
911
|
+
},
|
|
912
|
+
error: (err) => {
|
|
913
|
+
console.error('Error admitting user:', err);
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Dismiss the waiting room notification for a user
|
|
919
|
+
*/
|
|
920
|
+
dismissWaitingNotification(userId) {
|
|
921
|
+
this.dismissedUsers.push(userId);
|
|
922
|
+
this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Close the location panel
|
|
926
|
+
*/
|
|
927
|
+
closeLocationPanel() {
|
|
928
|
+
this.showLocationPanel = false;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Request the user to share their location
|
|
932
|
+
*/
|
|
933
|
+
requestUserLocation() {
|
|
934
|
+
if (!this.videoCallId) {
|
|
935
|
+
console.error('Cannot request location: videoCallId not set');
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const body = {
|
|
939
|
+
videoCallId: this.videoCallId,
|
|
940
|
+
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
941
|
+
};
|
|
942
|
+
// TODO: Send location request action to backend when endpoint is ready
|
|
943
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
944
|
+
next: () => console.log('Location request sent'),
|
|
945
|
+
error: (err) => console.error('Error requesting location:', err),
|
|
946
|
+
});
|
|
947
|
+
}
|
|
334
948
|
// Private Methods
|
|
335
949
|
setupSubscriptions() {
|
|
336
950
|
// Call state subscription
|
|
337
|
-
this.subscriptions.add(this.tasService.callState$.subscribe(state => {
|
|
951
|
+
this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
|
|
338
952
|
this.callState = state;
|
|
339
953
|
if (state === CallState.DISCONNECTED) {
|
|
340
954
|
this.activeModal.close('hangup');
|
|
341
955
|
}
|
|
956
|
+
// Track if we have an active video stream
|
|
957
|
+
this.hasVideoStream = state === CallState.CONNECTED;
|
|
342
958
|
}));
|
|
343
959
|
// View mode subscription
|
|
344
|
-
this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
|
|
960
|
+
this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
|
|
345
961
|
if (mode === ViewMode.PIP) {
|
|
346
962
|
this.activeModal.close('pip');
|
|
347
963
|
}
|
|
348
964
|
}));
|
|
349
965
|
// Mute state subscription
|
|
350
|
-
this.subscriptions.add(this.tasService.isMuted$.subscribe(muted => {
|
|
966
|
+
this.subscriptions.add(this.tasService.isMuted$.subscribe((muted) => {
|
|
351
967
|
this.isMuted = muted;
|
|
352
968
|
}));
|
|
969
|
+
// Waiting room users subscription
|
|
970
|
+
this.subscriptions.add(this.tasService.waitingRoomUsers$.subscribe((users) => {
|
|
971
|
+
// Filter out dismissed users
|
|
972
|
+
this.waitingRoomUsers = users.filter((u) => !this.dismissedUsers.includes(u.userId));
|
|
973
|
+
}));
|
|
974
|
+
// Owner join status subscription
|
|
975
|
+
this.subscriptions.add(this.tasService.ownerHasJoined$.subscribe((joined) => {
|
|
976
|
+
this.ownerHasJoined = joined;
|
|
977
|
+
}));
|
|
978
|
+
// Owner left subscription - disconnect non-owners
|
|
979
|
+
this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
|
|
980
|
+
if (hasLeft && !this.canAdmitUsers) { // Non-owner user
|
|
981
|
+
console.log('[TAS DEBUG] Owner left, disconnecting user');
|
|
982
|
+
this.hangUp();
|
|
983
|
+
this.activeModal.close('owner_left');
|
|
984
|
+
}
|
|
985
|
+
}));
|
|
986
|
+
// ActivateGeo subscription - only for non-owners (users)
|
|
987
|
+
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
988
|
+
if (activateGeo && !this.canAdmitUsers) {
|
|
989
|
+
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
990
|
+
this.handleActivateGeo();
|
|
991
|
+
}
|
|
992
|
+
}));
|
|
993
|
+
// GeoRequestActive subscription - for owners
|
|
994
|
+
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
995
|
+
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
996
|
+
this.geoRequestActive = active;
|
|
997
|
+
}));
|
|
998
|
+
// AllGeoGranted subscription - for owners
|
|
999
|
+
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1000
|
+
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1001
|
+
this.allGeoGranted = granted;
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Handle activateGeo request from backend (for non-owner users).
|
|
1006
|
+
* If geo is already active, report it. If not, prompt user.
|
|
1007
|
+
*/
|
|
1008
|
+
async handleActivateGeo() {
|
|
1009
|
+
console.log('[TAS DEBUG] handleActivateGeo - current status:', this.geoLocationStatus);
|
|
1010
|
+
// Check if we already have a cached position
|
|
1011
|
+
const cachedPosition = this.geolocationService.getCachedPosition();
|
|
1012
|
+
if (cachedPosition) {
|
|
1013
|
+
console.log('[TAS DEBUG] Geolocation already active, reporting to backend:', cachedPosition);
|
|
1014
|
+
this.geoLocationStatus = 'active';
|
|
1015
|
+
this.reportGeoStatus(cachedPosition.latitude, cachedPosition.longitude);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
// Try to get position
|
|
1019
|
+
console.log('[TAS DEBUG] Requesting geolocation from user...');
|
|
1020
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1021
|
+
if (position) {
|
|
1022
|
+
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1023
|
+
this.geoLocationStatus = 'active';
|
|
1024
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1028
|
+
this.geoLocationStatus = 'denied';
|
|
1029
|
+
// Report that geo is not available (with no coordinates)
|
|
1030
|
+
this.reportGeoStatus();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Report geolocation status to backend.
|
|
1035
|
+
*/
|
|
1036
|
+
reportGeoStatus(latitude, longitude) {
|
|
1037
|
+
if (!this.videoCallId) {
|
|
1038
|
+
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
// TODO: If the permission for geo location is not granted, we should not report it to the backend
|
|
1042
|
+
if (!this.geoLocationStatus) {
|
|
1043
|
+
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const body = {
|
|
1047
|
+
userId: this.userId,
|
|
1048
|
+
videoCallId: this.videoCallId,
|
|
1049
|
+
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1050
|
+
latitude,
|
|
1051
|
+
longitude,
|
|
1052
|
+
};
|
|
1053
|
+
console.log('[TAS DEBUG] Reporting geo status to backend:', body);
|
|
1054
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1055
|
+
next: () => console.log('[TAS DEBUG] Geo status reported successfully'),
|
|
1056
|
+
error: (err) => console.error('[TAS DEBUG] Error reporting geo status:', err),
|
|
1057
|
+
});
|
|
353
1058
|
}
|
|
354
1059
|
initializeCall() {
|
|
355
1060
|
if (this.isReturningFromPip) {
|
|
@@ -358,9 +1063,22 @@ class TasVideocallComponent {
|
|
|
358
1063
|
}
|
|
359
1064
|
else if (this.sessionId && this.token) {
|
|
360
1065
|
// New call - connect to session
|
|
361
|
-
this.tasService
|
|
1066
|
+
this.tasService
|
|
1067
|
+
.connectSession(this.sessionId, this.token, 'publisher-container', 'subscriber-container', this.businessRole)
|
|
1068
|
+
.catch((err) => {
|
|
362
1069
|
console.error('Error connecting to video call:', err);
|
|
363
1070
|
});
|
|
1071
|
+
// Start status polling with shorter interval (5s) to detect owner leaving/location requests
|
|
1072
|
+
if (this.appointmentId) {
|
|
1073
|
+
this.tasService.startStatusPolling({
|
|
1074
|
+
appointmentId: this.appointmentId,
|
|
1075
|
+
tenant: this.tenant,
|
|
1076
|
+
businessRole: this.businessRole,
|
|
1077
|
+
roomType: undefined,
|
|
1078
|
+
entityId: undefined,
|
|
1079
|
+
sessionId: this.sessionId
|
|
1080
|
+
}, 5000);
|
|
1081
|
+
}
|
|
364
1082
|
}
|
|
365
1083
|
}
|
|
366
1084
|
resetVideoPositions() {
|
|
@@ -385,8 +1103,8 @@ class TasVideocallComponent {
|
|
|
385
1103
|
modifiers: [
|
|
386
1104
|
interact.modifiers.restrictRect({
|
|
387
1105
|
restriction: 'parent',
|
|
388
|
-
endOnly: true
|
|
389
|
-
})
|
|
1106
|
+
endOnly: true,
|
|
1107
|
+
}),
|
|
390
1108
|
],
|
|
391
1109
|
autoScroll: true,
|
|
392
1110
|
listeners: {
|
|
@@ -397,8 +1115,8 @@ class TasVideocallComponent {
|
|
|
397
1115
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
|
398
1116
|
target.setAttribute('data-x', String(x));
|
|
399
1117
|
target.setAttribute('data-y', String(y));
|
|
400
|
-
}
|
|
401
|
-
}
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
402
1120
|
})
|
|
403
1121
|
.resizable({
|
|
404
1122
|
edges: { left: false, right: true, bottom: true, top: false },
|
|
@@ -414,26 +1132,38 @@ class TasVideocallComponent {
|
|
|
414
1132
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
|
415
1133
|
target.setAttribute('data-x', String(x));
|
|
416
1134
|
target.setAttribute('data-y', String(y));
|
|
417
|
-
}
|
|
1135
|
+
},
|
|
418
1136
|
},
|
|
419
1137
|
modifiers: [
|
|
420
1138
|
interact.modifiers.restrictEdges({ outer: 'parent' }),
|
|
421
1139
|
interact.modifiers.restrictSize({ min: { width: 150, height: 100 } }),
|
|
422
|
-
interact.modifiers.aspectRatio({ ratio: 'preserve' })
|
|
1140
|
+
interact.modifiers.aspectRatio({ ratio: 'preserve' }),
|
|
423
1141
|
],
|
|
424
|
-
inertia: true
|
|
1142
|
+
inertia: true,
|
|
425
1143
|
});
|
|
426
1144
|
}
|
|
427
1145
|
}
|
|
428
|
-
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.
|
|
429
|
-
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-container\">\n\t<div id=\"subscriber-container\" \n\t\t[class.subscriber-view]=\"isPublisherSmall\" \n\t\t[class.publisher-view]=\"!isPublisherSmall\"\n\t\t#subscriberContainer\n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\n\t<div id=\"publisher-container\" \n\t\t[class.publisher-view]=\"isPublisherSmall\" \n\t\t[class.subscriber-view]=\"!isPublisherSmall\"\n\t\t#publisherContainer \n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\t\n\t<div class=\"controls-container\">\n\t\t<button class=\"btn swap-btn\" (click)=\"toggleSwap()\" title=\"Swap view\">\n\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t</button>\n\t\t<button class=\"btn pip-btn\" (click)=\"minimize()\" title=\"Minimize (Picture in Picture)\">\n\t\t\t<i class=\"fa fa-compress\"></i>\n\t\t</button>\n\t\t<button class=\"btn mute-btn\" [class.muted]=\"isMuted\" (click)=\"toggleMute()\" [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\">\n\t\t\t<i class=\"fa\" [class.fa-microphone]=\"!isMuted\" [class.fa-microphone-slash]=\"isMuted\"></i>\n\t\t</button>\n\t\t<button class=\"btn hangup-btn\" (click)=\"hangUp()\" title=\"Hang up\">\n\t\t\t<i class=\"fa fa-phone\" style=\"transform: rotate(135deg);\"></i>\n\t\t</button>\n\t</div>\n</div>\n\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;background-color:#000;overflow:hidden}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#333}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:20px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#00000080;padding:15px 25px;border-radius:50px;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.tas-videocall-container .controls-container .hangup-btn,.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;border:none;box-shadow:0 4px 6px #0000004d;transition:all .2s ease}.tas-videocall-container .controls-container .hangup-btn i,.tas-videocall-container .controls-container .swap-btn i,.tas-videocall-container .controls-container .pip-btn i,.tas-videocall-container .controls-container .mute-btn i{color:#fff}.tas-videocall-container .controls-container .hangup-btn{background:#dc3545}.tas-videocall-container .controls-container .hangup-btn:hover{background:#c82333;transform:scale(1.05)}.tas-videocall-container .controls-container .swap-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .swap-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .pip-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .pip-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .mute-btn.muted:hover{background:#e67e22}\n"] });
|
|
430
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.
|
|
1146
|
+
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1147
|
+
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", userId: "userId", 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-wrapper\">\n <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\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;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}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1148
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
431
1149
|
type: Component,
|
|
432
|
-
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-container\">\n\t<div id=\"subscriber-container\" \n\t\t[class.subscriber-view]=\"isPublisherSmall\" \n\t\t[class.publisher-view]=\"!isPublisherSmall\"\n\t\t#subscriberContainer\n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\n\t<div id=\"publisher-container\" \n\t\t[class.publisher-view]=\"isPublisherSmall\" \n\t\t[class.subscriber-view]=\"!isPublisherSmall\"\n\t\t#publisherContainer \n\t\t(dblclick)=\"onDoubleClick()\">\n\t</div>\n\t\n\t<div class=\"controls-container\">\n\t\t<button class=\"btn swap-btn\" (click)=\"toggleSwap()\" title=\"Swap view\">\n\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t</button>\n\t\t<button class=\"btn pip-btn\" (click)=\"minimize()\" title=\"Minimize (Picture in Picture)\">\n\t\t\t<i class=\"fa fa-compress\"></i>\n\t\t</button>\n\t\t<button class=\"btn mute-btn\" [class.muted]=\"isMuted\" (click)=\"toggleMute()\" [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\">\n\t\t\t<i class=\"fa\" [class.fa-microphone]=\"!isMuted\" [class.fa-microphone-slash]=\"isMuted\"></i>\n\t\t</button>\n\t\t<button class=\"btn hangup-btn\" (click)=\"hangUp()\" title=\"Hang up\">\n\t\t\t<i class=\"fa fa-phone\" style=\"transform: rotate(135deg);\"></i>\n\t\t</button>\n\t</div>\n</div>\n\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;background-color:#000;overflow:hidden}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#333}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:20px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#00000080;padding:15px 25px;border-radius:50px;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.tas-videocall-container .controls-container .hangup-btn,.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;border:none;box-shadow:0 4px 6px #0000004d;transition:all .2s ease}.tas-videocall-container .controls-container .hangup-btn i,.tas-videocall-container .controls-container .swap-btn i,.tas-videocall-container .controls-container .pip-btn i,.tas-videocall-container .controls-container .mute-btn i{color:#fff}.tas-videocall-container .controls-container .hangup-btn{background:#dc3545}.tas-videocall-container .controls-container .hangup-btn:hover{background:#c82333;transform:scale(1.05)}.tas-videocall-container .controls-container .swap-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .swap-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .pip-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .pip-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.05)}.tas-videocall-container .controls-container .mute-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .mute-btn.muted:hover{background:#e67e22}\n"] }]
|
|
433
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }]; }, propDecorators: { sessionId: [{
|
|
1150
|
+
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-wrapper\">\n <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\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;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}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
1151
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
434
1152
|
type: Input
|
|
435
1153
|
}], token: [{
|
|
436
1154
|
type: Input
|
|
1155
|
+
}], appointmentId: [{
|
|
1156
|
+
type: Input
|
|
1157
|
+
}], videoCallId: [{
|
|
1158
|
+
type: Input
|
|
1159
|
+
}], userId: [{
|
|
1160
|
+
type: Input
|
|
1161
|
+
}], participantName: [{
|
|
1162
|
+
type: Input
|
|
1163
|
+
}], tenant: [{
|
|
1164
|
+
type: Input
|
|
1165
|
+
}], businessRole: [{
|
|
1166
|
+
type: Input
|
|
437
1167
|
}], isReturningFromPip: [{
|
|
438
1168
|
type: Input
|
|
439
1169
|
}], publisherContainer: [{
|
|
@@ -446,236 +1176,303 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
|
|
|
446
1176
|
|
|
447
1177
|
var WaitingRoomState;
|
|
448
1178
|
(function (WaitingRoomState) {
|
|
449
|
-
WaitingRoomState["
|
|
450
|
-
WaitingRoomState["
|
|
451
|
-
WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
|
|
1179
|
+
WaitingRoomState["CHECKING_STATUS"] = "CHECKING_STATUS";
|
|
1180
|
+
WaitingRoomState["WAITING_FOR_JOINABLE"] = "WAITING_FOR_JOINABLE";
|
|
452
1181
|
WaitingRoomState["READY"] = "READY";
|
|
1182
|
+
WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
|
|
1183
|
+
WaitingRoomState["JOINING"] = "JOINING";
|
|
453
1184
|
WaitingRoomState["ERROR"] = "ERROR";
|
|
454
1185
|
})(WaitingRoomState || (WaitingRoomState = {}));
|
|
455
1186
|
class TasWaitingRoomComponent {
|
|
456
|
-
constructor(activeModal, tasService, modalService) {
|
|
1187
|
+
constructor(activeModal, tasService, geolocationService, modalService, cdr) {
|
|
457
1188
|
this.activeModal = activeModal;
|
|
458
1189
|
this.tasService = tasService;
|
|
1190
|
+
this.geolocationService = geolocationService;
|
|
459
1191
|
this.modalService = modalService;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
this.
|
|
463
|
-
this.
|
|
464
|
-
this.regularUserIds = [];
|
|
465
|
-
this.moderatorUserIds = [];
|
|
466
|
-
/** Optional: If provided, skips room creation and goes directly to getting a token */
|
|
467
|
-
this.existingSessionId = '';
|
|
1192
|
+
this.cdr = cdr;
|
|
1193
|
+
// Status endpoint params
|
|
1194
|
+
this.roomType = TasRoomType.TAS;
|
|
1195
|
+
this.businessRole = TasBusinessRole.USER;
|
|
468
1196
|
// Component state
|
|
469
|
-
this.state = WaitingRoomState.
|
|
1197
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
470
1198
|
this.WaitingRoomState = WaitingRoomState; // Expose enum to template
|
|
471
1199
|
this.errorMessage = '';
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.
|
|
476
|
-
/** Manual session ID input by user */
|
|
477
|
-
this.manualSessionId = '';
|
|
478
|
-
/** Track if we're joining an existing session (for UI display) */
|
|
479
|
-
this.isJoiningExisting = false;
|
|
480
|
-
// Session data
|
|
481
|
-
this.sessionId = '';
|
|
1200
|
+
this.isJoinable = false;
|
|
1201
|
+
// Session data from status response
|
|
1202
|
+
this.resolvedSessionId = '';
|
|
1203
|
+
this.resolvedAppointmentId = null;
|
|
482
1204
|
this.token = '';
|
|
483
|
-
this.
|
|
1205
|
+
this.videoCallId = null;
|
|
1206
|
+
// Subscriptions
|
|
484
1207
|
this.subscriptions = new Subscription();
|
|
485
1208
|
this.videoCallModalRef = null;
|
|
1209
|
+
// Geolocation
|
|
1210
|
+
this.geoPosition = null;
|
|
1211
|
+
}
|
|
1212
|
+
/** Whether current user is an owner */
|
|
1213
|
+
get isOwner() {
|
|
1214
|
+
return this.currentUser?.role === TasUserRole.OWNER;
|
|
486
1215
|
}
|
|
487
1216
|
ngOnInit() {
|
|
488
|
-
|
|
489
|
-
this.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1217
|
+
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1218
|
+
this.requestMediaPermissions();
|
|
1219
|
+
this.requestGeolocation();
|
|
1220
|
+
this.checkStatus();
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Request camera and microphone permissions.
|
|
1224
|
+
*/
|
|
1225
|
+
async requestMediaPermissions() {
|
|
1226
|
+
console.log('[TAS DEBUG] Requesting media permissions...');
|
|
1227
|
+
try {
|
|
1228
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
|
1229
|
+
// Stop tracks immediately - we just needed the permission
|
|
1230
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1231
|
+
console.log('[TAS DEBUG] Media permissions granted');
|
|
1232
|
+
}
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
495
1235
|
}
|
|
496
1236
|
}
|
|
497
|
-
|
|
498
|
-
|
|
1237
|
+
/**
|
|
1238
|
+
* Request geolocation immediately on init.
|
|
1239
|
+
* Only for regular users (not owners/backoffice).
|
|
1240
|
+
* If user allows, store position and send to backend.
|
|
1241
|
+
*/
|
|
1242
|
+
async requestGeolocation() {
|
|
1243
|
+
// Only request geolocation for regular users, not owners/backoffice
|
|
1244
|
+
if (this.isOwner || this.isBackoffice) {
|
|
1245
|
+
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1249
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1250
|
+
if (position) {
|
|
1251
|
+
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1252
|
+
this.geoPosition = position;
|
|
1253
|
+
// Send to backend when videoCallId is available
|
|
1254
|
+
this.sendGeolocationToBackend();
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1258
|
+
}
|
|
499
1259
|
}
|
|
500
1260
|
/**
|
|
501
|
-
*
|
|
1261
|
+
* Send geolocation to backend via modify user endpoint.
|
|
1262
|
+
* NOTE: Endpoint call is prepared but may not be active yet.
|
|
502
1263
|
*/
|
|
503
|
-
|
|
504
|
-
if (!this.
|
|
505
|
-
this.state = WaitingRoomState.ERROR;
|
|
506
|
-
this.errorMessage = 'Missing configuration data (tenant or user)';
|
|
1264
|
+
sendGeolocationToBackend() {
|
|
1265
|
+
if (!this.geoPosition || !this.videoCallId) {
|
|
507
1266
|
return;
|
|
508
1267
|
}
|
|
509
|
-
|
|
510
|
-
this.state = WaitingRoomState.CREATING_ROOM;
|
|
511
|
-
this.errorMessage = '';
|
|
1268
|
+
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
512
1269
|
const body = {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
product: this.product
|
|
1270
|
+
userId: this.currentUser?.id,
|
|
1271
|
+
videoCallId: this.videoCallId,
|
|
1272
|
+
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1273
|
+
latitude: this.geoPosition.latitude,
|
|
1274
|
+
longitude: this.geoPosition.longitude,
|
|
519
1275
|
};
|
|
520
|
-
this.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1276
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1277
|
+
next: () => console.log('[TAS DEBUG] Geolocation sent successfully'),
|
|
1278
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
ngOnDestroy() {
|
|
1282
|
+
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1283
|
+
this.subscriptions.unsubscribe();
|
|
1284
|
+
this.tasService.stopStatusPolling();
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Check status to get session info
|
|
1288
|
+
*/
|
|
1289
|
+
checkStatus() {
|
|
1290
|
+
console.log('[TAS DEBUG] checkStatus called with:', {
|
|
1291
|
+
roomType: this.roomType,
|
|
1292
|
+
entityId: this.entityId,
|
|
1293
|
+
tenant: this.tenant,
|
|
1294
|
+
businessRole: this.businessRole,
|
|
1295
|
+
currentUser: this.currentUser,
|
|
1296
|
+
});
|
|
1297
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1298
|
+
this.errorMessage = '';
|
|
1299
|
+
const statusParams = {
|
|
1300
|
+
roomType: this.roomType,
|
|
1301
|
+
entityId: this.entityId,
|
|
1302
|
+
tenant: this.tenant,
|
|
1303
|
+
businessRole: this.businessRole,
|
|
1304
|
+
};
|
|
1305
|
+
console.log('[TAS DEBUG] Calling getProxyVideoStatus...');
|
|
1306
|
+
this.subscriptions.add(this.tasService.getProxyVideoStatus(statusParams).subscribe({
|
|
1307
|
+
next: (response) => {
|
|
1308
|
+
const content = response.content;
|
|
1309
|
+
// Store session info from response
|
|
1310
|
+
this.resolvedSessionId = content.sessionId;
|
|
1311
|
+
this.resolvedAppointmentId = content.appointmentId;
|
|
1312
|
+
this.videoCallId = content.videoCallId;
|
|
1313
|
+
console.log('[TAS DEBUG] Status response:', content);
|
|
1314
|
+
// Try to send geolocation now that we have videoCallId
|
|
1315
|
+
this.sendGeolocationToBackend();
|
|
1316
|
+
// Start polling for status updates
|
|
1317
|
+
this.tasService.startStatusPolling(statusParams);
|
|
1318
|
+
// Subscribe to joinable status
|
|
1319
|
+
this.subscriptions.add(this.tasService.joinable$.subscribe((joinable) => {
|
|
1320
|
+
this.handleJoinableChange(joinable);
|
|
1321
|
+
}));
|
|
533
1322
|
},
|
|
534
1323
|
error: (err) => {
|
|
535
|
-
console.error('
|
|
1324
|
+
console.error('[TAS DEBUG] Status check failed:', err);
|
|
536
1325
|
this.state = WaitingRoomState.ERROR;
|
|
537
|
-
this.errorMessage = 'Error
|
|
538
|
-
}
|
|
1326
|
+
this.errorMessage = 'Error checking session status. Please try again.';
|
|
1327
|
+
},
|
|
539
1328
|
}));
|
|
540
1329
|
}
|
|
541
1330
|
/**
|
|
542
|
-
*
|
|
1331
|
+
* Handle changes to joinable status
|
|
543
1332
|
*/
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.
|
|
1333
|
+
handleJoinableChange(joinable) {
|
|
1334
|
+
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
1335
|
+
joinable,
|
|
1336
|
+
currentState: this.state,
|
|
1337
|
+
resolvedSessionId: this.resolvedSessionId,
|
|
1338
|
+
});
|
|
1339
|
+
this.isJoinable = joinable;
|
|
1340
|
+
// Don't update state if already getting token, joining, or in error
|
|
1341
|
+
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
1342
|
+
this.state === WaitingRoomState.JOINING ||
|
|
1343
|
+
this.state === WaitingRoomState.ERROR) {
|
|
1344
|
+
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
548
1345
|
return;
|
|
549
1346
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1347
|
+
// Both users and owners: show join button based on joinable
|
|
1348
|
+
if (joinable) {
|
|
1349
|
+
console.log('[TAS DEBUG] Joinable is true, showing join button');
|
|
1350
|
+
this.state = WaitingRoomState.READY;
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
1354
|
+
this.state = WaitingRoomState.WAITING_FOR_JOINABLE;
|
|
1355
|
+
}
|
|
1356
|
+
this.cdr.detectChanges();
|
|
553
1357
|
}
|
|
554
1358
|
/**
|
|
555
|
-
*
|
|
1359
|
+
* Called when user clicks the join button.
|
|
1360
|
+
* Calls /start to get token then auto-joins.
|
|
556
1361
|
*/
|
|
557
|
-
|
|
558
|
-
|
|
1362
|
+
joinSession() {
|
|
1363
|
+
this.startSessionAndJoin();
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Check if user has owner/backoffice role
|
|
1367
|
+
*/
|
|
1368
|
+
get isBackoffice() {
|
|
1369
|
+
return (this.businessRole === TasBusinessRole.BACKOFFICE ||
|
|
1370
|
+
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
1371
|
+
this.businessRole === TasBusinessRole.MANAGER);
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Start session and join - called when user clicks join button
|
|
1375
|
+
*/
|
|
1376
|
+
startSessionAndJoin() {
|
|
1377
|
+
if (!this.resolvedSessionId) {
|
|
559
1378
|
this.state = WaitingRoomState.ERROR;
|
|
560
|
-
this.errorMessage = '
|
|
1379
|
+
this.errorMessage = 'Session ID not available';
|
|
1380
|
+
this.tasService.stopStatusPolling();
|
|
1381
|
+
this.cdr.detectChanges();
|
|
561
1382
|
return;
|
|
562
1383
|
}
|
|
563
|
-
this.isJoiningExisting = true;
|
|
564
1384
|
this.state = WaitingRoomState.GETTING_TOKEN;
|
|
565
1385
|
this.errorMessage = '';
|
|
566
|
-
|
|
567
|
-
|
|
1386
|
+
console.log('[TAS DEBUG] Calling /start for session:', this.resolvedSessionId);
|
|
1387
|
+
this.subscriptions.add(this.tasService
|
|
1388
|
+
.startProxyVideoSession({
|
|
1389
|
+
sessionId: this.resolvedSessionId,
|
|
568
1390
|
name: this.currentUser.name,
|
|
569
1391
|
lastname: this.currentUser.lastname,
|
|
570
|
-
|
|
571
|
-
|
|
1392
|
+
})
|
|
1393
|
+
.subscribe({
|
|
572
1394
|
next: (tokenResponse) => {
|
|
1395
|
+
console.log('[TAS DEBUG] Token response:', tokenResponse);
|
|
1396
|
+
// Handle case where HTTP adapter returns error in next instead of error handler
|
|
1397
|
+
if (!tokenResponse?.content?.token) {
|
|
1398
|
+
console.error('[TAS DEBUG] Invalid token response:', tokenResponse);
|
|
1399
|
+
this.state = WaitingRoomState.ERROR;
|
|
1400
|
+
this.errorMessage = tokenResponse?.message || 'Error al iniciar la sesión. Respuesta inválida.';
|
|
1401
|
+
this.tasService.stopStatusPolling();
|
|
1402
|
+
this.cdr.detectChanges();
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
console.log('[TAS DEBUG] Token obtained successfully, joining session...');
|
|
573
1406
|
this.token = tokenResponse.content.token;
|
|
574
|
-
this.state = WaitingRoomState.
|
|
1407
|
+
this.state = WaitingRoomState.JOINING;
|
|
1408
|
+
this.cdr.detectChanges();
|
|
1409
|
+
// Auto-join immediately
|
|
1410
|
+
this.tasService.stopStatusPolling();
|
|
1411
|
+
this.activeModal.close('joining');
|
|
1412
|
+
this.openVideoCallModal();
|
|
575
1413
|
},
|
|
576
1414
|
error: (err) => {
|
|
577
|
-
console.error('
|
|
1415
|
+
console.error('[TAS DEBUG] /start request failed:', err);
|
|
578
1416
|
this.state = WaitingRoomState.ERROR;
|
|
579
|
-
this.errorMessage =
|
|
580
|
-
|
|
1417
|
+
this.errorMessage = err?.error?.message || err?.message || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
|
|
1418
|
+
this.tasService.stopStatusPolling();
|
|
1419
|
+
console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
|
|
1420
|
+
this.cdr.detectChanges();
|
|
1421
|
+
},
|
|
581
1422
|
}));
|
|
582
1423
|
}
|
|
583
|
-
/**
|
|
584
|
-
* Joins the video call session
|
|
585
|
-
*/
|
|
586
|
-
joinSession() {
|
|
587
|
-
if (!this.sessionId || !this.token) {
|
|
588
|
-
this.errorMessage = 'Cannot join session. Incomplete data.';
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
// Close waiting room and open video call
|
|
592
|
-
this.activeModal.close('joining');
|
|
593
|
-
this.openVideoCallModal();
|
|
594
|
-
}
|
|
595
1424
|
/**
|
|
596
1425
|
* Closes the waiting room
|
|
597
1426
|
*/
|
|
598
1427
|
cancel() {
|
|
1428
|
+
this.tasService.stopStatusPolling();
|
|
599
1429
|
this.activeModal.dismiss('cancel');
|
|
600
1430
|
}
|
|
601
1431
|
/**
|
|
602
1432
|
* Retry after an error
|
|
603
1433
|
*/
|
|
604
1434
|
retry() {
|
|
605
|
-
this.state = WaitingRoomState.
|
|
1435
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
606
1436
|
this.errorMessage = '';
|
|
607
1437
|
this.token = '';
|
|
608
|
-
this.
|
|
609
|
-
// Only reset sessionId if we don't have an existing one from input
|
|
610
|
-
if (!this.hasExistingSession) {
|
|
611
|
-
this.sessionId = '';
|
|
612
|
-
}
|
|
1438
|
+
this.checkStatus();
|
|
613
1439
|
}
|
|
614
|
-
|
|
615
|
-
buildUsersArray() {
|
|
616
|
-
this.users = [];
|
|
617
|
-
// Add owners from input
|
|
618
|
-
this.ownerUserIds.forEach(id => {
|
|
619
|
-
this.users.push({ userExternalId: id, rol: TasUserRole.OWNER });
|
|
620
|
-
});
|
|
621
|
-
// Add regular users from input
|
|
622
|
-
this.regularUserIds.forEach(id => {
|
|
623
|
-
this.users.push({ userExternalId: id, rol: TasUserRole.USER });
|
|
624
|
-
});
|
|
625
|
-
// Add moderators from input
|
|
626
|
-
this.moderatorUserIds.forEach(id => {
|
|
627
|
-
this.users.push({ userExternalId: id, rol: TasUserRole.MODERATOR });
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
setupViewModeSubscription() {
|
|
631
|
-
this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
|
|
632
|
-
// Re-open video call modal when returning from PiP mode
|
|
633
|
-
if (mode === ViewMode.FULLSCREEN &&
|
|
634
|
-
this.tasService.isCallActive() &&
|
|
635
|
-
!this.videoCallModalRef) {
|
|
636
|
-
const sessionId = this.tasService.sessionId;
|
|
637
|
-
const token = this.tasService.token;
|
|
638
|
-
if (sessionId && token) {
|
|
639
|
-
this.openVideoCallModal(true);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}));
|
|
643
|
-
}
|
|
644
|
-
openVideoCallModal(isReturningFromPip = false) {
|
|
1440
|
+
openVideoCallModal() {
|
|
645
1441
|
this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
|
|
646
1442
|
size: 'xl',
|
|
647
1443
|
windowClass: 'tas-video-modal',
|
|
648
1444
|
backdrop: 'static',
|
|
649
|
-
keyboard: false
|
|
1445
|
+
keyboard: false,
|
|
1446
|
+
});
|
|
1447
|
+
this.videoCallModalRef.componentInstance.sessionId = this.resolvedSessionId;
|
|
1448
|
+
this.videoCallModalRef.componentInstance.token = this.token;
|
|
1449
|
+
this.videoCallModalRef.componentInstance.appointmentId = this.resolvedAppointmentId;
|
|
1450
|
+
this.videoCallModalRef.componentInstance.videoCallId = this.videoCallId;
|
|
1451
|
+
this.videoCallModalRef.componentInstance.userId = this.currentUser?.id;
|
|
1452
|
+
this.videoCallModalRef.componentInstance.tenant = this.tenant;
|
|
1453
|
+
this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
|
|
1454
|
+
this.videoCallModalRef.componentInstance.isReturningFromPip = false;
|
|
1455
|
+
this.videoCallModalRef.result.then(() => {
|
|
1456
|
+
this.videoCallModalRef = null;
|
|
1457
|
+
}, () => {
|
|
1458
|
+
this.videoCallModalRef = null;
|
|
650
1459
|
});
|
|
651
|
-
const sessionIdToUse = this.sessionId || this.tasService.sessionId;
|
|
652
|
-
const tokenToUse = this.token || this.tasService.token;
|
|
653
|
-
this.videoCallModalRef.componentInstance.sessionId = sessionIdToUse;
|
|
654
|
-
this.videoCallModalRef.componentInstance.token = tokenToUse;
|
|
655
|
-
this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
|
|
656
|
-
this.videoCallModalRef.result.then(() => { this.videoCallModalRef = null; }, () => { this.videoCallModalRef = null; });
|
|
657
1460
|
}
|
|
658
1461
|
}
|
|
659
|
-
TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.
|
|
660
|
-
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { appointmentId: "appointmentId", product: "product", tenantId: "tenantId", currentUser: "currentUser", ownerUserIds: "ownerUserIds", regularUserIds: "regularUserIds", moderatorUserIds: "moderatorUserIds", existingSessionId: "existingSessionId" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n\t<!-- Header -->\n\t<div class=\"waiting-room-header\">\n\t\t<div class=\"header-icon\">\n\t\t\t<i class=\"fa fa-video-camera\"></i>\n\t\t</div>\n\t\t<h2 class=\"header-title\">Waiting Room</h2>\n\t\t<p class=\"header-subtitle\">Prepare for your video call</p>\n\t\t<button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n\t\t\t<span aria-hidden=\"true\">×</span>\n\t\t</button>\n\t</div>\n\n\t<!-- Content -->\n\t<div class=\"waiting-room-content\">\n\t\t<!-- IDLE State - Show options -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.IDLE\">\n\t\t\t\n\t\t\t<!-- Tab switcher (only if no existingSessionId was passed) -->\n\t\t\t<div class=\"mode-tabs\" *ngIf=\"!hasExistingSession\">\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"mode-tab\" \n\t\t\t\t\t[class.active]=\"!showJoinExistingUI\"\n\t\t\t\t\t(click)=\"showJoinExistingUI = false\">\n\t\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t\t\tCreate New\n\t\t\t\t</button>\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"mode-tab\" \n\t\t\t\t\t[class.active]=\"showJoinExistingUI\"\n\t\t\t\t\t(click)=\"showJoinExistingUI = true\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t\tJoin Existing\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<!-- Create New Room UI -->\n\t\t\t<div class=\"mode-content\" *ngIf=\"!showJoinExistingUI && !hasExistingSession\">\n\t\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t\t</div>\n\t\t\t\t<p class=\"state-message\">\n\t\t\t\t\tCreate a new video call room\n\t\t\t\t</p>\n\t\t\t\t<p class=\"state-submessage\">\n\t\t\t\t\tStart a new session for this appointment\n\t\t\t\t</p>\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t\t(click)=\"createRoom()\">\n\t\t\t\t\t<i class=\"fa fa-plus\"></i>\n\t\t\t\t\tCreate Room\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<!-- Join Existing Room UI (manual input) -->\n\t\t\t<div class=\"mode-content\" *ngIf=\"showJoinExistingUI && !hasExistingSession\">\n\t\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t</div>\n\t\t\t\t<p class=\"state-message\">\n\t\t\t\t\tJoin an existing room\n\t\t\t\t</p>\n\t\t\t\t<p class=\"state-submessage\">\n\t\t\t\t\tEnter the session ID to join\n\t\t\t\t</p>\n\t\t\t\t<div class=\"session-input-container\">\n\t\t\t\t\t<input \n\t\t\t\t\t\ttype=\"text\" \n\t\t\t\t\t\tclass=\"session-input\"\n\t\t\t\t\t\t[(ngModel)]=\"manualSessionId\"\n\t\t\t\t\t\tplaceholder=\"Enter Session ID\"\n\t\t\t\t\t\t(keyup.enter)=\"joinExistingWithManualId()\">\n\t\t\t\t\t<button \n\t\t\t\t\t\ttype=\"button\" \n\t\t\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t\t\t[disabled]=\"!manualSessionId || manualSessionId.trim() === ''\"\n\t\t\t\t\t\t(click)=\"joinExistingWithManualId()\">\n\t\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t\t\tJoin Room\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Join Existing Room UI (pre-filled session ID) -->\n\t\t\t<div class=\"mode-content\" *ngIf=\"hasExistingSession\">\n\t\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t</div>\n\t\t\t\t<p class=\"state-message\">\n\t\t\t\t\tJoin existing video call room\n\t\t\t\t</p>\n\t\t\t\t<p class=\"state-submessage\">\n\t\t\t\t\tPress the button to get access\n\t\t\t\t</p>\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t\t(click)=\"getTokenForExistingSession()\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t\tJoin Room\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- CREATING_ROOM State (only for new sessions) -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CREATING_ROOM\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tCreating video call room...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Creating</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && !isJoiningExisting\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tPreparing room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - Joining Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && isJoiningExisting\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tGetting room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Connecting</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- READY State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && !isJoiningExisting\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tRoom is ready!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- READY State - Joining Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && isJoiningExisting\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tReady to join!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Connected</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- ERROR State -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n\t\t\t<div class=\"state-icon error\">\n\t\t\t\t<i class=\"fa fa-exclamation-triangle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message error\">\n\t\t\t\tAn error occurred\n\t\t\t</p>\n\t\t\t<p class=\"error-details\" *ngIf=\"errorMessage\">\n\t\t\t\t{{ errorMessage }}\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn retry-btn\"\n\t\t\t\t(click)=\"retry()\">\n\t\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t\t\tRetry\n\t\t\t</button>\n\t\t</div>\n\t</div>\n\n\t<!-- Footer -->\n\t<div class=\"waiting-room-footer\">\n\t\t<button \n\t\t\ttype=\"button\" \n\t\t\tclass=\"btn cancel-btn\"\n\t\t\t(click)=\"cancel()\">\n\t\t\tCancel\n\t\t</button>\n\t</div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:32px 40px 24px;text-align:center;border-bottom:1px solid #e9ecef}.waiting-room-header .header-icon{width:72px;height:72px;margin:0 auto 16px;background:linear-gradient(135deg,#1da4b1 0%,#0077b3 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #1da4b140}.waiting-room-header .header-icon i{font-size:28px;color:#fff}.waiting-room-header .header-title{margin:0 0 8px;font-size:20px;font-weight:700;line-height:28px;color:#212529}.waiting-room-header .header-subtitle{margin:0;font-size:14px;color:#6c757d;font-weight:400}.waiting-room-header .close-btn{position:absolute;top:16px;right:16px;width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.mode-tabs{display:flex;justify-content:center;gap:8px;margin-bottom:24px;padding:4px;background:#f8f9fa;border-radius:8px}.mode-tab{flex:1;padding:10px 16px;font-size:13px;font-weight:600;border:none;border-radius:6px;background:transparent;color:#6c757d;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px}.mode-tab i{font-size:14px}.mode-tab:hover{color:#212529}.mode-tab.active{background:#ffffff;color:#1da4b1;box-shadow:0 2px 8px #00000014}.mode-content{animation:fadeIn .3s ease}.session-input-container{display:flex;flex-direction:column;gap:16px;margin-top:8px}.session-input{width:100%;padding:14px 16px;font-size:14px;border:2px solid #e9ecef;border-radius:8px;background:#ffffff;color:#212529;transition:all .2s ease;text-align:center;font-family:Monaco,Consolas,monospace;letter-spacing:.5px}.session-input::placeholder{color:#6c757d;font-family:inherit;letter-spacing:normal}.session-input:focus{outline:none;border-color:#1da4b1;box-shadow:0 0 0 3px #1da4b126}.session-input:hover:not(:focus){border-color:#6c757d}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
661
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.
|
|
1462
|
+
TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }, { token: i1.NgbModal }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1463
|
+
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\">×</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_JOINABLE State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.WAITING_FOR_JOINABLE\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Esperando que la sesi\u00F3n est\u00E9 disponible...</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </div>\n\n <!-- READY State (Join button enabled) -->\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 <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 <!-- JOINING State (Auto-joining) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.JOINING\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Ingresando a la llamada...</p>\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: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1464
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
662
1465
|
type: Component,
|
|
663
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n\t<!-- Header -->\n\t<div class=\"waiting-room-header\">\n\t\t<div class=\"header-icon\">\n\t\t\t<i class=\"fa fa-video-camera\"></i>\n\t\t</div>\n\t\t<h2 class=\"header-title\">Waiting Room</h2>\n\t\t<p class=\"header-subtitle\">Prepare for your video call</p>\n\t\t<button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n\t\t\t<span aria-hidden=\"true\">×</span>\n\t\t</button>\n\t</div>\n\n\t<!-- Content -->\n\t<div class=\"waiting-room-content\">\n\t\t<!-- IDLE State - Show options -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.IDLE\">\n\t\t\t\n\t\t\t<!-- Tab switcher (only if no existingSessionId was passed) -->\n\t\t\t<div class=\"mode-tabs\" *ngIf=\"!hasExistingSession\">\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"mode-tab\" \n\t\t\t\t\t[class.active]=\"!showJoinExistingUI\"\n\t\t\t\t\t(click)=\"showJoinExistingUI = false\">\n\t\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t\t\tCreate New\n\t\t\t\t</button>\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"mode-tab\" \n\t\t\t\t\t[class.active]=\"showJoinExistingUI\"\n\t\t\t\t\t(click)=\"showJoinExistingUI = true\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t\tJoin Existing\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<!-- Create New Room UI -->\n\t\t\t<div class=\"mode-content\" *ngIf=\"!showJoinExistingUI && !hasExistingSession\">\n\t\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t\t</div>\n\t\t\t\t<p class=\"state-message\">\n\t\t\t\t\tCreate a new video call room\n\t\t\t\t</p>\n\t\t\t\t<p class=\"state-submessage\">\n\t\t\t\t\tStart a new session for this appointment\n\t\t\t\t</p>\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t\t(click)=\"createRoom()\">\n\t\t\t\t\t<i class=\"fa fa-plus\"></i>\n\t\t\t\t\tCreate Room\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<!-- Join Existing Room UI (manual input) -->\n\t\t\t<div class=\"mode-content\" *ngIf=\"showJoinExistingUI && !hasExistingSession\">\n\t\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t</div>\n\t\t\t\t<p class=\"state-message\">\n\t\t\t\t\tJoin an existing room\n\t\t\t\t</p>\n\t\t\t\t<p class=\"state-submessage\">\n\t\t\t\t\tEnter the session ID to join\n\t\t\t\t</p>\n\t\t\t\t<div class=\"session-input-container\">\n\t\t\t\t\t<input \n\t\t\t\t\t\ttype=\"text\" \n\t\t\t\t\t\tclass=\"session-input\"\n\t\t\t\t\t\t[(ngModel)]=\"manualSessionId\"\n\t\t\t\t\t\tplaceholder=\"Enter Session ID\"\n\t\t\t\t\t\t(keyup.enter)=\"joinExistingWithManualId()\">\n\t\t\t\t\t<button \n\t\t\t\t\t\ttype=\"button\" \n\t\t\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t\t\t[disabled]=\"!manualSessionId || manualSessionId.trim() === ''\"\n\t\t\t\t\t\t(click)=\"joinExistingWithManualId()\">\n\t\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t\t\tJoin Room\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Join Existing Room UI (pre-filled session ID) -->\n\t\t\t<div class=\"mode-content\" *ngIf=\"hasExistingSession\">\n\t\t\t\t<div class=\"state-icon idle\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t</div>\n\t\t\t\t<p class=\"state-message\">\n\t\t\t\t\tJoin existing video call room\n\t\t\t\t</p>\n\t\t\t\t<p class=\"state-submessage\">\n\t\t\t\t\tPress the button to get access\n\t\t\t\t</p>\n\t\t\t\t<button \n\t\t\t\t\ttype=\"button\" \n\t\t\t\t\tclass=\"btn action-btn create-btn\"\n\t\t\t\t\t(click)=\"getTokenForExistingSession()\">\n\t\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\t\tJoin Room\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- CREATING_ROOM State (only for new sessions) -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CREATING_ROOM\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tCreating video call room...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Creating</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && !isJoiningExisting\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tPreparing room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GETTING_TOKEN State - Joining Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN && isJoiningExisting\">\n\t\t\t<div class=\"state-icon loading\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t</div>\n\t\t\t<p class=\"state-message\">\n\t\t\t\tGetting room access...\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step active\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Connecting</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step\">\n\t\t\t\t\t<span class=\"step-indicator\"></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- READY State - New Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && !isJoiningExisting\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tRoom is ready!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Created</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Access</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- READY State - Joining Existing Session -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY && isJoiningExisting\">\n\t\t\t<div class=\"state-icon ready\">\n\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message success\">\n\t\t\t\tReady to join!\n\t\t\t</p>\n\t\t\t<p class=\"state-submessage\">\n\t\t\t\tYou can join the video call when ready\n\t\t\t</p>\n\t\t\t<div class=\"progress-steps\">\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Connected</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"step completed\">\n\t\t\t\t\t<span class=\"step-indicator\"><i class=\"fa fa-check\"></i></span>\n\t\t\t\t\t<span class=\"step-label\">Ready</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn join-btn\"\n\t\t\t\t(click)=\"joinSession()\">\n\t\t\t\t<i class=\"fa fa-sign-in\"></i>\n\t\t\t\tJoin Session\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- ERROR State -->\n\t\t<div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n\t\t\t<div class=\"state-icon error\">\n\t\t\t\t<i class=\"fa fa-exclamation-triangle\"></i>\n\t\t\t</div>\n\t\t\t<p class=\"state-message error\">\n\t\t\t\tAn error occurred\n\t\t\t</p>\n\t\t\t<p class=\"error-details\" *ngIf=\"errorMessage\">\n\t\t\t\t{{ errorMessage }}\n\t\t\t</p>\n\t\t\t<button \n\t\t\t\ttype=\"button\" \n\t\t\t\tclass=\"btn action-btn retry-btn\"\n\t\t\t\t(click)=\"retry()\">\n\t\t\t\t<i class=\"fa fa-refresh\"></i>\n\t\t\t\tRetry\n\t\t\t</button>\n\t\t</div>\n\t</div>\n\n\t<!-- Footer -->\n\t<div class=\"waiting-room-footer\">\n\t\t<button \n\t\t\ttype=\"button\" \n\t\t\tclass=\"btn cancel-btn\"\n\t\t\t(click)=\"cancel()\">\n\t\t\tCancel\n\t\t</button>\n\t</div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:32px 40px 24px;text-align:center;border-bottom:1px solid #e9ecef}.waiting-room-header .header-icon{width:72px;height:72px;margin:0 auto 16px;background:linear-gradient(135deg,#1da4b1 0%,#0077b3 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #1da4b140}.waiting-room-header .header-icon i{font-size:28px;color:#fff}.waiting-room-header .header-title{margin:0 0 8px;font-size:20px;font-weight:700;line-height:28px;color:#212529}.waiting-room-header .header-subtitle{margin:0;font-size:14px;color:#6c757d;font-weight:400}.waiting-room-header .close-btn{position:absolute;top:16px;right:16px;width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.mode-tabs{display:flex;justify-content:center;gap:8px;margin-bottom:24px;padding:4px;background:#f8f9fa;border-radius:8px}.mode-tab{flex:1;padding:10px 16px;font-size:13px;font-weight:600;border:none;border-radius:6px;background:transparent;color:#6c757d;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px}.mode-tab i{font-size:14px}.mode-tab:hover{color:#212529}.mode-tab.active{background:#ffffff;color:#1da4b1;box-shadow:0 2px 8px #00000014}.mode-content{animation:fadeIn .3s ease}.session-input-container{display:flex;flex-direction:column;gap:16px;margin-top:8px}.session-input{width:100%;padding:14px 16px;font-size:14px;border:2px solid #e9ecef;border-radius:8px;background:#ffffff;color:#212529;transition:all .2s ease;text-align:center;font-family:Monaco,Consolas,monospace;letter-spacing:.5px}.session-input::placeholder{color:#6c757d;font-family:inherit;letter-spacing:normal}.session-input:focus{outline:none;border-color:#1da4b1;box-shadow:0 0 0 3px #1da4b126}.session-input:hover:not(:focus){border-color:#6c757d}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"] }]
|
|
664
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: i1.NgbModal }]; }, propDecorators: {
|
|
665
|
-
type: Input
|
|
666
|
-
}], product: [{
|
|
667
|
-
type: Input
|
|
668
|
-
}], tenantId: [{
|
|
1466
|
+
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\">×</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_JOINABLE State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.WAITING_FOR_JOINABLE\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Esperando que la sesi\u00F3n est\u00E9 disponible...</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </div>\n\n <!-- READY State (Join button enabled) -->\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 <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 <!-- JOINING State (Auto-joining) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.JOINING\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Ingresando a la llamada...</p>\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"] }]
|
|
1467
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
669
1468
|
type: Input
|
|
670
|
-
}],
|
|
671
|
-
type: Input
|
|
672
|
-
}], ownerUserIds: [{
|
|
1469
|
+
}], entityId: [{
|
|
673
1470
|
type: Input
|
|
674
|
-
}],
|
|
1471
|
+
}], tenant: [{
|
|
675
1472
|
type: Input
|
|
676
|
-
}],
|
|
1473
|
+
}], businessRole: [{
|
|
677
1474
|
type: Input
|
|
678
|
-
}],
|
|
1475
|
+
}], currentUser: [{
|
|
679
1476
|
type: Input
|
|
680
1477
|
}] } });
|
|
681
1478
|
|
|
@@ -683,116 +1480,203 @@ class TasButtonComponent {
|
|
|
683
1480
|
constructor(modalService, tasService) {
|
|
684
1481
|
this.modalService = modalService;
|
|
685
1482
|
this.tasService = tasService;
|
|
686
|
-
|
|
687
|
-
this.
|
|
688
|
-
this.
|
|
689
|
-
|
|
690
|
-
this.
|
|
691
|
-
|
|
692
|
-
this.existingSessionId = "";
|
|
693
|
-
/** Optional: Custom button text */
|
|
694
|
-
this.buttonText = "Iniciar TAS";
|
|
1483
|
+
// Status endpoint params
|
|
1484
|
+
this.roomType = TasRoomType.TAS;
|
|
1485
|
+
this.businessRole = TasBusinessRole.USER;
|
|
1486
|
+
// Style customization
|
|
1487
|
+
this.variant = 'default';
|
|
1488
|
+
this.buttonLabel = 'Iniciar TAS';
|
|
695
1489
|
this.isLoading = false;
|
|
1490
|
+
// Status check state
|
|
1491
|
+
this.isCheckingStatus = false;
|
|
1492
|
+
this.isStatusError = false;
|
|
1493
|
+
this.statusErrorMessage = '';
|
|
1494
|
+
this.isJoinable = false; // Tracks joinable field from status response
|
|
696
1495
|
this.subscriptions = new Subscription();
|
|
697
1496
|
this.currentModalRef = null;
|
|
698
1497
|
this.videoCallModalRef = null;
|
|
1498
|
+
this.statusPollingInterval = null;
|
|
1499
|
+
this.STATUS_POLL_INTERVAL_MS = 30000; // 30 seconds
|
|
699
1500
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1501
|
+
/** Whether user is backoffice (or admin/manager) */
|
|
1502
|
+
get isBackoffice() {
|
|
1503
|
+
return (this.businessRole === TasBusinessRole.BACKOFFICE ||
|
|
1504
|
+
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
1505
|
+
this.businessRole === TasBusinessRole.MANAGER);
|
|
1506
|
+
}
|
|
1507
|
+
/** Whether the button should be disabled */
|
|
1508
|
+
get isDisabled() {
|
|
1509
|
+
return this.isLoading || this.isStatusError || !this.isJoinable;
|
|
1510
|
+
}
|
|
1511
|
+
/** Reason why the button is disabled (for tooltip) */
|
|
1512
|
+
get disabledReason() {
|
|
1513
|
+
if (this.isLoading) {
|
|
1514
|
+
return '';
|
|
703
1515
|
}
|
|
1516
|
+
if (this.isStatusError) {
|
|
1517
|
+
return this.statusErrorMessage || 'Error al verificar el estado';
|
|
1518
|
+
}
|
|
1519
|
+
if (!this.isJoinable) {
|
|
1520
|
+
return 'Todavía no es el horario de la llamada';
|
|
1521
|
+
}
|
|
1522
|
+
return '';
|
|
1523
|
+
}
|
|
1524
|
+
ngOnInit() {
|
|
704
1525
|
// Subscribe to viewMode to handle PiP return
|
|
705
|
-
this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
|
|
706
|
-
// Reopen video call modal when returning from PiP
|
|
707
|
-
if (mode === ViewMode.FULLSCREEN &&
|
|
708
|
-
this.tasService.isCallActive() &&
|
|
709
|
-
!this.videoCallModalRef) {
|
|
710
|
-
const sessionId = this.tasService.sessionId;
|
|
711
|
-
const token = this.tasService.token;
|
|
712
|
-
if (sessionId && token) {
|
|
713
|
-
this.openVideoCallModal(true);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
1526
|
+
this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
|
|
716
1527
|
// When entering PiP, clear the videoCallModalRef since modal will close
|
|
717
1528
|
if (mode === ViewMode.PIP) {
|
|
718
1529
|
this.videoCallModalRef = null;
|
|
719
1530
|
}
|
|
720
1531
|
}));
|
|
1532
|
+
// Start status checking
|
|
1533
|
+
this.startStatusPolling();
|
|
721
1534
|
}
|
|
722
1535
|
ngOnDestroy() {
|
|
723
1536
|
this.subscriptions.unsubscribe();
|
|
1537
|
+
this.stopStatusPolling();
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Start polling status every 30 seconds
|
|
1541
|
+
*/
|
|
1542
|
+
startStatusPolling() {
|
|
1543
|
+
// Initial status check
|
|
1544
|
+
this.checkStatus();
|
|
1545
|
+
// Set up periodic polling
|
|
1546
|
+
this.statusPollingInterval = setInterval(() => {
|
|
1547
|
+
this.checkStatus();
|
|
1548
|
+
}, this.STATUS_POLL_INTERVAL_MS);
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Stop status polling
|
|
1552
|
+
*/
|
|
1553
|
+
stopStatusPolling() {
|
|
1554
|
+
if (this.statusPollingInterval) {
|
|
1555
|
+
clearInterval(this.statusPollingInterval);
|
|
1556
|
+
this.statusPollingInterval = null;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Check status endpoint to determine if button should be enabled
|
|
1561
|
+
*/
|
|
1562
|
+
checkStatus() {
|
|
1563
|
+
// Skip if required inputs are not available
|
|
1564
|
+
if (!this.tenant || !this.entityId) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
this.isCheckingStatus = true;
|
|
1568
|
+
this.statusErrorMessage = '';
|
|
1569
|
+
this.subscriptions.add(this.tasService.getProxyVideoStatus({
|
|
1570
|
+
roomType: this.roomType,
|
|
1571
|
+
entityId: this.entityId,
|
|
1572
|
+
tenant: this.tenant,
|
|
1573
|
+
businessRole: this.businessRole,
|
|
1574
|
+
}).subscribe({
|
|
1575
|
+
next: (response) => {
|
|
1576
|
+
// Check if response is actually an error (some HTTP adapters return errors in next)
|
|
1577
|
+
// Also check for undefined/null or missing content
|
|
1578
|
+
const isErrorResponse = !response ||
|
|
1579
|
+
!response.content ||
|
|
1580
|
+
response?.ok === false ||
|
|
1581
|
+
response?.status >= 400 ||
|
|
1582
|
+
response?.error ||
|
|
1583
|
+
response?.name === 'HttpErrorResponse';
|
|
1584
|
+
if (isErrorResponse) {
|
|
1585
|
+
this.isCheckingStatus = false;
|
|
1586
|
+
this.isStatusError = true;
|
|
1587
|
+
this.statusErrorMessage = response?.error?.message || response?.message || 'Error checking status';
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
this.isCheckingStatus = false;
|
|
1591
|
+
this.isStatusError = false;
|
|
1592
|
+
this.statusErrorMessage = '';
|
|
1593
|
+
// Update joinable state from response
|
|
1594
|
+
this.isJoinable = response.content?.joinable ?? false;
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
error: (err) => {
|
|
1598
|
+
this.isCheckingStatus = false;
|
|
1599
|
+
this.isStatusError = true;
|
|
1600
|
+
this.statusErrorMessage = err?.error?.message || err?.message || 'Error checking status';
|
|
1601
|
+
},
|
|
1602
|
+
}));
|
|
724
1603
|
}
|
|
725
1604
|
onClick() {
|
|
726
|
-
if (!this.
|
|
727
|
-
|
|
1605
|
+
if (!this.tenant || !this.currentUser?.name) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (!this.entityId) {
|
|
728
1609
|
return;
|
|
729
1610
|
}
|
|
730
1611
|
this.openWaitingRoomModal();
|
|
731
1612
|
}
|
|
732
1613
|
openWaitingRoomModal() {
|
|
733
1614
|
this.currentModalRef = this.modalService.open(TasWaitingRoomComponent, {
|
|
734
|
-
size:
|
|
735
|
-
windowClass:
|
|
736
|
-
backdrop:
|
|
1615
|
+
size: 'lg',
|
|
1616
|
+
windowClass: 'tas-waiting-room-modal',
|
|
1617
|
+
backdrop: 'static',
|
|
737
1618
|
keyboard: false,
|
|
738
|
-
centered: true
|
|
1619
|
+
centered: true,
|
|
739
1620
|
});
|
|
740
1621
|
// Pass all necessary inputs to the waiting room component
|
|
741
|
-
this.currentModalRef.componentInstance.
|
|
742
|
-
this.currentModalRef.componentInstance.
|
|
743
|
-
this.currentModalRef.componentInstance.
|
|
1622
|
+
this.currentModalRef.componentInstance.roomType = this.roomType;
|
|
1623
|
+
this.currentModalRef.componentInstance.entityId = this.entityId;
|
|
1624
|
+
this.currentModalRef.componentInstance.tenant = this.tenant;
|
|
1625
|
+
this.currentModalRef.componentInstance.businessRole = this.businessRole;
|
|
744
1626
|
this.currentModalRef.componentInstance.currentUser = this.currentUser;
|
|
745
|
-
this.currentModalRef.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
this.currentModalRef.result.then(() => { this.currentModalRef = null; }, () => { this.currentModalRef = null; });
|
|
1627
|
+
this.currentModalRef.result.then(() => {
|
|
1628
|
+
this.currentModalRef = null;
|
|
1629
|
+
}, () => {
|
|
1630
|
+
this.currentModalRef = null;
|
|
1631
|
+
});
|
|
751
1632
|
}
|
|
752
1633
|
openVideoCallModal(isReturningFromPip = false) {
|
|
753
1634
|
this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
|
|
754
1635
|
size: 'xl',
|
|
755
1636
|
windowClass: 'tas-video-modal',
|
|
756
1637
|
backdrop: 'static',
|
|
757
|
-
keyboard: false
|
|
1638
|
+
keyboard: false,
|
|
758
1639
|
});
|
|
759
1640
|
this.videoCallModalRef.componentInstance.sessionId = this.tasService.sessionId;
|
|
760
1641
|
this.videoCallModalRef.componentInstance.token = this.tasService.token;
|
|
1642
|
+
this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
|
|
761
1643
|
this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
|
|
762
|
-
this.videoCallModalRef.result.then(() => {
|
|
1644
|
+
this.videoCallModalRef.result.then(() => {
|
|
1645
|
+
this.videoCallModalRef = null;
|
|
1646
|
+
}, () => {
|
|
1647
|
+
this.videoCallModalRef = null;
|
|
1648
|
+
});
|
|
763
1649
|
}
|
|
764
1650
|
}
|
|
765
|
-
TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.
|
|
766
|
-
TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.
|
|
767
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.
|
|
1651
|
+
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 });
|
|
1652
|
+
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", variant: "variant", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.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}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;padding:10px 24px;font-weight:500;margin-right:0}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"], directives: [{ type: i1.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "triggers", "container", "disableTooltip", "tooltipClass", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }, { type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1653
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
|
|
768
1654
|
type: Component,
|
|
769
|
-
args: [{ selector:
|
|
770
|
-
}], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: {
|
|
1655
|
+
args: [{ selector: 'tas-btn', template: "<span\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.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}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;padding:10px 24px;font-weight:500;margin-right:0}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"] }]
|
|
1656
|
+
}], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: { roomType: [{
|
|
771
1657
|
type: Input
|
|
772
|
-
}],
|
|
773
|
-
type: Input
|
|
774
|
-
}], tenantId: [{
|
|
775
|
-
type: Input
|
|
776
|
-
}], currentUser: [{
|
|
1658
|
+
}], entityId: [{
|
|
777
1659
|
type: Input
|
|
778
|
-
}],
|
|
1660
|
+
}], tenant: [{
|
|
779
1661
|
type: Input
|
|
780
|
-
}],
|
|
1662
|
+
}], businessRole: [{
|
|
781
1663
|
type: Input
|
|
782
|
-
}],
|
|
1664
|
+
}], currentUser: [{
|
|
783
1665
|
type: Input
|
|
784
|
-
}],
|
|
1666
|
+
}], variant: [{
|
|
785
1667
|
type: Input
|
|
786
|
-
}],
|
|
1668
|
+
}], buttonLabel: [{
|
|
787
1669
|
type: Input
|
|
788
1670
|
}] } });
|
|
789
1671
|
|
|
790
1672
|
class TasFloatingCallComponent {
|
|
791
|
-
constructor(tasService) {
|
|
1673
|
+
constructor(tasService, modalService) {
|
|
792
1674
|
this.tasService = tasService;
|
|
1675
|
+
this.modalService = modalService;
|
|
793
1676
|
this.isVisible = false;
|
|
794
1677
|
this.isMuted = false;
|
|
795
1678
|
this.subscriptions = new Subscription();
|
|
1679
|
+
this.videoCallModalRef = null;
|
|
796
1680
|
// Margin from screen edges (in pixels)
|
|
797
1681
|
this.PIP_MARGIN = 20;
|
|
798
1682
|
}
|
|
@@ -805,6 +1689,7 @@ class TasFloatingCallComponent {
|
|
|
805
1689
|
}
|
|
806
1690
|
// Public Methods
|
|
807
1691
|
onExpand() {
|
|
1692
|
+
this.openVideoCallModal(true);
|
|
808
1693
|
this.tasService.exitPipMode();
|
|
809
1694
|
}
|
|
810
1695
|
onHangUp() {
|
|
@@ -816,23 +1701,45 @@ class TasFloatingCallComponent {
|
|
|
816
1701
|
// Private Methods
|
|
817
1702
|
setupSubscriptions() {
|
|
818
1703
|
// Call state subscription
|
|
819
|
-
this.subscriptions.add(this.tasService.callState$.subscribe(state => {
|
|
1704
|
+
this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
|
|
820
1705
|
if (state === CallState.DISCONNECTED) {
|
|
821
1706
|
this.isVisible = false;
|
|
822
1707
|
}
|
|
823
1708
|
}));
|
|
824
1709
|
// View mode subscription
|
|
825
|
-
this.subscriptions.add(this.tasService.viewMode$.subscribe(mode => {
|
|
1710
|
+
this.subscriptions.add(this.tasService.viewMode$.subscribe((mode) => {
|
|
826
1711
|
this.isVisible = mode === ViewMode.PIP && this.tasService.isCallActive();
|
|
827
1712
|
if (this.isVisible) {
|
|
828
1713
|
setTimeout(() => this.initInteract(), 100);
|
|
1714
|
+
// Clear modal ref if we enter PiP mode (modal closes itself)
|
|
1715
|
+
this.videoCallModalRef = null;
|
|
829
1716
|
}
|
|
830
1717
|
}));
|
|
831
1718
|
// Mute state subscription
|
|
832
|
-
this.subscriptions.add(this.tasService.isMuted$.subscribe(muted => {
|
|
1719
|
+
this.subscriptions.add(this.tasService.isMuted$.subscribe((muted) => {
|
|
833
1720
|
this.isMuted = muted;
|
|
834
1721
|
}));
|
|
835
1722
|
}
|
|
1723
|
+
openVideoCallModal(isReturningFromPip = false) {
|
|
1724
|
+
if (this.videoCallModalRef) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
|
|
1728
|
+
size: 'xl',
|
|
1729
|
+
windowClass: 'tas-video-modal',
|
|
1730
|
+
backdrop: 'static',
|
|
1731
|
+
keyboard: false,
|
|
1732
|
+
});
|
|
1733
|
+
this.videoCallModalRef.componentInstance.sessionId = this.tasService.sessionId;
|
|
1734
|
+
this.videoCallModalRef.componentInstance.token = this.tasService.token;
|
|
1735
|
+
this.videoCallModalRef.componentInstance.businessRole = this.tasService.businessRole;
|
|
1736
|
+
this.videoCallModalRef.componentInstance.isReturningFromPip = isReturningFromPip;
|
|
1737
|
+
this.videoCallModalRef.result.then(() => {
|
|
1738
|
+
this.videoCallModalRef = null;
|
|
1739
|
+
}, () => {
|
|
1740
|
+
this.videoCallModalRef = null;
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
836
1743
|
initInteract() {
|
|
837
1744
|
interact('.tas-floating-container').unset();
|
|
838
1745
|
// Create restriction area with margin
|
|
@@ -843,17 +1750,15 @@ class TasFloatingCallComponent {
|
|
|
843
1750
|
left: margin,
|
|
844
1751
|
top: margin,
|
|
845
1752
|
right: window.innerWidth - margin,
|
|
846
|
-
bottom: window.innerHeight - margin
|
|
1753
|
+
bottom: window.innerHeight - margin,
|
|
847
1754
|
};
|
|
848
1755
|
},
|
|
849
|
-
elementRect: { left: 0, right: 1, top: 0, bottom: 1 }
|
|
1756
|
+
elementRect: { left: 0, right: 1, top: 0, bottom: 1 },
|
|
850
1757
|
};
|
|
851
1758
|
interact('.tas-floating-container')
|
|
852
1759
|
.draggable({
|
|
853
1760
|
inertia: true,
|
|
854
|
-
modifiers: [
|
|
855
|
-
interact.modifiers.restrict(restrictToBodyWithMargin)
|
|
856
|
-
],
|
|
1761
|
+
modifiers: [interact.modifiers.restrict(restrictToBodyWithMargin)],
|
|
857
1762
|
autoScroll: false,
|
|
858
1763
|
listeners: {
|
|
859
1764
|
move: (event) => {
|
|
@@ -863,8 +1768,8 @@ class TasFloatingCallComponent {
|
|
|
863
1768
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
|
864
1769
|
target.setAttribute('data-x', String(x));
|
|
865
1770
|
target.setAttribute('data-y', String(y));
|
|
866
|
-
}
|
|
867
|
-
}
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
868
1773
|
})
|
|
869
1774
|
.resizable({
|
|
870
1775
|
edges: { left: false, right: true, bottom: true, top: false },
|
|
@@ -882,7 +1787,7 @@ class TasFloatingCallComponent {
|
|
|
882
1787
|
target.style.transform = `translate(${x}px, ${y}px)`;
|
|
883
1788
|
target.setAttribute('data-x', String(x));
|
|
884
1789
|
target.setAttribute('data-y', String(y));
|
|
885
|
-
}
|
|
1790
|
+
},
|
|
886
1791
|
},
|
|
887
1792
|
modifiers: [
|
|
888
1793
|
interact.modifiers.restrictEdges({
|
|
@@ -890,25 +1795,126 @@ class TasFloatingCallComponent {
|
|
|
890
1795
|
left: margin,
|
|
891
1796
|
top: margin,
|
|
892
1797
|
right: window.innerWidth - margin,
|
|
893
|
-
bottom: window.innerHeight - margin
|
|
894
|
-
}
|
|
1798
|
+
bottom: window.innerHeight - margin,
|
|
1799
|
+
},
|
|
895
1800
|
}),
|
|
896
1801
|
interact.modifiers.restrictSize({
|
|
897
1802
|
min: { width: 200, height: 130 },
|
|
898
|
-
max: { width: 500, height: 350 }
|
|
1803
|
+
max: { width: 500, height: 350 },
|
|
899
1804
|
}),
|
|
900
|
-
interact.modifiers.aspectRatio({ ratio: 'preserve' })
|
|
1805
|
+
interact.modifiers.aspectRatio({ ratio: 'preserve' }),
|
|
901
1806
|
],
|
|
902
|
-
inertia: true
|
|
1807
|
+
inertia: true,
|
|
903
1808
|
});
|
|
904
1809
|
}
|
|
905
1810
|
}
|
|
906
|
-
TasFloatingCallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.
|
|
907
|
-
TasFloatingCallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.
|
|
908
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.
|
|
1811
|
+
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 });
|
|
1812
|
+
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"] });
|
|
1813
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFloatingCallComponent, decorators: [{
|
|
909
1814
|
type: Component,
|
|
910
|
-
args: [{ selector: 'tas-floating-call', template: "<div class=\"tas-floating-container\" [class.visible]=\"isVisible\">\n
|
|
911
|
-
}], ctorParameters: function () { return [{ type: TasService }]; } });
|
|
1815
|
+
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"] }]
|
|
1816
|
+
}], ctorParameters: function () { return [{ type: TasService }, { type: i1.NgbModal }]; } });
|
|
1817
|
+
|
|
1818
|
+
class TasIncomingAppointmentComponent {
|
|
1819
|
+
constructor(tasService) {
|
|
1820
|
+
this.tasService = tasService;
|
|
1821
|
+
// Passthrough inputs for tas-btn
|
|
1822
|
+
this.roomType = TasRoomType.TAS;
|
|
1823
|
+
this.businessRole = TasBusinessRole.USER;
|
|
1824
|
+
this.enterCall = new EventEmitter();
|
|
1825
|
+
this.appointment = null;
|
|
1826
|
+
this.isLoading = true;
|
|
1827
|
+
this.hasError = false;
|
|
1828
|
+
this.subscriptions = new Subscription();
|
|
1829
|
+
}
|
|
1830
|
+
ngOnInit() {
|
|
1831
|
+
this.loadAppointments();
|
|
1832
|
+
}
|
|
1833
|
+
ngOnDestroy() {
|
|
1834
|
+
this.subscriptions.unsubscribe();
|
|
1835
|
+
}
|
|
1836
|
+
loadAppointments() {
|
|
1837
|
+
const today = new Date();
|
|
1838
|
+
const in7Days = new Date(today);
|
|
1839
|
+
in7Days.setDate(today.getDate() + 7);
|
|
1840
|
+
this.subscriptions.add(this.tasService
|
|
1841
|
+
.getAppointments({ fromDate: this.formatDate(today), toDate: this.formatDate(in7Days) })
|
|
1842
|
+
.subscribe({
|
|
1843
|
+
next: (response) => {
|
|
1844
|
+
// Handle both array response and wrapped response (e.g., { content: [...] })
|
|
1845
|
+
const appointments = Array.isArray(response)
|
|
1846
|
+
? response
|
|
1847
|
+
: response?.content || [];
|
|
1848
|
+
// Filter for confirmed appointments and get the first one
|
|
1849
|
+
const confirmed = appointments.filter((a) => a.status === AppointmentStatus.CONFIRMED);
|
|
1850
|
+
this.appointment = confirmed.length > 0 ? confirmed[0] : null;
|
|
1851
|
+
this.isLoading = false;
|
|
1852
|
+
},
|
|
1853
|
+
error: () => {
|
|
1854
|
+
this.hasError = true;
|
|
1855
|
+
this.isLoading = false;
|
|
1856
|
+
},
|
|
1857
|
+
}));
|
|
1858
|
+
}
|
|
1859
|
+
onEnterCall() {
|
|
1860
|
+
if (this.appointment) {
|
|
1861
|
+
this.enterCall.emit(this.appointment);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Format date to Spanish format: "Lunes 8 de diciembre"
|
|
1866
|
+
*/
|
|
1867
|
+
get formattedDate() {
|
|
1868
|
+
if (!this.appointment)
|
|
1869
|
+
return '';
|
|
1870
|
+
const [year, month, day] = this.appointment.date.split('-').map(Number);
|
|
1871
|
+
const date = new Date(year, month - 1, day);
|
|
1872
|
+
const dayNames = [
|
|
1873
|
+
'Domingo', 'Lunes', 'Martes', 'Miércoles',
|
|
1874
|
+
'Jueves', 'Viernes', 'Sábado'
|
|
1875
|
+
];
|
|
1876
|
+
const monthNames = [
|
|
1877
|
+
'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
|
|
1878
|
+
'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
|
|
1879
|
+
];
|
|
1880
|
+
const dayName = dayNames[date.getDay()];
|
|
1881
|
+
const dayNum = date.getDate();
|
|
1882
|
+
const monthName = monthNames[date.getMonth()];
|
|
1883
|
+
return `${dayName} ${dayNum} de ${monthName}`;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Format time range: "9:00 - 9:30"
|
|
1887
|
+
*/
|
|
1888
|
+
get formattedTimeRange() {
|
|
1889
|
+
if (!this.appointment)
|
|
1890
|
+
return '';
|
|
1891
|
+
return `${this.appointment.startTime} - ${this.appointment.endTime}`;
|
|
1892
|
+
}
|
|
1893
|
+
formatDate(date) {
|
|
1894
|
+
const year = date.getFullYear();
|
|
1895
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1896
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
1897
|
+
return `${year}-${month}-${day}`;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1901
|
+
TasIncomingAppointmentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasIncomingAppointmentComponent, selector: "tas-incoming-appointment", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, outputs: { enterCall: "enterCall" }, ngImport: i0, template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && !appointment\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointment state -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointment\">\n \n <div class=\"appointment-card\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"appointment.title\" [size]=\"48\"></tas-avatar>\n \n <span class=\"doctor-name\">{{ appointment.title }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formattedDate }}</span>\n <span class=\"time\">{{ formattedTimeRange }}</span>\n </div>\n <tas-btn\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"roomType\"\n [entityId]=\"entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }, { type: TasButtonComponent, selector: "tas-btn", inputs: ["roomType", "entityId", "tenant", "businessRole", "currentUser", "variant", "buttonLabel"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1902
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
1903
|
+
type: Component,
|
|
1904
|
+
args: [{ selector: 'tas-incoming-appointment', template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && !appointment\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointment state -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointment\">\n \n <div class=\"appointment-card\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"appointment.title\" [size]=\"48\"></tas-avatar>\n \n <span class=\"doctor-name\">{{ appointment.title }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formattedDate }}</span>\n <span class=\"time\">{{ formattedTimeRange }}</span>\n </div>\n <tas-btn\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"roomType\"\n [entityId]=\"entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"] }]
|
|
1905
|
+
}], ctorParameters: function () { return [{ type: TasService }]; }, propDecorators: { roomType: [{
|
|
1906
|
+
type: Input
|
|
1907
|
+
}], entityId: [{
|
|
1908
|
+
type: Input
|
|
1909
|
+
}], tenant: [{
|
|
1910
|
+
type: Input
|
|
1911
|
+
}], businessRole: [{
|
|
1912
|
+
type: Input
|
|
1913
|
+
}], currentUser: [{
|
|
1914
|
+
type: Input
|
|
1915
|
+
}], enterCall: [{
|
|
1916
|
+
type: Output
|
|
1917
|
+
}] } });
|
|
912
1918
|
|
|
913
1919
|
class TasUellSdkModule {
|
|
914
1920
|
/**
|
|
@@ -946,43 +1952,44 @@ class TasUellSdkModule {
|
|
|
946
1952
|
providers: [
|
|
947
1953
|
{ provide: TAS_CONFIG, useValue: options.config },
|
|
948
1954
|
{ provide: TAS_HTTP_CLIENT, useClass: options.httpClient },
|
|
949
|
-
TasService
|
|
950
|
-
]
|
|
1955
|
+
TasService,
|
|
1956
|
+
],
|
|
951
1957
|
};
|
|
952
1958
|
}
|
|
953
1959
|
}
|
|
954
|
-
TasUellSdkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.
|
|
955
|
-
TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.
|
|
1960
|
+
TasUellSdkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
1961
|
+
TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, declarations: [TasButtonComponent,
|
|
956
1962
|
TasVideocallComponent,
|
|
957
1963
|
TasFloatingCallComponent,
|
|
958
|
-
TasWaitingRoomComponent
|
|
959
|
-
|
|
1964
|
+
TasWaitingRoomComponent,
|
|
1965
|
+
TasAvatarComponent,
|
|
1966
|
+
TasIncomingAppointmentComponent], imports: [CommonModule, FormsModule, NgbTooltipModule], exports: [TasButtonComponent,
|
|
960
1967
|
TasVideocallComponent,
|
|
961
1968
|
TasFloatingCallComponent,
|
|
962
|
-
TasWaitingRoomComponent
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: TasUellSdkModule, decorators: [{
|
|
1969
|
+
TasWaitingRoomComponent,
|
|
1970
|
+
TasAvatarComponent,
|
|
1971
|
+
TasIncomingAppointmentComponent] });
|
|
1972
|
+
TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, imports: [[CommonModule, FormsModule, NgbTooltipModule]] });
|
|
1973
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, decorators: [{
|
|
968
1974
|
type: NgModule,
|
|
969
1975
|
args: [{
|
|
970
1976
|
declarations: [
|
|
971
1977
|
TasButtonComponent,
|
|
972
1978
|
TasVideocallComponent,
|
|
973
1979
|
TasFloatingCallComponent,
|
|
974
|
-
TasWaitingRoomComponent
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
CommonModule,
|
|
978
|
-
FormsModule,
|
|
1980
|
+
TasWaitingRoomComponent,
|
|
1981
|
+
TasAvatarComponent,
|
|
1982
|
+
TasIncomingAppointmentComponent,
|
|
979
1983
|
],
|
|
1984
|
+
imports: [CommonModule, FormsModule, NgbTooltipModule],
|
|
980
1985
|
exports: [
|
|
981
1986
|
TasButtonComponent,
|
|
982
1987
|
TasVideocallComponent,
|
|
983
1988
|
TasFloatingCallComponent,
|
|
984
|
-
TasWaitingRoomComponent
|
|
985
|
-
|
|
1989
|
+
TasWaitingRoomComponent,
|
|
1990
|
+
TasAvatarComponent,
|
|
1991
|
+
TasIncomingAppointmentComponent,
|
|
1992
|
+
],
|
|
986
1993
|
}]
|
|
987
1994
|
}] });
|
|
988
1995
|
|
|
@@ -994,5 +2001,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
|
|
|
994
2001
|
* Generated bundle index. Do not edit.
|
|
995
2002
|
*/
|
|
996
2003
|
|
|
997
|
-
export { CallState, TAS_CONFIG, TAS_HTTP_CLIENT, TasButtonComponent, TasFloatingCallComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, ViewMode, WaitingRoomState };
|
|
2004
|
+
export { AppointmentStatus, CallState, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
998
2005
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|