tas-uell-sdk 0.1.3 → 0.3.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 +3 -0
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +17 -15
- package/esm2020/lib/components/tas-feedback-modal/tas-feedback-modal.component.mjs +229 -0
- package/esm2020/lib/components/tas-floating-call/tas-floating-call.component.mjs +38 -3
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +46 -8
- package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +163 -43
- package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +150 -34
- package/esm2020/lib/icons/tas-icons.mjs +17 -0
- package/esm2020/lib/interfaces/tas.interfaces.mjs +13 -1
- package/esm2020/lib/services/tas.service.mjs +127 -26
- package/esm2020/lib/tas-uell-sdk.module.mjs +8 -3
- package/esm2020/public-api.mjs +2 -1
- package/fesm2015/tas-uell-sdk.mjs +794 -124
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +787 -123
- package/fesm2020/tas-uell-sdk.mjs.map +1 -1
- package/lib/components/tas-btn/tas-btn.component.d.ts +1 -0
- package/lib/components/tas-feedback-modal/tas-feedback-modal.component.d.ts +101 -0
- package/lib/components/tas-floating-call/tas-floating-call.component.d.ts +5 -0
- package/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.d.ts +12 -1
- package/lib/components/tas-videocall/tas-videocall.component.d.ts +49 -6
- package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +40 -12
- package/lib/icons/tas-icons.d.ts +8 -0
- package/lib/interfaces/tas.interfaces.d.ts +36 -3
- package/lib/services/tas.service.d.ts +27 -2
- package/lib/tas-uell-sdk.module.d.ts +5 -4
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
- package/src/lib/styles/tas-global.scss +17 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Inject, Component,
|
|
3
|
-
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
4
|
-
import { map, catchError } from 'rxjs/operators';
|
|
2
|
+
import { InjectionToken, Injectable, Inject, Component, Input, ChangeDetectionStrategy, ViewChild, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
|
4
|
+
import { map, catchError, finalize, shareReplay } from 'rxjs/operators';
|
|
5
5
|
import * as OT from '@opentok/client';
|
|
6
6
|
import { __awaiter } from 'tslib';
|
|
7
|
-
import interact from 'interactjs';
|
|
8
7
|
import * as i1 from '@ng-bootstrap/ng-bootstrap';
|
|
9
8
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
10
9
|
import * as i4 from '@angular/common';
|
|
11
10
|
import { CommonModule } from '@angular/common';
|
|
11
|
+
import * as i4$1 from '@angular/forms';
|
|
12
12
|
import { FormsModule } from '@angular/forms';
|
|
13
|
+
import interact from 'interactjs';
|
|
14
|
+
import * as i4$2 from '@angular/platform-browser';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Injection token for TAS configuration
|
|
@@ -67,7 +69,14 @@ var UserCallAction;
|
|
|
67
69
|
UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
|
|
68
70
|
UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
|
|
69
71
|
UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
|
|
72
|
+
UserCallAction["DENY_GEOLOCATION"] = "DENY_GEOLOCATION";
|
|
70
73
|
})(UserCallAction || (UserCallAction = {}));
|
|
74
|
+
var GeoStatus;
|
|
75
|
+
(function (GeoStatus) {
|
|
76
|
+
GeoStatus["PENDING"] = "PENDING";
|
|
77
|
+
GeoStatus["GRANTED"] = "GRANTED";
|
|
78
|
+
GeoStatus["DENIED"] = "DENIED";
|
|
79
|
+
})(GeoStatus || (GeoStatus = {}));
|
|
71
80
|
var RoomUserStatus;
|
|
72
81
|
(function (RoomUserStatus) {
|
|
73
82
|
RoomUserStatus["ASSIGNED"] = "ASSIGNED";
|
|
@@ -88,6 +97,11 @@ var ViewMode;
|
|
|
88
97
|
ViewMode["FULLSCREEN"] = "FULLSCREEN";
|
|
89
98
|
ViewMode["PIP"] = "PIP";
|
|
90
99
|
})(ViewMode || (ViewMode = {}));
|
|
100
|
+
var FeedbackMotiveType;
|
|
101
|
+
(function (FeedbackMotiveType) {
|
|
102
|
+
FeedbackMotiveType["TECHNICAL"] = "TECHNICAL";
|
|
103
|
+
FeedbackMotiveType["BUSINESS"] = "BUSINESS";
|
|
104
|
+
})(FeedbackMotiveType || (FeedbackMotiveType = {}));
|
|
91
105
|
// Appointment types
|
|
92
106
|
var AppointmentStatus;
|
|
93
107
|
(function (AppointmentStatus) {
|
|
@@ -199,6 +213,9 @@ class TasService {
|
|
|
199
213
|
// Observable for when all geo has been granted
|
|
200
214
|
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
201
215
|
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
216
|
+
// Observable for individual user geo status (for owner panel)
|
|
217
|
+
this.userGeoInfoSubject = new BehaviorSubject([]);
|
|
218
|
+
this.userGeoInfo$ = this.userGeoInfoSubject.asObservable();
|
|
202
219
|
this.statusPollingInterval = null;
|
|
203
220
|
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
204
221
|
this.wasOwnerPresent = false;
|
|
@@ -206,6 +223,10 @@ class TasService {
|
|
|
206
223
|
this.currentAppointmentId = null;
|
|
207
224
|
this.currentVideoCallId = null;
|
|
208
225
|
this.currentTenant = null;
|
|
226
|
+
// Status cache (1 second TTL)
|
|
227
|
+
this.STATUS_CACHE_TTL_MS = 1000;
|
|
228
|
+
this.statusCache = null;
|
|
229
|
+
this.inflightStatusRequests = new Map();
|
|
209
230
|
}
|
|
210
231
|
// ... (Getters and other methods remain unchanged)
|
|
211
232
|
/**
|
|
@@ -224,7 +245,6 @@ class TasService {
|
|
|
224
245
|
if (params.tenant) {
|
|
225
246
|
this.currentTenant = params.tenant;
|
|
226
247
|
}
|
|
227
|
-
console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
|
|
228
248
|
// Initial status fetch
|
|
229
249
|
this.fetchAndProcessStatus(params);
|
|
230
250
|
// Set up periodic polling
|
|
@@ -328,7 +348,6 @@ class TasService {
|
|
|
328
348
|
}
|
|
329
349
|
// Session Management
|
|
330
350
|
disconnectSession(clearStorage = true) {
|
|
331
|
-
console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
|
|
332
351
|
// Call finishSession before disconnecting if we have a sessionId
|
|
333
352
|
const sessionIdToFinish = this.currentSessionId;
|
|
334
353
|
// Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
|
|
@@ -359,11 +378,9 @@ class TasService {
|
|
|
359
378
|
businessRole: this.currentBusinessRole,
|
|
360
379
|
}).subscribe({
|
|
361
380
|
next: (response) => {
|
|
362
|
-
console.log('[TAS DEBUG] Session finished successfully:', response);
|
|
363
381
|
this.isFinishingSession = false;
|
|
364
382
|
},
|
|
365
383
|
error: (error) => {
|
|
366
|
-
console.error('[TAS DEBUG] Error finishing session:', error);
|
|
367
384
|
this.isFinishingSession = false;
|
|
368
385
|
},
|
|
369
386
|
});
|
|
@@ -398,7 +415,6 @@ class TasService {
|
|
|
398
415
|
// If so, don't restore it on page reload
|
|
399
416
|
const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
|
|
400
417
|
if (wasDisconnected === 'true') {
|
|
401
|
-
console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
|
|
402
418
|
this.clearSessionState();
|
|
403
419
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
404
420
|
return;
|
|
@@ -406,14 +422,12 @@ class TasService {
|
|
|
406
422
|
// Don't restore if we're already disconnected or if there's no active session
|
|
407
423
|
// This prevents restoring sessions that were properly disconnected
|
|
408
424
|
if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
|
|
409
|
-
console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
|
|
410
425
|
this.clearSessionState();
|
|
411
426
|
return;
|
|
412
427
|
}
|
|
413
428
|
try {
|
|
414
429
|
const state = JSON.parse(savedState);
|
|
415
430
|
if (state.sessionId && state.token) {
|
|
416
|
-
console.log('[TAS DEBUG] Restoring session from storage');
|
|
417
431
|
// Force PiP mode for restoration to ensure UI consistency
|
|
418
432
|
this.viewModeSubject.next(ViewMode.PIP);
|
|
419
433
|
if (state.businessRole) {
|
|
@@ -422,18 +436,15 @@ class TasService {
|
|
|
422
436
|
this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
|
|
423
437
|
containerId, this.currentBusinessRole)
|
|
424
438
|
.then(() => {
|
|
425
|
-
console.log('[TAS DEBUG] Session restored successfully');
|
|
426
439
|
// Clear the disconnected flag if restoration succeeds
|
|
427
440
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
428
441
|
})
|
|
429
442
|
.catch((err) => {
|
|
430
|
-
console.error('[TAS DEBUG] Failed to restore session:', err);
|
|
431
443
|
this.clearSessionState(); // Clear bad state
|
|
432
444
|
});
|
|
433
445
|
}
|
|
434
446
|
}
|
|
435
447
|
catch (e) {
|
|
436
|
-
console.error('[TAS DEBUG] Error parsing saved session state', e);
|
|
437
448
|
this.clearSessionState();
|
|
438
449
|
}
|
|
439
450
|
}
|
|
@@ -452,19 +463,77 @@ class TasService {
|
|
|
452
463
|
throw error;
|
|
453
464
|
}));
|
|
454
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Generate cache key from request payload
|
|
468
|
+
*/
|
|
469
|
+
getStatusCacheKey(payload) {
|
|
470
|
+
return `${payload.roomType}-${payload.entityId}-${payload.tenant}-${payload.sessionId || ''}`;
|
|
471
|
+
}
|
|
455
472
|
/**
|
|
456
473
|
* PROXY circuit status: /v2/proxy/video/status
|
|
474
|
+
* Uses a 1-second cache to avoid redundant API calls from multiple components
|
|
475
|
+
* Implements request deduplication for simultaneous calls
|
|
457
476
|
*/
|
|
458
477
|
getProxyVideoStatus(payload) {
|
|
459
|
-
|
|
478
|
+
const cacheKey = this.getStatusCacheKey(payload);
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
// 1. Check if we have a valid cached result (less than 1 second old)
|
|
481
|
+
if (this.statusCache &&
|
|
482
|
+
this.statusCache.key === cacheKey &&
|
|
483
|
+
(now - this.statusCache.timestamp) < this.STATUS_CACHE_TTL_MS) {
|
|
484
|
+
// Return cached error if present
|
|
485
|
+
if (this.statusCache.error) {
|
|
486
|
+
return new Observable(observer => {
|
|
487
|
+
observer.error(this.statusCache.error);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
// Return cached response
|
|
491
|
+
if (this.statusCache.response) {
|
|
492
|
+
return new Observable(observer => {
|
|
493
|
+
observer.next(this.statusCache.response);
|
|
494
|
+
observer.complete();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// 2. Check if there is already an inflight request for this key
|
|
499
|
+
if (this.inflightStatusRequests.has(cacheKey)) {
|
|
500
|
+
return this.inflightStatusRequests.get(cacheKey);
|
|
501
|
+
}
|
|
502
|
+
// 3. Make the API call, share it, and cache the Observable
|
|
503
|
+
const request$ = this.httpClient
|
|
460
504
|
.post('v2/proxy/video/status', {
|
|
461
505
|
body: payload,
|
|
462
506
|
headers: {},
|
|
463
507
|
})
|
|
464
|
-
.pipe(map((response) =>
|
|
508
|
+
.pipe(map((response) => {
|
|
509
|
+
const typedResponse = response;
|
|
510
|
+
// Cache successful response
|
|
511
|
+
this.statusCache = {
|
|
512
|
+
key: cacheKey,
|
|
513
|
+
response: typedResponse,
|
|
514
|
+
error: null,
|
|
515
|
+
timestamp: Date.now(),
|
|
516
|
+
};
|
|
517
|
+
return typedResponse;
|
|
518
|
+
}), catchError((error) => {
|
|
465
519
|
console.error('TAS Service: getProxyVideoStatus failed', error);
|
|
520
|
+
// Cache error response
|
|
521
|
+
this.statusCache = {
|
|
522
|
+
key: cacheKey,
|
|
523
|
+
response: null,
|
|
524
|
+
error: error,
|
|
525
|
+
timestamp: Date.now(),
|
|
526
|
+
};
|
|
466
527
|
throw error;
|
|
467
|
-
})
|
|
528
|
+
}),
|
|
529
|
+
// Clean up inflight map when the observable completes or errors
|
|
530
|
+
finalize(() => {
|
|
531
|
+
this.inflightStatusRequests.delete(cacheKey);
|
|
532
|
+
}),
|
|
533
|
+
// Share the result with all subscribers (deduplication)
|
|
534
|
+
shareReplay(1));
|
|
535
|
+
this.inflightStatusRequests.set(cacheKey, request$);
|
|
536
|
+
return request$;
|
|
468
537
|
}
|
|
469
538
|
/**
|
|
470
539
|
* PROXY circuit user modification: /v2/proxy/video/user/modify
|
|
@@ -518,6 +587,42 @@ class TasService {
|
|
|
518
587
|
throw error;
|
|
519
588
|
}));
|
|
520
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Save video call feedback.
|
|
592
|
+
* POST /v2/proxy/video/save/feedback
|
|
593
|
+
* @param payload Feedback data including videoCallId, motiveId, motiveType, observation, rating (1-5), and tenant
|
|
594
|
+
* @returns Observable that completes on success (HTTP 200 OK, no response body)
|
|
595
|
+
*/
|
|
596
|
+
saveFeedback(payload) {
|
|
597
|
+
// Validate rating is between 1 and 5
|
|
598
|
+
if (payload.rating < 1 || payload.rating > 5) {
|
|
599
|
+
return new Observable(observer => {
|
|
600
|
+
observer.error(new Error('Rating must be between 1 and 5'));
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return this.httpClient
|
|
604
|
+
.post('v2/proxy/video/save/feedback', {
|
|
605
|
+
body: payload,
|
|
606
|
+
headers: {},
|
|
607
|
+
})
|
|
608
|
+
.pipe(catchError((error) => {
|
|
609
|
+
console.error('TAS Service: saveFeedback failed', error);
|
|
610
|
+
throw error;
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get available feedback motives.
|
|
615
|
+
* GET /v2/proxy/video/motives
|
|
616
|
+
* @returns Observable of feedback motives array
|
|
617
|
+
*/
|
|
618
|
+
getMotives() {
|
|
619
|
+
return this.httpClient
|
|
620
|
+
.get('v2/proxy/video/motives', { headers: {} })
|
|
621
|
+
.pipe(catchError((error) => {
|
|
622
|
+
console.error('TAS Service: getMotives failed', error);
|
|
623
|
+
throw error;
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
521
626
|
/**
|
|
522
627
|
* Start automatic status polling for the current session.
|
|
523
628
|
* Status is polled every STATUS_POLL_INTERVAL_MS milliseconds.
|
|
@@ -534,7 +639,6 @@ class TasService {
|
|
|
534
639
|
this.processStatusResponse(response);
|
|
535
640
|
},
|
|
536
641
|
error: (err) => {
|
|
537
|
-
console.error('[TAS DEBUG] Status polling error:', err);
|
|
538
642
|
},
|
|
539
643
|
});
|
|
540
644
|
}
|
|
@@ -556,17 +660,18 @@ class TasService {
|
|
|
556
660
|
this.joinableSubject.next(content.joinable);
|
|
557
661
|
// Update activateGeo status
|
|
558
662
|
if (content.activateGeo !== undefined) {
|
|
559
|
-
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
560
663
|
this.activateGeoSubject.next(content.activateGeo);
|
|
561
664
|
}
|
|
562
665
|
// Update geoRequestActive status (owner waiting for user geo response)
|
|
563
|
-
if (content.geoRequestActive !== undefined) {
|
|
564
|
-
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
666
|
+
if (content.geoRequestActive !== undefined && content.geoRequestActive !== null) {
|
|
565
667
|
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
566
668
|
}
|
|
669
|
+
else if (content.geoRequestActive === null) {
|
|
670
|
+
// Reset to false when null
|
|
671
|
+
this.geoRequestActiveSubject.next(false);
|
|
672
|
+
}
|
|
567
673
|
// Update allGeoGranted status (all users responded with geo)
|
|
568
674
|
if (content.allGeoGranted !== undefined) {
|
|
569
|
-
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
570
675
|
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
571
676
|
}
|
|
572
677
|
// Check if owner has joined
|
|
@@ -574,7 +679,6 @@ class TasService {
|
|
|
574
679
|
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
575
680
|
// Detect if owner left: was present, now not present
|
|
576
681
|
if (this.wasOwnerPresent && !ownerJoined) {
|
|
577
|
-
console.log('[TAS DEBUG] Owner has left the session');
|
|
578
682
|
this.ownerHasLeftSubject.next(true);
|
|
579
683
|
}
|
|
580
684
|
if (ownerJoined) {
|
|
@@ -589,6 +693,16 @@ class TasService {
|
|
|
589
693
|
status: RoomUserStatus.WAITING,
|
|
590
694
|
}));
|
|
591
695
|
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
696
|
+
// Extract geo info for USER role participants (for owner panel)
|
|
697
|
+
const userGeoInfo = content.users
|
|
698
|
+
.filter((u) => u.rol === 'USER')
|
|
699
|
+
.map((u) => ({
|
|
700
|
+
userId: u.userId,
|
|
701
|
+
geoStatus: u.geoStatus,
|
|
702
|
+
latitude: u.latitude,
|
|
703
|
+
longitude: u.longitude,
|
|
704
|
+
}));
|
|
705
|
+
this.userGeoInfoSubject.next(userGeoInfo);
|
|
592
706
|
}
|
|
593
707
|
/**
|
|
594
708
|
* Admit a user from the waiting room by changing their status.
|
|
@@ -607,6 +721,9 @@ class TasService {
|
|
|
607
721
|
get videoCallId() {
|
|
608
722
|
return this.currentVideoCallId;
|
|
609
723
|
}
|
|
724
|
+
get tenant() {
|
|
725
|
+
return this.currentTenant;
|
|
726
|
+
}
|
|
610
727
|
/**
|
|
611
728
|
* Connects to a TokBox video session
|
|
612
729
|
*/
|
|
@@ -660,11 +777,9 @@ class TasService {
|
|
|
660
777
|
businessRole: this.currentBusinessRole,
|
|
661
778
|
}).subscribe({
|
|
662
779
|
next: (response) => {
|
|
663
|
-
console.log('[TAS DEBUG] Session finished on disconnect event:', response);
|
|
664
780
|
this.isFinishingSession = false;
|
|
665
781
|
},
|
|
666
782
|
error: (error) => {
|
|
667
|
-
console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
|
|
668
783
|
this.isFinishingSession = false;
|
|
669
784
|
},
|
|
670
785
|
});
|
|
@@ -828,6 +943,246 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
828
943
|
}]
|
|
829
944
|
}] });
|
|
830
945
|
|
|
946
|
+
class TasFeedbackModalComponent {
|
|
947
|
+
constructor(activeModal, tasService) {
|
|
948
|
+
this.activeModal = activeModal;
|
|
949
|
+
this.tasService = tasService;
|
|
950
|
+
this.businessRole = TasBusinessRole.USER;
|
|
951
|
+
this.motives = [];
|
|
952
|
+
this.filteredMotives = [];
|
|
953
|
+
this.selectedMotive = null;
|
|
954
|
+
this.rating = 0;
|
|
955
|
+
this.hoverRating = 0;
|
|
956
|
+
this.observation = '';
|
|
957
|
+
this.isSubmitting = false;
|
|
958
|
+
this.showToast = false;
|
|
959
|
+
this.isDropdownOpen = false;
|
|
960
|
+
this.subscriptions = new Subscription();
|
|
961
|
+
this.toastTimeout = null;
|
|
962
|
+
}
|
|
963
|
+
ngOnInit() {
|
|
964
|
+
this.loadMotives();
|
|
965
|
+
}
|
|
966
|
+
ngOnDestroy() {
|
|
967
|
+
this.subscriptions.unsubscribe();
|
|
968
|
+
if (this.toastTimeout) {
|
|
969
|
+
clearTimeout(this.toastTimeout);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Check if current user can see all motives (BUSINESS + TECHNICAL)
|
|
974
|
+
* Only BACKOFFICE, ADMIN_MANAGER, MANAGER roles see BUSINESS motives
|
|
975
|
+
*/
|
|
976
|
+
get canSeeBusinessMotives() {
|
|
977
|
+
return (this.businessRole === TasBusinessRole.BACKOFFICE ||
|
|
978
|
+
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
979
|
+
this.businessRole === TasBusinessRole.MANAGER);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Observation is required when "Otro problema tecnico" is selected
|
|
983
|
+
*/
|
|
984
|
+
get isObservationRequired() {
|
|
985
|
+
var _a;
|
|
986
|
+
return ((_a = this.selectedMotive) === null || _a === void 0 ? void 0 : _a.description) === 'Otro problema tecnico';
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Can submit if rating > 0 OR motive is selected
|
|
990
|
+
* If observation is required, it must not be empty
|
|
991
|
+
*/
|
|
992
|
+
get canSubmit() {
|
|
993
|
+
const hasRatingOrMotive = this.rating > 0 || this.selectedMotive !== null;
|
|
994
|
+
if (!hasRatingOrMotive)
|
|
995
|
+
return false;
|
|
996
|
+
if (this.isObservationRequired && !this.observation.trim())
|
|
997
|
+
return false;
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Progress percentage for the divider line
|
|
1002
|
+
* 0 = nothing selected, 50 = one selected, 100 = both selected
|
|
1003
|
+
*/
|
|
1004
|
+
get feedbackProgress() {
|
|
1005
|
+
const hasRating = this.rating > 0;
|
|
1006
|
+
const hasMotive = this.selectedMotive !== null;
|
|
1007
|
+
if (hasRating && hasMotive)
|
|
1008
|
+
return 100;
|
|
1009
|
+
if (hasRating || hasMotive)
|
|
1010
|
+
return 50;
|
|
1011
|
+
return 0;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Set star rating
|
|
1015
|
+
*/
|
|
1016
|
+
setRating(value) {
|
|
1017
|
+
// Toggle off if clicking the same rating
|
|
1018
|
+
if (this.rating === value) {
|
|
1019
|
+
this.rating = 0;
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
this.rating = value;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Set hover preview rating
|
|
1027
|
+
*/
|
|
1028
|
+
setHoverRating(value) {
|
|
1029
|
+
this.hoverRating = value;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Clear hover preview
|
|
1033
|
+
*/
|
|
1034
|
+
clearHoverRating() {
|
|
1035
|
+
this.hoverRating = 0;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get the display rating (hover preview or actual)
|
|
1039
|
+
*/
|
|
1040
|
+
getDisplayRating() {
|
|
1041
|
+
return this.hoverRating > 0 ? this.hoverRating : this.rating;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Toggle dropdown open/close
|
|
1045
|
+
*/
|
|
1046
|
+
toggleDropdown() {
|
|
1047
|
+
this.isDropdownOpen = !this.isDropdownOpen;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Close dropdown
|
|
1051
|
+
*/
|
|
1052
|
+
closeDropdown() {
|
|
1053
|
+
this.isDropdownOpen = false;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Select a motive from dropdown
|
|
1057
|
+
*/
|
|
1058
|
+
selectMotive(motive) {
|
|
1059
|
+
this.selectedMotive = motive;
|
|
1060
|
+
this.closeDropdown();
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Clear selected motive
|
|
1064
|
+
*/
|
|
1065
|
+
clearMotive() {
|
|
1066
|
+
this.selectedMotive = null;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Submit feedback
|
|
1070
|
+
*/
|
|
1071
|
+
submit() {
|
|
1072
|
+
var _a, _b;
|
|
1073
|
+
if (!this.canSubmit || this.isSubmitting)
|
|
1074
|
+
return;
|
|
1075
|
+
this.isSubmitting = true;
|
|
1076
|
+
const payload = {
|
|
1077
|
+
videoCallId: this.videoCallId,
|
|
1078
|
+
motiveId: (_b = (_a = this.selectedMotive) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : 0,
|
|
1079
|
+
observation: this.observation.trim(),
|
|
1080
|
+
rating: this.rating || 1,
|
|
1081
|
+
tenant: this.tenant,
|
|
1082
|
+
};
|
|
1083
|
+
if (this.selectedMotive) {
|
|
1084
|
+
payload.motiveType = this.selectedMotive.motiveType;
|
|
1085
|
+
}
|
|
1086
|
+
this.subscriptions.add(this.tasService.saveFeedback(payload).subscribe({
|
|
1087
|
+
next: () => {
|
|
1088
|
+
this.isSubmitting = false;
|
|
1089
|
+
this.showToastNotification();
|
|
1090
|
+
},
|
|
1091
|
+
error: (err) => {
|
|
1092
|
+
console.error('Error saving feedback:', err);
|
|
1093
|
+
this.isSubmitting = false;
|
|
1094
|
+
// Still close modal on error, just don't show toast
|
|
1095
|
+
this.activeModal.close('submitted');
|
|
1096
|
+
},
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Close modal without saving
|
|
1101
|
+
*/
|
|
1102
|
+
dismiss() {
|
|
1103
|
+
this.activeModal.dismiss('dismissed');
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Load motives from API
|
|
1107
|
+
*/
|
|
1108
|
+
loadMotives() {
|
|
1109
|
+
this.subscriptions.add(this.tasService.getMotives().subscribe({
|
|
1110
|
+
next: (motives) => {
|
|
1111
|
+
this.motives = motives;
|
|
1112
|
+
this.filterMotives();
|
|
1113
|
+
},
|
|
1114
|
+
error: (err) => {
|
|
1115
|
+
console.error('Error loading motives:', err);
|
|
1116
|
+
// Fallback mock motives for development/testing
|
|
1117
|
+
this.motives = [
|
|
1118
|
+
{ id: 1, description: 'Problemas de audio', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1119
|
+
{ id: 2, description: 'Problemas de video', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1120
|
+
{ id: 3, description: 'Conexión inestable', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1121
|
+
{ id: 4, description: 'Otro problema tecnico', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1122
|
+
{ id: 5, description: 'Problema de negocio', motiveType: FeedbackMotiveType.BUSINESS },
|
|
1123
|
+
];
|
|
1124
|
+
this.filterMotives();
|
|
1125
|
+
},
|
|
1126
|
+
}));
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Filter motives based on user role
|
|
1130
|
+
* USERs see only TECHNICAL motives
|
|
1131
|
+
* Owners (BACKOFFICE, ADMIN_MANAGER, MANAGER) see all motives
|
|
1132
|
+
*/
|
|
1133
|
+
filterMotives() {
|
|
1134
|
+
if (this.canSeeBusinessMotives) {
|
|
1135
|
+
this.filteredMotives = this.motives;
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
this.filteredMotives = this.motives.filter((m) => !m.motiveType || m.motiveType === FeedbackMotiveType.TECHNICAL);
|
|
1139
|
+
}
|
|
1140
|
+
// If still empty after filtering, and we have motives, just show all as fallback
|
|
1141
|
+
if (this.filteredMotives.length === 0 && this.motives.length > 0) {
|
|
1142
|
+
this.filteredMotives = this.motives;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Show toast notification and auto-dismiss after 3s
|
|
1147
|
+
*/
|
|
1148
|
+
showToastNotification() {
|
|
1149
|
+
this.showToast = true;
|
|
1150
|
+
this.toastTimeout = setTimeout(() => {
|
|
1151
|
+
this.showToast = false;
|
|
1152
|
+
this.activeModal.close('submitted');
|
|
1153
|
+
}, 3000);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
TasFeedbackModalComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFeedbackModalComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1157
|
+
TasFeedbackModalComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasFeedbackModalComponent, selector: "tas-feedback-modal", inputs: { videoCallId: "videoCallId", tenant: "tenant", businessRole: "businessRole" }, ngImport: i0, template: "<div class=\"tas-feedback-modal\">\n <!-- Header with subtitle -->\n <div class=\"feedback-header\">\n <div class=\"header-text\">\n <span class=\"subtitle\">Cierre de la consulta</span>\n <h2>Calidad de la videollamada</h2>\n </div>\n <button\n type=\"button\"\n class=\"close-btn\"\n (click)=\"dismiss()\"\n aria-label=\"Cerrar\"\n >\n <i class=\"fa fa-times\"></i>\n </button>\n </div>\n\n <!-- Progress Divider -->\n <div class=\"progress-divider\">\n <div class=\"progress-fill\" [style.width.%]=\"feedbackProgress\"></div>\n </div>\n <div class=\"accent-line\"></div>\n\n <div class=\"feedback-content\">\n <!-- Star Rating with question -->\n <div class=\"rating-section\">\n <p class=\"rating-question\">\u00BFC\u00F3mo fue la calidad de la llamada?</p>\n <div\n class=\"stars-container\"\n (mouseleave)=\"clearHoverRating()\"\n >\n <button\n *ngFor=\"let star of [1, 2, 3, 4, 5]\"\n type=\"button\"\n class=\"star-btn\"\n [class.filled]=\"star <= getDisplayRating()\"\n (click)=\"setRating(star)\"\n (mouseenter)=\"setHoverRating(star)\"\n [attr.aria-label]=\"'Calificar ' + star + ' estrellas'\"\n >\n <i class=\"fa\" [class.fa-star]=\"star <= getDisplayRating()\" [class.fa-star-o]=\"star > getDisplayRating()\"></i>\n </button>\n </div>\n </div>\n\n <!-- Motive Dropdown -->\n <div class=\"motive-section\">\n <label class=\"motive-label\">Motivo (opcional)</label>\n <div class=\"custom-dropdown\" [class.open]=\"isDropdownOpen\">\n <button\n type=\"button\"\n class=\"dropdown-trigger\"\n (click)=\"toggleDropdown()\"\n [attr.aria-expanded]=\"isDropdownOpen\"\n aria-haspopup=\"listbox\"\n >\n <span class=\"dropdown-text\">\n {{ selectedMotive?.description || 'Seleccionar motivo' }}\n </span>\n <i class=\"fa fa-chevron-down dropdown-arrow\"></i>\n </button>\n <div\n class=\"dropdown-menu\"\n *ngIf=\"isDropdownOpen\"\n role=\"listbox\"\n >\n <button\n *ngFor=\"let motive of filteredMotives\"\n type=\"button\"\n class=\"dropdown-item\"\n [class.selected]=\"selectedMotive?.id === motive.id\"\n (click)=\"selectMotive(motive)\"\n role=\"option\"\n [attr.aria-selected]=\"selectedMotive?.id === motive.id\"\n >\n {{ motive.description }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Observation Textarea -->\n <div class=\"observation-section\">\n <label class=\"observation-label\" [class.required]=\"isObservationRequired\">\n {{ isObservationRequired ? 'Observacion (requerida)' : 'Observacion (opcional)' }}\n </label>\n <textarea\n class=\"observation-textarea\"\n [(ngModel)]=\"observation\"\n [placeholder]=\"isObservationRequired ? 'Por favor, describe el problema...' : 'Escribe tu comentario...'\"\n rows=\"3\"\n [attr.aria-required]=\"isObservationRequired\"\n ></textarea>\n </div>\n </div>\n\n <div class=\"feedback-footer\">\n <button\n type=\"button\"\n class=\"submit-btn\"\n [disabled]=\"!canSubmit || isSubmitting\"\n (click)=\"submit()\"\n >\n <span *ngIf=\"!isSubmitting\">Enviar</span>\n <span *ngIf=\"isSubmitting\">\n <i class=\"fa fa-spinner fa-spin\"></i>\n Enviando...\n </span>\n </button>\n </div>\n\n <!-- Toast Notification -->\n <div class=\"toast-notification\" *ngIf=\"showToast\" role=\"alert\" aria-live=\"polite\">\n <i class=\"fa fa-check-circle\"></i>\n <span>Gracias por tu feedback</span>\n </div>\n</div>\n\n<!-- Backdrop to close dropdown when clicking outside -->\n<div\n class=\"dropdown-backdrop\"\n *ngIf=\"isDropdownOpen\"\n (click)=\"closeDropdown()\"\n></div>\n", styles: [":host{display:block;width:100%}::ng-deep .tas-feedback-modal-wrapper .modal-content{overflow:visible!important;border:none;background:transparent}::ng-deep .tas-feedback-modal-wrapper .modal-dialog{max-width:450px}.tas-feedback-modal{width:100%;background:#fff;border-radius:16px;overflow:visible;position:relative;padding:.5rem;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.feedback-header{display:flex;justify-content:space-between;align-items:flex-start;padding:1.25rem 1.5rem 1rem;background:#fff;border-radius:16px 16px 0 0}.feedback-header .header-text{display:flex;flex-direction:column;gap:.25rem}.feedback-header .header-text .subtitle{font-size:.8125rem;color:#6b7280;font-weight:400}.feedback-header .header-text h2{margin:0;font-size:1.25rem;font-weight:600;color:#1f2937;letter-spacing:-.01em}.feedback-header .close-btn{background:transparent;border:none;padding:8px;cursor:pointer;color:#9ca3af;font-size:1.25rem;line-height:1;transition:all .2s ease;margin-top:-4px}.feedback-header .close-btn:hover{color:#4b5563}.feedback-header .close-btn:focus{outline:none;color:#4b5563}.progress-divider{height:4px;background:#e5e7eb;margin:0;position:relative;overflow:hidden}.progress-divider .progress-fill{height:100%;background:linear-gradient(90deg,var(--Primary-Uell, #1da4b1) 0%,#4fd1c5 100%);transition:width .4s cubic-bezier(.4,0,.2,1);border-radius:0 2px 2px 0}.feedback-content{padding:1.5rem;display:flex;flex-direction:column;gap:1.5rem}.rating-section{display:flex;flex-direction:column;align-items:flex-start;gap:1rem}.rating-section .rating-question{margin:0;font-size:.9375rem;color:#4b5563;font-weight:400;align-self:center}.rating-section .stars-container{display:flex;gap:8px;width:100%;justify-content:center}.rating-section .star-btn{background:transparent;border:none;padding:4px;cursor:pointer;font-size:2rem;color:#d1d5db;transition:all .15s ease;line-height:1}.rating-section .star-btn:hover{transform:scale(1.15)}.rating-section .star-btn.filled{color:#f5a623}.rating-section .star-btn.filled i.fa-star{text-shadow:0 2px 4px rgba(245,166,35,.3)}.rating-section .star-btn i.fa-star-o{color:#d1d5db}.rating-section .star-btn:focus{outline:none}.motive-section{display:flex;flex-direction:column;gap:.5rem;position:relative;overflow:visible}.motive-section .motive-label{font-size:.875rem;font-weight:500;color:#4b5563}.motive-section .custom-dropdown{position:relative;overflow:visible}.motive-section .custom-dropdown .dropdown-trigger{width:100%;display:flex;justify-content:space-between;align-items:center;padding:1rem;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;font-size:.9375rem;color:#4b5563;transition:all .2s ease;min-height:52px}.motive-section .custom-dropdown .dropdown-trigger:hover{border-color:#9ca3af}.motive-section .custom-dropdown .dropdown-trigger:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown .dropdown-trigger .dropdown-text{flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.motive-section .custom-dropdown .dropdown-trigger .dropdown-arrow{font-size:.875rem;color:#6b7280;transition:transform .2s ease;margin-left:.5rem}.motive-section .custom-dropdown.open .dropdown-trigger{border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown.open .dropdown-trigger .dropdown-arrow{transform:rotate(180deg)}.motive-section .custom-dropdown .dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;display:block;min-height:40px;min-width:100%;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 16px #0000001f;max-height:250px;overflow:hidden;overflow-y:auto;z-index:1050;visibility:visible;opacity:1;padding:0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item{width:100%;padding:1rem 1.25rem;background:transparent;border:none;border-bottom:1px dashed #e5e7eb;text-align:left;font-size:.9375rem;color:#4b5563;cursor:pointer;transition:all .15s ease;position:relative}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-bottom:none}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:hover{background:rgba(29,164,177,.05);color:#374151}.motive-section .custom-dropdown .dropdown-menu .dropdown-item.selected{background:rgba(29,164,177,.08);color:var(--Primary-Uell, #1da4b1);border-left:3px solid var(--Primary-Uell, #1da4b1);padding-left:calc(1.25rem - 3px)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:focus{outline:none;background:rgba(29,164,177,.05)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:first-child{border-radius:8px 8px 0 0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-radius:0 0 8px 8px}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:only-child{border-radius:8px}.motive-section .clear-motive-btn{position:absolute;right:40px;top:32px;background:#f3f4f6;border:none;padding:4px 6px;border-radius:4px;cursor:pointer;color:#6b7280;font-size:.625rem;transition:all .2s ease}.motive-section .clear-motive-btn:hover{background:#e5e7eb;color:#374151}.motive-section .clear-motive-btn:focus{outline:none}.observation-section{display:flex;flex-direction:column;gap:.5rem}.observation-section .observation-label{font-size:.875rem;font-weight:500;color:#4b5563}.observation-section .observation-label.required{color:#dc2626}.observation-section .observation-label.required:after{content:\" *\";font-weight:600}.observation-section .observation-textarea{width:100%;padding:.75rem 1rem;border:1px solid #d1d5db;border-radius:12px;font-size:.875rem;font-family:inherit;resize:vertical;min-height:80px;transition:all .2s ease;background:#fff}.observation-section .observation-textarea::placeholder{color:#9ca3af}.observation-section .observation-textarea:hover{border-color:#9ca3af}.observation-section .observation-textarea:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1);box-shadow:0 0 0 3px #1da4b126}.feedback-footer{padding:1rem 1.5rem 1.5rem;display:flex;justify-content:flex-end}.feedback-footer .submit-btn{padding:.75rem 2rem;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:.9375rem;font-weight:600;cursor:pointer;transition:all .2s ease;min-width:120px}.feedback-footer .submit-btn:hover:not(:disabled){background:#178e99}.feedback-footer .submit-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.feedback-footer .submit-btn:focus{outline:none;box-shadow:0 0 0 3px #1da4b126}.feedback-footer .submit-btn i.fa-spinner{margin-right:6px}.toast-notification{position:fixed;bottom:24px;left:24px;display:flex;align-items:center;gap:10px;padding:14px 20px;background:#383e52;color:#fff;border-radius:8px;box-shadow:0 4px 12px #00000040;font-size:.875rem;font-weight:500;z-index:1060;animation:slideIn .3s ease}.toast-notification i.fa-check-circle{color:#4ade80;font-size:1.125rem}@keyframes slideIn{0%{transform:translate(-100%);opacity:0}to{transform:translate(0);opacity:1}}.dropdown-backdrop{position:fixed;inset:0;z-index:99}\n"], directives: [{ type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i4$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1158
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFeedbackModalComponent, decorators: [{
|
|
1159
|
+
type: Component,
|
|
1160
|
+
args: [{ selector: 'tas-feedback-modal', template: "<div class=\"tas-feedback-modal\">\n <!-- Header with subtitle -->\n <div class=\"feedback-header\">\n <div class=\"header-text\">\n <span class=\"subtitle\">Cierre de la consulta</span>\n <h2>Calidad de la videollamada</h2>\n </div>\n <button\n type=\"button\"\n class=\"close-btn\"\n (click)=\"dismiss()\"\n aria-label=\"Cerrar\"\n >\n <i class=\"fa fa-times\"></i>\n </button>\n </div>\n\n <!-- Progress Divider -->\n <div class=\"progress-divider\">\n <div class=\"progress-fill\" [style.width.%]=\"feedbackProgress\"></div>\n </div>\n <div class=\"accent-line\"></div>\n\n <div class=\"feedback-content\">\n <!-- Star Rating with question -->\n <div class=\"rating-section\">\n <p class=\"rating-question\">\u00BFC\u00F3mo fue la calidad de la llamada?</p>\n <div\n class=\"stars-container\"\n (mouseleave)=\"clearHoverRating()\"\n >\n <button\n *ngFor=\"let star of [1, 2, 3, 4, 5]\"\n type=\"button\"\n class=\"star-btn\"\n [class.filled]=\"star <= getDisplayRating()\"\n (click)=\"setRating(star)\"\n (mouseenter)=\"setHoverRating(star)\"\n [attr.aria-label]=\"'Calificar ' + star + ' estrellas'\"\n >\n <i class=\"fa\" [class.fa-star]=\"star <= getDisplayRating()\" [class.fa-star-o]=\"star > getDisplayRating()\"></i>\n </button>\n </div>\n </div>\n\n <!-- Motive Dropdown -->\n <div class=\"motive-section\">\n <label class=\"motive-label\">Motivo (opcional)</label>\n <div class=\"custom-dropdown\" [class.open]=\"isDropdownOpen\">\n <button\n type=\"button\"\n class=\"dropdown-trigger\"\n (click)=\"toggleDropdown()\"\n [attr.aria-expanded]=\"isDropdownOpen\"\n aria-haspopup=\"listbox\"\n >\n <span class=\"dropdown-text\">\n {{ selectedMotive?.description || 'Seleccionar motivo' }}\n </span>\n <i class=\"fa fa-chevron-down dropdown-arrow\"></i>\n </button>\n <div\n class=\"dropdown-menu\"\n *ngIf=\"isDropdownOpen\"\n role=\"listbox\"\n >\n <button\n *ngFor=\"let motive of filteredMotives\"\n type=\"button\"\n class=\"dropdown-item\"\n [class.selected]=\"selectedMotive?.id === motive.id\"\n (click)=\"selectMotive(motive)\"\n role=\"option\"\n [attr.aria-selected]=\"selectedMotive?.id === motive.id\"\n >\n {{ motive.description }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Observation Textarea -->\n <div class=\"observation-section\">\n <label class=\"observation-label\" [class.required]=\"isObservationRequired\">\n {{ isObservationRequired ? 'Observacion (requerida)' : 'Observacion (opcional)' }}\n </label>\n <textarea\n class=\"observation-textarea\"\n [(ngModel)]=\"observation\"\n [placeholder]=\"isObservationRequired ? 'Por favor, describe el problema...' : 'Escribe tu comentario...'\"\n rows=\"3\"\n [attr.aria-required]=\"isObservationRequired\"\n ></textarea>\n </div>\n </div>\n\n <div class=\"feedback-footer\">\n <button\n type=\"button\"\n class=\"submit-btn\"\n [disabled]=\"!canSubmit || isSubmitting\"\n (click)=\"submit()\"\n >\n <span *ngIf=\"!isSubmitting\">Enviar</span>\n <span *ngIf=\"isSubmitting\">\n <i class=\"fa fa-spinner fa-spin\"></i>\n Enviando...\n </span>\n </button>\n </div>\n\n <!-- Toast Notification -->\n <div class=\"toast-notification\" *ngIf=\"showToast\" role=\"alert\" aria-live=\"polite\">\n <i class=\"fa fa-check-circle\"></i>\n <span>Gracias por tu feedback</span>\n </div>\n</div>\n\n<!-- Backdrop to close dropdown when clicking outside -->\n<div\n class=\"dropdown-backdrop\"\n *ngIf=\"isDropdownOpen\"\n (click)=\"closeDropdown()\"\n></div>\n", styles: [":host{display:block;width:100%}::ng-deep .tas-feedback-modal-wrapper .modal-content{overflow:visible!important;border:none;background:transparent}::ng-deep .tas-feedback-modal-wrapper .modal-dialog{max-width:450px}.tas-feedback-modal{width:100%;background:#fff;border-radius:16px;overflow:visible;position:relative;padding:.5rem;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.feedback-header{display:flex;justify-content:space-between;align-items:flex-start;padding:1.25rem 1.5rem 1rem;background:#fff;border-radius:16px 16px 0 0}.feedback-header .header-text{display:flex;flex-direction:column;gap:.25rem}.feedback-header .header-text .subtitle{font-size:.8125rem;color:#6b7280;font-weight:400}.feedback-header .header-text h2{margin:0;font-size:1.25rem;font-weight:600;color:#1f2937;letter-spacing:-.01em}.feedback-header .close-btn{background:transparent;border:none;padding:8px;cursor:pointer;color:#9ca3af;font-size:1.25rem;line-height:1;transition:all .2s ease;margin-top:-4px}.feedback-header .close-btn:hover{color:#4b5563}.feedback-header .close-btn:focus{outline:none;color:#4b5563}.progress-divider{height:4px;background:#e5e7eb;margin:0;position:relative;overflow:hidden}.progress-divider .progress-fill{height:100%;background:linear-gradient(90deg,var(--Primary-Uell, #1da4b1) 0%,#4fd1c5 100%);transition:width .4s cubic-bezier(.4,0,.2,1);border-radius:0 2px 2px 0}.feedback-content{padding:1.5rem;display:flex;flex-direction:column;gap:1.5rem}.rating-section{display:flex;flex-direction:column;align-items:flex-start;gap:1rem}.rating-section .rating-question{margin:0;font-size:.9375rem;color:#4b5563;font-weight:400;align-self:center}.rating-section .stars-container{display:flex;gap:8px;width:100%;justify-content:center}.rating-section .star-btn{background:transparent;border:none;padding:4px;cursor:pointer;font-size:2rem;color:#d1d5db;transition:all .15s ease;line-height:1}.rating-section .star-btn:hover{transform:scale(1.15)}.rating-section .star-btn.filled{color:#f5a623}.rating-section .star-btn.filled i.fa-star{text-shadow:0 2px 4px rgba(245,166,35,.3)}.rating-section .star-btn i.fa-star-o{color:#d1d5db}.rating-section .star-btn:focus{outline:none}.motive-section{display:flex;flex-direction:column;gap:.5rem;position:relative;overflow:visible}.motive-section .motive-label{font-size:.875rem;font-weight:500;color:#4b5563}.motive-section .custom-dropdown{position:relative;overflow:visible}.motive-section .custom-dropdown .dropdown-trigger{width:100%;display:flex;justify-content:space-between;align-items:center;padding:1rem;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;font-size:.9375rem;color:#4b5563;transition:all .2s ease;min-height:52px}.motive-section .custom-dropdown .dropdown-trigger:hover{border-color:#9ca3af}.motive-section .custom-dropdown .dropdown-trigger:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown .dropdown-trigger .dropdown-text{flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.motive-section .custom-dropdown .dropdown-trigger .dropdown-arrow{font-size:.875rem;color:#6b7280;transition:transform .2s ease;margin-left:.5rem}.motive-section .custom-dropdown.open .dropdown-trigger{border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown.open .dropdown-trigger .dropdown-arrow{transform:rotate(180deg)}.motive-section .custom-dropdown .dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;display:block;min-height:40px;min-width:100%;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 16px #0000001f;max-height:250px;overflow:hidden;overflow-y:auto;z-index:1050;visibility:visible;opacity:1;padding:0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item{width:100%;padding:1rem 1.25rem;background:transparent;border:none;border-bottom:1px dashed #e5e7eb;text-align:left;font-size:.9375rem;color:#4b5563;cursor:pointer;transition:all .15s ease;position:relative}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-bottom:none}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:hover{background:rgba(29,164,177,.05);color:#374151}.motive-section .custom-dropdown .dropdown-menu .dropdown-item.selected{background:rgba(29,164,177,.08);color:var(--Primary-Uell, #1da4b1);border-left:3px solid var(--Primary-Uell, #1da4b1);padding-left:calc(1.25rem - 3px)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:focus{outline:none;background:rgba(29,164,177,.05)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:first-child{border-radius:8px 8px 0 0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-radius:0 0 8px 8px}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:only-child{border-radius:8px}.motive-section .clear-motive-btn{position:absolute;right:40px;top:32px;background:#f3f4f6;border:none;padding:4px 6px;border-radius:4px;cursor:pointer;color:#6b7280;font-size:.625rem;transition:all .2s ease}.motive-section .clear-motive-btn:hover{background:#e5e7eb;color:#374151}.motive-section .clear-motive-btn:focus{outline:none}.observation-section{display:flex;flex-direction:column;gap:.5rem}.observation-section .observation-label{font-size:.875rem;font-weight:500;color:#4b5563}.observation-section .observation-label.required{color:#dc2626}.observation-section .observation-label.required:after{content:\" *\";font-weight:600}.observation-section .observation-textarea{width:100%;padding:.75rem 1rem;border:1px solid #d1d5db;border-radius:12px;font-size:.875rem;font-family:inherit;resize:vertical;min-height:80px;transition:all .2s ease;background:#fff}.observation-section .observation-textarea::placeholder{color:#9ca3af}.observation-section .observation-textarea:hover{border-color:#9ca3af}.observation-section .observation-textarea:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1);box-shadow:0 0 0 3px #1da4b126}.feedback-footer{padding:1rem 1.5rem 1.5rem;display:flex;justify-content:flex-end}.feedback-footer .submit-btn{padding:.75rem 2rem;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:.9375rem;font-weight:600;cursor:pointer;transition:all .2s ease;min-width:120px}.feedback-footer .submit-btn:hover:not(:disabled){background:#178e99}.feedback-footer .submit-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.feedback-footer .submit-btn:focus{outline:none;box-shadow:0 0 0 3px #1da4b126}.feedback-footer .submit-btn i.fa-spinner{margin-right:6px}.toast-notification{position:fixed;bottom:24px;left:24px;display:flex;align-items:center;gap:10px;padding:14px 20px;background:#383e52;color:#fff;border-radius:8px;box-shadow:0 4px 12px #00000040;font-size:.875rem;font-weight:500;z-index:1060;animation:slideIn .3s ease}.toast-notification i.fa-check-circle{color:#4ade80;font-size:1.125rem}@keyframes slideIn{0%{transform:translate(-100%);opacity:0}to{transform:translate(0);opacity:1}}.dropdown-backdrop{position:fixed;inset:0;z-index:99}\n"] }]
|
|
1161
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }]; }, propDecorators: { videoCallId: [{
|
|
1162
|
+
type: Input
|
|
1163
|
+
}], tenant: [{
|
|
1164
|
+
type: Input
|
|
1165
|
+
}], businessRole: [{
|
|
1166
|
+
type: Input
|
|
1167
|
+
}] } });
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* SVG icons used internally by TAS SDK components.
|
|
1171
|
+
* Icons are stored as strings to avoid asset bundling complexity.
|
|
1172
|
+
*/
|
|
1173
|
+
const TAS_ICONS = {
|
|
1174
|
+
home: `<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1175
|
+
<rect x="10" width="100" height="100" rx="50" fill="#44D8E8" fill-opacity="0.2"/>
|
|
1176
|
+
<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"/>
|
|
1177
|
+
<path d="M110 5L106.575 3.425L105 0L103.425 3.425L100 5L103.425 6.575L105 10L106.575 6.575L110 5Z" fill="#44D8E8"/>
|
|
1178
|
+
<path d="M10 51L6.575 49.425L5 46L3.425 49.425L0 51L3.425 52.575L5 56L6.575 52.575L10 51Z" fill="#44D8E8"/>
|
|
1179
|
+
<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"/>
|
|
1180
|
+
<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"/>
|
|
1181
|
+
<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"/>
|
|
1182
|
+
<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"/>
|
|
1183
|
+
</svg>`,
|
|
1184
|
+
};
|
|
1185
|
+
|
|
831
1186
|
class TasAvatarComponent {
|
|
832
1187
|
constructor() {
|
|
833
1188
|
this.name = '';
|
|
@@ -901,11 +1256,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
901
1256
|
type: Input
|
|
902
1257
|
}] } });
|
|
903
1258
|
|
|
1259
|
+
/** User geolocation panel view states */
|
|
1260
|
+
var UserGeoViewState;
|
|
1261
|
+
(function (UserGeoViewState) {
|
|
1262
|
+
UserGeoViewState["HIDDEN"] = "HIDDEN";
|
|
1263
|
+
UserGeoViewState["INITIAL"] = "INITIAL";
|
|
1264
|
+
UserGeoViewState["VERIFYING"] = "VERIFYING";
|
|
1265
|
+
UserGeoViewState["VERIFIED"] = "VERIFIED";
|
|
1266
|
+
UserGeoViewState["DENIED"] = "DENIED";
|
|
1267
|
+
})(UserGeoViewState || (UserGeoViewState = {}));
|
|
904
1268
|
class TasVideocallComponent {
|
|
905
|
-
constructor(activeModal, tasService, geolocationService) {
|
|
1269
|
+
constructor(activeModal, tasService, geolocationService, sanitizer, modalService) {
|
|
906
1270
|
this.activeModal = activeModal;
|
|
907
1271
|
this.tasService = tasService;
|
|
908
1272
|
this.geolocationService = geolocationService;
|
|
1273
|
+
this.sanitizer = sanitizer;
|
|
1274
|
+
this.modalService = modalService;
|
|
909
1275
|
this.participantName = '';
|
|
910
1276
|
this.tenant = '';
|
|
911
1277
|
this.businessRole = TasBusinessRole.USER;
|
|
@@ -923,11 +1289,23 @@ class TasVideocallComponent {
|
|
|
923
1289
|
// Geo panel states for owner
|
|
924
1290
|
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
925
1291
|
this.allGeoGranted = false; // All users responded with geo
|
|
1292
|
+
this.userGeoInfo = []; // Individual user geo status
|
|
1293
|
+
// User geo panel state (for owners)
|
|
1294
|
+
this.userGeoViewState = UserGeoViewState.HIDDEN;
|
|
1295
|
+
this.UserGeoViewState = UserGeoViewState; // Expose enum to template
|
|
1296
|
+
this.devModeEnabled = false; // Enable dev controls for testing
|
|
1297
|
+
this.geoPanelDismissed = false; // Track if owner manually closed the panel
|
|
1298
|
+
this.feedbackShown = false; // Track if feedback modal has been shown
|
|
926
1299
|
this.subscriptions = new Subscription();
|
|
1300
|
+
this.homeIcon = this.sanitizer.bypassSecurityTrustHtml(TAS_ICONS.home);
|
|
927
1301
|
}
|
|
928
1302
|
ngOnInit() {
|
|
929
1303
|
this.setupSubscriptions();
|
|
930
1304
|
this.initializeCall();
|
|
1305
|
+
// For owners: show the geo panel to request user location (unless dismissed)
|
|
1306
|
+
if (this.canAdmitUsers && !this.geoPanelDismissed) {
|
|
1307
|
+
this.userGeoViewState = UserGeoViewState.INITIAL;
|
|
1308
|
+
}
|
|
931
1309
|
}
|
|
932
1310
|
ngAfterViewInit() {
|
|
933
1311
|
this.initInteract();
|
|
@@ -945,6 +1323,38 @@ class TasVideocallComponent {
|
|
|
945
1323
|
hangUp() {
|
|
946
1324
|
this.tasService.disconnectSession();
|
|
947
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Open feedback modal when call ends
|
|
1328
|
+
*/
|
|
1329
|
+
openFeedbackModal() {
|
|
1330
|
+
var _a;
|
|
1331
|
+
// Prevent opening feedback modal multiple times
|
|
1332
|
+
if (this.feedbackShown) {
|
|
1333
|
+
this.activeModal.close('hangup');
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
// Get videoCallId from input or service
|
|
1337
|
+
const videoCallId = (_a = this.videoCallId) !== null && _a !== void 0 ? _a : this.tasService.videoCallId;
|
|
1338
|
+
// If no videoCallId, skip feedback and close directly
|
|
1339
|
+
if (!videoCallId) {
|
|
1340
|
+
this.activeModal.close('hangup');
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
this.feedbackShown = true;
|
|
1344
|
+
const modalRef = this.modalService.open(TasFeedbackModalComponent, {
|
|
1345
|
+
centered: true,
|
|
1346
|
+
backdrop: true,
|
|
1347
|
+
keyboard: true,
|
|
1348
|
+
windowClass: 'tas-feedback-modal-wrapper',
|
|
1349
|
+
});
|
|
1350
|
+
modalRef.componentInstance.videoCallId = videoCallId;
|
|
1351
|
+
modalRef.componentInstance.tenant = this.tenant;
|
|
1352
|
+
modalRef.componentInstance.businessRole = this.businessRole;
|
|
1353
|
+
// Close videocall modal after feedback modal is closed/dismissed
|
|
1354
|
+
modalRef.result.finally(() => {
|
|
1355
|
+
this.activeModal.close('hangup');
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
948
1358
|
toggleMute() {
|
|
949
1359
|
this.tasService.toggleMute();
|
|
950
1360
|
}
|
|
@@ -969,6 +1379,43 @@ class TasVideocallComponent {
|
|
|
969
1379
|
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
970
1380
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
971
1381
|
}
|
|
1382
|
+
/** Users with pending geo status */
|
|
1383
|
+
get pendingUsers() {
|
|
1384
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.PENDING);
|
|
1385
|
+
}
|
|
1386
|
+
/** Users who granted geo */
|
|
1387
|
+
get grantedUsers() {
|
|
1388
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1389
|
+
}
|
|
1390
|
+
/** Users who denied geo */
|
|
1391
|
+
get deniedUsers() {
|
|
1392
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.DENIED);
|
|
1393
|
+
}
|
|
1394
|
+
/** Show location panel only if owner and there are users who haven't granted */
|
|
1395
|
+
get shouldShowLocationPanel() {
|
|
1396
|
+
if (!this.canAdmitUsers || this.userGeoInfo.length === 0) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
// Hide if all users have granted
|
|
1400
|
+
const allGranted = this.userGeoInfo.every(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1401
|
+
return !allGranted;
|
|
1402
|
+
}
|
|
1403
|
+
/** Check if any user has denied geo */
|
|
1404
|
+
get hasAnyDenied() {
|
|
1405
|
+
return this.deniedUsers.length > 0;
|
|
1406
|
+
}
|
|
1407
|
+
/** Show user geo panel for owners when not hidden */
|
|
1408
|
+
get shouldShowUserGeoPanel() {
|
|
1409
|
+
return this.canAdmitUsers && this.userGeoViewState !== UserGeoViewState.HIDDEN;
|
|
1410
|
+
}
|
|
1411
|
+
/** Set user geo view state (for dev controls or close button) */
|
|
1412
|
+
setUserGeoViewState(state) {
|
|
1413
|
+
this.userGeoViewState = state;
|
|
1414
|
+
// Track if owner manually dismissed the panel
|
|
1415
|
+
if (state === UserGeoViewState.HIDDEN) {
|
|
1416
|
+
this.geoPanelDismissed = true;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
972
1419
|
/**
|
|
973
1420
|
* Admit a user from the waiting room
|
|
974
1421
|
*/
|
|
@@ -1007,7 +1454,7 @@ class TasVideocallComponent {
|
|
|
1007
1454
|
this.showLocationPanel = false;
|
|
1008
1455
|
}
|
|
1009
1456
|
/**
|
|
1010
|
-
* Request the user to share their location
|
|
1457
|
+
* Request the user to share their location (called by owner)
|
|
1011
1458
|
*/
|
|
1012
1459
|
requestUserLocation() {
|
|
1013
1460
|
if (!this.videoCallId) {
|
|
@@ -1018,9 +1465,12 @@ class TasVideocallComponent {
|
|
|
1018
1465
|
videoCallId: this.videoCallId,
|
|
1019
1466
|
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
1020
1467
|
};
|
|
1021
|
-
// TODO: Send location request action to backend when endpoint is ready
|
|
1022
1468
|
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1023
|
-
next: () =>
|
|
1469
|
+
next: () => {
|
|
1470
|
+
console.log('Location request sent');
|
|
1471
|
+
// Set panel to verifying state while waiting for user response
|
|
1472
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1473
|
+
},
|
|
1024
1474
|
error: (err) => console.error('Error requesting location:', err),
|
|
1025
1475
|
});
|
|
1026
1476
|
}
|
|
@@ -1030,7 +1480,7 @@ class TasVideocallComponent {
|
|
|
1030
1480
|
this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
|
|
1031
1481
|
this.callState = state;
|
|
1032
1482
|
if (state === CallState.DISCONNECTED) {
|
|
1033
|
-
this.
|
|
1483
|
+
this.openFeedbackModal();
|
|
1034
1484
|
}
|
|
1035
1485
|
// Track if we have an active video stream
|
|
1036
1486
|
this.hasVideoStream = state === CallState.CONNECTED;
|
|
@@ -1057,71 +1507,86 @@ class TasVideocallComponent {
|
|
|
1057
1507
|
// Owner left subscription - disconnect non-owners
|
|
1058
1508
|
this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
|
|
1059
1509
|
if (hasLeft && !this.canAdmitUsers) { // Non-owner user
|
|
1060
|
-
|
|
1510
|
+
// hangUp() triggers DISCONNECTED state, which opens feedback modal
|
|
1511
|
+
// then closes videocall modal after feedback is done
|
|
1061
1512
|
this.hangUp();
|
|
1062
|
-
this.activeModal.close('owner_left');
|
|
1063
1513
|
}
|
|
1064
1514
|
}));
|
|
1065
1515
|
// ActivateGeo subscription - only for non-owners (users)
|
|
1066
1516
|
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
1067
1517
|
if (activateGeo && !this.canAdmitUsers) {
|
|
1068
|
-
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
1069
1518
|
this.handleActivateGeo();
|
|
1070
1519
|
}
|
|
1071
1520
|
}));
|
|
1072
1521
|
// GeoRequestActive subscription - for owners
|
|
1073
1522
|
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
1074
|
-
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
1075
1523
|
this.geoRequestActive = active;
|
|
1076
1524
|
}));
|
|
1077
|
-
// AllGeoGranted subscription - for owners
|
|
1525
|
+
// AllGeoGranted subscription - for owners to update panel state
|
|
1078
1526
|
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1079
|
-
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1080
1527
|
this.allGeoGranted = granted;
|
|
1528
|
+
// For owners: update panel state based on geo status
|
|
1529
|
+
if (this.canAdmitUsers && granted) {
|
|
1530
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1531
|
+
}
|
|
1532
|
+
}));
|
|
1533
|
+
// UserGeoInfo subscription - for owner geo panel
|
|
1534
|
+
this.subscriptions.add(this.tasService.userGeoInfo$.subscribe((info) => {
|
|
1535
|
+
this.userGeoInfo = info;
|
|
1536
|
+
// Note: We don't auto-switch to DENIED here - that only happens
|
|
1537
|
+
// after owner requests and user denies (via geoRequestActive flow)
|
|
1081
1538
|
}));
|
|
1082
1539
|
}
|
|
1083
1540
|
/**
|
|
1084
1541
|
* Handle activateGeo request from backend (for non-owner users).
|
|
1085
|
-
*
|
|
1542
|
+
* Directly prompts browser for geolocation - no panel for users.
|
|
1086
1543
|
*/
|
|
1087
1544
|
handleActivateGeo() {
|
|
1088
1545
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1546
|
+
// Clear any cached position to force re-prompting
|
|
1547
|
+
this.geolocationService.clearCache();
|
|
1548
|
+
// Request geolocation from user (browser will prompt)
|
|
1549
|
+
const position = yield this.geolocationService.getCurrentPosition();
|
|
1550
|
+
if (position) {
|
|
1094
1551
|
this.geoLocationStatus = 'active';
|
|
1095
|
-
this.reportGeoStatus(
|
|
1096
|
-
|
|
1552
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
this.geoLocationStatus = 'denied';
|
|
1556
|
+
this.denyGeoLocation();
|
|
1097
1557
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
/** Start geolocation verification (called from template button) */
|
|
1561
|
+
startGeoVerification() {
|
|
1562
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1563
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1564
|
+
// Clear any cached position to force re-prompting
|
|
1565
|
+
this.geolocationService.clearCache();
|
|
1566
|
+
// Request geolocation from user (browser will prompt)
|
|
1100
1567
|
const position = yield this.geolocationService.getCurrentPosition();
|
|
1101
1568
|
if (position) {
|
|
1102
|
-
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1103
1569
|
this.geoLocationStatus = 'active';
|
|
1570
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1104
1571
|
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1105
1572
|
}
|
|
1106
1573
|
else {
|
|
1107
|
-
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1108
1574
|
this.geoLocationStatus = 'denied';
|
|
1109
|
-
|
|
1110
|
-
this.
|
|
1575
|
+
this.userGeoViewState = UserGeoViewState.DENIED;
|
|
1576
|
+
this.denyGeoLocation();
|
|
1111
1577
|
}
|
|
1112
1578
|
});
|
|
1113
1579
|
}
|
|
1114
1580
|
/**
|
|
1115
|
-
* Report geolocation
|
|
1581
|
+
* Report granted geolocation to backend.
|
|
1582
|
+
* IMPORTANT: Only call with valid coordinates.
|
|
1116
1583
|
*/
|
|
1117
1584
|
reportGeoStatus(latitude, longitude) {
|
|
1118
1585
|
if (!this.videoCallId) {
|
|
1119
|
-
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1120
1586
|
return;
|
|
1121
1587
|
}
|
|
1122
|
-
//
|
|
1123
|
-
if (
|
|
1124
|
-
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1588
|
+
// Validate coordinates are present
|
|
1589
|
+
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
|
|
1125
1590
|
return;
|
|
1126
1591
|
}
|
|
1127
1592
|
const body = {
|
|
@@ -1131,11 +1596,21 @@ class TasVideocallComponent {
|
|
|
1131
1596
|
latitude,
|
|
1132
1597
|
longitude,
|
|
1133
1598
|
};
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1599
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Report denied geolocation to backend.
|
|
1603
|
+
*/
|
|
1604
|
+
denyGeoLocation() {
|
|
1605
|
+
if (!this.videoCallId) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const body = {
|
|
1609
|
+
userId: this.userId,
|
|
1610
|
+
videoCallId: this.videoCallId,
|
|
1611
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1612
|
+
};
|
|
1613
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1139
1614
|
}
|
|
1140
1615
|
initializeCall() {
|
|
1141
1616
|
if (this.isReturningFromPip) {
|
|
@@ -1225,12 +1700,12 @@ class TasVideocallComponent {
|
|
|
1225
1700
|
});
|
|
1226
1701
|
}
|
|
1227
1702
|
}
|
|
1228
|
-
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 });
|
|
1229
|
-
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"] }] });
|
|
1703
|
+
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$2.DomSanitizer }, { token: i1.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
|
|
1704
|
+
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.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1230
1705
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
1231
1706
|
type: Component,
|
|
1232
|
-
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"] }]
|
|
1233
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
1707
|
+
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"] }]
|
|
1708
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i4$2.DomSanitizer }, { type: i1.NgbModal }]; }, propDecorators: { sessionId: [{
|
|
1234
1709
|
type: Input
|
|
1235
1710
|
}], token: [{
|
|
1236
1711
|
type: Input
|
|
@@ -1279,6 +1754,7 @@ class TasWaitingRoomComponent {
|
|
|
1279
1754
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1280
1755
|
this.WaitingRoomState = WaitingRoomState; // Expose enum to template
|
|
1281
1756
|
this.errorMessage = '';
|
|
1757
|
+
this.showErrorDetails = false;
|
|
1282
1758
|
this.isJoinable = false;
|
|
1283
1759
|
// Session data from status response
|
|
1284
1760
|
this.resolvedSessionId = '';
|
|
@@ -1290,20 +1766,41 @@ class TasWaitingRoomComponent {
|
|
|
1290
1766
|
this.videoCallModalRef = null;
|
|
1291
1767
|
// Geolocation
|
|
1292
1768
|
this.geoPosition = null;
|
|
1769
|
+
this.geoWasDenied = false; // Tracks if user denied geo permission
|
|
1770
|
+
this.geoResultSent = false; // Tracks if geo result was already sent
|
|
1771
|
+
// Permission tracking for auto-join
|
|
1772
|
+
this.mediaPermissionsGranted = false;
|
|
1773
|
+
this.geoPermissionsResolved = false;
|
|
1774
|
+
// Auto-retry tracking
|
|
1775
|
+
this.retryCount = 0;
|
|
1776
|
+
this.MAX_RETRIES = 1;
|
|
1293
1777
|
}
|
|
1294
1778
|
/** Whether current user is an owner */
|
|
1295
1779
|
get isOwner() {
|
|
1296
1780
|
var _a;
|
|
1297
1781
|
return ((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.role) === TasUserRole.OWNER;
|
|
1298
1782
|
}
|
|
1783
|
+
/** Whether we're still requesting permissions (for template) */
|
|
1784
|
+
get isRequestingPermissions() {
|
|
1785
|
+
return !this.mediaPermissionsGranted || (!this.geoPermissionsResolved && !this.isOwner && !this.isBackoffice);
|
|
1786
|
+
}
|
|
1787
|
+
/** Whether we're waiting for admission after permissions granted (for template) */
|
|
1788
|
+
get isWaitingForAdmission() {
|
|
1789
|
+
return this.mediaPermissionsGranted &&
|
|
1790
|
+
(this.geoPermissionsResolved || this.isOwner || this.isBackoffice) &&
|
|
1791
|
+
!this.isJoinable;
|
|
1792
|
+
}
|
|
1299
1793
|
ngOnInit() {
|
|
1300
1794
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1301
1795
|
this.requestMediaPermissions();
|
|
1796
|
+
// Request geolocation permission and cache it (but don't send to backend yet)
|
|
1797
|
+
// The cached position will be sent when owner requests it via activateGeo
|
|
1302
1798
|
this.requestGeolocation();
|
|
1303
1799
|
this.checkStatus();
|
|
1304
1800
|
}
|
|
1305
1801
|
/**
|
|
1306
1802
|
* Request camera and microphone permissions.
|
|
1803
|
+
* Triggers auto-join when permissions are resolved.
|
|
1307
1804
|
*/
|
|
1308
1805
|
requestMediaPermissions() {
|
|
1309
1806
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1313,49 +1810,81 @@ class TasWaitingRoomComponent {
|
|
|
1313
1810
|
// Stop tracks immediately - we just needed the permission
|
|
1314
1811
|
stream.getTracks().forEach(track => track.stop());
|
|
1315
1812
|
console.log('[TAS DEBUG] Media permissions granted');
|
|
1813
|
+
this.mediaPermissionsGranted = true;
|
|
1316
1814
|
}
|
|
1317
1815
|
catch (error) {
|
|
1318
1816
|
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
1817
|
+
// Still allow joining - some users may not have camera/mic
|
|
1818
|
+
this.mediaPermissionsGranted = true;
|
|
1319
1819
|
}
|
|
1820
|
+
this.tryAutoJoin();
|
|
1821
|
+
this.cdr.detectChanges();
|
|
1320
1822
|
});
|
|
1321
1823
|
}
|
|
1322
1824
|
/**
|
|
1323
|
-
* Request geolocation
|
|
1825
|
+
* Request geolocation permission and cache result.
|
|
1324
1826
|
* Only for regular users (not owners/backoffice).
|
|
1325
|
-
*
|
|
1827
|
+
* Actual send to backend happens after we have videoCallId.
|
|
1828
|
+
* Triggers auto-join when geolocation is resolved.
|
|
1326
1829
|
*/
|
|
1327
1830
|
requestGeolocation() {
|
|
1328
1831
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1329
1832
|
// Only request geolocation for regular users, not owners/backoffice
|
|
1330
1833
|
if (this.isOwner || this.isBackoffice) {
|
|
1331
1834
|
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1835
|
+
this.geoPermissionsResolved = true;
|
|
1332
1836
|
return;
|
|
1333
1837
|
}
|
|
1334
|
-
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1838
|
+
console.log('[TAS DEBUG] Requesting geolocation permission...');
|
|
1335
1839
|
const position = yield this.geolocationService.getCurrentPosition();
|
|
1336
1840
|
if (position) {
|
|
1337
|
-
console.log('[TAS DEBUG] Geolocation
|
|
1841
|
+
console.log('[TAS DEBUG] Geolocation granted:', position);
|
|
1338
1842
|
this.geoPosition = position;
|
|
1339
|
-
|
|
1340
|
-
this.sendGeolocationToBackend();
|
|
1843
|
+
this.geoWasDenied = false;
|
|
1341
1844
|
}
|
|
1342
1845
|
else {
|
|
1343
1846
|
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1847
|
+
this.geoPosition = null;
|
|
1848
|
+
this.geoWasDenied = true;
|
|
1344
1849
|
}
|
|
1850
|
+
this.geoPermissionsResolved = true;
|
|
1851
|
+
// If we already have videoCallId, send now. Otherwise, it will be sent after status response.
|
|
1852
|
+
this.sendGeoResultToBackend();
|
|
1853
|
+
this.tryAutoJoin();
|
|
1854
|
+
this.cdr.detectChanges();
|
|
1345
1855
|
});
|
|
1346
1856
|
}
|
|
1347
1857
|
/**
|
|
1348
|
-
* Send
|
|
1349
|
-
*
|
|
1858
|
+
* Send geo result to backend (granted or denied).
|
|
1859
|
+
* Only sends if videoCallId is available and not already sent.
|
|
1860
|
+
*/
|
|
1861
|
+
sendGeoResultToBackend() {
|
|
1862
|
+
if (!this.videoCallId) {
|
|
1863
|
+
console.log('[TAS DEBUG] Cannot send geo result: videoCallId not available yet');
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (this.geoResultSent) {
|
|
1867
|
+
console.log('[TAS DEBUG] Geo result already sent, skipping');
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (this.geoPosition) {
|
|
1871
|
+
this.sendGeolocationToBackend();
|
|
1872
|
+
this.geoResultSent = true;
|
|
1873
|
+
}
|
|
1874
|
+
else if (this.geoWasDenied) {
|
|
1875
|
+
this.sendGeoDenialToBackend();
|
|
1876
|
+
this.geoResultSent = true;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Send granted geolocation to backend via modify user endpoint.
|
|
1350
1881
|
*/
|
|
1351
1882
|
sendGeolocationToBackend() {
|
|
1352
|
-
var _a;
|
|
1353
1883
|
if (!this.geoPosition || !this.videoCallId) {
|
|
1354
1884
|
return;
|
|
1355
1885
|
}
|
|
1356
1886
|
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
1357
1887
|
const body = {
|
|
1358
|
-
userId: (_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.id,
|
|
1359
1888
|
videoCallId: this.videoCallId,
|
|
1360
1889
|
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1361
1890
|
latitude: this.geoPosition.latitude,
|
|
@@ -1366,6 +1895,23 @@ class TasWaitingRoomComponent {
|
|
|
1366
1895
|
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1367
1896
|
});
|
|
1368
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Send geolocation denial to backend via modify user endpoint.
|
|
1900
|
+
*/
|
|
1901
|
+
sendGeoDenialToBackend() {
|
|
1902
|
+
if (!this.videoCallId) {
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
console.log('[TAS DEBUG] Sending geo denial to backend...');
|
|
1906
|
+
const body = {
|
|
1907
|
+
videoCallId: this.videoCallId,
|
|
1908
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1909
|
+
};
|
|
1910
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1911
|
+
next: () => console.log('[TAS DEBUG] Geo denial sent successfully'),
|
|
1912
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geo denial:', err),
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1369
1915
|
ngOnDestroy() {
|
|
1370
1916
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1371
1917
|
this.subscriptions.unsubscribe();
|
|
@@ -1399,8 +1945,8 @@ class TasWaitingRoomComponent {
|
|
|
1399
1945
|
this.resolvedAppointmentId = content.appointmentId;
|
|
1400
1946
|
this.videoCallId = content.videoCallId;
|
|
1401
1947
|
console.log('[TAS DEBUG] Status response:', content);
|
|
1402
|
-
//
|
|
1403
|
-
this.
|
|
1948
|
+
// Now that we have videoCallId, send geo result if not already sent
|
|
1949
|
+
this.sendGeoResultToBackend();
|
|
1404
1950
|
// Start polling for status updates
|
|
1405
1951
|
this.tasService.startStatusPolling(statusParams);
|
|
1406
1952
|
// Subscribe to joinable status
|
|
@@ -1416,7 +1962,8 @@ class TasWaitingRoomComponent {
|
|
|
1416
1962
|
}));
|
|
1417
1963
|
}
|
|
1418
1964
|
/**
|
|
1419
|
-
* Handle changes to joinable status
|
|
1965
|
+
* Handle changes to joinable status.
|
|
1966
|
+
* Triggers auto-join when joinable becomes true.
|
|
1420
1967
|
*/
|
|
1421
1968
|
handleJoinableChange(joinable) {
|
|
1422
1969
|
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
@@ -1432,10 +1979,11 @@ class TasWaitingRoomComponent {
|
|
|
1432
1979
|
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
1433
1980
|
return;
|
|
1434
1981
|
}
|
|
1435
|
-
//
|
|
1982
|
+
// Update state and attempt auto-join
|
|
1436
1983
|
if (joinable) {
|
|
1437
|
-
console.log('[TAS DEBUG] Joinable is true,
|
|
1984
|
+
console.log('[TAS DEBUG] Joinable is true, attempting auto-join');
|
|
1438
1985
|
this.state = WaitingRoomState.READY;
|
|
1986
|
+
this.tryAutoJoin();
|
|
1439
1987
|
}
|
|
1440
1988
|
else {
|
|
1441
1989
|
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
@@ -1444,11 +1992,33 @@ class TasWaitingRoomComponent {
|
|
|
1444
1992
|
this.cdr.detectChanges();
|
|
1445
1993
|
}
|
|
1446
1994
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1995
|
+
* Attempt to auto-join the session when all conditions are met.
|
|
1996
|
+
* - For owners: just need joinable + sessionId
|
|
1997
|
+
* - For users: need media permissions + geo resolved + joinable + sessionId
|
|
1449
1998
|
*/
|
|
1450
|
-
|
|
1451
|
-
|
|
1999
|
+
tryAutoJoin() {
|
|
2000
|
+
// Don't try if already joining or in error
|
|
2001
|
+
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
2002
|
+
this.state === WaitingRoomState.JOINING) {
|
|
2003
|
+
console.log('[TAS DEBUG] tryAutoJoin skipped - already in state:', this.state);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
// For owners: just need joinable
|
|
2007
|
+
if (this.isOwner) {
|
|
2008
|
+
if (this.isJoinable && this.resolvedSessionId) {
|
|
2009
|
+
console.log('[TAS DEBUG] Owner auto-joining...');
|
|
2010
|
+
this.startSessionAndJoin();
|
|
2011
|
+
}
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
// For users: need media + geo resolved + joinable
|
|
2015
|
+
if (this.mediaPermissionsGranted &&
|
|
2016
|
+
this.geoPermissionsResolved &&
|
|
2017
|
+
this.isJoinable &&
|
|
2018
|
+
this.resolvedSessionId) {
|
|
2019
|
+
console.log('[TAS DEBUG] User auto-joining...');
|
|
2020
|
+
this.startSessionAndJoin();
|
|
2021
|
+
}
|
|
1452
2022
|
}
|
|
1453
2023
|
/**
|
|
1454
2024
|
* Check if user has owner/backoffice role
|
|
@@ -1503,30 +2073,50 @@ class TasWaitingRoomComponent {
|
|
|
1503
2073
|
error: (err) => {
|
|
1504
2074
|
var _a;
|
|
1505
2075
|
console.error('[TAS DEBUG] /start request failed:', err);
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2076
|
+
// Auto-retry on first failure
|
|
2077
|
+
if (this.retryCount < this.MAX_RETRIES) {
|
|
2078
|
+
this.retryCount++;
|
|
2079
|
+
console.log('[TAS DEBUG] Auto-retrying... attempt:', this.retryCount);
|
|
2080
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
2081
|
+
this.cdr.detectChanges();
|
|
2082
|
+
setTimeout(() => this.startSessionAndJoin(), 2000);
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
// Show error after retry fails
|
|
2086
|
+
this.state = WaitingRoomState.ERROR;
|
|
2087
|
+
this.errorMessage = ((_a = err === null || err === void 0 ? void 0 : err.error) === null || _a === void 0 ? void 0 : _a.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
|
|
2088
|
+
this.tasService.stopStatusPolling();
|
|
2089
|
+
console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
|
|
2090
|
+
this.cdr.detectChanges();
|
|
2091
|
+
}
|
|
1511
2092
|
},
|
|
1512
2093
|
}));
|
|
1513
2094
|
}
|
|
1514
2095
|
/**
|
|
1515
|
-
*
|
|
1516
|
-
|
|
1517
|
-
cancel() {
|
|
1518
|
-
this.tasService.stopStatusPolling();
|
|
1519
|
-
this.activeModal.dismiss('cancel');
|
|
1520
|
-
}
|
|
1521
|
-
/**
|
|
1522
|
-
* Retry after an error
|
|
2096
|
+
* Retry after an error.
|
|
2097
|
+
* Resets retry count and restarts the flow.
|
|
1523
2098
|
*/
|
|
1524
2099
|
retry() {
|
|
1525
2100
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1526
2101
|
this.errorMessage = '';
|
|
2102
|
+
this.showErrorDetails = false;
|
|
1527
2103
|
this.token = '';
|
|
2104
|
+
this.retryCount = 0; // Reset retry count for auto-retry
|
|
1528
2105
|
this.checkStatus();
|
|
1529
2106
|
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Toggle the error details dropdown visibility.
|
|
2109
|
+
*/
|
|
2110
|
+
toggleErrorDetails() {
|
|
2111
|
+
this.showErrorDetails = !this.showErrorDetails;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Close the waiting room modal.
|
|
2115
|
+
*/
|
|
2116
|
+
close() {
|
|
2117
|
+
this.tasService.stopStatusPolling();
|
|
2118
|
+
this.activeModal.dismiss('close');
|
|
2119
|
+
}
|
|
1530
2120
|
openVideoCallModal() {
|
|
1531
2121
|
var _a;
|
|
1532
2122
|
this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
|
|
@@ -1551,10 +2141,10 @@ class TasWaitingRoomComponent {
|
|
|
1551
2141
|
}
|
|
1552
2142
|
}
|
|
1553
2143
|
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 });
|
|
1554
|
-
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
|
|
2144
|
+
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 \n <!-- Collapsible error details -->\n <div class=\"error-dropdown\" *ngIf=\"errorMessage\">\n <button \n type=\"button\" \n class=\"error-toggle-btn\"\n (click)=\"toggleErrorDetails()\"\n [attr.aria-expanded]=\"showErrorDetails\"\n aria-controls=\"error-details-content\"\n >\n <span>Ver detalles del error</span>\n <i class=\"fa\" [class.fa-chevron-down]=\"!showErrorDetails\" [class.fa-chevron-up]=\"showErrorDetails\"></i>\n </button>\n <div \n id=\"error-details-content\"\n class=\"error-details-content\"\n [class.expanded]=\"showErrorDetails\"\n >\n <p class=\"error-details-text\">{{ errorMessage }}</p>\n </div>\n </div>\n \n <div class=\"error-actions\">\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n <button type=\"button\" class=\"btn action-btn close-btn\" (click)=\"close()\">\n Cerrar\n </button>\n </div>\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-dropdown{margin:16px 0;width:100%}.error-toggle-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:10px 16px;font-size:13px;font-weight:500;color:#6c757d;background:transparent;border:1px solid #e9ecef;border-radius:6px;cursor:pointer;transition:all .2s ease}.error-toggle-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.error-toggle-btn i{font-size:12px;transition:transform .2s ease}.error-details-content{max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease}.error-details-content.expanded{max-height:200px;padding-top:12px}.error-details-text{font-size:12px;color:#ee316b;margin:0;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2);text-align:left;word-break:break-word;max-height:150px;overflow-y:auto}.error-actions{display:flex;justify-content:center;gap:12px;margin-top:16px}.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}.action-btn.close-btn{background:#ee316b;color:#fff;border:none}.action-btn.close-btn:hover{background:#da124f}@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.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1555
2145
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
1556
2146
|
type: Component,
|
|
1557
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n
|
|
2147
|
+
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 \n <!-- Collapsible error details -->\n <div class=\"error-dropdown\" *ngIf=\"errorMessage\">\n <button \n type=\"button\" \n class=\"error-toggle-btn\"\n (click)=\"toggleErrorDetails()\"\n [attr.aria-expanded]=\"showErrorDetails\"\n aria-controls=\"error-details-content\"\n >\n <span>Ver detalles del error</span>\n <i class=\"fa\" [class.fa-chevron-down]=\"!showErrorDetails\" [class.fa-chevron-up]=\"showErrorDetails\"></i>\n </button>\n <div \n id=\"error-details-content\"\n class=\"error-details-content\"\n [class.expanded]=\"showErrorDetails\"\n >\n <p class=\"error-details-text\">{{ errorMessage }}</p>\n </div>\n </div>\n \n <div class=\"error-actions\">\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n <button type=\"button\" class=\"btn action-btn close-btn\" (click)=\"close()\">\n Cerrar\n </button>\n </div>\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-dropdown{margin:16px 0;width:100%}.error-toggle-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:10px 16px;font-size:13px;font-weight:500;color:#6c757d;background:transparent;border:1px solid #e9ecef;border-radius:6px;cursor:pointer;transition:all .2s ease}.error-toggle-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.error-toggle-btn i{font-size:12px;transition:transform .2s ease}.error-details-content{max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease}.error-details-content.expanded{max-height:200px;padding-top:12px}.error-details-text{font-size:12px;color:#ee316b;margin:0;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2);text-align:left;word-break:break-word;max-height:150px;overflow-y:auto}.error-actions{display:flex;justify-content:center;gap:12px;margin-top:16px}.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}.action-btn.close-btn{background:#ee316b;color:#fff;border:none}.action-btn.close-btn:hover{background:#da124f}@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"] }]
|
|
1558
2148
|
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1559
2149
|
type: Input
|
|
1560
2150
|
}], entityId: [{
|
|
@@ -1669,6 +2259,11 @@ class TasButtonComponent {
|
|
|
1669
2259
|
.subscribe({
|
|
1670
2260
|
next: (response) => {
|
|
1671
2261
|
var _a, _b;
|
|
2262
|
+
// Validate response structure
|
|
2263
|
+
if (!response || !response.content) {
|
|
2264
|
+
this.handleStatusError('Invalid response');
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
1672
2267
|
this.isCheckingStatus = false;
|
|
1673
2268
|
this.isStatusError = false;
|
|
1674
2269
|
this.statusErrorMessage = '';
|
|
@@ -1677,23 +2272,20 @@ class TasButtonComponent {
|
|
|
1677
2272
|
this.isJoinable = (_b = (_a = response.content) === null || _a === void 0 ? void 0 : _a.joinable) !== null && _b !== void 0 ? _b : false;
|
|
1678
2273
|
},
|
|
1679
2274
|
error: (err) => {
|
|
1680
|
-
this.
|
|
1681
|
-
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1682
|
-
this.statusErrorMessage = errorMessage;
|
|
1683
|
-
// Use utility service to determine if button should be shown
|
|
1684
|
-
this.shouldShowButton = this.tasUtilityService.shouldShowButton(errorMessage);
|
|
1685
|
-
// Stop polling on error
|
|
1686
|
-
this.stopStatusPolling();
|
|
1687
|
-
// If button should be hidden, don't treat as error
|
|
1688
|
-
if (!this.shouldShowButton) {
|
|
1689
|
-
this.isStatusError = false;
|
|
1690
|
-
}
|
|
1691
|
-
else {
|
|
1692
|
-
this.isStatusError = true;
|
|
1693
|
-
}
|
|
2275
|
+
this.handleStatusError(err);
|
|
1694
2276
|
},
|
|
1695
2277
|
}));
|
|
1696
2278
|
}
|
|
2279
|
+
handleStatusError(err) {
|
|
2280
|
+
this.isCheckingStatus = false;
|
|
2281
|
+
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
2282
|
+
this.statusErrorMessage = errorMessage;
|
|
2283
|
+
// On any status error, hide the button
|
|
2284
|
+
this.shouldShowButton = false;
|
|
2285
|
+
this.isStatusError = false; // We don't show error UI, just hide the button
|
|
2286
|
+
// Stop polling on error
|
|
2287
|
+
this.stopStatusPolling();
|
|
2288
|
+
}
|
|
1697
2289
|
onClick() {
|
|
1698
2290
|
var _a;
|
|
1699
2291
|
if (!this.tenant || !((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.name)) {
|
|
@@ -1771,6 +2363,7 @@ class TasFloatingCallComponent {
|
|
|
1771
2363
|
this.isMuted = false;
|
|
1772
2364
|
this.subscriptions = new Subscription();
|
|
1773
2365
|
this.videoCallModalRef = null;
|
|
2366
|
+
this.feedbackShown = false; // Track if feedback modal has been shown
|
|
1774
2367
|
// Margin from screen edges (in pixels)
|
|
1775
2368
|
this.PIP_MARGIN = 20;
|
|
1776
2369
|
}
|
|
@@ -1792,12 +2385,46 @@ class TasFloatingCallComponent {
|
|
|
1792
2385
|
toggleMute() {
|
|
1793
2386
|
this.tasService.toggleMute();
|
|
1794
2387
|
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Open feedback modal when call ends from PiP mode
|
|
2390
|
+
*/
|
|
2391
|
+
openFeedbackModal() {
|
|
2392
|
+
var _a;
|
|
2393
|
+
// Prevent opening feedback modal multiple times
|
|
2394
|
+
if (this.feedbackShown) {
|
|
2395
|
+
this.isVisible = false;
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
// Get videoCallId from service
|
|
2399
|
+
const videoCallId = this.tasService.videoCallId;
|
|
2400
|
+
// If no videoCallId, skip feedback and hide directly
|
|
2401
|
+
if (!videoCallId) {
|
|
2402
|
+
this.isVisible = false;
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
this.feedbackShown = true;
|
|
2406
|
+
const modalRef = this.modalService.open(TasFeedbackModalComponent, {
|
|
2407
|
+
centered: true,
|
|
2408
|
+
backdrop: true,
|
|
2409
|
+
keyboard: true,
|
|
2410
|
+
windowClass: 'tas-feedback-modal-wrapper',
|
|
2411
|
+
});
|
|
2412
|
+
modalRef.componentInstance.videoCallId = videoCallId;
|
|
2413
|
+
modalRef.componentInstance.tenant = (_a = this.tasService.tenant) !== null && _a !== void 0 ? _a : '';
|
|
2414
|
+
modalRef.componentInstance.businessRole = this.tasService.businessRole;
|
|
2415
|
+
// Hide floating call after feedback modal is closed/dismissed
|
|
2416
|
+
modalRef.result.finally(() => {
|
|
2417
|
+
this.isVisible = false;
|
|
2418
|
+
this.feedbackShown = false; // Reset for potential future calls
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
1795
2421
|
// Private Methods
|
|
1796
2422
|
setupSubscriptions() {
|
|
1797
2423
|
// Call state subscription
|
|
1798
2424
|
this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
|
|
1799
|
-
if
|
|
1800
|
-
|
|
2425
|
+
// Only handle disconnect feedback if we're in PiP mode (isVisible)
|
|
2426
|
+
if (state === CallState.DISCONNECTED && this.isVisible) {
|
|
2427
|
+
this.openFeedbackModal();
|
|
1801
2428
|
}
|
|
1802
2429
|
}));
|
|
1803
2430
|
// View mode subscription
|
|
@@ -1919,10 +2546,13 @@ class TasIncomingAppointmentComponent {
|
|
|
1919
2546
|
this.appointments = [];
|
|
1920
2547
|
this.isLoading = true;
|
|
1921
2548
|
this.hasError = false;
|
|
2549
|
+
// The appointmentId from status API - only this appointment shows tas-btn
|
|
2550
|
+
this.activeAppointmentId = null;
|
|
1922
2551
|
this.subscriptions = new Subscription();
|
|
1923
2552
|
}
|
|
1924
2553
|
ngOnInit() {
|
|
1925
2554
|
this.loadAppointments();
|
|
2555
|
+
this.checkStatus();
|
|
1926
2556
|
}
|
|
1927
2557
|
ngOnDestroy() {
|
|
1928
2558
|
this.subscriptions.unsubscribe();
|
|
@@ -1940,8 +2570,9 @@ class TasIncomingAppointmentComponent {
|
|
|
1940
2570
|
const appointments = Array.isArray(response)
|
|
1941
2571
|
? response
|
|
1942
2572
|
: (response === null || response === void 0 ? void 0 : response.content) || [];
|
|
1943
|
-
//
|
|
1944
|
-
|
|
2573
|
+
// Deduplicate by id and sort by date and startTime ascending (earliest first)
|
|
2574
|
+
const uniqueAppointments = appointments.filter((appt, index, self) => index === self.findIndex((a) => a.id === appt.id));
|
|
2575
|
+
this.appointments = uniqueAppointments.sort((a, b) => {
|
|
1945
2576
|
const dateTimeA = `${a.date}T${a.startTime}`;
|
|
1946
2577
|
const dateTimeB = `${b.date}T${b.startTime}`;
|
|
1947
2578
|
return dateTimeB.localeCompare(dateTimeA);
|
|
@@ -1954,15 +2585,50 @@ class TasIncomingAppointmentComponent {
|
|
|
1954
2585
|
},
|
|
1955
2586
|
}));
|
|
1956
2587
|
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Check status endpoint to get the active appointmentId
|
|
2590
|
+
*/
|
|
2591
|
+
checkStatus() {
|
|
2592
|
+
if (!this.tenant || !this.entityId) {
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
this.subscriptions.add(this.tasService
|
|
2596
|
+
.getProxyVideoStatus({
|
|
2597
|
+
roomType: this.roomType,
|
|
2598
|
+
entityId: this.entityId,
|
|
2599
|
+
tenant: this.tenant,
|
|
2600
|
+
businessRole: this.businessRole,
|
|
2601
|
+
})
|
|
2602
|
+
.subscribe({
|
|
2603
|
+
next: (response) => {
|
|
2604
|
+
var _a, _b;
|
|
2605
|
+
// Store the appointmentId from status - tas-btn only shows for this one
|
|
2606
|
+
this.activeAppointmentId = (_b = (_a = response === null || response === void 0 ? void 0 : response.content) === null || _a === void 0 ? void 0 : _a.appointmentId) !== null && _b !== void 0 ? _b : null;
|
|
2607
|
+
},
|
|
2608
|
+
error: () => {
|
|
2609
|
+
this.activeAppointmentId = null;
|
|
2610
|
+
},
|
|
2611
|
+
}));
|
|
2612
|
+
}
|
|
1957
2613
|
onEnterCall(appointment) {
|
|
1958
2614
|
this.enterCall.emit(appointment);
|
|
1959
2615
|
}
|
|
1960
2616
|
/**
|
|
1961
|
-
* Check if tas-btn should be shown for an appointment
|
|
2617
|
+
* Check if tas-btn should be shown for an appointment.
|
|
2618
|
+
* Only shows when appointment.id matches the activeAppointmentId from status API.
|
|
2619
|
+
* tas-btn handles its own polling for joinable state.
|
|
1962
2620
|
*/
|
|
1963
2621
|
shouldShowTasBtn(appointment) {
|
|
1964
|
-
|
|
2622
|
+
const hasValidStatus = appointment.status === AppointmentStatus.CONFIRMED ||
|
|
1965
2623
|
appointment.status === AppointmentStatus.ACTIVE;
|
|
2624
|
+
// Only show for the appointment that matches status API response
|
|
2625
|
+
return hasValidStatus && appointment.id === this.activeAppointmentId;
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* TrackBy function for ngFor
|
|
2629
|
+
*/
|
|
2630
|
+
trackByAppointmentId(index, appointment) {
|
|
2631
|
+
return appointment.id;
|
|
1966
2632
|
}
|
|
1967
2633
|
/**
|
|
1968
2634
|
* Format date to Spanish format: "Lunes 8 de diciembre"
|
|
@@ -1994,17 +2660,17 @@ class TasIncomingAppointmentComponent {
|
|
|
1994
2660
|
*/
|
|
1995
2661
|
getOtherParticipant(appointment) {
|
|
1996
2662
|
if (!appointment.participants || appointment.participants.length === 0) {
|
|
1997
|
-
return appointment.title;
|
|
2663
|
+
return appointment.title;
|
|
1998
2664
|
}
|
|
1999
2665
|
const otherParticipant = appointment.participants.find(p => { var _a; return p.userId !== ((_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.id); });
|
|
2000
2666
|
return (otherParticipant === null || otherParticipant === void 0 ? void 0 : otherParticipant.name) || appointment.title;
|
|
2001
2667
|
}
|
|
2002
2668
|
}
|
|
2003
2669
|
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2004
|
-
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"] }] });
|
|
2670
|
+
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; trackBy: trackByAppointmentId\">\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"] }] });
|
|
2005
2671
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
2006
2672
|
type: Component,
|
|
2007
|
-
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"] }]
|
|
2673
|
+
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; trackBy: trackByAppointmentId\">\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"] }]
|
|
2008
2674
|
}], ctorParameters: function () { return [{ type: TasService }]; }, propDecorators: { roomType: [{
|
|
2009
2675
|
type: Input
|
|
2010
2676
|
}], entityId: [{
|
|
@@ -2070,12 +2736,14 @@ TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", versio
|
|
|
2070
2736
|
TasFloatingCallComponent,
|
|
2071
2737
|
TasWaitingRoomComponent,
|
|
2072
2738
|
TasAvatarComponent,
|
|
2073
|
-
TasIncomingAppointmentComponent
|
|
2739
|
+
TasIncomingAppointmentComponent,
|
|
2740
|
+
TasFeedbackModalComponent], imports: [CommonModule, FormsModule, NgbTooltipModule], exports: [TasButtonComponent,
|
|
2074
2741
|
TasVideocallComponent,
|
|
2075
2742
|
TasFloatingCallComponent,
|
|
2076
2743
|
TasWaitingRoomComponent,
|
|
2077
2744
|
TasAvatarComponent,
|
|
2078
|
-
TasIncomingAppointmentComponent
|
|
2745
|
+
TasIncomingAppointmentComponent,
|
|
2746
|
+
TasFeedbackModalComponent] });
|
|
2079
2747
|
TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, imports: [[CommonModule, FormsModule, NgbTooltipModule]] });
|
|
2080
2748
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, decorators: [{
|
|
2081
2749
|
type: NgModule,
|
|
@@ -2087,6 +2755,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2087
2755
|
TasWaitingRoomComponent,
|
|
2088
2756
|
TasAvatarComponent,
|
|
2089
2757
|
TasIncomingAppointmentComponent,
|
|
2758
|
+
TasFeedbackModalComponent,
|
|
2090
2759
|
],
|
|
2091
2760
|
imports: [CommonModule, FormsModule, NgbTooltipModule],
|
|
2092
2761
|
exports: [
|
|
@@ -2096,6 +2765,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2096
2765
|
TasWaitingRoomComponent,
|
|
2097
2766
|
TasAvatarComponent,
|
|
2098
2767
|
TasIncomingAppointmentComponent,
|
|
2768
|
+
TasFeedbackModalComponent,
|
|
2099
2769
|
],
|
|
2100
2770
|
}]
|
|
2101
2771
|
}] });
|
|
@@ -2108,5 +2778,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2108
2778
|
* Generated bundle index. Do not edit.
|
|
2109
2779
|
*/
|
|
2110
2780
|
|
|
2111
|
-
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 };
|
|
2781
|
+
export { AppointmentStatus, CallState, FeedbackMotiveType, GeoStatus, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFeedbackModalComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserGeoViewState, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2112
2782
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|