tas-uell-sdk 0.1.3 → 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 +1 -0
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +29 -16
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +1 -1
- 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 +8 -1
- package/esm2020/lib/services/tas.service.mjs +88 -26
- package/fesm2015/tas-uell-sdk.mjs +397 -112
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +395 -111
- 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-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 +19 -3
- package/lib/services/tas.service.d.ts +13 -2
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
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 interact from 'interactjs';
|
|
7
7
|
import * as i1 from '@ng-bootstrap/ng-bootstrap';
|
|
8
8
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
9
|
-
import * as i4 from '@angular/
|
|
9
|
+
import * as i4 from '@angular/platform-browser';
|
|
10
|
+
import * as i4$1 from '@angular/common';
|
|
10
11
|
import { CommonModule } from '@angular/common';
|
|
11
12
|
import { FormsModule } from '@angular/forms';
|
|
12
13
|
|
|
@@ -66,7 +67,14 @@ var UserCallAction;
|
|
|
66
67
|
UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
|
|
67
68
|
UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
|
|
68
69
|
UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
|
|
70
|
+
UserCallAction["DENY_GEOLOCATION"] = "DENY_GEOLOCATION";
|
|
69
71
|
})(UserCallAction || (UserCallAction = {}));
|
|
72
|
+
var GeoStatus;
|
|
73
|
+
(function (GeoStatus) {
|
|
74
|
+
GeoStatus["PENDING"] = "PENDING";
|
|
75
|
+
GeoStatus["GRANTED"] = "GRANTED";
|
|
76
|
+
GeoStatus["DENIED"] = "DENIED";
|
|
77
|
+
})(GeoStatus || (GeoStatus = {}));
|
|
70
78
|
var RoomUserStatus;
|
|
71
79
|
(function (RoomUserStatus) {
|
|
72
80
|
RoomUserStatus["ASSIGNED"] = "ASSIGNED";
|
|
@@ -197,6 +205,9 @@ class TasService {
|
|
|
197
205
|
// Observable for when all geo has been granted
|
|
198
206
|
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
199
207
|
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
208
|
+
// Observable for individual user geo status (for owner panel)
|
|
209
|
+
this.userGeoInfoSubject = new BehaviorSubject([]);
|
|
210
|
+
this.userGeoInfo$ = this.userGeoInfoSubject.asObservable();
|
|
200
211
|
this.statusPollingInterval = null;
|
|
201
212
|
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
202
213
|
this.wasOwnerPresent = false;
|
|
@@ -204,6 +215,10 @@ class TasService {
|
|
|
204
215
|
this.currentAppointmentId = null;
|
|
205
216
|
this.currentVideoCallId = null;
|
|
206
217
|
this.currentTenant = null;
|
|
218
|
+
// Status cache (1 second TTL)
|
|
219
|
+
this.STATUS_CACHE_TTL_MS = 1000;
|
|
220
|
+
this.statusCache = null;
|
|
221
|
+
this.inflightStatusRequests = new Map();
|
|
207
222
|
}
|
|
208
223
|
// ... (Getters and other methods remain unchanged)
|
|
209
224
|
/**
|
|
@@ -222,7 +237,6 @@ class TasService {
|
|
|
222
237
|
if (params.tenant) {
|
|
223
238
|
this.currentTenant = params.tenant;
|
|
224
239
|
}
|
|
225
|
-
console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
|
|
226
240
|
// Initial status fetch
|
|
227
241
|
this.fetchAndProcessStatus(params);
|
|
228
242
|
// Set up periodic polling
|
|
@@ -325,7 +339,6 @@ class TasService {
|
|
|
325
339
|
}
|
|
326
340
|
// Session Management
|
|
327
341
|
disconnectSession(clearStorage = true) {
|
|
328
|
-
console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
|
|
329
342
|
// Call finishSession before disconnecting if we have a sessionId
|
|
330
343
|
const sessionIdToFinish = this.currentSessionId;
|
|
331
344
|
// Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
|
|
@@ -356,11 +369,9 @@ class TasService {
|
|
|
356
369
|
businessRole: this.currentBusinessRole,
|
|
357
370
|
}).subscribe({
|
|
358
371
|
next: (response) => {
|
|
359
|
-
console.log('[TAS DEBUG] Session finished successfully:', response);
|
|
360
372
|
this.isFinishingSession = false;
|
|
361
373
|
},
|
|
362
374
|
error: (error) => {
|
|
363
|
-
console.error('[TAS DEBUG] Error finishing session:', error);
|
|
364
375
|
this.isFinishingSession = false;
|
|
365
376
|
},
|
|
366
377
|
});
|
|
@@ -395,7 +406,6 @@ class TasService {
|
|
|
395
406
|
// If so, don't restore it on page reload
|
|
396
407
|
const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
|
|
397
408
|
if (wasDisconnected === 'true') {
|
|
398
|
-
console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
|
|
399
409
|
this.clearSessionState();
|
|
400
410
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
401
411
|
return;
|
|
@@ -403,14 +413,12 @@ class TasService {
|
|
|
403
413
|
// Don't restore if we're already disconnected or if there's no active session
|
|
404
414
|
// This prevents restoring sessions that were properly disconnected
|
|
405
415
|
if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
|
|
406
|
-
console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
|
|
407
416
|
this.clearSessionState();
|
|
408
417
|
return;
|
|
409
418
|
}
|
|
410
419
|
try {
|
|
411
420
|
const state = JSON.parse(savedState);
|
|
412
421
|
if (state.sessionId && state.token) {
|
|
413
|
-
console.log('[TAS DEBUG] Restoring session from storage');
|
|
414
422
|
// Force PiP mode for restoration to ensure UI consistency
|
|
415
423
|
this.viewModeSubject.next(ViewMode.PIP);
|
|
416
424
|
if (state.businessRole) {
|
|
@@ -419,18 +427,15 @@ class TasService {
|
|
|
419
427
|
this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
|
|
420
428
|
containerId, this.currentBusinessRole)
|
|
421
429
|
.then(() => {
|
|
422
|
-
console.log('[TAS DEBUG] Session restored successfully');
|
|
423
430
|
// Clear the disconnected flag if restoration succeeds
|
|
424
431
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
425
432
|
})
|
|
426
433
|
.catch((err) => {
|
|
427
|
-
console.error('[TAS DEBUG] Failed to restore session:', err);
|
|
428
434
|
this.clearSessionState(); // Clear bad state
|
|
429
435
|
});
|
|
430
436
|
}
|
|
431
437
|
}
|
|
432
438
|
catch (e) {
|
|
433
|
-
console.error('[TAS DEBUG] Error parsing saved session state', e);
|
|
434
439
|
this.clearSessionState();
|
|
435
440
|
}
|
|
436
441
|
}
|
|
@@ -449,19 +454,77 @@ class TasService {
|
|
|
449
454
|
throw error;
|
|
450
455
|
}));
|
|
451
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Generate cache key from request payload
|
|
459
|
+
*/
|
|
460
|
+
getStatusCacheKey(payload) {
|
|
461
|
+
return `${payload.roomType}-${payload.entityId}-${payload.tenant}-${payload.sessionId || ''}`;
|
|
462
|
+
}
|
|
452
463
|
/**
|
|
453
464
|
* PROXY circuit status: /v2/proxy/video/status
|
|
465
|
+
* Uses a 1-second cache to avoid redundant API calls from multiple components
|
|
466
|
+
* Implements request deduplication for simultaneous calls
|
|
454
467
|
*/
|
|
455
468
|
getProxyVideoStatus(payload) {
|
|
456
|
-
|
|
469
|
+
const cacheKey = this.getStatusCacheKey(payload);
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
// 1. Check if we have a valid cached result (less than 1 second old)
|
|
472
|
+
if (this.statusCache &&
|
|
473
|
+
this.statusCache.key === cacheKey &&
|
|
474
|
+
(now - this.statusCache.timestamp) < this.STATUS_CACHE_TTL_MS) {
|
|
475
|
+
// Return cached error if present
|
|
476
|
+
if (this.statusCache.error) {
|
|
477
|
+
return new Observable(observer => {
|
|
478
|
+
observer.error(this.statusCache.error);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
// Return cached response
|
|
482
|
+
if (this.statusCache.response) {
|
|
483
|
+
return new Observable(observer => {
|
|
484
|
+
observer.next(this.statusCache.response);
|
|
485
|
+
observer.complete();
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 2. Check if there is already an inflight request for this key
|
|
490
|
+
if (this.inflightStatusRequests.has(cacheKey)) {
|
|
491
|
+
return this.inflightStatusRequests.get(cacheKey);
|
|
492
|
+
}
|
|
493
|
+
// 3. Make the API call, share it, and cache the Observable
|
|
494
|
+
const request$ = this.httpClient
|
|
457
495
|
.post('v2/proxy/video/status', {
|
|
458
496
|
body: payload,
|
|
459
497
|
headers: {},
|
|
460
498
|
})
|
|
461
|
-
.pipe(map((response) =>
|
|
499
|
+
.pipe(map((response) => {
|
|
500
|
+
const typedResponse = response;
|
|
501
|
+
// Cache successful response
|
|
502
|
+
this.statusCache = {
|
|
503
|
+
key: cacheKey,
|
|
504
|
+
response: typedResponse,
|
|
505
|
+
error: null,
|
|
506
|
+
timestamp: Date.now(),
|
|
507
|
+
};
|
|
508
|
+
return typedResponse;
|
|
509
|
+
}), catchError((error) => {
|
|
462
510
|
console.error('TAS Service: getProxyVideoStatus failed', error);
|
|
511
|
+
// Cache error response
|
|
512
|
+
this.statusCache = {
|
|
513
|
+
key: cacheKey,
|
|
514
|
+
response: null,
|
|
515
|
+
error: error,
|
|
516
|
+
timestamp: Date.now(),
|
|
517
|
+
};
|
|
463
518
|
throw error;
|
|
464
|
-
})
|
|
519
|
+
}),
|
|
520
|
+
// Clean up inflight map when the observable completes or errors
|
|
521
|
+
finalize(() => {
|
|
522
|
+
this.inflightStatusRequests.delete(cacheKey);
|
|
523
|
+
}),
|
|
524
|
+
// Share the result with all subscribers (deduplication)
|
|
525
|
+
shareReplay(1));
|
|
526
|
+
this.inflightStatusRequests.set(cacheKey, request$);
|
|
527
|
+
return request$;
|
|
465
528
|
}
|
|
466
529
|
/**
|
|
467
530
|
* PROXY circuit user modification: /v2/proxy/video/user/modify
|
|
@@ -531,7 +594,6 @@ class TasService {
|
|
|
531
594
|
this.processStatusResponse(response);
|
|
532
595
|
},
|
|
533
596
|
error: (err) => {
|
|
534
|
-
console.error('[TAS DEBUG] Status polling error:', err);
|
|
535
597
|
},
|
|
536
598
|
});
|
|
537
599
|
}
|
|
@@ -553,17 +615,18 @@ class TasService {
|
|
|
553
615
|
this.joinableSubject.next(content.joinable);
|
|
554
616
|
// Update activateGeo status
|
|
555
617
|
if (content.activateGeo !== undefined) {
|
|
556
|
-
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
557
618
|
this.activateGeoSubject.next(content.activateGeo);
|
|
558
619
|
}
|
|
559
620
|
// Update geoRequestActive status (owner waiting for user geo response)
|
|
560
|
-
if (content.geoRequestActive !== undefined) {
|
|
561
|
-
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
621
|
+
if (content.geoRequestActive !== undefined && content.geoRequestActive !== null) {
|
|
562
622
|
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
563
623
|
}
|
|
624
|
+
else if (content.geoRequestActive === null) {
|
|
625
|
+
// Reset to false when null
|
|
626
|
+
this.geoRequestActiveSubject.next(false);
|
|
627
|
+
}
|
|
564
628
|
// Update allGeoGranted status (all users responded with geo)
|
|
565
629
|
if (content.allGeoGranted !== undefined) {
|
|
566
|
-
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
567
630
|
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
568
631
|
}
|
|
569
632
|
// Check if owner has joined
|
|
@@ -571,7 +634,6 @@ class TasService {
|
|
|
571
634
|
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
572
635
|
// Detect if owner left: was present, now not present
|
|
573
636
|
if (this.wasOwnerPresent && !ownerJoined) {
|
|
574
|
-
console.log('[TAS DEBUG] Owner has left the session');
|
|
575
637
|
this.ownerHasLeftSubject.next(true);
|
|
576
638
|
}
|
|
577
639
|
if (ownerJoined) {
|
|
@@ -586,6 +648,16 @@ class TasService {
|
|
|
586
648
|
status: RoomUserStatus.WAITING,
|
|
587
649
|
}));
|
|
588
650
|
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
651
|
+
// Extract geo info for USER role participants (for owner panel)
|
|
652
|
+
const userGeoInfo = content.users
|
|
653
|
+
.filter((u) => u.rol === 'USER')
|
|
654
|
+
.map((u) => ({
|
|
655
|
+
userId: u.userId,
|
|
656
|
+
geoStatus: u.geoStatus,
|
|
657
|
+
latitude: u.latitude,
|
|
658
|
+
longitude: u.longitude,
|
|
659
|
+
}));
|
|
660
|
+
this.userGeoInfoSubject.next(userGeoInfo);
|
|
589
661
|
}
|
|
590
662
|
/**
|
|
591
663
|
* Admit a user from the waiting room by changing their status.
|
|
@@ -656,11 +728,9 @@ class TasService {
|
|
|
656
728
|
businessRole: this.currentBusinessRole,
|
|
657
729
|
}).subscribe({
|
|
658
730
|
next: (response) => {
|
|
659
|
-
console.log('[TAS DEBUG] Session finished on disconnect event:', response);
|
|
660
731
|
this.isFinishingSession = false;
|
|
661
732
|
},
|
|
662
733
|
error: (error) => {
|
|
663
|
-
console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
|
|
664
734
|
this.isFinishingSession = false;
|
|
665
735
|
},
|
|
666
736
|
});
|
|
@@ -818,6 +888,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
818
888
|
}]
|
|
819
889
|
}] });
|
|
820
890
|
|
|
891
|
+
/**
|
|
892
|
+
* SVG icons used internally by TAS SDK components.
|
|
893
|
+
* Icons are stored as strings to avoid asset bundling complexity.
|
|
894
|
+
*/
|
|
895
|
+
const TAS_ICONS = {
|
|
896
|
+
home: `<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
897
|
+
<rect x="10" width="100" height="100" rx="50" fill="#44D8E8" fill-opacity="0.2"/>
|
|
898
|
+
<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"/>
|
|
899
|
+
<path d="M110 5L106.575 3.425L105 0L103.425 3.425L100 5L103.425 6.575L105 10L106.575 6.575L110 5Z" fill="#44D8E8"/>
|
|
900
|
+
<path d="M10 51L6.575 49.425L5 46L3.425 49.425L0 51L3.425 52.575L5 56L6.575 52.575L10 51Z" fill="#44D8E8"/>
|
|
901
|
+
<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"/>
|
|
902
|
+
<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"/>
|
|
903
|
+
<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"/>
|
|
904
|
+
<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"/>
|
|
905
|
+
</svg>`,
|
|
906
|
+
};
|
|
907
|
+
|
|
821
908
|
class TasAvatarComponent {
|
|
822
909
|
constructor() {
|
|
823
910
|
this.name = '';
|
|
@@ -891,11 +978,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
891
978
|
type: Input
|
|
892
979
|
}] } });
|
|
893
980
|
|
|
981
|
+
/** User geolocation panel view states */
|
|
982
|
+
var UserGeoViewState;
|
|
983
|
+
(function (UserGeoViewState) {
|
|
984
|
+
UserGeoViewState["HIDDEN"] = "HIDDEN";
|
|
985
|
+
UserGeoViewState["INITIAL"] = "INITIAL";
|
|
986
|
+
UserGeoViewState["VERIFYING"] = "VERIFYING";
|
|
987
|
+
UserGeoViewState["VERIFIED"] = "VERIFIED";
|
|
988
|
+
UserGeoViewState["DENIED"] = "DENIED";
|
|
989
|
+
})(UserGeoViewState || (UserGeoViewState = {}));
|
|
894
990
|
class TasVideocallComponent {
|
|
895
|
-
constructor(activeModal, tasService, geolocationService) {
|
|
991
|
+
constructor(activeModal, tasService, geolocationService, sanitizer) {
|
|
896
992
|
this.activeModal = activeModal;
|
|
897
993
|
this.tasService = tasService;
|
|
898
994
|
this.geolocationService = geolocationService;
|
|
995
|
+
this.sanitizer = sanitizer;
|
|
899
996
|
this.participantName = '';
|
|
900
997
|
this.tenant = '';
|
|
901
998
|
this.businessRole = TasBusinessRole.USER;
|
|
@@ -913,11 +1010,22 @@ class TasVideocallComponent {
|
|
|
913
1010
|
// Geo panel states for owner
|
|
914
1011
|
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
915
1012
|
this.allGeoGranted = false; // All users responded with geo
|
|
1013
|
+
this.userGeoInfo = []; // Individual user geo status
|
|
1014
|
+
// User geo panel state (for owners)
|
|
1015
|
+
this.userGeoViewState = UserGeoViewState.HIDDEN;
|
|
1016
|
+
this.UserGeoViewState = UserGeoViewState; // Expose enum to template
|
|
1017
|
+
this.devModeEnabled = false; // Enable dev controls for testing
|
|
1018
|
+
this.geoPanelDismissed = false; // Track if owner manually closed the panel
|
|
916
1019
|
this.subscriptions = new Subscription();
|
|
1020
|
+
this.homeIcon = this.sanitizer.bypassSecurityTrustHtml(TAS_ICONS.home);
|
|
917
1021
|
}
|
|
918
1022
|
ngOnInit() {
|
|
919
1023
|
this.setupSubscriptions();
|
|
920
1024
|
this.initializeCall();
|
|
1025
|
+
// For owners: show the geo panel to request user location (unless dismissed)
|
|
1026
|
+
if (this.canAdmitUsers && !this.geoPanelDismissed) {
|
|
1027
|
+
this.userGeoViewState = UserGeoViewState.INITIAL;
|
|
1028
|
+
}
|
|
921
1029
|
}
|
|
922
1030
|
ngAfterViewInit() {
|
|
923
1031
|
this.initInteract();
|
|
@@ -959,6 +1067,43 @@ class TasVideocallComponent {
|
|
|
959
1067
|
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
960
1068
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
961
1069
|
}
|
|
1070
|
+
/** Users with pending geo status */
|
|
1071
|
+
get pendingUsers() {
|
|
1072
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.PENDING);
|
|
1073
|
+
}
|
|
1074
|
+
/** Users who granted geo */
|
|
1075
|
+
get grantedUsers() {
|
|
1076
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1077
|
+
}
|
|
1078
|
+
/** Users who denied geo */
|
|
1079
|
+
get deniedUsers() {
|
|
1080
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.DENIED);
|
|
1081
|
+
}
|
|
1082
|
+
/** Show location panel only if owner and there are users who haven't granted */
|
|
1083
|
+
get shouldShowLocationPanel() {
|
|
1084
|
+
if (!this.canAdmitUsers || this.userGeoInfo.length === 0) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
// Hide if all users have granted
|
|
1088
|
+
const allGranted = this.userGeoInfo.every(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1089
|
+
return !allGranted;
|
|
1090
|
+
}
|
|
1091
|
+
/** Check if any user has denied geo */
|
|
1092
|
+
get hasAnyDenied() {
|
|
1093
|
+
return this.deniedUsers.length > 0;
|
|
1094
|
+
}
|
|
1095
|
+
/** Show user geo panel for owners when not hidden */
|
|
1096
|
+
get shouldShowUserGeoPanel() {
|
|
1097
|
+
return this.canAdmitUsers && this.userGeoViewState !== UserGeoViewState.HIDDEN;
|
|
1098
|
+
}
|
|
1099
|
+
/** Set user geo view state (for dev controls or close button) */
|
|
1100
|
+
setUserGeoViewState(state) {
|
|
1101
|
+
this.userGeoViewState = state;
|
|
1102
|
+
// Track if owner manually dismissed the panel
|
|
1103
|
+
if (state === UserGeoViewState.HIDDEN) {
|
|
1104
|
+
this.geoPanelDismissed = true;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
962
1107
|
/**
|
|
963
1108
|
* Admit a user from the waiting room
|
|
964
1109
|
*/
|
|
@@ -997,7 +1142,7 @@ class TasVideocallComponent {
|
|
|
997
1142
|
this.showLocationPanel = false;
|
|
998
1143
|
}
|
|
999
1144
|
/**
|
|
1000
|
-
* Request the user to share their location
|
|
1145
|
+
* Request the user to share their location (called by owner)
|
|
1001
1146
|
*/
|
|
1002
1147
|
requestUserLocation() {
|
|
1003
1148
|
if (!this.videoCallId) {
|
|
@@ -1008,9 +1153,12 @@ class TasVideocallComponent {
|
|
|
1008
1153
|
videoCallId: this.videoCallId,
|
|
1009
1154
|
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
1010
1155
|
};
|
|
1011
|
-
// TODO: Send location request action to backend when endpoint is ready
|
|
1012
1156
|
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1013
|
-
next: () =>
|
|
1157
|
+
next: () => {
|
|
1158
|
+
console.log('Location request sent');
|
|
1159
|
+
// Set panel to verifying state while waiting for user response
|
|
1160
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1161
|
+
},
|
|
1014
1162
|
error: (err) => console.error('Error requesting location:', err),
|
|
1015
1163
|
});
|
|
1016
1164
|
}
|
|
@@ -1047,7 +1195,6 @@ class TasVideocallComponent {
|
|
|
1047
1195
|
// Owner left subscription - disconnect non-owners
|
|
1048
1196
|
this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
|
|
1049
1197
|
if (hasLeft && !this.canAdmitUsers) { // Non-owner user
|
|
1050
|
-
console.log('[TAS DEBUG] Owner left, disconnecting user');
|
|
1051
1198
|
this.hangUp();
|
|
1052
1199
|
this.activeModal.close('owner_left');
|
|
1053
1200
|
}
|
|
@@ -1055,61 +1202,74 @@ class TasVideocallComponent {
|
|
|
1055
1202
|
// ActivateGeo subscription - only for non-owners (users)
|
|
1056
1203
|
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
1057
1204
|
if (activateGeo && !this.canAdmitUsers) {
|
|
1058
|
-
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
1059
1205
|
this.handleActivateGeo();
|
|
1060
1206
|
}
|
|
1061
1207
|
}));
|
|
1062
1208
|
// GeoRequestActive subscription - for owners
|
|
1063
1209
|
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
1064
|
-
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
1065
1210
|
this.geoRequestActive = active;
|
|
1066
1211
|
}));
|
|
1067
|
-
// AllGeoGranted subscription - for owners
|
|
1212
|
+
// AllGeoGranted subscription - for owners to update panel state
|
|
1068
1213
|
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1069
|
-
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1070
1214
|
this.allGeoGranted = granted;
|
|
1215
|
+
// For owners: update panel state based on geo status
|
|
1216
|
+
if (this.canAdmitUsers && granted) {
|
|
1217
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1218
|
+
}
|
|
1219
|
+
}));
|
|
1220
|
+
// UserGeoInfo subscription - for owner geo panel
|
|
1221
|
+
this.subscriptions.add(this.tasService.userGeoInfo$.subscribe((info) => {
|
|
1222
|
+
this.userGeoInfo = info;
|
|
1223
|
+
// Note: We don't auto-switch to DENIED here - that only happens
|
|
1224
|
+
// after owner requests and user denies (via geoRequestActive flow)
|
|
1071
1225
|
}));
|
|
1072
1226
|
}
|
|
1073
1227
|
/**
|
|
1074
1228
|
* Handle activateGeo request from backend (for non-owner users).
|
|
1075
|
-
*
|
|
1229
|
+
* Directly prompts browser for geolocation - no panel for users.
|
|
1076
1230
|
*/
|
|
1077
1231
|
async handleActivateGeo() {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1232
|
+
// Clear any cached position to force re-prompting
|
|
1233
|
+
this.geolocationService.clearCache();
|
|
1234
|
+
// Request geolocation from user (browser will prompt)
|
|
1235
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1236
|
+
if (position) {
|
|
1083
1237
|
this.geoLocationStatus = 'active';
|
|
1084
|
-
this.reportGeoStatus(
|
|
1085
|
-
return;
|
|
1238
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1086
1239
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1240
|
+
else {
|
|
1241
|
+
this.geoLocationStatus = 'denied';
|
|
1242
|
+
this.denyGeoLocation();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/** Start geolocation verification (called from template button) */
|
|
1246
|
+
async startGeoVerification() {
|
|
1247
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1248
|
+
// Clear any cached position to force re-prompting
|
|
1249
|
+
this.geolocationService.clearCache();
|
|
1250
|
+
// Request geolocation from user (browser will prompt)
|
|
1089
1251
|
const position = await this.geolocationService.getCurrentPosition();
|
|
1090
1252
|
if (position) {
|
|
1091
|
-
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1092
1253
|
this.geoLocationStatus = 'active';
|
|
1254
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1093
1255
|
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1094
1256
|
}
|
|
1095
1257
|
else {
|
|
1096
|
-
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1097
1258
|
this.geoLocationStatus = 'denied';
|
|
1098
|
-
|
|
1099
|
-
this.
|
|
1259
|
+
this.userGeoViewState = UserGeoViewState.DENIED;
|
|
1260
|
+
this.denyGeoLocation();
|
|
1100
1261
|
}
|
|
1101
1262
|
}
|
|
1102
1263
|
/**
|
|
1103
|
-
* Report geolocation
|
|
1264
|
+
* Report granted geolocation to backend.
|
|
1265
|
+
* IMPORTANT: Only call with valid coordinates.
|
|
1104
1266
|
*/
|
|
1105
1267
|
reportGeoStatus(latitude, longitude) {
|
|
1106
1268
|
if (!this.videoCallId) {
|
|
1107
|
-
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1108
1269
|
return;
|
|
1109
1270
|
}
|
|
1110
|
-
//
|
|
1111
|
-
if (
|
|
1112
|
-
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1271
|
+
// Validate coordinates are present
|
|
1272
|
+
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
|
|
1113
1273
|
return;
|
|
1114
1274
|
}
|
|
1115
1275
|
const body = {
|
|
@@ -1119,11 +1279,21 @@ class TasVideocallComponent {
|
|
|
1119
1279
|
latitude,
|
|
1120
1280
|
longitude,
|
|
1121
1281
|
};
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1282
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Report denied geolocation to backend.
|
|
1286
|
+
*/
|
|
1287
|
+
denyGeoLocation() {
|
|
1288
|
+
if (!this.videoCallId) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const body = {
|
|
1292
|
+
userId: this.userId,
|
|
1293
|
+
videoCallId: this.videoCallId,
|
|
1294
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1295
|
+
};
|
|
1296
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1127
1297
|
}
|
|
1128
1298
|
initializeCall() {
|
|
1129
1299
|
if (this.isReturningFromPip) {
|
|
@@ -1212,12 +1382,12 @@ class TasVideocallComponent {
|
|
|
1212
1382
|
});
|
|
1213
1383
|
}
|
|
1214
1384
|
}
|
|
1215
|
-
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 });
|
|
1216
|
-
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 <!-- 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: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-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"] }] });
|
|
1385
|
+
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 });
|
|
1386
|
+
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"] }] });
|
|
1217
1387
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
1218
1388
|
type: Component,
|
|
1219
|
-
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 <!-- 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: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-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"] }]
|
|
1220
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
1389
|
+
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"] }]
|
|
1390
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i4.DomSanitizer }]; }, propDecorators: { sessionId: [{
|
|
1221
1391
|
type: Input
|
|
1222
1392
|
}], token: [{
|
|
1223
1393
|
type: Input
|
|
@@ -1277,19 +1447,40 @@ class TasWaitingRoomComponent {
|
|
|
1277
1447
|
this.videoCallModalRef = null;
|
|
1278
1448
|
// Geolocation
|
|
1279
1449
|
this.geoPosition = null;
|
|
1450
|
+
this.geoWasDenied = false; // Tracks if user denied geo permission
|
|
1451
|
+
this.geoResultSent = false; // Tracks if geo result was already sent
|
|
1452
|
+
// Permission tracking for auto-join
|
|
1453
|
+
this.mediaPermissionsGranted = false;
|
|
1454
|
+
this.geoPermissionsResolved = false;
|
|
1455
|
+
// Auto-retry tracking
|
|
1456
|
+
this.retryCount = 0;
|
|
1457
|
+
this.MAX_RETRIES = 1;
|
|
1280
1458
|
}
|
|
1281
1459
|
/** Whether current user is an owner */
|
|
1282
1460
|
get isOwner() {
|
|
1283
1461
|
return this.currentUser?.role === TasUserRole.OWNER;
|
|
1284
1462
|
}
|
|
1463
|
+
/** Whether we're still requesting permissions (for template) */
|
|
1464
|
+
get isRequestingPermissions() {
|
|
1465
|
+
return !this.mediaPermissionsGranted || (!this.geoPermissionsResolved && !this.isOwner && !this.isBackoffice);
|
|
1466
|
+
}
|
|
1467
|
+
/** Whether we're waiting for admission after permissions granted (for template) */
|
|
1468
|
+
get isWaitingForAdmission() {
|
|
1469
|
+
return this.mediaPermissionsGranted &&
|
|
1470
|
+
(this.geoPermissionsResolved || this.isOwner || this.isBackoffice) &&
|
|
1471
|
+
!this.isJoinable;
|
|
1472
|
+
}
|
|
1285
1473
|
ngOnInit() {
|
|
1286
1474
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1287
1475
|
this.requestMediaPermissions();
|
|
1476
|
+
// Request geolocation permission and cache it (but don't send to backend yet)
|
|
1477
|
+
// The cached position will be sent when owner requests it via activateGeo
|
|
1288
1478
|
this.requestGeolocation();
|
|
1289
1479
|
this.checkStatus();
|
|
1290
1480
|
}
|
|
1291
1481
|
/**
|
|
1292
1482
|
* Request camera and microphone permissions.
|
|
1483
|
+
* Triggers auto-join when permissions are resolved.
|
|
1293
1484
|
*/
|
|
1294
1485
|
async requestMediaPermissions() {
|
|
1295
1486
|
console.log('[TAS DEBUG] Requesting media permissions...');
|
|
@@ -1298,37 +1489,71 @@ class TasWaitingRoomComponent {
|
|
|
1298
1489
|
// Stop tracks immediately - we just needed the permission
|
|
1299
1490
|
stream.getTracks().forEach(track => track.stop());
|
|
1300
1491
|
console.log('[TAS DEBUG] Media permissions granted');
|
|
1492
|
+
this.mediaPermissionsGranted = true;
|
|
1301
1493
|
}
|
|
1302
1494
|
catch (error) {
|
|
1303
1495
|
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
1496
|
+
// Still allow joining - some users may not have camera/mic
|
|
1497
|
+
this.mediaPermissionsGranted = true;
|
|
1304
1498
|
}
|
|
1499
|
+
this.tryAutoJoin();
|
|
1500
|
+
this.cdr.detectChanges();
|
|
1305
1501
|
}
|
|
1306
1502
|
/**
|
|
1307
|
-
* Request geolocation
|
|
1503
|
+
* Request geolocation permission and cache result.
|
|
1308
1504
|
* Only for regular users (not owners/backoffice).
|
|
1309
|
-
*
|
|
1505
|
+
* Actual send to backend happens after we have videoCallId.
|
|
1506
|
+
* Triggers auto-join when geolocation is resolved.
|
|
1310
1507
|
*/
|
|
1311
1508
|
async requestGeolocation() {
|
|
1312
1509
|
// Only request geolocation for regular users, not owners/backoffice
|
|
1313
1510
|
if (this.isOwner || this.isBackoffice) {
|
|
1314
1511
|
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1512
|
+
this.geoPermissionsResolved = true;
|
|
1315
1513
|
return;
|
|
1316
1514
|
}
|
|
1317
|
-
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1515
|
+
console.log('[TAS DEBUG] Requesting geolocation permission...');
|
|
1318
1516
|
const position = await this.geolocationService.getCurrentPosition();
|
|
1319
1517
|
if (position) {
|
|
1320
|
-
console.log('[TAS DEBUG] Geolocation
|
|
1518
|
+
console.log('[TAS DEBUG] Geolocation granted:', position);
|
|
1321
1519
|
this.geoPosition = position;
|
|
1322
|
-
|
|
1323
|
-
this.sendGeolocationToBackend();
|
|
1520
|
+
this.geoWasDenied = false;
|
|
1324
1521
|
}
|
|
1325
1522
|
else {
|
|
1326
1523
|
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1524
|
+
this.geoPosition = null;
|
|
1525
|
+
this.geoWasDenied = true;
|
|
1526
|
+
}
|
|
1527
|
+
this.geoPermissionsResolved = true;
|
|
1528
|
+
// If we already have videoCallId, send now. Otherwise, it will be sent after status response.
|
|
1529
|
+
this.sendGeoResultToBackend();
|
|
1530
|
+
this.tryAutoJoin();
|
|
1531
|
+
this.cdr.detectChanges();
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Send geo result to backend (granted or denied).
|
|
1535
|
+
* Only sends if videoCallId is available and not already sent.
|
|
1536
|
+
*/
|
|
1537
|
+
sendGeoResultToBackend() {
|
|
1538
|
+
if (!this.videoCallId) {
|
|
1539
|
+
console.log('[TAS DEBUG] Cannot send geo result: videoCallId not available yet');
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (this.geoResultSent) {
|
|
1543
|
+
console.log('[TAS DEBUG] Geo result already sent, skipping');
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (this.geoPosition) {
|
|
1547
|
+
this.sendGeolocationToBackend();
|
|
1548
|
+
this.geoResultSent = true;
|
|
1549
|
+
}
|
|
1550
|
+
else if (this.geoWasDenied) {
|
|
1551
|
+
this.sendGeoDenialToBackend();
|
|
1552
|
+
this.geoResultSent = true;
|
|
1327
1553
|
}
|
|
1328
1554
|
}
|
|
1329
1555
|
/**
|
|
1330
|
-
* Send geolocation to backend via modify user endpoint.
|
|
1331
|
-
* NOTE: Endpoint call is prepared but may not be active yet.
|
|
1556
|
+
* Send granted geolocation to backend via modify user endpoint.
|
|
1332
1557
|
*/
|
|
1333
1558
|
sendGeolocationToBackend() {
|
|
1334
1559
|
if (!this.geoPosition || !this.videoCallId) {
|
|
@@ -1336,7 +1561,6 @@ class TasWaitingRoomComponent {
|
|
|
1336
1561
|
}
|
|
1337
1562
|
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
1338
1563
|
const body = {
|
|
1339
|
-
userId: this.currentUser?.id,
|
|
1340
1564
|
videoCallId: this.videoCallId,
|
|
1341
1565
|
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1342
1566
|
latitude: this.geoPosition.latitude,
|
|
@@ -1347,6 +1571,23 @@ class TasWaitingRoomComponent {
|
|
|
1347
1571
|
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1348
1572
|
});
|
|
1349
1573
|
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Send geolocation denial to backend via modify user endpoint.
|
|
1576
|
+
*/
|
|
1577
|
+
sendGeoDenialToBackend() {
|
|
1578
|
+
if (!this.videoCallId) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
console.log('[TAS DEBUG] Sending geo denial to backend...');
|
|
1582
|
+
const body = {
|
|
1583
|
+
videoCallId: this.videoCallId,
|
|
1584
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1585
|
+
};
|
|
1586
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1587
|
+
next: () => console.log('[TAS DEBUG] Geo denial sent successfully'),
|
|
1588
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geo denial:', err),
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1350
1591
|
ngOnDestroy() {
|
|
1351
1592
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1352
1593
|
this.subscriptions.unsubscribe();
|
|
@@ -1380,8 +1621,8 @@ class TasWaitingRoomComponent {
|
|
|
1380
1621
|
this.resolvedAppointmentId = content.appointmentId;
|
|
1381
1622
|
this.videoCallId = content.videoCallId;
|
|
1382
1623
|
console.log('[TAS DEBUG] Status response:', content);
|
|
1383
|
-
//
|
|
1384
|
-
this.
|
|
1624
|
+
// Now that we have videoCallId, send geo result if not already sent
|
|
1625
|
+
this.sendGeoResultToBackend();
|
|
1385
1626
|
// Start polling for status updates
|
|
1386
1627
|
this.tasService.startStatusPolling(statusParams);
|
|
1387
1628
|
// Subscribe to joinable status
|
|
@@ -1397,7 +1638,8 @@ class TasWaitingRoomComponent {
|
|
|
1397
1638
|
}));
|
|
1398
1639
|
}
|
|
1399
1640
|
/**
|
|
1400
|
-
* Handle changes to joinable status
|
|
1641
|
+
* Handle changes to joinable status.
|
|
1642
|
+
* Triggers auto-join when joinable becomes true.
|
|
1401
1643
|
*/
|
|
1402
1644
|
handleJoinableChange(joinable) {
|
|
1403
1645
|
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
@@ -1413,10 +1655,11 @@ class TasWaitingRoomComponent {
|
|
|
1413
1655
|
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
1414
1656
|
return;
|
|
1415
1657
|
}
|
|
1416
|
-
//
|
|
1658
|
+
// Update state and attempt auto-join
|
|
1417
1659
|
if (joinable) {
|
|
1418
|
-
console.log('[TAS DEBUG] Joinable is true,
|
|
1660
|
+
console.log('[TAS DEBUG] Joinable is true, attempting auto-join');
|
|
1419
1661
|
this.state = WaitingRoomState.READY;
|
|
1662
|
+
this.tryAutoJoin();
|
|
1420
1663
|
}
|
|
1421
1664
|
else {
|
|
1422
1665
|
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
@@ -1425,11 +1668,33 @@ class TasWaitingRoomComponent {
|
|
|
1425
1668
|
this.cdr.detectChanges();
|
|
1426
1669
|
}
|
|
1427
1670
|
/**
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1671
|
+
* Attempt to auto-join the session when all conditions are met.
|
|
1672
|
+
* - For owners: just need joinable + sessionId
|
|
1673
|
+
* - For users: need media permissions + geo resolved + joinable + sessionId
|
|
1430
1674
|
*/
|
|
1431
|
-
|
|
1432
|
-
|
|
1675
|
+
tryAutoJoin() {
|
|
1676
|
+
// Don't try if already joining or in error
|
|
1677
|
+
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
1678
|
+
this.state === WaitingRoomState.JOINING) {
|
|
1679
|
+
console.log('[TAS DEBUG] tryAutoJoin skipped - already in state:', this.state);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
// For owners: just need joinable
|
|
1683
|
+
if (this.isOwner) {
|
|
1684
|
+
if (this.isJoinable && this.resolvedSessionId) {
|
|
1685
|
+
console.log('[TAS DEBUG] Owner auto-joining...');
|
|
1686
|
+
this.startSessionAndJoin();
|
|
1687
|
+
}
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
// For users: need media + geo resolved + joinable
|
|
1691
|
+
if (this.mediaPermissionsGranted &&
|
|
1692
|
+
this.geoPermissionsResolved &&
|
|
1693
|
+
this.isJoinable &&
|
|
1694
|
+
this.resolvedSessionId) {
|
|
1695
|
+
console.log('[TAS DEBUG] User auto-joining...');
|
|
1696
|
+
this.startSessionAndJoin();
|
|
1697
|
+
}
|
|
1433
1698
|
}
|
|
1434
1699
|
/**
|
|
1435
1700
|
* Check if user has owner/backoffice role
|
|
@@ -1482,28 +1747,34 @@ class TasWaitingRoomComponent {
|
|
|
1482
1747
|
},
|
|
1483
1748
|
error: (err) => {
|
|
1484
1749
|
console.error('[TAS DEBUG] /start request failed:', err);
|
|
1485
|
-
|
|
1486
|
-
this.
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1750
|
+
// Auto-retry on first failure
|
|
1751
|
+
if (this.retryCount < this.MAX_RETRIES) {
|
|
1752
|
+
this.retryCount++;
|
|
1753
|
+
console.log('[TAS DEBUG] Auto-retrying... attempt:', this.retryCount);
|
|
1754
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1755
|
+
this.cdr.detectChanges();
|
|
1756
|
+
setTimeout(() => this.startSessionAndJoin(), 2000);
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
// Show error after retry fails
|
|
1760
|
+
this.state = WaitingRoomState.ERROR;
|
|
1761
|
+
this.errorMessage = err?.error?.message || err?.message || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
|
|
1762
|
+
this.tasService.stopStatusPolling();
|
|
1763
|
+
console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
|
|
1764
|
+
this.cdr.detectChanges();
|
|
1765
|
+
}
|
|
1490
1766
|
},
|
|
1491
1767
|
}));
|
|
1492
1768
|
}
|
|
1493
1769
|
/**
|
|
1494
|
-
*
|
|
1495
|
-
|
|
1496
|
-
cancel() {
|
|
1497
|
-
this.tasService.stopStatusPolling();
|
|
1498
|
-
this.activeModal.dismiss('cancel');
|
|
1499
|
-
}
|
|
1500
|
-
/**
|
|
1501
|
-
* Retry after an error
|
|
1770
|
+
* Retry after an error.
|
|
1771
|
+
* Resets retry count and restarts the flow.
|
|
1502
1772
|
*/
|
|
1503
1773
|
retry() {
|
|
1504
1774
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1505
1775
|
this.errorMessage = '';
|
|
1506
1776
|
this.token = '';
|
|
1777
|
+
this.retryCount = 0; // Reset retry count for auto-retry
|
|
1507
1778
|
this.checkStatus();
|
|
1508
1779
|
}
|
|
1509
1780
|
openVideoCallModal() {
|
|
@@ -1529,10 +1800,10 @@ class TasWaitingRoomComponent {
|
|
|
1529
1800
|
}
|
|
1530
1801
|
}
|
|
1531
1802
|
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 });
|
|
1532
|
-
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
|
|
1803
|
+
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"] }] });
|
|
1533
1804
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
1534
1805
|
type: Component,
|
|
1535
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n
|
|
1806
|
+
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"] }]
|
|
1536
1807
|
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1537
1808
|
type: Input
|
|
1538
1809
|
}], entityId: [{
|
|
@@ -1556,6 +1827,9 @@ class TasButtonComponent {
|
|
|
1556
1827
|
// Style customization
|
|
1557
1828
|
this.variant = 'default';
|
|
1558
1829
|
this.buttonLabel = 'Iniciar TAS';
|
|
1830
|
+
// Skip status check - when true, button shows immediately without API call
|
|
1831
|
+
// Useful when parent component already knows the appointment is valid
|
|
1832
|
+
this.skipStatusCheck = false;
|
|
1559
1833
|
this.isLoading = false;
|
|
1560
1834
|
// Status check state
|
|
1561
1835
|
this.isCheckingStatus = false;
|
|
@@ -1600,6 +1874,12 @@ class TasButtonComponent {
|
|
|
1600
1874
|
this.videoCallModalRef = null;
|
|
1601
1875
|
}
|
|
1602
1876
|
}));
|
|
1877
|
+
// If skipStatusCheck is true, show button immediately without polling
|
|
1878
|
+
if (this.skipStatusCheck) {
|
|
1879
|
+
this.shouldShowButton = true;
|
|
1880
|
+
this.isJoinable = true;
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1603
1883
|
// Start status checking
|
|
1604
1884
|
this.startStatusPolling();
|
|
1605
1885
|
}
|
|
@@ -1646,6 +1926,11 @@ class TasButtonComponent {
|
|
|
1646
1926
|
})
|
|
1647
1927
|
.subscribe({
|
|
1648
1928
|
next: (response) => {
|
|
1929
|
+
// Validate response structure
|
|
1930
|
+
if (!response || !response.content) {
|
|
1931
|
+
this.handleStatusError('Invalid response');
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1649
1934
|
this.isCheckingStatus = false;
|
|
1650
1935
|
this.isStatusError = false;
|
|
1651
1936
|
this.statusErrorMessage = '';
|
|
@@ -1654,23 +1939,20 @@ class TasButtonComponent {
|
|
|
1654
1939
|
this.isJoinable = response.content?.joinable ?? false;
|
|
1655
1940
|
},
|
|
1656
1941
|
error: (err) => {
|
|
1657
|
-
this.
|
|
1658
|
-
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1659
|
-
this.statusErrorMessage = errorMessage;
|
|
1660
|
-
// Use utility service to determine if button should be shown
|
|
1661
|
-
this.shouldShowButton = this.tasUtilityService.shouldShowButton(errorMessage);
|
|
1662
|
-
// Stop polling on error
|
|
1663
|
-
this.stopStatusPolling();
|
|
1664
|
-
// If button should be hidden, don't treat as error
|
|
1665
|
-
if (!this.shouldShowButton) {
|
|
1666
|
-
this.isStatusError = false;
|
|
1667
|
-
}
|
|
1668
|
-
else {
|
|
1669
|
-
this.isStatusError = true;
|
|
1670
|
-
}
|
|
1942
|
+
this.handleStatusError(err);
|
|
1671
1943
|
},
|
|
1672
1944
|
}));
|
|
1673
1945
|
}
|
|
1946
|
+
handleStatusError(err) {
|
|
1947
|
+
this.isCheckingStatus = false;
|
|
1948
|
+
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1949
|
+
this.statusErrorMessage = errorMessage;
|
|
1950
|
+
// On any status error, hide the button
|
|
1951
|
+
this.shouldShowButton = false;
|
|
1952
|
+
this.isStatusError = false; // We don't show error UI, just hide the button
|
|
1953
|
+
// Stop polling on error
|
|
1954
|
+
this.stopStatusPolling();
|
|
1955
|
+
}
|
|
1674
1956
|
onClick() {
|
|
1675
1957
|
if (!this.tenant || !this.currentUser?.name) {
|
|
1676
1958
|
return;
|
|
@@ -1719,7 +2001,7 @@ class TasButtonComponent {
|
|
|
1719
2001
|
}
|
|
1720
2002
|
}
|
|
1721
2003
|
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 });
|
|
1722
|
-
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"] }] });
|
|
2004
|
+
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"] }] });
|
|
1723
2005
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
|
|
1724
2006
|
type: Component,
|
|
1725
2007
|
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"] }]
|
|
@@ -1737,6 +2019,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1737
2019
|
type: Input
|
|
1738
2020
|
}], buttonLabel: [{
|
|
1739
2021
|
type: Input
|
|
2022
|
+
}], skipStatusCheck: [{
|
|
2023
|
+
type: Input
|
|
1740
2024
|
}] } });
|
|
1741
2025
|
|
|
1742
2026
|
class TasFloatingCallComponent {
|
|
@@ -1977,7 +2261,7 @@ class TasIncomingAppointmentComponent {
|
|
|
1977
2261
|
}
|
|
1978
2262
|
}
|
|
1979
2263
|
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1980
|
-
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"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
2264
|
+
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"] }] });
|
|
1981
2265
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
1982
2266
|
type: Component,
|
|
1983
2267
|
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"] }]
|
|
@@ -2084,5 +2368,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2084
2368
|
* Generated bundle index. Do not edit.
|
|
2085
2369
|
*/
|
|
2086
2370
|
|
|
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 };
|
|
2371
|
+
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
2372
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|