tas-uell-sdk 0.1.2 → 0.2.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 +8 -1
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +30 -17
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +41 -28
- package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +126 -41
- package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +135 -34
- package/esm2020/lib/icons/tas-icons.mjs +17 -0
- package/esm2020/lib/interfaces/tas.interfaces.mjs +9 -1
- package/esm2020/lib/services/tas.service.mjs +99 -27
- package/fesm2015/tas-uell-sdk.mjs +449 -140
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +447 -139
- package/fesm2020/tas-uell-sdk.mjs.map +1 -1
- package/lib/components/tas-btn/tas-btn.component.d.ts +3 -1
- package/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.d.ts +15 -6
- package/lib/components/tas-videocall/tas-videocall.component.d.ts +42 -5
- package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +32 -13
- package/lib/icons/tas-icons.d.ts +8 -0
- package/lib/interfaces/tas.interfaces.d.ts +26 -4
- package/lib/services/tas.service.d.ts +13 -2
- package/package.json +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, Injectable, Inject, Component, ChangeDetectionStrategy, Input, ViewChild, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
|
-
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
4
|
-
import { map, catchError } from 'rxjs/operators';
|
|
3
|
+
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
|
4
|
+
import { map, catchError, finalize, shareReplay } from 'rxjs/operators';
|
|
5
5
|
import * as OT from '@opentok/client';
|
|
6
6
|
import { __awaiter } from 'tslib';
|
|
7
7
|
import interact from 'interactjs';
|
|
8
8
|
import * as i1 from '@ng-bootstrap/ng-bootstrap';
|
|
9
9
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
10
|
-
import * as i4 from '@angular/
|
|
10
|
+
import * as i4 from '@angular/platform-browser';
|
|
11
|
+
import * as i4$1 from '@angular/common';
|
|
11
12
|
import { CommonModule } from '@angular/common';
|
|
12
13
|
import { FormsModule } from '@angular/forms';
|
|
13
14
|
|
|
@@ -67,7 +68,14 @@ var UserCallAction;
|
|
|
67
68
|
UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
|
|
68
69
|
UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
|
|
69
70
|
UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
|
|
71
|
+
UserCallAction["DENY_GEOLOCATION"] = "DENY_GEOLOCATION";
|
|
70
72
|
})(UserCallAction || (UserCallAction = {}));
|
|
73
|
+
var GeoStatus;
|
|
74
|
+
(function (GeoStatus) {
|
|
75
|
+
GeoStatus["PENDING"] = "PENDING";
|
|
76
|
+
GeoStatus["GRANTED"] = "GRANTED";
|
|
77
|
+
GeoStatus["DENIED"] = "DENIED";
|
|
78
|
+
})(GeoStatus || (GeoStatus = {}));
|
|
71
79
|
var RoomUserStatus;
|
|
72
80
|
(function (RoomUserStatus) {
|
|
73
81
|
RoomUserStatus["ASSIGNED"] = "ASSIGNED";
|
|
@@ -94,6 +102,7 @@ var AppointmentStatus;
|
|
|
94
102
|
AppointmentStatus["CONFIRMED"] = "CONFIRMED";
|
|
95
103
|
AppointmentStatus["CANCELLED"] = "CANCELLED";
|
|
96
104
|
AppointmentStatus["RESCHEDULED"] = "RESCHEDULED";
|
|
105
|
+
AppointmentStatus["ACTIVE"] = "ACTIVE";
|
|
97
106
|
})(AppointmentStatus || (AppointmentStatus = {}));
|
|
98
107
|
|
|
99
108
|
class TasUtilityService {
|
|
@@ -198,6 +207,9 @@ class TasService {
|
|
|
198
207
|
// Observable for when all geo has been granted
|
|
199
208
|
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
200
209
|
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
210
|
+
// Observable for individual user geo status (for owner panel)
|
|
211
|
+
this.userGeoInfoSubject = new BehaviorSubject([]);
|
|
212
|
+
this.userGeoInfo$ = this.userGeoInfoSubject.asObservable();
|
|
201
213
|
this.statusPollingInterval = null;
|
|
202
214
|
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
203
215
|
this.wasOwnerPresent = false;
|
|
@@ -205,6 +217,10 @@ class TasService {
|
|
|
205
217
|
this.currentAppointmentId = null;
|
|
206
218
|
this.currentVideoCallId = null;
|
|
207
219
|
this.currentTenant = null;
|
|
220
|
+
// Status cache (1 second TTL)
|
|
221
|
+
this.STATUS_CACHE_TTL_MS = 1000;
|
|
222
|
+
this.statusCache = null;
|
|
223
|
+
this.inflightStatusRequests = new Map();
|
|
208
224
|
}
|
|
209
225
|
// ... (Getters and other methods remain unchanged)
|
|
210
226
|
/**
|
|
@@ -223,7 +239,6 @@ class TasService {
|
|
|
223
239
|
if (params.tenant) {
|
|
224
240
|
this.currentTenant = params.tenant;
|
|
225
241
|
}
|
|
226
|
-
console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
|
|
227
242
|
// Initial status fetch
|
|
228
243
|
this.fetchAndProcessStatus(params);
|
|
229
244
|
// Set up periodic polling
|
|
@@ -327,7 +342,6 @@ class TasService {
|
|
|
327
342
|
}
|
|
328
343
|
// Session Management
|
|
329
344
|
disconnectSession(clearStorage = true) {
|
|
330
|
-
console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
|
|
331
345
|
// Call finishSession before disconnecting if we have a sessionId
|
|
332
346
|
const sessionIdToFinish = this.currentSessionId;
|
|
333
347
|
// Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
|
|
@@ -358,11 +372,9 @@ class TasService {
|
|
|
358
372
|
businessRole: this.currentBusinessRole,
|
|
359
373
|
}).subscribe({
|
|
360
374
|
next: (response) => {
|
|
361
|
-
console.log('[TAS DEBUG] Session finished successfully:', response);
|
|
362
375
|
this.isFinishingSession = false;
|
|
363
376
|
},
|
|
364
377
|
error: (error) => {
|
|
365
|
-
console.error('[TAS DEBUG] Error finishing session:', error);
|
|
366
378
|
this.isFinishingSession = false;
|
|
367
379
|
},
|
|
368
380
|
});
|
|
@@ -397,7 +409,6 @@ class TasService {
|
|
|
397
409
|
// If so, don't restore it on page reload
|
|
398
410
|
const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
|
|
399
411
|
if (wasDisconnected === 'true') {
|
|
400
|
-
console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
|
|
401
412
|
this.clearSessionState();
|
|
402
413
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
403
414
|
return;
|
|
@@ -405,14 +416,12 @@ class TasService {
|
|
|
405
416
|
// Don't restore if we're already disconnected or if there's no active session
|
|
406
417
|
// This prevents restoring sessions that were properly disconnected
|
|
407
418
|
if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
|
|
408
|
-
console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
|
|
409
419
|
this.clearSessionState();
|
|
410
420
|
return;
|
|
411
421
|
}
|
|
412
422
|
try {
|
|
413
423
|
const state = JSON.parse(savedState);
|
|
414
424
|
if (state.sessionId && state.token) {
|
|
415
|
-
console.log('[TAS DEBUG] Restoring session from storage');
|
|
416
425
|
// Force PiP mode for restoration to ensure UI consistency
|
|
417
426
|
this.viewModeSubject.next(ViewMode.PIP);
|
|
418
427
|
if (state.businessRole) {
|
|
@@ -421,18 +430,15 @@ class TasService {
|
|
|
421
430
|
this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
|
|
422
431
|
containerId, this.currentBusinessRole)
|
|
423
432
|
.then(() => {
|
|
424
|
-
console.log('[TAS DEBUG] Session restored successfully');
|
|
425
433
|
// Clear the disconnected flag if restoration succeeds
|
|
426
434
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
427
435
|
})
|
|
428
436
|
.catch((err) => {
|
|
429
|
-
console.error('[TAS DEBUG] Failed to restore session:', err);
|
|
430
437
|
this.clearSessionState(); // Clear bad state
|
|
431
438
|
});
|
|
432
439
|
}
|
|
433
440
|
}
|
|
434
441
|
catch (e) {
|
|
435
|
-
console.error('[TAS DEBUG] Error parsing saved session state', e);
|
|
436
442
|
this.clearSessionState();
|
|
437
443
|
}
|
|
438
444
|
}
|
|
@@ -451,19 +457,77 @@ class TasService {
|
|
|
451
457
|
throw error;
|
|
452
458
|
}));
|
|
453
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Generate cache key from request payload
|
|
462
|
+
*/
|
|
463
|
+
getStatusCacheKey(payload) {
|
|
464
|
+
return `${payload.roomType}-${payload.entityId}-${payload.tenant}-${payload.sessionId || ''}`;
|
|
465
|
+
}
|
|
454
466
|
/**
|
|
455
467
|
* PROXY circuit status: /v2/proxy/video/status
|
|
468
|
+
* Uses a 1-second cache to avoid redundant API calls from multiple components
|
|
469
|
+
* Implements request deduplication for simultaneous calls
|
|
456
470
|
*/
|
|
457
471
|
getProxyVideoStatus(payload) {
|
|
458
|
-
|
|
472
|
+
const cacheKey = this.getStatusCacheKey(payload);
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
// 1. Check if we have a valid cached result (less than 1 second old)
|
|
475
|
+
if (this.statusCache &&
|
|
476
|
+
this.statusCache.key === cacheKey &&
|
|
477
|
+
(now - this.statusCache.timestamp) < this.STATUS_CACHE_TTL_MS) {
|
|
478
|
+
// Return cached error if present
|
|
479
|
+
if (this.statusCache.error) {
|
|
480
|
+
return new Observable(observer => {
|
|
481
|
+
observer.error(this.statusCache.error);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
// Return cached response
|
|
485
|
+
if (this.statusCache.response) {
|
|
486
|
+
return new Observable(observer => {
|
|
487
|
+
observer.next(this.statusCache.response);
|
|
488
|
+
observer.complete();
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// 2. Check if there is already an inflight request for this key
|
|
493
|
+
if (this.inflightStatusRequests.has(cacheKey)) {
|
|
494
|
+
return this.inflightStatusRequests.get(cacheKey);
|
|
495
|
+
}
|
|
496
|
+
// 3. Make the API call, share it, and cache the Observable
|
|
497
|
+
const request$ = this.httpClient
|
|
459
498
|
.post('v2/proxy/video/status', {
|
|
460
499
|
body: payload,
|
|
461
500
|
headers: {},
|
|
462
501
|
})
|
|
463
|
-
.pipe(map((response) =>
|
|
502
|
+
.pipe(map((response) => {
|
|
503
|
+
const typedResponse = response;
|
|
504
|
+
// Cache successful response
|
|
505
|
+
this.statusCache = {
|
|
506
|
+
key: cacheKey,
|
|
507
|
+
response: typedResponse,
|
|
508
|
+
error: null,
|
|
509
|
+
timestamp: Date.now(),
|
|
510
|
+
};
|
|
511
|
+
return typedResponse;
|
|
512
|
+
}), catchError((error) => {
|
|
464
513
|
console.error('TAS Service: getProxyVideoStatus failed', error);
|
|
514
|
+
// Cache error response
|
|
515
|
+
this.statusCache = {
|
|
516
|
+
key: cacheKey,
|
|
517
|
+
response: null,
|
|
518
|
+
error: error,
|
|
519
|
+
timestamp: Date.now(),
|
|
520
|
+
};
|
|
465
521
|
throw error;
|
|
466
|
-
})
|
|
522
|
+
}),
|
|
523
|
+
// Clean up inflight map when the observable completes or errors
|
|
524
|
+
finalize(() => {
|
|
525
|
+
this.inflightStatusRequests.delete(cacheKey);
|
|
526
|
+
}),
|
|
527
|
+
// Share the result with all subscribers (deduplication)
|
|
528
|
+
shareReplay(1));
|
|
529
|
+
this.inflightStatusRequests.set(cacheKey, request$);
|
|
530
|
+
return request$;
|
|
467
531
|
}
|
|
468
532
|
/**
|
|
469
533
|
* PROXY circuit user modification: /v2/proxy/video/user/modify
|
|
@@ -496,7 +560,17 @@ class TasService {
|
|
|
496
560
|
* @returns Observable of appointment array
|
|
497
561
|
*/
|
|
498
562
|
getAppointments(params) {
|
|
499
|
-
|
|
563
|
+
// Normalize dates to YYYY-MM-DD format (handles ISO timestamps like 2026-01-15T03:00:00.000+0000)
|
|
564
|
+
const normalizeDate = (date) => date.split('T')[0];
|
|
565
|
+
const fromDate = normalizeDate(params.fromDate);
|
|
566
|
+
const toDate = normalizeDate(params.toDate);
|
|
567
|
+
let url;
|
|
568
|
+
// If dates are the same, use only initDate
|
|
569
|
+
// if (fromDate === toDate) {
|
|
570
|
+
// url = `v2/proxy/appointment/agendas/user/appointments?fromDate=${fromDate}&roomType=TAS`;
|
|
571
|
+
// } else {
|
|
572
|
+
url = `v2/proxy/appointment/agendas/user/appointments?fromDate=${fromDate}&toDate=${toDate}&roomType=TAS`;
|
|
573
|
+
// }
|
|
500
574
|
if (params.entityId !== undefined) {
|
|
501
575
|
url += `&entityId=${params.entityId}`;
|
|
502
576
|
}
|
|
@@ -523,7 +597,6 @@ class TasService {
|
|
|
523
597
|
this.processStatusResponse(response);
|
|
524
598
|
},
|
|
525
599
|
error: (err) => {
|
|
526
|
-
console.error('[TAS DEBUG] Status polling error:', err);
|
|
527
600
|
},
|
|
528
601
|
});
|
|
529
602
|
}
|
|
@@ -545,17 +618,18 @@ class TasService {
|
|
|
545
618
|
this.joinableSubject.next(content.joinable);
|
|
546
619
|
// Update activateGeo status
|
|
547
620
|
if (content.activateGeo !== undefined) {
|
|
548
|
-
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
549
621
|
this.activateGeoSubject.next(content.activateGeo);
|
|
550
622
|
}
|
|
551
623
|
// Update geoRequestActive status (owner waiting for user geo response)
|
|
552
|
-
if (content.geoRequestActive !== undefined) {
|
|
553
|
-
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
624
|
+
if (content.geoRequestActive !== undefined && content.geoRequestActive !== null) {
|
|
554
625
|
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
555
626
|
}
|
|
627
|
+
else if (content.geoRequestActive === null) {
|
|
628
|
+
// Reset to false when null
|
|
629
|
+
this.geoRequestActiveSubject.next(false);
|
|
630
|
+
}
|
|
556
631
|
// Update allGeoGranted status (all users responded with geo)
|
|
557
632
|
if (content.allGeoGranted !== undefined) {
|
|
558
|
-
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
559
633
|
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
560
634
|
}
|
|
561
635
|
// Check if owner has joined
|
|
@@ -563,7 +637,6 @@ class TasService {
|
|
|
563
637
|
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
564
638
|
// Detect if owner left: was present, now not present
|
|
565
639
|
if (this.wasOwnerPresent && !ownerJoined) {
|
|
566
|
-
console.log('[TAS DEBUG] Owner has left the session');
|
|
567
640
|
this.ownerHasLeftSubject.next(true);
|
|
568
641
|
}
|
|
569
642
|
if (ownerJoined) {
|
|
@@ -578,6 +651,16 @@ class TasService {
|
|
|
578
651
|
status: RoomUserStatus.WAITING,
|
|
579
652
|
}));
|
|
580
653
|
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
654
|
+
// Extract geo info for USER role participants (for owner panel)
|
|
655
|
+
const userGeoInfo = content.users
|
|
656
|
+
.filter((u) => u.rol === 'USER')
|
|
657
|
+
.map((u) => ({
|
|
658
|
+
userId: u.userId,
|
|
659
|
+
geoStatus: u.geoStatus,
|
|
660
|
+
latitude: u.latitude,
|
|
661
|
+
longitude: u.longitude,
|
|
662
|
+
}));
|
|
663
|
+
this.userGeoInfoSubject.next(userGeoInfo);
|
|
581
664
|
}
|
|
582
665
|
/**
|
|
583
666
|
* Admit a user from the waiting room by changing their status.
|
|
@@ -649,11 +732,9 @@ class TasService {
|
|
|
649
732
|
businessRole: this.currentBusinessRole,
|
|
650
733
|
}).subscribe({
|
|
651
734
|
next: (response) => {
|
|
652
|
-
console.log('[TAS DEBUG] Session finished on disconnect event:', response);
|
|
653
735
|
this.isFinishingSession = false;
|
|
654
736
|
},
|
|
655
737
|
error: (error) => {
|
|
656
|
-
console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
|
|
657
738
|
this.isFinishingSession = false;
|
|
658
739
|
},
|
|
659
740
|
});
|
|
@@ -817,6 +898,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
817
898
|
}]
|
|
818
899
|
}] });
|
|
819
900
|
|
|
901
|
+
/**
|
|
902
|
+
* SVG icons used internally by TAS SDK components.
|
|
903
|
+
* Icons are stored as strings to avoid asset bundling complexity.
|
|
904
|
+
*/
|
|
905
|
+
const TAS_ICONS = {
|
|
906
|
+
home: `<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
907
|
+
<rect x="10" width="100" height="100" rx="50" fill="#44D8E8" fill-opacity="0.2"/>
|
|
908
|
+
<path d="M45 70C43.625 70 42.4479 69.5104 41.4688 68.5313C40.4896 67.5521 40 66.375 40 65V46.5625L37.5 48.5C36.9583 48.9167 36.3438 49.0833 35.6562 49C34.9688 48.9167 34.4167 48.5833 34 48C33.5833 47.4583 33.4271 46.8542 33.5312 46.1875C33.6354 45.5208 33.9583 44.9792 34.5 44.5625L56.9375 27.3125C57.3958 26.9792 57.8854 26.7292 58.4062 26.5625C58.9271 26.3958 59.4583 26.3125 60 26.3125C60.5417 26.3125 61.0729 26.3958 61.5938 26.5625C62.1146 26.7292 62.6042 26.9792 63.0625 27.3125L85.5 44.5C86.0417 44.9167 86.3646 45.4583 86.4688 46.125C86.5729 46.7917 86.4167 47.4167 86 48C85.5833 48.5833 85.0417 48.9063 84.375 48.9688C83.7083 49.0313 83.0833 48.8542 82.5 48.4375L60 31.25L45 42.75V65H50.0625C50.7708 65 51.3542 65.2396 51.8125 65.7188C52.2708 66.1979 52.5 66.7917 52.5 67.5C52.5 68.2083 52.2604 68.8021 51.7812 69.2813C51.3021 69.7604 50.7083 70 50 70H45ZM67.3125 73.9375C66.9792 73.9375 66.6667 73.875 66.375 73.75C66.0833 73.625 65.8125 73.4375 65.5625 73.1875L58.5 66.125C58 65.625 57.75 65.0417 57.75 64.375C57.75 63.7083 58 63.125 58.5 62.625C59 62.125 59.5833 61.875 60.25 61.875C60.9167 61.875 61.5 62.125 62 62.625L67.3125 67.875L79.6875 55.5C80.1875 55 80.7812 54.7604 81.4688 54.7813C82.1562 54.8021 82.75 55.0625 83.25 55.5625C83.75 56.0625 84 56.6458 84 57.3125C84 57.9792 83.75 58.5625 83.25 59.0625L69.0625 73.1875C68.8125 73.4375 68.5417 73.625 68.25 73.75C67.9583 73.875 67.6458 73.9375 67.3125 73.9375Z" fill="white"/>
|
|
909
|
+
<path d="M110 5L106.575 3.425L105 0L103.425 3.425L100 5L103.425 6.575L105 10L106.575 6.575L110 5Z" fill="#44D8E8"/>
|
|
910
|
+
<path d="M10 51L6.575 49.425L5 46L3.425 49.425L0 51L3.425 52.575L5 56L6.575 52.575L10 51Z" fill="#44D8E8"/>
|
|
911
|
+
<path d="M95 43.5L93.2875 42.7125L92.5 41L91.7125 42.7125L90 43.5L91.7125 44.2875L92.5 46L93.2875 44.2875L95 43.5Z" fill="#44D8E8"/>
|
|
912
|
+
<path d="M42 4.5L40.2875 3.7125L39.5 2L38.7125 3.7125L37 4.5L38.7125 5.2875L39.5 7L40.2875 5.2875L42 4.5Z" fill="#44D8E8"/>
|
|
913
|
+
<path d="M88 83.5L86.2875 82.7125L85.5 81L84.7125 82.7125L83 83.5L84.7125 84.2875L85.5 86L86.2875 84.2875L88 83.5Z" fill="#44D8E8"/>
|
|
914
|
+
<path d="M120 97.5L118.287 96.7125L117.5 95L116.713 96.7125L115 97.5L116.713 98.2875L117.5 100L118.287 98.2875L120 97.5Z" fill="#44D8E8"/>
|
|
915
|
+
</svg>`,
|
|
916
|
+
};
|
|
917
|
+
|
|
820
918
|
class TasAvatarComponent {
|
|
821
919
|
constructor() {
|
|
822
920
|
this.name = '';
|
|
@@ -890,11 +988,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
890
988
|
type: Input
|
|
891
989
|
}] } });
|
|
892
990
|
|
|
991
|
+
/** User geolocation panel view states */
|
|
992
|
+
var UserGeoViewState;
|
|
993
|
+
(function (UserGeoViewState) {
|
|
994
|
+
UserGeoViewState["HIDDEN"] = "HIDDEN";
|
|
995
|
+
UserGeoViewState["INITIAL"] = "INITIAL";
|
|
996
|
+
UserGeoViewState["VERIFYING"] = "VERIFYING";
|
|
997
|
+
UserGeoViewState["VERIFIED"] = "VERIFIED";
|
|
998
|
+
UserGeoViewState["DENIED"] = "DENIED";
|
|
999
|
+
})(UserGeoViewState || (UserGeoViewState = {}));
|
|
893
1000
|
class TasVideocallComponent {
|
|
894
|
-
constructor(activeModal, tasService, geolocationService) {
|
|
1001
|
+
constructor(activeModal, tasService, geolocationService, sanitizer) {
|
|
895
1002
|
this.activeModal = activeModal;
|
|
896
1003
|
this.tasService = tasService;
|
|
897
1004
|
this.geolocationService = geolocationService;
|
|
1005
|
+
this.sanitizer = sanitizer;
|
|
898
1006
|
this.participantName = '';
|
|
899
1007
|
this.tenant = '';
|
|
900
1008
|
this.businessRole = TasBusinessRole.USER;
|
|
@@ -912,11 +1020,22 @@ class TasVideocallComponent {
|
|
|
912
1020
|
// Geo panel states for owner
|
|
913
1021
|
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
914
1022
|
this.allGeoGranted = false; // All users responded with geo
|
|
1023
|
+
this.userGeoInfo = []; // Individual user geo status
|
|
1024
|
+
// User geo panel state (for owners)
|
|
1025
|
+
this.userGeoViewState = UserGeoViewState.HIDDEN;
|
|
1026
|
+
this.UserGeoViewState = UserGeoViewState; // Expose enum to template
|
|
1027
|
+
this.devModeEnabled = false; // Enable dev controls for testing
|
|
1028
|
+
this.geoPanelDismissed = false; // Track if owner manually closed the panel
|
|
915
1029
|
this.subscriptions = new Subscription();
|
|
1030
|
+
this.homeIcon = this.sanitizer.bypassSecurityTrustHtml(TAS_ICONS.home);
|
|
916
1031
|
}
|
|
917
1032
|
ngOnInit() {
|
|
918
1033
|
this.setupSubscriptions();
|
|
919
1034
|
this.initializeCall();
|
|
1035
|
+
// For owners: show the geo panel to request user location (unless dismissed)
|
|
1036
|
+
if (this.canAdmitUsers && !this.geoPanelDismissed) {
|
|
1037
|
+
this.userGeoViewState = UserGeoViewState.INITIAL;
|
|
1038
|
+
}
|
|
920
1039
|
}
|
|
921
1040
|
ngAfterViewInit() {
|
|
922
1041
|
this.initInteract();
|
|
@@ -958,6 +1077,43 @@ class TasVideocallComponent {
|
|
|
958
1077
|
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
959
1078
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
960
1079
|
}
|
|
1080
|
+
/** Users with pending geo status */
|
|
1081
|
+
get pendingUsers() {
|
|
1082
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.PENDING);
|
|
1083
|
+
}
|
|
1084
|
+
/** Users who granted geo */
|
|
1085
|
+
get grantedUsers() {
|
|
1086
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1087
|
+
}
|
|
1088
|
+
/** Users who denied geo */
|
|
1089
|
+
get deniedUsers() {
|
|
1090
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.DENIED);
|
|
1091
|
+
}
|
|
1092
|
+
/** Show location panel only if owner and there are users who haven't granted */
|
|
1093
|
+
get shouldShowLocationPanel() {
|
|
1094
|
+
if (!this.canAdmitUsers || this.userGeoInfo.length === 0) {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
// Hide if all users have granted
|
|
1098
|
+
const allGranted = this.userGeoInfo.every(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1099
|
+
return !allGranted;
|
|
1100
|
+
}
|
|
1101
|
+
/** Check if any user has denied geo */
|
|
1102
|
+
get hasAnyDenied() {
|
|
1103
|
+
return this.deniedUsers.length > 0;
|
|
1104
|
+
}
|
|
1105
|
+
/** Show user geo panel for owners when not hidden */
|
|
1106
|
+
get shouldShowUserGeoPanel() {
|
|
1107
|
+
return this.canAdmitUsers && this.userGeoViewState !== UserGeoViewState.HIDDEN;
|
|
1108
|
+
}
|
|
1109
|
+
/** Set user geo view state (for dev controls or close button) */
|
|
1110
|
+
setUserGeoViewState(state) {
|
|
1111
|
+
this.userGeoViewState = state;
|
|
1112
|
+
// Track if owner manually dismissed the panel
|
|
1113
|
+
if (state === UserGeoViewState.HIDDEN) {
|
|
1114
|
+
this.geoPanelDismissed = true;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
961
1117
|
/**
|
|
962
1118
|
* Admit a user from the waiting room
|
|
963
1119
|
*/
|
|
@@ -996,7 +1152,7 @@ class TasVideocallComponent {
|
|
|
996
1152
|
this.showLocationPanel = false;
|
|
997
1153
|
}
|
|
998
1154
|
/**
|
|
999
|
-
* Request the user to share their location
|
|
1155
|
+
* Request the user to share their location (called by owner)
|
|
1000
1156
|
*/
|
|
1001
1157
|
requestUserLocation() {
|
|
1002
1158
|
if (!this.videoCallId) {
|
|
@@ -1007,9 +1163,12 @@ class TasVideocallComponent {
|
|
|
1007
1163
|
videoCallId: this.videoCallId,
|
|
1008
1164
|
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
1009
1165
|
};
|
|
1010
|
-
// TODO: Send location request action to backend when endpoint is ready
|
|
1011
1166
|
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1012
|
-
next: () =>
|
|
1167
|
+
next: () => {
|
|
1168
|
+
console.log('Location request sent');
|
|
1169
|
+
// Set panel to verifying state while waiting for user response
|
|
1170
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1171
|
+
},
|
|
1013
1172
|
error: (err) => console.error('Error requesting location:', err),
|
|
1014
1173
|
});
|
|
1015
1174
|
}
|
|
@@ -1046,7 +1205,6 @@ class TasVideocallComponent {
|
|
|
1046
1205
|
// Owner left subscription - disconnect non-owners
|
|
1047
1206
|
this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
|
|
1048
1207
|
if (hasLeft && !this.canAdmitUsers) { // Non-owner user
|
|
1049
|
-
console.log('[TAS DEBUG] Owner left, disconnecting user');
|
|
1050
1208
|
this.hangUp();
|
|
1051
1209
|
this.activeModal.close('owner_left');
|
|
1052
1210
|
}
|
|
@@ -1054,63 +1212,78 @@ class TasVideocallComponent {
|
|
|
1054
1212
|
// ActivateGeo subscription - only for non-owners (users)
|
|
1055
1213
|
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
1056
1214
|
if (activateGeo && !this.canAdmitUsers) {
|
|
1057
|
-
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
1058
1215
|
this.handleActivateGeo();
|
|
1059
1216
|
}
|
|
1060
1217
|
}));
|
|
1061
1218
|
// GeoRequestActive subscription - for owners
|
|
1062
1219
|
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
1063
|
-
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
1064
1220
|
this.geoRequestActive = active;
|
|
1065
1221
|
}));
|
|
1066
|
-
// AllGeoGranted subscription - for owners
|
|
1222
|
+
// AllGeoGranted subscription - for owners to update panel state
|
|
1067
1223
|
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1068
|
-
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1069
1224
|
this.allGeoGranted = granted;
|
|
1225
|
+
// For owners: update panel state based on geo status
|
|
1226
|
+
if (this.canAdmitUsers && granted) {
|
|
1227
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1228
|
+
}
|
|
1229
|
+
}));
|
|
1230
|
+
// UserGeoInfo subscription - for owner geo panel
|
|
1231
|
+
this.subscriptions.add(this.tasService.userGeoInfo$.subscribe((info) => {
|
|
1232
|
+
this.userGeoInfo = info;
|
|
1233
|
+
// Note: We don't auto-switch to DENIED here - that only happens
|
|
1234
|
+
// after owner requests and user denies (via geoRequestActive flow)
|
|
1070
1235
|
}));
|
|
1071
1236
|
}
|
|
1072
1237
|
/**
|
|
1073
1238
|
* Handle activateGeo request from backend (for non-owner users).
|
|
1074
|
-
*
|
|
1239
|
+
* Directly prompts browser for geolocation - no panel for users.
|
|
1075
1240
|
*/
|
|
1076
1241
|
handleActivateGeo() {
|
|
1077
1242
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1243
|
+
// Clear any cached position to force re-prompting
|
|
1244
|
+
this.geolocationService.clearCache();
|
|
1245
|
+
// Request geolocation from user (browser will prompt)
|
|
1246
|
+
const position = yield this.geolocationService.getCurrentPosition();
|
|
1247
|
+
if (position) {
|
|
1083
1248
|
this.geoLocationStatus = 'active';
|
|
1084
|
-
this.reportGeoStatus(
|
|
1085
|
-
|
|
1249
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
this.geoLocationStatus = 'denied';
|
|
1253
|
+
this.denyGeoLocation();
|
|
1086
1254
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
/** Start geolocation verification (called from template button) */
|
|
1258
|
+
startGeoVerification() {
|
|
1259
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1260
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1261
|
+
// Clear any cached position to force re-prompting
|
|
1262
|
+
this.geolocationService.clearCache();
|
|
1263
|
+
// Request geolocation from user (browser will prompt)
|
|
1089
1264
|
const position = yield this.geolocationService.getCurrentPosition();
|
|
1090
1265
|
if (position) {
|
|
1091
|
-
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1092
1266
|
this.geoLocationStatus = 'active';
|
|
1267
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1093
1268
|
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1094
1269
|
}
|
|
1095
1270
|
else {
|
|
1096
|
-
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1097
1271
|
this.geoLocationStatus = 'denied';
|
|
1098
|
-
|
|
1099
|
-
this.
|
|
1272
|
+
this.userGeoViewState = UserGeoViewState.DENIED;
|
|
1273
|
+
this.denyGeoLocation();
|
|
1100
1274
|
}
|
|
1101
1275
|
});
|
|
1102
1276
|
}
|
|
1103
1277
|
/**
|
|
1104
|
-
* Report geolocation
|
|
1278
|
+
* Report granted geolocation to backend.
|
|
1279
|
+
* IMPORTANT: Only call with valid coordinates.
|
|
1105
1280
|
*/
|
|
1106
1281
|
reportGeoStatus(latitude, longitude) {
|
|
1107
1282
|
if (!this.videoCallId) {
|
|
1108
|
-
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1109
1283
|
return;
|
|
1110
1284
|
}
|
|
1111
|
-
//
|
|
1112
|
-
if (
|
|
1113
|
-
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1285
|
+
// Validate coordinates are present
|
|
1286
|
+
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
|
|
1114
1287
|
return;
|
|
1115
1288
|
}
|
|
1116
1289
|
const body = {
|
|
@@ -1120,11 +1293,21 @@ class TasVideocallComponent {
|
|
|
1120
1293
|
latitude,
|
|
1121
1294
|
longitude,
|
|
1122
1295
|
};
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1296
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Report denied geolocation to backend.
|
|
1300
|
+
*/
|
|
1301
|
+
denyGeoLocation() {
|
|
1302
|
+
if (!this.videoCallId) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const body = {
|
|
1306
|
+
userId: this.userId,
|
|
1307
|
+
videoCallId: this.videoCallId,
|
|
1308
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1309
|
+
};
|
|
1310
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1128
1311
|
}
|
|
1129
1312
|
initializeCall() {
|
|
1130
1313
|
if (this.isReturningFromPip) {
|
|
@@ -1214,12 +1397,12 @@ class TasVideocallComponent {
|
|
|
1214
1397
|
});
|
|
1215
1398
|
}
|
|
1216
1399
|
}
|
|
1217
|
-
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 });
|
|
1218
|
-
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"] }] });
|
|
1400
|
+
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }, { token: i4.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
|
|
1401
|
+
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-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></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 </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 <!-- Owner geolocation panel (for owners to request user location) -->\n <div class=\"user-geo-panel\" *ngIf=\"shouldShowUserGeoPanel || devModeEnabled\">\n <div class=\"user-geo-header\">\n <div class=\"header-title-row\">\n <i class=\"fa fa-map-marker header-icon\" *ngIf=\"userGeoViewState !== UserGeoViewState.INITIAL\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n </div>\n <button class=\"close-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n\n <div class=\"user-geo-description\" *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\">\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\n <div class=\"user-geo-content\">\n <!-- INITIAL state: just the button -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.INITIAL\">\n <!-- spacer -->\n </ng-container>\n\n <!-- VERIFYING state: spinner + loading message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFYING\">\n <div class=\"user-geo-verifying\">\n <div class=\"geo-spinner-large\"></div>\n <p class=\"verifying-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"verifying-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n </ng-container>\n\n <!-- VERIFIED state: home icon with sparkles -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFIED\">\n <div class=\"user-geo-verified\">\n <div class=\"verified-icon-container\">\n <span class=\"home-icon\" [innerHTML]=\"homeIcon\"></span>\n </div>\n <p class=\"verified-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n\n <!-- DENIED state: error icon and message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.DENIED\">\n <div class=\"user-geo-denied\">\n <div class=\"denied-icon-container\">\n <i class=\"fa fa-times-circle\"></i>\n </div>\n <p class=\"denied-title\">La ubicaci\u00F3n fue rechazada</p>\n <p class=\"denied-subtitle\">El colaborador no permiti\u00F3 el acceso a su ubicaci\u00F3n.</p>\n </div>\n </ng-container>\n </div>\n\n <!-- Button (hidden in verified and denied states) -->\n <button \n class=\"user-geo-btn\" \n *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\"\n [class.disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n [disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n\n <!-- Dev controls -->\n <div class=\"dev-controls\" *ngIf=\"devModeEnabled\">\n <span class=\"dev-label\">Dev:</span>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.INITIAL)\">Initial</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFYING)\">Verifying</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFIED)\">Verified</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.DENIED)\">Denied</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\">Hide</button>\n </div>\n </div>\n</div>\n\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:transparent;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 .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,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.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)}.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-user-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:200px;overflow-y:auto}.location-panel .location-user-list .user-geo-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:rgba(255,255,255,.05);border-radius:6px}.location-panel .location-user-list .user-geo-item .user-icon{color:#fff9;font-size:14px}.location-panel .location-user-list .user-geo-item .user-name{flex:1;font-size:14px;color:#fff}.location-panel .location-user-list .user-geo-item .geo-status{display:flex;align-items:center;gap:.35rem;font-size:12px;padding:2px 8px;border-radius:12px}.location-panel .location-user-list .user-geo-item .geo-status.pending{color:#ffc107;background:rgba(255,193,7,.15)}.location-panel .location-user-list .user-geo-item .geo-status.granted{color:#4caf50;background:rgba(76,175,80,.15)}.location-panel .location-user-list .user-geo-item .geo-status.denied{color:#f44336;background:rgba(244,67,54,.15)}.location-panel .location-user-list .user-geo-item .geo-status i{font-size:10px}.location-panel .geo-denied-warning{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(244,67,54,.15);border-radius:6px;margin-bottom:1rem;color:#f44336;font-size:13px}.location-panel .geo-denied-warning i{font-size:14px}.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)}}.user-geo-panel{width:360px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.user-geo-panel .user-geo-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2.5rem;padding-top:2rem}.user-geo-panel .user-geo-header .header-title-row{display:flex;align-items:center;gap:.5rem}.user-geo-panel .user-geo-header .header-title-row .header-icon{font-size:16px;color:#fff}.user-geo-panel .user-geo-header .header-title-row h3{font-size:18px;font-weight:600;margin:0;color:#fff}.user-geo-panel .user-geo-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.user-geo-panel .user-geo-header .close-btn:hover{opacity:1}.user-geo-panel .user-geo-description{font-size:14px;color:#fffc;line-height:1.6;margin-bottom:1rem}.user-geo-panel .user-geo-description p{margin:0 0 .75rem}.user-geo-panel .user-geo-content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}.user-geo-panel .user-geo-verifying{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verifying .geo-spinner-large{width:80px;height:80px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite;margin-bottom:1.5rem}.user-geo-panel .user-geo-verifying .verifying-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .25rem}.user-geo-panel .user-geo-verifying .verifying-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-verified{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verified .verified-icon-container{margin-bottom:1rem}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon{display:block}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon svg{width:120px;height:100px}.user-geo-panel .user-geo-verified .verified-title{color:#fff;font-size:16px;font-weight:600;margin:0}.user-geo-panel .user-geo-denied{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-denied .denied-icon-container{width:100px;height:100px;border-radius:50%;background:rgba(244,67,54,.2);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.user-geo-panel .user-geo-denied .denied-icon-container i{font-size:48px;color:#f44336}.user-geo-panel .user-geo-denied .denied-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .5rem}.user-geo-panel .user-geo-denied .denied-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-btn{width:100%;padding:14px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-top:auto;margin-bottom:1rem}.user-geo-panel .user-geo-btn:hover:not(.disabled){background:#178e99}.user-geo-panel .user-geo-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.user-geo-panel .dev-controls{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(0,0,0,.3);border-radius:6px;margin-bottom:1rem;flex-wrap:wrap}.user-geo-panel .dev-controls .dev-label{font-size:12px;color:#fff9;font-weight:600}.user-geo-panel .dev-controls .dev-btn{padding:4px 8px;font-size:11px;background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.user-geo-panel .dev-controls .dev-btn:hover{background:rgba(255,255,255,.2)}.user-geo-panel .user-geo-footer{display:flex;justify-content:center;gap:.75rem;padding-top:.5rem}.user-geo-panel .user-geo-footer .footer-icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff9;font-size:16px}.user-geo-panel .user-geo-footer .footer-icon.active{background:var(--Primary-Uell, #1da4b1);color:#fff}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1219
1402
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
1220
1403
|
type: Component,
|
|
1221
|
-
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"] }]
|
|
1222
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
1404
|
+
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-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></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 </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 <!-- Owner geolocation panel (for owners to request user location) -->\n <div class=\"user-geo-panel\" *ngIf=\"shouldShowUserGeoPanel || devModeEnabled\">\n <div class=\"user-geo-header\">\n <div class=\"header-title-row\">\n <i class=\"fa fa-map-marker header-icon\" *ngIf=\"userGeoViewState !== UserGeoViewState.INITIAL\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n </div>\n <button class=\"close-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n\n <div class=\"user-geo-description\" *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\">\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\n <div class=\"user-geo-content\">\n <!-- INITIAL state: just the button -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.INITIAL\">\n <!-- spacer -->\n </ng-container>\n\n <!-- VERIFYING state: spinner + loading message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFYING\">\n <div class=\"user-geo-verifying\">\n <div class=\"geo-spinner-large\"></div>\n <p class=\"verifying-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"verifying-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n </ng-container>\n\n <!-- VERIFIED state: home icon with sparkles -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFIED\">\n <div class=\"user-geo-verified\">\n <div class=\"verified-icon-container\">\n <span class=\"home-icon\" [innerHTML]=\"homeIcon\"></span>\n </div>\n <p class=\"verified-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n\n <!-- DENIED state: error icon and message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.DENIED\">\n <div class=\"user-geo-denied\">\n <div class=\"denied-icon-container\">\n <i class=\"fa fa-times-circle\"></i>\n </div>\n <p class=\"denied-title\">La ubicaci\u00F3n fue rechazada</p>\n <p class=\"denied-subtitle\">El colaborador no permiti\u00F3 el acceso a su ubicaci\u00F3n.</p>\n </div>\n </ng-container>\n </div>\n\n <!-- Button (hidden in verified and denied states) -->\n <button \n class=\"user-geo-btn\" \n *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\"\n [class.disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n [disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n\n <!-- Dev controls -->\n <div class=\"dev-controls\" *ngIf=\"devModeEnabled\">\n <span class=\"dev-label\">Dev:</span>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.INITIAL)\">Initial</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFYING)\">Verifying</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFIED)\">Verified</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.DENIED)\">Denied</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\">Hide</button>\n </div>\n </div>\n</div>\n\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:transparent;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 .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,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.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)}.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-user-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:200px;overflow-y:auto}.location-panel .location-user-list .user-geo-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:rgba(255,255,255,.05);border-radius:6px}.location-panel .location-user-list .user-geo-item .user-icon{color:#fff9;font-size:14px}.location-panel .location-user-list .user-geo-item .user-name{flex:1;font-size:14px;color:#fff}.location-panel .location-user-list .user-geo-item .geo-status{display:flex;align-items:center;gap:.35rem;font-size:12px;padding:2px 8px;border-radius:12px}.location-panel .location-user-list .user-geo-item .geo-status.pending{color:#ffc107;background:rgba(255,193,7,.15)}.location-panel .location-user-list .user-geo-item .geo-status.granted{color:#4caf50;background:rgba(76,175,80,.15)}.location-panel .location-user-list .user-geo-item .geo-status.denied{color:#f44336;background:rgba(244,67,54,.15)}.location-panel .location-user-list .user-geo-item .geo-status i{font-size:10px}.location-panel .geo-denied-warning{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(244,67,54,.15);border-radius:6px;margin-bottom:1rem;color:#f44336;font-size:13px}.location-panel .geo-denied-warning i{font-size:14px}.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)}}.user-geo-panel{width:360px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.user-geo-panel .user-geo-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2.5rem;padding-top:2rem}.user-geo-panel .user-geo-header .header-title-row{display:flex;align-items:center;gap:.5rem}.user-geo-panel .user-geo-header .header-title-row .header-icon{font-size:16px;color:#fff}.user-geo-panel .user-geo-header .header-title-row h3{font-size:18px;font-weight:600;margin:0;color:#fff}.user-geo-panel .user-geo-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.user-geo-panel .user-geo-header .close-btn:hover{opacity:1}.user-geo-panel .user-geo-description{font-size:14px;color:#fffc;line-height:1.6;margin-bottom:1rem}.user-geo-panel .user-geo-description p{margin:0 0 .75rem}.user-geo-panel .user-geo-content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}.user-geo-panel .user-geo-verifying{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verifying .geo-spinner-large{width:80px;height:80px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite;margin-bottom:1.5rem}.user-geo-panel .user-geo-verifying .verifying-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .25rem}.user-geo-panel .user-geo-verifying .verifying-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-verified{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verified .verified-icon-container{margin-bottom:1rem}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon{display:block}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon svg{width:120px;height:100px}.user-geo-panel .user-geo-verified .verified-title{color:#fff;font-size:16px;font-weight:600;margin:0}.user-geo-panel .user-geo-denied{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-denied .denied-icon-container{width:100px;height:100px;border-radius:50%;background:rgba(244,67,54,.2);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.user-geo-panel .user-geo-denied .denied-icon-container i{font-size:48px;color:#f44336}.user-geo-panel .user-geo-denied .denied-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .5rem}.user-geo-panel .user-geo-denied .denied-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-btn{width:100%;padding:14px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-top:auto;margin-bottom:1rem}.user-geo-panel .user-geo-btn:hover:not(.disabled){background:#178e99}.user-geo-panel .user-geo-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.user-geo-panel .dev-controls{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(0,0,0,.3);border-radius:6px;margin-bottom:1rem;flex-wrap:wrap}.user-geo-panel .dev-controls .dev-label{font-size:12px;color:#fff9;font-weight:600}.user-geo-panel .dev-controls .dev-btn{padding:4px 8px;font-size:11px;background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.user-geo-panel .dev-controls .dev-btn:hover{background:rgba(255,255,255,.2)}.user-geo-panel .user-geo-footer{display:flex;justify-content:center;gap:.75rem;padding-top:.5rem}.user-geo-panel .user-geo-footer .footer-icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff9;font-size:16px}.user-geo-panel .user-geo-footer .footer-icon.active{background:var(--Primary-Uell, #1da4b1);color:#fff}\n"] }]
|
|
1405
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i4.DomSanitizer }]; }, propDecorators: { sessionId: [{
|
|
1223
1406
|
type: Input
|
|
1224
1407
|
}], token: [{
|
|
1225
1408
|
type: Input
|
|
@@ -1279,20 +1462,41 @@ class TasWaitingRoomComponent {
|
|
|
1279
1462
|
this.videoCallModalRef = null;
|
|
1280
1463
|
// Geolocation
|
|
1281
1464
|
this.geoPosition = null;
|
|
1465
|
+
this.geoWasDenied = false; // Tracks if user denied geo permission
|
|
1466
|
+
this.geoResultSent = false; // Tracks if geo result was already sent
|
|
1467
|
+
// Permission tracking for auto-join
|
|
1468
|
+
this.mediaPermissionsGranted = false;
|
|
1469
|
+
this.geoPermissionsResolved = false;
|
|
1470
|
+
// Auto-retry tracking
|
|
1471
|
+
this.retryCount = 0;
|
|
1472
|
+
this.MAX_RETRIES = 1;
|
|
1282
1473
|
}
|
|
1283
1474
|
/** Whether current user is an owner */
|
|
1284
1475
|
get isOwner() {
|
|
1285
1476
|
var _a;
|
|
1286
1477
|
return ((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.role) === TasUserRole.OWNER;
|
|
1287
1478
|
}
|
|
1479
|
+
/** Whether we're still requesting permissions (for template) */
|
|
1480
|
+
get isRequestingPermissions() {
|
|
1481
|
+
return !this.mediaPermissionsGranted || (!this.geoPermissionsResolved && !this.isOwner && !this.isBackoffice);
|
|
1482
|
+
}
|
|
1483
|
+
/** Whether we're waiting for admission after permissions granted (for template) */
|
|
1484
|
+
get isWaitingForAdmission() {
|
|
1485
|
+
return this.mediaPermissionsGranted &&
|
|
1486
|
+
(this.geoPermissionsResolved || this.isOwner || this.isBackoffice) &&
|
|
1487
|
+
!this.isJoinable;
|
|
1488
|
+
}
|
|
1288
1489
|
ngOnInit() {
|
|
1289
1490
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1290
1491
|
this.requestMediaPermissions();
|
|
1492
|
+
// Request geolocation permission and cache it (but don't send to backend yet)
|
|
1493
|
+
// The cached position will be sent when owner requests it via activateGeo
|
|
1291
1494
|
this.requestGeolocation();
|
|
1292
1495
|
this.checkStatus();
|
|
1293
1496
|
}
|
|
1294
1497
|
/**
|
|
1295
1498
|
* Request camera and microphone permissions.
|
|
1499
|
+
* Triggers auto-join when permissions are resolved.
|
|
1296
1500
|
*/
|
|
1297
1501
|
requestMediaPermissions() {
|
|
1298
1502
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1302,49 +1506,81 @@ class TasWaitingRoomComponent {
|
|
|
1302
1506
|
// Stop tracks immediately - we just needed the permission
|
|
1303
1507
|
stream.getTracks().forEach(track => track.stop());
|
|
1304
1508
|
console.log('[TAS DEBUG] Media permissions granted');
|
|
1509
|
+
this.mediaPermissionsGranted = true;
|
|
1305
1510
|
}
|
|
1306
1511
|
catch (error) {
|
|
1307
1512
|
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
1513
|
+
// Still allow joining - some users may not have camera/mic
|
|
1514
|
+
this.mediaPermissionsGranted = true;
|
|
1308
1515
|
}
|
|
1516
|
+
this.tryAutoJoin();
|
|
1517
|
+
this.cdr.detectChanges();
|
|
1309
1518
|
});
|
|
1310
1519
|
}
|
|
1311
1520
|
/**
|
|
1312
|
-
* Request geolocation
|
|
1521
|
+
* Request geolocation permission and cache result.
|
|
1313
1522
|
* Only for regular users (not owners/backoffice).
|
|
1314
|
-
*
|
|
1523
|
+
* Actual send to backend happens after we have videoCallId.
|
|
1524
|
+
* Triggers auto-join when geolocation is resolved.
|
|
1315
1525
|
*/
|
|
1316
1526
|
requestGeolocation() {
|
|
1317
1527
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1318
1528
|
// Only request geolocation for regular users, not owners/backoffice
|
|
1319
1529
|
if (this.isOwner || this.isBackoffice) {
|
|
1320
1530
|
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1531
|
+
this.geoPermissionsResolved = true;
|
|
1321
1532
|
return;
|
|
1322
1533
|
}
|
|
1323
|
-
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1534
|
+
console.log('[TAS DEBUG] Requesting geolocation permission...');
|
|
1324
1535
|
const position = yield this.geolocationService.getCurrentPosition();
|
|
1325
1536
|
if (position) {
|
|
1326
|
-
console.log('[TAS DEBUG] Geolocation
|
|
1537
|
+
console.log('[TAS DEBUG] Geolocation granted:', position);
|
|
1327
1538
|
this.geoPosition = position;
|
|
1328
|
-
|
|
1329
|
-
this.sendGeolocationToBackend();
|
|
1539
|
+
this.geoWasDenied = false;
|
|
1330
1540
|
}
|
|
1331
1541
|
else {
|
|
1332
1542
|
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1543
|
+
this.geoPosition = null;
|
|
1544
|
+
this.geoWasDenied = true;
|
|
1333
1545
|
}
|
|
1546
|
+
this.geoPermissionsResolved = true;
|
|
1547
|
+
// If we already have videoCallId, send now. Otherwise, it will be sent after status response.
|
|
1548
|
+
this.sendGeoResultToBackend();
|
|
1549
|
+
this.tryAutoJoin();
|
|
1550
|
+
this.cdr.detectChanges();
|
|
1334
1551
|
});
|
|
1335
1552
|
}
|
|
1336
1553
|
/**
|
|
1337
|
-
* Send
|
|
1338
|
-
*
|
|
1554
|
+
* Send geo result to backend (granted or denied).
|
|
1555
|
+
* Only sends if videoCallId is available and not already sent.
|
|
1556
|
+
*/
|
|
1557
|
+
sendGeoResultToBackend() {
|
|
1558
|
+
if (!this.videoCallId) {
|
|
1559
|
+
console.log('[TAS DEBUG] Cannot send geo result: videoCallId not available yet');
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (this.geoResultSent) {
|
|
1563
|
+
console.log('[TAS DEBUG] Geo result already sent, skipping');
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (this.geoPosition) {
|
|
1567
|
+
this.sendGeolocationToBackend();
|
|
1568
|
+
this.geoResultSent = true;
|
|
1569
|
+
}
|
|
1570
|
+
else if (this.geoWasDenied) {
|
|
1571
|
+
this.sendGeoDenialToBackend();
|
|
1572
|
+
this.geoResultSent = true;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Send granted geolocation to backend via modify user endpoint.
|
|
1339
1577
|
*/
|
|
1340
1578
|
sendGeolocationToBackend() {
|
|
1341
|
-
var _a;
|
|
1342
1579
|
if (!this.geoPosition || !this.videoCallId) {
|
|
1343
1580
|
return;
|
|
1344
1581
|
}
|
|
1345
1582
|
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
1346
1583
|
const body = {
|
|
1347
|
-
userId: (_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.id,
|
|
1348
1584
|
videoCallId: this.videoCallId,
|
|
1349
1585
|
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1350
1586
|
latitude: this.geoPosition.latitude,
|
|
@@ -1355,6 +1591,23 @@ class TasWaitingRoomComponent {
|
|
|
1355
1591
|
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1356
1592
|
});
|
|
1357
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Send geolocation denial to backend via modify user endpoint.
|
|
1596
|
+
*/
|
|
1597
|
+
sendGeoDenialToBackend() {
|
|
1598
|
+
if (!this.videoCallId) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
console.log('[TAS DEBUG] Sending geo denial to backend...');
|
|
1602
|
+
const body = {
|
|
1603
|
+
videoCallId: this.videoCallId,
|
|
1604
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1605
|
+
};
|
|
1606
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1607
|
+
next: () => console.log('[TAS DEBUG] Geo denial sent successfully'),
|
|
1608
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geo denial:', err),
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1358
1611
|
ngOnDestroy() {
|
|
1359
1612
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1360
1613
|
this.subscriptions.unsubscribe();
|
|
@@ -1388,8 +1641,8 @@ class TasWaitingRoomComponent {
|
|
|
1388
1641
|
this.resolvedAppointmentId = content.appointmentId;
|
|
1389
1642
|
this.videoCallId = content.videoCallId;
|
|
1390
1643
|
console.log('[TAS DEBUG] Status response:', content);
|
|
1391
|
-
//
|
|
1392
|
-
this.
|
|
1644
|
+
// Now that we have videoCallId, send geo result if not already sent
|
|
1645
|
+
this.sendGeoResultToBackend();
|
|
1393
1646
|
// Start polling for status updates
|
|
1394
1647
|
this.tasService.startStatusPolling(statusParams);
|
|
1395
1648
|
// Subscribe to joinable status
|
|
@@ -1405,7 +1658,8 @@ class TasWaitingRoomComponent {
|
|
|
1405
1658
|
}));
|
|
1406
1659
|
}
|
|
1407
1660
|
/**
|
|
1408
|
-
* Handle changes to joinable status
|
|
1661
|
+
* Handle changes to joinable status.
|
|
1662
|
+
* Triggers auto-join when joinable becomes true.
|
|
1409
1663
|
*/
|
|
1410
1664
|
handleJoinableChange(joinable) {
|
|
1411
1665
|
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
@@ -1421,10 +1675,11 @@ class TasWaitingRoomComponent {
|
|
|
1421
1675
|
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
1422
1676
|
return;
|
|
1423
1677
|
}
|
|
1424
|
-
//
|
|
1678
|
+
// Update state and attempt auto-join
|
|
1425
1679
|
if (joinable) {
|
|
1426
|
-
console.log('[TAS DEBUG] Joinable is true,
|
|
1680
|
+
console.log('[TAS DEBUG] Joinable is true, attempting auto-join');
|
|
1427
1681
|
this.state = WaitingRoomState.READY;
|
|
1682
|
+
this.tryAutoJoin();
|
|
1428
1683
|
}
|
|
1429
1684
|
else {
|
|
1430
1685
|
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
@@ -1433,11 +1688,33 @@ class TasWaitingRoomComponent {
|
|
|
1433
1688
|
this.cdr.detectChanges();
|
|
1434
1689
|
}
|
|
1435
1690
|
/**
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1691
|
+
* Attempt to auto-join the session when all conditions are met.
|
|
1692
|
+
* - For owners: just need joinable + sessionId
|
|
1693
|
+
* - For users: need media permissions + geo resolved + joinable + sessionId
|
|
1438
1694
|
*/
|
|
1439
|
-
|
|
1440
|
-
|
|
1695
|
+
tryAutoJoin() {
|
|
1696
|
+
// Don't try if already joining or in error
|
|
1697
|
+
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
1698
|
+
this.state === WaitingRoomState.JOINING) {
|
|
1699
|
+
console.log('[TAS DEBUG] tryAutoJoin skipped - already in state:', this.state);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
// For owners: just need joinable
|
|
1703
|
+
if (this.isOwner) {
|
|
1704
|
+
if (this.isJoinable && this.resolvedSessionId) {
|
|
1705
|
+
console.log('[TAS DEBUG] Owner auto-joining...');
|
|
1706
|
+
this.startSessionAndJoin();
|
|
1707
|
+
}
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
// For users: need media + geo resolved + joinable
|
|
1711
|
+
if (this.mediaPermissionsGranted &&
|
|
1712
|
+
this.geoPermissionsResolved &&
|
|
1713
|
+
this.isJoinable &&
|
|
1714
|
+
this.resolvedSessionId) {
|
|
1715
|
+
console.log('[TAS DEBUG] User auto-joining...');
|
|
1716
|
+
this.startSessionAndJoin();
|
|
1717
|
+
}
|
|
1441
1718
|
}
|
|
1442
1719
|
/**
|
|
1443
1720
|
* Check if user has owner/backoffice role
|
|
@@ -1492,28 +1769,34 @@ class TasWaitingRoomComponent {
|
|
|
1492
1769
|
error: (err) => {
|
|
1493
1770
|
var _a;
|
|
1494
1771
|
console.error('[TAS DEBUG] /start request failed:', err);
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1772
|
+
// Auto-retry on first failure
|
|
1773
|
+
if (this.retryCount < this.MAX_RETRIES) {
|
|
1774
|
+
this.retryCount++;
|
|
1775
|
+
console.log('[TAS DEBUG] Auto-retrying... attempt:', this.retryCount);
|
|
1776
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1777
|
+
this.cdr.detectChanges();
|
|
1778
|
+
setTimeout(() => this.startSessionAndJoin(), 2000);
|
|
1779
|
+
}
|
|
1780
|
+
else {
|
|
1781
|
+
// Show error after retry fails
|
|
1782
|
+
this.state = WaitingRoomState.ERROR;
|
|
1783
|
+
this.errorMessage = ((_a = err === null || err === void 0 ? void 0 : err.error) === null || _a === void 0 ? void 0 : _a.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
|
|
1784
|
+
this.tasService.stopStatusPolling();
|
|
1785
|
+
console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
|
|
1786
|
+
this.cdr.detectChanges();
|
|
1787
|
+
}
|
|
1500
1788
|
},
|
|
1501
1789
|
}));
|
|
1502
1790
|
}
|
|
1503
1791
|
/**
|
|
1504
|
-
*
|
|
1505
|
-
|
|
1506
|
-
cancel() {
|
|
1507
|
-
this.tasService.stopStatusPolling();
|
|
1508
|
-
this.activeModal.dismiss('cancel');
|
|
1509
|
-
}
|
|
1510
|
-
/**
|
|
1511
|
-
* Retry after an error
|
|
1792
|
+
* Retry after an error.
|
|
1793
|
+
* Resets retry count and restarts the flow.
|
|
1512
1794
|
*/
|
|
1513
1795
|
retry() {
|
|
1514
1796
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1515
1797
|
this.errorMessage = '';
|
|
1516
1798
|
this.token = '';
|
|
1799
|
+
this.retryCount = 0; // Reset retry count for auto-retry
|
|
1517
1800
|
this.checkStatus();
|
|
1518
1801
|
}
|
|
1519
1802
|
openVideoCallModal() {
|
|
@@ -1540,10 +1823,10 @@ class TasWaitingRoomComponent {
|
|
|
1540
1823
|
}
|
|
1541
1824
|
}
|
|
1542
1825
|
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 });
|
|
1543
|
-
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
|
|
1826
|
+
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 <div class=\"waiting-room-content\">\n <div class=\"state-container\">\n <!-- Loading states (show spinner) -->\n <ng-container *ngIf=\"state !== WaitingRoomState.ERROR\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n \n <!-- Requesting permissions -->\n <ng-container *ngIf=\"isRequestingPermissions\">\n <p class=\"state-message\">Solicitando permisos...</p>\n <p class=\"state-submessage\">Por favor, acept\u00E1 los permisos de c\u00E1mara y micr\u00F3fono.</p>\n </ng-container>\n \n <!-- Waiting for admission (permissions granted, not joinable yet) -->\n <ng-container *ngIf=\"isWaitingForAdmission\">\n <p class=\"state-message\">Medicina laboral va a admitirte</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </ng-container>\n \n <!-- Getting token / Joining -->\n <ng-container *ngIf=\"state === WaitingRoomState.GETTING_TOKEN || state === WaitingRoomState.JOINING\">\n <p class=\"state-message\">Conectando...</p>\n </ng-container>\n \n <!-- Checking status (when not requesting permissions) -->\n <ng-container *ngIf=\"state === WaitingRoomState.CHECKING_STATUS && !isRequestingPermissions\">\n <p class=\"state-message\">Verificando estado...</p>\n </ng-container>\n </ng-container>\n\n <!-- Error state (show error icon and message) -->\n <ng-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\">{{ errorMessage }}</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 </ng-container>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:300px;background:#ffffff;border-radius:5px;overflow:hidden}.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%}.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.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.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.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)}.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.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 576px){.tas-waiting-room{min-height:250px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.action-btn{padding:10px 24px;font-size:14px}}\n"], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1544
1827
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
1545
1828
|
type: Component,
|
|
1546
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n
|
|
1829
|
+
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n <div class=\"waiting-room-content\">\n <div class=\"state-container\">\n <!-- Loading states (show spinner) -->\n <ng-container *ngIf=\"state !== WaitingRoomState.ERROR\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n \n <!-- Requesting permissions -->\n <ng-container *ngIf=\"isRequestingPermissions\">\n <p class=\"state-message\">Solicitando permisos...</p>\n <p class=\"state-submessage\">Por favor, acept\u00E1 los permisos de c\u00E1mara y micr\u00F3fono.</p>\n </ng-container>\n \n <!-- Waiting for admission (permissions granted, not joinable yet) -->\n <ng-container *ngIf=\"isWaitingForAdmission\">\n <p class=\"state-message\">Medicina laboral va a admitirte</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </ng-container>\n \n <!-- Getting token / Joining -->\n <ng-container *ngIf=\"state === WaitingRoomState.GETTING_TOKEN || state === WaitingRoomState.JOINING\">\n <p class=\"state-message\">Conectando...</p>\n </ng-container>\n \n <!-- Checking status (when not requesting permissions) -->\n <ng-container *ngIf=\"state === WaitingRoomState.CHECKING_STATUS && !isRequestingPermissions\">\n <p class=\"state-message\">Verificando estado...</p>\n </ng-container>\n </ng-container>\n\n <!-- Error state (show error icon and message) -->\n <ng-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\">{{ errorMessage }}</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 </ng-container>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:300px;background:#ffffff;border-radius:5px;overflow:hidden}.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%}.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.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.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.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)}.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.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 576px){.tas-waiting-room{min-height:250px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.action-btn{padding:10px 24px;font-size:14px}}\n"] }]
|
|
1547
1830
|
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1548
1831
|
type: Input
|
|
1549
1832
|
}], entityId: [{
|
|
@@ -1567,13 +1850,16 @@ class TasButtonComponent {
|
|
|
1567
1850
|
// Style customization
|
|
1568
1851
|
this.variant = 'default';
|
|
1569
1852
|
this.buttonLabel = 'Iniciar TAS';
|
|
1853
|
+
// Skip status check - when true, button shows immediately without API call
|
|
1854
|
+
// Useful when parent component already knows the appointment is valid
|
|
1855
|
+
this.skipStatusCheck = false;
|
|
1570
1856
|
this.isLoading = false;
|
|
1571
1857
|
// Status check state
|
|
1572
1858
|
this.isCheckingStatus = false;
|
|
1573
1859
|
this.isStatusError = false;
|
|
1574
1860
|
this.statusErrorMessage = '';
|
|
1575
1861
|
this.isJoinable = false; // Tracks joinable field from status response
|
|
1576
|
-
this.shouldShowButton =
|
|
1862
|
+
this.shouldShowButton = false; // Hidden by default, shown after status check confirms visibility
|
|
1577
1863
|
this.subscriptions = new Subscription();
|
|
1578
1864
|
this.currentModalRef = null;
|
|
1579
1865
|
this.videoCallModalRef = null;
|
|
@@ -1611,6 +1897,12 @@ class TasButtonComponent {
|
|
|
1611
1897
|
this.videoCallModalRef = null;
|
|
1612
1898
|
}
|
|
1613
1899
|
}));
|
|
1900
|
+
// If skipStatusCheck is true, show button immediately without polling
|
|
1901
|
+
if (this.skipStatusCheck) {
|
|
1902
|
+
this.shouldShowButton = true;
|
|
1903
|
+
this.isJoinable = true;
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1614
1906
|
// Start status checking
|
|
1615
1907
|
this.startStatusPolling();
|
|
1616
1908
|
}
|
|
@@ -1658,6 +1950,11 @@ class TasButtonComponent {
|
|
|
1658
1950
|
.subscribe({
|
|
1659
1951
|
next: (response) => {
|
|
1660
1952
|
var _a, _b;
|
|
1953
|
+
// Validate response structure
|
|
1954
|
+
if (!response || !response.content) {
|
|
1955
|
+
this.handleStatusError('Invalid response');
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1661
1958
|
this.isCheckingStatus = false;
|
|
1662
1959
|
this.isStatusError = false;
|
|
1663
1960
|
this.statusErrorMessage = '';
|
|
@@ -1666,23 +1963,20 @@ class TasButtonComponent {
|
|
|
1666
1963
|
this.isJoinable = (_b = (_a = response.content) === null || _a === void 0 ? void 0 : _a.joinable) !== null && _b !== void 0 ? _b : false;
|
|
1667
1964
|
},
|
|
1668
1965
|
error: (err) => {
|
|
1669
|
-
this.
|
|
1670
|
-
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1671
|
-
this.statusErrorMessage = errorMessage;
|
|
1672
|
-
// Use utility service to determine if button should be shown
|
|
1673
|
-
this.shouldShowButton = this.tasUtilityService.shouldShowButton(errorMessage);
|
|
1674
|
-
// Stop polling on error
|
|
1675
|
-
this.stopStatusPolling();
|
|
1676
|
-
// If button should be hidden, don't treat as error
|
|
1677
|
-
if (!this.shouldShowButton) {
|
|
1678
|
-
this.isStatusError = false;
|
|
1679
|
-
}
|
|
1680
|
-
else {
|
|
1681
|
-
this.isStatusError = true;
|
|
1682
|
-
}
|
|
1966
|
+
this.handleStatusError(err);
|
|
1683
1967
|
},
|
|
1684
1968
|
}));
|
|
1685
1969
|
}
|
|
1970
|
+
handleStatusError(err) {
|
|
1971
|
+
this.isCheckingStatus = false;
|
|
1972
|
+
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1973
|
+
this.statusErrorMessage = errorMessage;
|
|
1974
|
+
// On any status error, hide the button
|
|
1975
|
+
this.shouldShowButton = false;
|
|
1976
|
+
this.isStatusError = false; // We don't show error UI, just hide the button
|
|
1977
|
+
// Stop polling on error
|
|
1978
|
+
this.stopStatusPolling();
|
|
1979
|
+
}
|
|
1686
1980
|
onClick() {
|
|
1687
1981
|
var _a;
|
|
1688
1982
|
if (!this.tenant || !((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.name)) {
|
|
@@ -1732,7 +2026,7 @@ class TasButtonComponent {
|
|
|
1732
2026
|
}
|
|
1733
2027
|
}
|
|
1734
2028
|
TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, deps: [{ token: i1.NgbModal }, { token: TasService }, { token: TasUtilityService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1735
|
-
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 *ngIf=\"shouldShowButton\"\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;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "triggers", "container", "disableTooltip", "tooltipClass", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }] });
|
|
2029
|
+
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", skipStatusCheck: "skipStatusCheck" }, ngImport: i0, template: "<span\n *ngIf=\"shouldShowButton\"\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;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "triggers", "container", "disableTooltip", "tooltipClass", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }] });
|
|
1736
2030
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
|
|
1737
2031
|
type: Component,
|
|
1738
2032
|
args: [{ selector: 'tas-btn', template: "<span\n *ngIf=\"shouldShowButton\"\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;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"] }]
|
|
@@ -1750,6 +2044,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1750
2044
|
type: Input
|
|
1751
2045
|
}], buttonLabel: [{
|
|
1752
2046
|
type: Input
|
|
2047
|
+
}], skipStatusCheck: [{
|
|
2048
|
+
type: Input
|
|
1753
2049
|
}] } });
|
|
1754
2050
|
|
|
1755
2051
|
class TasFloatingCallComponent {
|
|
@@ -1905,7 +2201,7 @@ class TasIncomingAppointmentComponent {
|
|
|
1905
2201
|
this.roomType = TasRoomType.TAS;
|
|
1906
2202
|
this.businessRole = TasBusinessRole.USER;
|
|
1907
2203
|
this.enterCall = new EventEmitter();
|
|
1908
|
-
this.
|
|
2204
|
+
this.appointments = [];
|
|
1909
2205
|
this.isLoading = true;
|
|
1910
2206
|
this.hasError = false;
|
|
1911
2207
|
this.subscriptions = new Subscription();
|
|
@@ -1917,20 +2213,24 @@ class TasIncomingAppointmentComponent {
|
|
|
1917
2213
|
this.subscriptions.unsubscribe();
|
|
1918
2214
|
}
|
|
1919
2215
|
loadAppointments() {
|
|
1920
|
-
const today = new Date();
|
|
1921
|
-
const in7Days = new Date(today);
|
|
1922
|
-
in7Days.setDate(today.getDate() + 7);
|
|
1923
2216
|
this.subscriptions.add(this.tasService
|
|
1924
|
-
.getAppointments({
|
|
2217
|
+
.getAppointments({
|
|
2218
|
+
fromDate: this.fromDate,
|
|
2219
|
+
toDate: this.toDate,
|
|
2220
|
+
entityId: this.entityId
|
|
2221
|
+
})
|
|
1925
2222
|
.subscribe({
|
|
1926
2223
|
next: (response) => {
|
|
1927
2224
|
// Handle both array response and wrapped response (e.g., { content: [...] })
|
|
1928
2225
|
const appointments = Array.isArray(response)
|
|
1929
2226
|
? response
|
|
1930
2227
|
: (response === null || response === void 0 ? void 0 : response.content) || [];
|
|
1931
|
-
//
|
|
1932
|
-
|
|
1933
|
-
|
|
2228
|
+
// Sort by date and startTime descending (most recent first)
|
|
2229
|
+
this.appointments = appointments.sort((a, b) => {
|
|
2230
|
+
const dateTimeA = `${a.date}T${a.startTime}`;
|
|
2231
|
+
const dateTimeB = `${b.date}T${b.startTime}`;
|
|
2232
|
+
return dateTimeB.localeCompare(dateTimeA);
|
|
2233
|
+
});
|
|
1934
2234
|
this.isLoading = false;
|
|
1935
2235
|
},
|
|
1936
2236
|
error: () => {
|
|
@@ -1939,18 +2239,21 @@ class TasIncomingAppointmentComponent {
|
|
|
1939
2239
|
},
|
|
1940
2240
|
}));
|
|
1941
2241
|
}
|
|
1942
|
-
onEnterCall() {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2242
|
+
onEnterCall(appointment) {
|
|
2243
|
+
this.enterCall.emit(appointment);
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Check if tas-btn should be shown for an appointment (CONFIRMED or ACTIVE status)
|
|
2247
|
+
*/
|
|
2248
|
+
shouldShowTasBtn(appointment) {
|
|
2249
|
+
return appointment.status === AppointmentStatus.CONFIRMED ||
|
|
2250
|
+
appointment.status === AppointmentStatus.ACTIVE;
|
|
1946
2251
|
}
|
|
1947
2252
|
/**
|
|
1948
2253
|
* Format date to Spanish format: "Lunes 8 de diciembre"
|
|
1949
2254
|
*/
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
return '';
|
|
1953
|
-
const [year, month, day] = this.appointment.date.split('-').map(Number);
|
|
2255
|
+
formatAppointmentDate(appointment) {
|
|
2256
|
+
const [year, month, day] = appointment.date.split('-').map(Number);
|
|
1954
2257
|
const date = new Date(year, month - 1, day);
|
|
1955
2258
|
const dayNames = [
|
|
1956
2259
|
'Domingo', 'Lunes', 'Martes', 'Miércoles',
|
|
@@ -1968,23 +2271,25 @@ class TasIncomingAppointmentComponent {
|
|
|
1968
2271
|
/**
|
|
1969
2272
|
* Format time range: "9:00 - 9:30"
|
|
1970
2273
|
*/
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
return '';
|
|
1974
|
-
return `${this.appointment.startTime} - ${this.appointment.endTime}`;
|
|
2274
|
+
formatTimeRange(appointment) {
|
|
2275
|
+
return `${appointment.startTime} - ${appointment.endTime}`;
|
|
1975
2276
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2277
|
+
/**
|
|
2278
|
+
* Get the other participant in the call (not the current user)
|
|
2279
|
+
*/
|
|
2280
|
+
getOtherParticipant(appointment) {
|
|
2281
|
+
if (!appointment.participants || appointment.participants.length === 0) {
|
|
2282
|
+
return appointment.title; // Fallback to title if no participants
|
|
2283
|
+
}
|
|
2284
|
+
const otherParticipant = appointment.participants.find(p => { var _a; return p.userId !== ((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.id); });
|
|
2285
|
+
return (otherParticipant === null || otherParticipant === void 0 ? void 0 : otherParticipant.name) || appointment.title;
|
|
1981
2286
|
}
|
|
1982
2287
|
}
|
|
1983
2288
|
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1984
|
-
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 &&
|
|
2289
|
+
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", fromDate: "fromDate", toDate: "toDate" }, 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 && appointments.length === 0\">\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 <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\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;gap:16px}.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", "skipStatusCheck"] }], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
1985
2290
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
1986
2291
|
type: Component,
|
|
1987
|
-
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 &&
|
|
2292
|
+
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 && appointments.length === 0\">\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 <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\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;gap:16px}.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"] }]
|
|
1988
2293
|
}], ctorParameters: function () { return [{ type: TasService }]; }, propDecorators: { roomType: [{
|
|
1989
2294
|
type: Input
|
|
1990
2295
|
}], entityId: [{
|
|
@@ -1995,6 +2300,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1995
2300
|
type: Input
|
|
1996
2301
|
}], currentUser: [{
|
|
1997
2302
|
type: Input
|
|
2303
|
+
}], fromDate: [{
|
|
2304
|
+
type: Input
|
|
2305
|
+
}], toDate: [{
|
|
2306
|
+
type: Input
|
|
1998
2307
|
}], enterCall: [{
|
|
1999
2308
|
type: Output
|
|
2000
2309
|
}] } });
|
|
@@ -2084,5 +2393,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2084
2393
|
* Generated bundle index. Do not edit.
|
|
2085
2394
|
*/
|
|
2086
2395
|
|
|
2087
|
-
export { AppointmentStatus, CallState, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2396
|
+
export { AppointmentStatus, CallState, GeoStatus, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserGeoViewState, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2088
2397
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|