tas-uell-sdk 0.0.6 → 0.1.1
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 +74 -3
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +55 -61
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +109 -0
- package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +114 -8
- package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +104 -46
- package/esm2020/lib/config/tas.config.mjs +1 -1
- package/esm2020/lib/interfaces/tas.interfaces.mjs +8 -1
- package/esm2020/lib/services/geolocation.service.mjs +56 -0
- package/esm2020/lib/services/tas-utility.service.mjs +60 -0
- package/esm2020/lib/services/tas.service.mjs +49 -12
- package/esm2020/lib/tas-uell-sdk.module.mjs +11 -5
- package/esm2020/public-api.mjs +4 -1
- package/fesm2015/tas-uell-sdk.mjs +553 -126
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +540 -124
- package/fesm2020/tas-uell-sdk.mjs.map +1 -1
- package/lib/components/tas-btn/tas-btn.component.d.ts +10 -5
- package/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.d.ts +33 -0
- package/lib/components/tas-videocall/tas-videocall.component.d.ts +27 -2
- package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +29 -8
- package/lib/config/tas.config.d.ts +3 -0
- package/lib/interfaces/tas.interfaces.d.ts +30 -2
- package/lib/services/geolocation.service.d.ts +24 -0
- package/lib/services/tas-utility.service.d.ts +26 -0
- package/lib/services/tas.service.d.ts +16 -6
- package/lib/tas-uell-sdk.module.d.ts +5 -3
- package/package.json +1 -1
- package/public-api.d.ts +3 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Inject, Component, ChangeDetectionStrategy, Input, ViewChild, NgModule } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, Inject, Component, ChangeDetectionStrategy, Input, ViewChild, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
4
4
|
import { map, catchError } from 'rxjs/operators';
|
|
5
5
|
import * as OT from '@opentok/client';
|
|
6
6
|
import interact from 'interactjs';
|
|
7
7
|
import * as i1 from '@ng-bootstrap/ng-bootstrap';
|
|
8
|
-
import
|
|
8
|
+
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
9
|
+
import * as i4 from '@angular/common';
|
|
9
10
|
import { CommonModule } from '@angular/common';
|
|
10
11
|
import { FormsModule } from '@angular/forms';
|
|
11
12
|
|
|
@@ -86,11 +87,77 @@ var ViewMode;
|
|
|
86
87
|
ViewMode["FULLSCREEN"] = "FULLSCREEN";
|
|
87
88
|
ViewMode["PIP"] = "PIP";
|
|
88
89
|
})(ViewMode || (ViewMode = {}));
|
|
90
|
+
// Appointment types
|
|
91
|
+
var AppointmentStatus;
|
|
92
|
+
(function (AppointmentStatus) {
|
|
93
|
+
AppointmentStatus["CONFIRMED"] = "CONFIRMED";
|
|
94
|
+
AppointmentStatus["CANCELLED"] = "CANCELLED";
|
|
95
|
+
AppointmentStatus["RESCHEDULED"] = "RESCHEDULED";
|
|
96
|
+
})(AppointmentStatus || (AppointmentStatus = {}));
|
|
97
|
+
|
|
98
|
+
class TasUtilityService {
|
|
99
|
+
/**
|
|
100
|
+
* Determines if the button should be shown based on the error message.
|
|
101
|
+
* Returns false if the error message matches any of the patterns that indicate
|
|
102
|
+
* the button should be hidden (but polling should continue).
|
|
103
|
+
*
|
|
104
|
+
* @param errorMessage The error message from the status call
|
|
105
|
+
* @returns true if button should be shown, false if it should be hidden
|
|
106
|
+
*/
|
|
107
|
+
shouldShowButton(errorMessage) {
|
|
108
|
+
if (!errorMessage) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const message = errorMessage.trim();
|
|
112
|
+
// List of error messages that should hide the button
|
|
113
|
+
const hideButtonMessages = [
|
|
114
|
+
'Debe enviarse sessionId o roomType + entityId',
|
|
115
|
+
'No existe videollamada con ese sessionId',
|
|
116
|
+
'Usuario no pertenece a la llamada',
|
|
117
|
+
];
|
|
118
|
+
// Check exact matches
|
|
119
|
+
if (hideButtonMessages.some((msg) => message === msg)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Check pattern match for "No existe videocall vigente para roomType=x y entityId=x"
|
|
123
|
+
// Using startsWith as primary check to handle any variations
|
|
124
|
+
if (message.startsWith('No existe videocall vigente para roomType=')) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
// Also check regex pattern as backup
|
|
128
|
+
const vigentePattern = /^No existe videocall vigente para roomType=.+ y entityId=\s*\d+$/;
|
|
129
|
+
if (vigentePattern.test(message)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if at least one OWNER has joined the call.
|
|
136
|
+
*/
|
|
137
|
+
checkIfOwnerJoined(users) {
|
|
138
|
+
return users.some((u) => u.rol === 'OWNER' && u.status === 'JOINED');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract error message from various error structures.
|
|
142
|
+
*/
|
|
143
|
+
extractErrorMessage(err, fallback = 'Error desconocido') {
|
|
144
|
+
return err?.error?.message || err?.message || fallback;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
TasUtilityService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUtilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
148
|
+
TasUtilityService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUtilityService, providedIn: 'root' });
|
|
149
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUtilityService, decorators: [{
|
|
150
|
+
type: Injectable,
|
|
151
|
+
args: [{
|
|
152
|
+
providedIn: 'root',
|
|
153
|
+
}]
|
|
154
|
+
}] });
|
|
89
155
|
|
|
90
156
|
class TasService {
|
|
91
|
-
constructor(httpClient, config) {
|
|
157
|
+
constructor(httpClient, config, tasUtilityService) {
|
|
92
158
|
this.httpClient = httpClient;
|
|
93
159
|
this.config = config;
|
|
160
|
+
this.tasUtilityService = tasUtilityService;
|
|
94
161
|
this.session = null;
|
|
95
162
|
this.publisher = null;
|
|
96
163
|
this.subscribers = [];
|
|
@@ -120,6 +187,15 @@ class TasService {
|
|
|
120
187
|
this.ownerHasLeft$ = this.ownerHasLeftSubject.asObservable();
|
|
121
188
|
this.joinableSubject = new BehaviorSubject(false);
|
|
122
189
|
this.joinable$ = this.joinableSubject.asObservable();
|
|
190
|
+
// Observable for when backend requests geolocation activation
|
|
191
|
+
this.activateGeoSubject = new BehaviorSubject(false);
|
|
192
|
+
this.activateGeo$ = this.activateGeoSubject.asObservable();
|
|
193
|
+
// Observable for when geo request is active (owner waiting for user response)
|
|
194
|
+
this.geoRequestActiveSubject = new BehaviorSubject(false);
|
|
195
|
+
this.geoRequestActive$ = this.geoRequestActiveSubject.asObservable();
|
|
196
|
+
// Observable for when all geo has been granted
|
|
197
|
+
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
198
|
+
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
123
199
|
this.statusPollingInterval = null;
|
|
124
200
|
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
125
201
|
this.wasOwnerPresent = false;
|
|
@@ -411,6 +487,23 @@ class TasService {
|
|
|
411
487
|
throw error;
|
|
412
488
|
}));
|
|
413
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* Get user appointments within a date range.
|
|
492
|
+
* @param params Date range for querying appointments
|
|
493
|
+
* @returns Observable of appointment array
|
|
494
|
+
*/
|
|
495
|
+
getAppointments(params) {
|
|
496
|
+
let url = `v2/proxy/appointment/agendas/user/appointments?fromDate=${params.fromDate}&toDate=${params.toDate}&roomType=TAS`;
|
|
497
|
+
if (params.entityId !== undefined) {
|
|
498
|
+
url += `&entityId=${params.entityId}`;
|
|
499
|
+
}
|
|
500
|
+
return this.httpClient
|
|
501
|
+
.get(url, { headers: {} })
|
|
502
|
+
.pipe(catchError((error) => {
|
|
503
|
+
console.error('TAS Service: getAppointments failed', error);
|
|
504
|
+
throw error;
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
414
507
|
/**
|
|
415
508
|
* Start automatic status polling for the current session.
|
|
416
509
|
* Status is polled every STATUS_POLL_INTERVAL_MS milliseconds.
|
|
@@ -447,8 +540,23 @@ class TasService {
|
|
|
447
540
|
}
|
|
448
541
|
// Update joinable status
|
|
449
542
|
this.joinableSubject.next(content.joinable);
|
|
543
|
+
// Update activateGeo status
|
|
544
|
+
if (content.activateGeo !== undefined) {
|
|
545
|
+
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
546
|
+
this.activateGeoSubject.next(content.activateGeo);
|
|
547
|
+
}
|
|
548
|
+
// Update geoRequestActive status (owner waiting for user geo response)
|
|
549
|
+
if (content.geoRequestActive !== undefined) {
|
|
550
|
+
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
551
|
+
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
552
|
+
}
|
|
553
|
+
// Update allGeoGranted status (all users responded with geo)
|
|
554
|
+
if (content.allGeoGranted !== undefined) {
|
|
555
|
+
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
556
|
+
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
557
|
+
}
|
|
450
558
|
// Check if owner has joined
|
|
451
|
-
const ownerJoined = this.checkIfOwnerJoined(content.users);
|
|
559
|
+
const ownerJoined = this.tasUtilityService.checkIfOwnerJoined(content.users);
|
|
452
560
|
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
453
561
|
// Detect if owner left: was present, now not present
|
|
454
562
|
if (this.wasOwnerPresent && !ownerJoined) {
|
|
@@ -468,12 +576,6 @@ class TasService {
|
|
|
468
576
|
}));
|
|
469
577
|
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
470
578
|
}
|
|
471
|
-
/**
|
|
472
|
-
* Check if at least one OWNER has joined the call.
|
|
473
|
-
*/
|
|
474
|
-
checkIfOwnerJoined(users) {
|
|
475
|
-
return users.some((u) => u.rol === TasUserRole.OWNER && u.status === 'JOINED');
|
|
476
|
-
}
|
|
477
579
|
/**
|
|
478
580
|
* Admit a user from the waiting room by changing their status.
|
|
479
581
|
*/
|
|
@@ -636,7 +738,7 @@ class TasService {
|
|
|
636
738
|
this.movePublisherTo('publisher-container');
|
|
637
739
|
}
|
|
638
740
|
}
|
|
639
|
-
TasService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, deps: [{ token: TAS_HTTP_CLIENT }, { token: TAS_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
741
|
+
TasService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, deps: [{ token: TAS_HTTP_CLIENT }, { token: TAS_CONFIG }, { token: TasUtilityService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
640
742
|
TasService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, providedIn: 'root' });
|
|
641
743
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasService, decorators: [{
|
|
642
744
|
type: Injectable,
|
|
@@ -649,7 +751,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
649
751
|
}] }, { type: undefined, decorators: [{
|
|
650
752
|
type: Inject,
|
|
651
753
|
args: [TAS_CONFIG]
|
|
652
|
-
}] }]; } });
|
|
754
|
+
}] }, { type: TasUtilityService }]; } });
|
|
755
|
+
|
|
756
|
+
class GeolocationService {
|
|
757
|
+
constructor() {
|
|
758
|
+
this.cachedPosition = null;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Request current geolocation position using Web Geolocation API.
|
|
762
|
+
* Works in both browser and Capacitor environments.
|
|
763
|
+
* @returns Promise with {latitude, longitude} or null if denied/unavailable
|
|
764
|
+
*/
|
|
765
|
+
async getCurrentPosition() {
|
|
766
|
+
if (!navigator.geolocation) {
|
|
767
|
+
console.warn('[GeolocationService] Geolocation not supported');
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
return new Promise((resolve) => {
|
|
771
|
+
navigator.geolocation.getCurrentPosition((position) => {
|
|
772
|
+
const geoPosition = {
|
|
773
|
+
latitude: position.coords.latitude,
|
|
774
|
+
longitude: position.coords.longitude,
|
|
775
|
+
};
|
|
776
|
+
this.cachedPosition = geoPosition;
|
|
777
|
+
resolve(geoPosition);
|
|
778
|
+
}, (error) => {
|
|
779
|
+
console.warn('[GeolocationService] Geolocation error:', error.message);
|
|
780
|
+
resolve(null);
|
|
781
|
+
}, {
|
|
782
|
+
enableHighAccuracy: false,
|
|
783
|
+
timeout: 30000,
|
|
784
|
+
maximumAge: 60000,
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Get the cached position from the last successful request.
|
|
790
|
+
*/
|
|
791
|
+
getCachedPosition() {
|
|
792
|
+
return this.cachedPosition;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Clear the cached position.
|
|
796
|
+
*/
|
|
797
|
+
clearCache() {
|
|
798
|
+
this.cachedPosition = null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
GeolocationService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: GeolocationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
802
|
+
GeolocationService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: GeolocationService, providedIn: 'root' });
|
|
803
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: GeolocationService, decorators: [{
|
|
804
|
+
type: Injectable,
|
|
805
|
+
args: [{
|
|
806
|
+
providedIn: 'root',
|
|
807
|
+
}]
|
|
808
|
+
}] });
|
|
653
809
|
|
|
654
810
|
class TasAvatarComponent {
|
|
655
811
|
constructor() {
|
|
@@ -725,9 +881,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
725
881
|
}] } });
|
|
726
882
|
|
|
727
883
|
class TasVideocallComponent {
|
|
728
|
-
constructor(activeModal, tasService) {
|
|
884
|
+
constructor(activeModal, tasService, geolocationService) {
|
|
729
885
|
this.activeModal = activeModal;
|
|
730
886
|
this.tasService = tasService;
|
|
887
|
+
this.geolocationService = geolocationService;
|
|
731
888
|
this.participantName = '';
|
|
732
889
|
this.tenant = '';
|
|
733
890
|
this.businessRole = TasBusinessRole.USER;
|
|
@@ -739,6 +896,12 @@ class TasVideocallComponent {
|
|
|
739
896
|
this.ownerHasJoined = false;
|
|
740
897
|
this.hasVideoStream = false;
|
|
741
898
|
this.dismissedUsers = [];
|
|
899
|
+
this.showLocationPanel = true; // Show by default for owners, will be updated based on user's location status
|
|
900
|
+
this.userHasLocation = false; // Tracks if the user has shared their location
|
|
901
|
+
this.geoLocationStatus = 'unknown';
|
|
902
|
+
// Geo panel states for owner
|
|
903
|
+
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
904
|
+
this.allGeoGranted = false; // All users responded with geo
|
|
742
905
|
this.subscriptions = new Subscription();
|
|
743
906
|
}
|
|
744
907
|
ngOnInit() {
|
|
@@ -816,6 +979,30 @@ class TasVideocallComponent {
|
|
|
816
979
|
this.dismissedUsers.push(userId);
|
|
817
980
|
this.waitingRoomUsers = this.waitingRoomUsers.filter((u) => u.userId !== userId);
|
|
818
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Close the location panel
|
|
984
|
+
*/
|
|
985
|
+
closeLocationPanel() {
|
|
986
|
+
this.showLocationPanel = false;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Request the user to share their location
|
|
990
|
+
*/
|
|
991
|
+
requestUserLocation() {
|
|
992
|
+
if (!this.videoCallId) {
|
|
993
|
+
console.error('Cannot request location: videoCallId not set');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const body = {
|
|
997
|
+
videoCallId: this.videoCallId,
|
|
998
|
+
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
999
|
+
};
|
|
1000
|
+
// TODO: Send location request action to backend when endpoint is ready
|
|
1001
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1002
|
+
next: () => console.log('Location request sent'),
|
|
1003
|
+
error: (err) => console.error('Error requesting location:', err),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
819
1006
|
// Private Methods
|
|
820
1007
|
setupSubscriptions() {
|
|
821
1008
|
// Call state subscription
|
|
@@ -854,6 +1041,78 @@ class TasVideocallComponent {
|
|
|
854
1041
|
this.activeModal.close('owner_left');
|
|
855
1042
|
}
|
|
856
1043
|
}));
|
|
1044
|
+
// ActivateGeo subscription - only for non-owners (users)
|
|
1045
|
+
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
1046
|
+
if (activateGeo && !this.canAdmitUsers) {
|
|
1047
|
+
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
1048
|
+
this.handleActivateGeo();
|
|
1049
|
+
}
|
|
1050
|
+
}));
|
|
1051
|
+
// GeoRequestActive subscription - for owners
|
|
1052
|
+
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
1053
|
+
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
1054
|
+
this.geoRequestActive = active;
|
|
1055
|
+
}));
|
|
1056
|
+
// AllGeoGranted subscription - for owners
|
|
1057
|
+
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1058
|
+
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1059
|
+
this.allGeoGranted = granted;
|
|
1060
|
+
}));
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Handle activateGeo request from backend (for non-owner users).
|
|
1064
|
+
* If geo is already active, report it. If not, prompt user.
|
|
1065
|
+
*/
|
|
1066
|
+
async handleActivateGeo() {
|
|
1067
|
+
console.log('[TAS DEBUG] handleActivateGeo - current status:', this.geoLocationStatus);
|
|
1068
|
+
// Check if we already have a cached position
|
|
1069
|
+
const cachedPosition = this.geolocationService.getCachedPosition();
|
|
1070
|
+
if (cachedPosition) {
|
|
1071
|
+
console.log('[TAS DEBUG] Geolocation already active, reporting to backend:', cachedPosition);
|
|
1072
|
+
this.geoLocationStatus = 'active';
|
|
1073
|
+
this.reportGeoStatus(cachedPosition.latitude, cachedPosition.longitude);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
// Try to get position
|
|
1077
|
+
console.log('[TAS DEBUG] Requesting geolocation from user...');
|
|
1078
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1079
|
+
if (position) {
|
|
1080
|
+
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1081
|
+
this.geoLocationStatus = 'active';
|
|
1082
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1086
|
+
this.geoLocationStatus = 'denied';
|
|
1087
|
+
// Report that geo is not available (with no coordinates)
|
|
1088
|
+
this.reportGeoStatus();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Report geolocation status to backend.
|
|
1093
|
+
*/
|
|
1094
|
+
reportGeoStatus(latitude, longitude) {
|
|
1095
|
+
if (!this.videoCallId) {
|
|
1096
|
+
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
// TODO: If the permission for geo location is not granted, we should not report it to the backend
|
|
1100
|
+
if (!this.geoLocationStatus) {
|
|
1101
|
+
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const body = {
|
|
1105
|
+
userId: this.userId,
|
|
1106
|
+
videoCallId: this.videoCallId,
|
|
1107
|
+
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1108
|
+
latitude,
|
|
1109
|
+
longitude,
|
|
1110
|
+
};
|
|
1111
|
+
console.log('[TAS DEBUG] Reporting geo status to backend:', body);
|
|
1112
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1113
|
+
next: () => console.log('[TAS DEBUG] Geo status reported successfully'),
|
|
1114
|
+
error: (err) => console.error('[TAS DEBUG] Error reporting geo status:', err),
|
|
1115
|
+
});
|
|
857
1116
|
}
|
|
858
1117
|
initializeCall() {
|
|
859
1118
|
if (this.isReturningFromPip) {
|
|
@@ -942,12 +1201,12 @@ class TasVideocallComponent {
|
|
|
942
1201
|
});
|
|
943
1202
|
}
|
|
944
1203
|
}
|
|
945
|
-
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
946
|
-
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n</div>\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1204
|
+
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 });
|
|
1205
|
+
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", userId: "userId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
947
1206
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
948
1207
|
type: Component,
|
|
949
|
-
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n</div>\n", styles: [".tas-videocall-container{position:relative;width:100vw;height:100vh;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}\n"] }]
|
|
950
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }]; }, propDecorators: { sessionId: [{
|
|
1208
|
+
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
1209
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
951
1210
|
type: Input
|
|
952
1211
|
}], token: [{
|
|
953
1212
|
type: Input
|
|
@@ -955,6 +1214,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
955
1214
|
type: Input
|
|
956
1215
|
}], videoCallId: [{
|
|
957
1216
|
type: Input
|
|
1217
|
+
}], userId: [{
|
|
1218
|
+
type: Input
|
|
958
1219
|
}], participantName: [{
|
|
959
1220
|
type: Input
|
|
960
1221
|
}], tenant: [{
|
|
@@ -974,15 +1235,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
974
1235
|
var WaitingRoomState;
|
|
975
1236
|
(function (WaitingRoomState) {
|
|
976
1237
|
WaitingRoomState["CHECKING_STATUS"] = "CHECKING_STATUS";
|
|
977
|
-
WaitingRoomState["
|
|
1238
|
+
WaitingRoomState["WAITING_FOR_JOINABLE"] = "WAITING_FOR_JOINABLE";
|
|
978
1239
|
WaitingRoomState["READY"] = "READY";
|
|
979
1240
|
WaitingRoomState["GETTING_TOKEN"] = "GETTING_TOKEN";
|
|
1241
|
+
WaitingRoomState["JOINING"] = "JOINING";
|
|
980
1242
|
WaitingRoomState["ERROR"] = "ERROR";
|
|
981
1243
|
})(WaitingRoomState || (WaitingRoomState = {}));
|
|
982
1244
|
class TasWaitingRoomComponent {
|
|
983
|
-
constructor(activeModal, tasService, modalService, cdr) {
|
|
1245
|
+
constructor(activeModal, tasService, geolocationService, modalService, cdr) {
|
|
984
1246
|
this.activeModal = activeModal;
|
|
985
1247
|
this.tasService = tasService;
|
|
1248
|
+
this.geolocationService = geolocationService;
|
|
986
1249
|
this.modalService = modalService;
|
|
987
1250
|
this.cdr = cdr;
|
|
988
1251
|
// Status endpoint params
|
|
@@ -992,6 +1255,7 @@ class TasWaitingRoomComponent {
|
|
|
992
1255
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
993
1256
|
this.WaitingRoomState = WaitingRoomState; // Expose enum to template
|
|
994
1257
|
this.errorMessage = '';
|
|
1258
|
+
this.isJoinable = false;
|
|
995
1259
|
// Session data from status response
|
|
996
1260
|
this.resolvedSessionId = '';
|
|
997
1261
|
this.resolvedAppointmentId = null;
|
|
@@ -1000,6 +1264,8 @@ class TasWaitingRoomComponent {
|
|
|
1000
1264
|
// Subscriptions
|
|
1001
1265
|
this.subscriptions = new Subscription();
|
|
1002
1266
|
this.videoCallModalRef = null;
|
|
1267
|
+
// Geolocation
|
|
1268
|
+
this.geoPosition = null;
|
|
1003
1269
|
}
|
|
1004
1270
|
/** Whether current user is an owner */
|
|
1005
1271
|
get isOwner() {
|
|
@@ -1007,8 +1273,69 @@ class TasWaitingRoomComponent {
|
|
|
1007
1273
|
}
|
|
1008
1274
|
ngOnInit() {
|
|
1009
1275
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1276
|
+
this.requestMediaPermissions();
|
|
1277
|
+
this.requestGeolocation();
|
|
1010
1278
|
this.checkStatus();
|
|
1011
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Request camera and microphone permissions.
|
|
1282
|
+
*/
|
|
1283
|
+
async requestMediaPermissions() {
|
|
1284
|
+
console.log('[TAS DEBUG] Requesting media permissions...');
|
|
1285
|
+
try {
|
|
1286
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
|
1287
|
+
// Stop tracks immediately - we just needed the permission
|
|
1288
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1289
|
+
console.log('[TAS DEBUG] Media permissions granted');
|
|
1290
|
+
}
|
|
1291
|
+
catch (error) {
|
|
1292
|
+
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Request geolocation immediately on init.
|
|
1297
|
+
* Only for regular users (not owners/backoffice).
|
|
1298
|
+
* If user allows, store position and send to backend.
|
|
1299
|
+
*/
|
|
1300
|
+
async requestGeolocation() {
|
|
1301
|
+
// Only request geolocation for regular users, not owners/backoffice
|
|
1302
|
+
if (this.isOwner || this.isBackoffice) {
|
|
1303
|
+
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1307
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1308
|
+
if (position) {
|
|
1309
|
+
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1310
|
+
this.geoPosition = position;
|
|
1311
|
+
// Send to backend when videoCallId is available
|
|
1312
|
+
this.sendGeolocationToBackend();
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Send geolocation to backend via modify user endpoint.
|
|
1320
|
+
* NOTE: Endpoint call is prepared but may not be active yet.
|
|
1321
|
+
*/
|
|
1322
|
+
sendGeolocationToBackend() {
|
|
1323
|
+
if (!this.geoPosition || !this.videoCallId) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
1327
|
+
const body = {
|
|
1328
|
+
userId: this.currentUser?.id,
|
|
1329
|
+
videoCallId: this.videoCallId,
|
|
1330
|
+
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1331
|
+
latitude: this.geoPosition.latitude,
|
|
1332
|
+
longitude: this.geoPosition.longitude,
|
|
1333
|
+
};
|
|
1334
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1335
|
+
next: () => console.log('[TAS DEBUG] Geolocation sent successfully'),
|
|
1336
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1012
1339
|
ngOnDestroy() {
|
|
1013
1340
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1014
1341
|
this.subscriptions.unsubscribe();
|
|
@@ -1042,6 +1369,8 @@ class TasWaitingRoomComponent {
|
|
|
1042
1369
|
this.resolvedAppointmentId = content.appointmentId;
|
|
1043
1370
|
this.videoCallId = content.videoCallId;
|
|
1044
1371
|
console.log('[TAS DEBUG] Status response:', content);
|
|
1372
|
+
// Try to send geolocation now that we have videoCallId
|
|
1373
|
+
this.sendGeolocationToBackend();
|
|
1045
1374
|
// Start polling for status updates
|
|
1046
1375
|
this.tasService.startStatusPolling(statusParams);
|
|
1047
1376
|
// Subscribe to joinable status
|
|
@@ -1063,36 +1392,33 @@ class TasWaitingRoomComponent {
|
|
|
1063
1392
|
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
1064
1393
|
joinable,
|
|
1065
1394
|
currentState: this.state,
|
|
1066
|
-
isOwner: this.isOwner,
|
|
1067
|
-
isBackoffice: this.isBackoffice,
|
|
1068
1395
|
resolvedSessionId: this.resolvedSessionId,
|
|
1069
1396
|
});
|
|
1070
|
-
|
|
1397
|
+
this.isJoinable = joinable;
|
|
1398
|
+
// Don't update state if already getting token, joining, or in error
|
|
1071
1399
|
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
1072
|
-
this.state === WaitingRoomState.
|
|
1400
|
+
this.state === WaitingRoomState.JOINING ||
|
|
1073
1401
|
this.state === WaitingRoomState.ERROR) {
|
|
1074
1402
|
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
1075
1403
|
return;
|
|
1076
1404
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
this.getTokenForOwner();
|
|
1082
|
-
}
|
|
1405
|
+
// Both users and owners: show join button based on joinable
|
|
1406
|
+
if (joinable) {
|
|
1407
|
+
console.log('[TAS DEBUG] Joinable is true, showing join button');
|
|
1408
|
+
this.state = WaitingRoomState.READY;
|
|
1083
1409
|
}
|
|
1084
1410
|
else {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
console.log('[TAS DEBUG] Non-owner: joinable is true, getting token');
|
|
1088
|
-
this.getTokenForOwner(); // Also get token when joinable
|
|
1089
|
-
}
|
|
1090
|
-
else {
|
|
1091
|
-
console.log('[TAS DEBUG] Non-owner: waiting for owner');
|
|
1092
|
-
this.state = WaitingRoomState.WAITING_FOR_OWNER;
|
|
1093
|
-
this.cdr.detectChanges();
|
|
1094
|
-
}
|
|
1411
|
+
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
1412
|
+
this.state = WaitingRoomState.WAITING_FOR_JOINABLE;
|
|
1095
1413
|
}
|
|
1414
|
+
this.cdr.detectChanges();
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Called when user clicks the join button.
|
|
1418
|
+
* Calls /start to get token then auto-joins.
|
|
1419
|
+
*/
|
|
1420
|
+
joinSession() {
|
|
1421
|
+
this.startSessionAndJoin();
|
|
1096
1422
|
}
|
|
1097
1423
|
/**
|
|
1098
1424
|
* Check if user has owner/backoffice role
|
|
@@ -1103,9 +1429,9 @@ class TasWaitingRoomComponent {
|
|
|
1103
1429
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
1104
1430
|
}
|
|
1105
1431
|
/**
|
|
1106
|
-
*
|
|
1432
|
+
* Start session and join - called when user clicks join button
|
|
1107
1433
|
*/
|
|
1108
|
-
|
|
1434
|
+
startSessionAndJoin() {
|
|
1109
1435
|
if (!this.resolvedSessionId) {
|
|
1110
1436
|
this.state = WaitingRoomState.ERROR;
|
|
1111
1437
|
this.errorMessage = 'Session ID not available';
|
|
@@ -1134,10 +1460,14 @@ class TasWaitingRoomComponent {
|
|
|
1134
1460
|
this.cdr.detectChanges();
|
|
1135
1461
|
return;
|
|
1136
1462
|
}
|
|
1137
|
-
console.log('[TAS DEBUG] Token obtained successfully');
|
|
1463
|
+
console.log('[TAS DEBUG] Token obtained successfully, joining session...');
|
|
1138
1464
|
this.token = tokenResponse.content.token;
|
|
1139
|
-
this.state = WaitingRoomState.
|
|
1465
|
+
this.state = WaitingRoomState.JOINING;
|
|
1140
1466
|
this.cdr.detectChanges();
|
|
1467
|
+
// Auto-join immediately
|
|
1468
|
+
this.tasService.stopStatusPolling();
|
|
1469
|
+
this.activeModal.close('joining');
|
|
1470
|
+
this.openVideoCallModal();
|
|
1141
1471
|
},
|
|
1142
1472
|
error: (err) => {
|
|
1143
1473
|
console.error('[TAS DEBUG] /start request failed:', err);
|
|
@@ -1149,19 +1479,6 @@ class TasWaitingRoomComponent {
|
|
|
1149
1479
|
},
|
|
1150
1480
|
}));
|
|
1151
1481
|
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Join the session - token already obtained
|
|
1154
|
-
*/
|
|
1155
|
-
joinSession() {
|
|
1156
|
-
if (!this.resolvedSessionId || !this.token) {
|
|
1157
|
-
this.errorMessage = 'Session not ready';
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
// Close waiting room and open video call
|
|
1161
|
-
this.tasService.stopStatusPolling();
|
|
1162
|
-
this.activeModal.close('joining');
|
|
1163
|
-
this.openVideoCallModal();
|
|
1164
|
-
}
|
|
1165
1482
|
/**
|
|
1166
1483
|
* Closes the waiting room
|
|
1167
1484
|
*/
|
|
@@ -1189,6 +1506,7 @@ class TasWaitingRoomComponent {
|
|
|
1189
1506
|
this.videoCallModalRef.componentInstance.token = this.token;
|
|
1190
1507
|
this.videoCallModalRef.componentInstance.appointmentId = this.resolvedAppointmentId;
|
|
1191
1508
|
this.videoCallModalRef.componentInstance.videoCallId = this.videoCallId;
|
|
1509
|
+
this.videoCallModalRef.componentInstance.userId = this.currentUser?.id;
|
|
1192
1510
|
this.videoCallModalRef.componentInstance.tenant = this.tenant;
|
|
1193
1511
|
this.videoCallModalRef.componentInstance.businessRole = this.businessRole;
|
|
1194
1512
|
this.videoCallModalRef.componentInstance.isReturningFromPip = false;
|
|
@@ -1199,12 +1517,12 @@ class TasWaitingRoomComponent {
|
|
|
1199
1517
|
});
|
|
1200
1518
|
}
|
|
1201
1519
|
}
|
|
1202
|
-
TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: i1.NgbModal }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1203
|
-
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n <!-- Header -->\n <div class=\"waiting-room-header\">\n <h2 class=\"header-title\">Iniciar turno</h2>\n <button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"waiting-room-content\">\n <!-- CHECKING_STATUS State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CHECKING_STATUS\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Verificando estado de la sesi\u00F3n...</p>\n </div>\n\n <!--
|
|
1520
|
+
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 });
|
|
1521
|
+
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n <!-- Header -->\n <div class=\"waiting-room-header\">\n <h2 class=\"header-title\">Iniciar turno</h2>\n <button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"waiting-room-content\">\n <!-- CHECKING_STATUS State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CHECKING_STATUS\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Verificando estado de la sesi\u00F3n...</p>\n </div>\n\n <!-- WAITING_FOR_JOINABLE State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.WAITING_FOR_JOINABLE\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Esperando que la sesi\u00F3n est\u00E9 disponible...</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </div>\n\n <!-- READY State (Join button enabled) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY\">\n <div class=\"state-icon ready\">\n <i class=\"fa fa-check-circle\"></i>\n </div>\n <p class=\"state-message success\">\u00A1La sala est\u00E1 lista!</p>\n <button type=\"button\" class=\"btn action-btn join-btn\" (click)=\"joinSession()\">\n <i class=\"fa fa-sign-in\"></i>\n Unirse a la llamada\n </button>\n </div>\n\n <!-- JOINING State (Auto-joining) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.JOINING\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Ingresando a la llamada...</p>\n </div>\n\n <!-- GETTING_TOKEN State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Conectando...</p>\n </div>\n\n <!-- ERROR State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n <p class=\"error-details\" *ngIf=\"errorMessage\">\n {{ errorMessage }}\n </p>\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:20px 40px;border-bottom:1px solid #e9ecef;display:flex;align-items:center;justify-content:space-between}.waiting-room-header .header-title{margin:0;font-size:18px;font-weight:600;line-height:24px;color:#212529}.waiting-room-header .close-btn{width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px;display:flex;align-items:center;justify-content:center}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-title{font-size:16px;font-weight:600;color:#212529;margin-bottom:8px}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.mode-tabs{display:flex;justify-content:center;gap:8px;margin-bottom:24px;padding:4px;background:#f8f9fa;border-radius:8px}.mode-tab{flex:1;padding:10px 16px;font-size:13px;font-weight:600;border:none;border-radius:6px;background:transparent;color:#6c757d;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px}.mode-tab i{font-size:14px}.mode-tab:hover{color:#212529}.mode-tab.active{background:#ffffff;color:#1da4b1;box-shadow:0 2px 8px #00000014}.mode-content{animation:fadeIn .3s ease}.session-input-container{display:flex;flex-direction:column;gap:16px;margin-top:8px}.session-input{width:100%;padding:14px 16px;font-size:14px;border:2px solid #e9ecef;border-radius:8px;background:#ffffff;color:#212529;transition:all .2s ease;text-align:center;font-family:Monaco,Consolas,monospace;letter-spacing:.5px}.session-input::placeholder{color:#6c757d;font-family:inherit;letter-spacing:normal}.session-input:focus{outline:none;border-color:#1da4b1;box-shadow:0 0 0 3px #1da4b126}.session-input:hover:not(:focus){border-color:#6c757d}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1204
1522
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
1205
1523
|
type: Component,
|
|
1206
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n <!-- Header -->\n <div class=\"waiting-room-header\">\n <h2 class=\"header-title\">Iniciar turno</h2>\n <button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"waiting-room-content\">\n <!-- CHECKING_STATUS State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CHECKING_STATUS\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Verificando estado de la sesi\u00F3n...</p>\n </div>\n\n <!--
|
|
1207
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1524
|
+
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n <!-- Header -->\n <div class=\"waiting-room-header\">\n <h2 class=\"header-title\">Iniciar turno</h2>\n <button type=\"button\" class=\"close-btn\" (click)=\"cancel()\" aria-label=\"Close\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"waiting-room-content\">\n <!-- CHECKING_STATUS State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.CHECKING_STATUS\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Verificando estado de la sesi\u00F3n...</p>\n </div>\n\n <!-- WAITING_FOR_JOINABLE State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.WAITING_FOR_JOINABLE\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Esperando que la sesi\u00F3n est\u00E9 disponible...</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </div>\n\n <!-- READY State (Join button enabled) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.READY\">\n <div class=\"state-icon ready\">\n <i class=\"fa fa-check-circle\"></i>\n </div>\n <p class=\"state-message success\">\u00A1La sala est\u00E1 lista!</p>\n <button type=\"button\" class=\"btn action-btn join-btn\" (click)=\"joinSession()\">\n <i class=\"fa fa-sign-in\"></i>\n Unirse a la llamada\n </button>\n </div>\n\n <!-- JOINING State (Auto-joining) -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.JOINING\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Ingresando a la llamada...</p>\n </div>\n\n <!-- GETTING_TOKEN State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.GETTING_TOKEN\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n <p class=\"state-message\">Conectando...</p>\n </div>\n\n <!-- ERROR State -->\n <div class=\"state-container\" *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n <p class=\"error-details\" *ngIf=\"errorMessage\">\n {{ errorMessage }}\n </p>\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:420px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-header{position:relative;padding:20px 40px;border-bottom:1px solid #e9ecef;display:flex;align-items:center;justify-content:space-between}.waiting-room-header .header-title{margin:0;font-size:18px;font-weight:600;line-height:24px;color:#212529}.waiting-room-header .close-btn{width:32px;height:32px;border:none;background:transparent;border-radius:4px;color:#6c757d;cursor:pointer;transition:all .2s ease;font-size:20px;display:flex;align-items:center;justify-content:center}.waiting-room-header .close-btn:hover{background:#f8f9fa;color:#212529}.waiting-title{font-size:16px;font-weight:600;color:#212529;margin-bottom:8px}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.mode-tabs{display:flex;justify-content:center;gap:8px;margin-bottom:24px;padding:4px;background:#f8f9fa;border-radius:8px}.mode-tab{flex:1;padding:10px 16px;font-size:13px;font-weight:600;border:none;border-radius:6px;background:transparent;color:#6c757d;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:8px}.mode-tab i{font-size:14px}.mode-tab:hover{color:#212529}.mode-tab.active{background:#ffffff;color:#1da4b1;box-shadow:0 2px 8px #00000014}.mode-content{animation:fadeIn .3s ease}.session-input-container{display:flex;flex-direction:column;gap:16px;margin-top:8px}.session-input{width:100%;padding:14px 16px;font-size:14px;border:2px solid #e9ecef;border-radius:8px;background:#ffffff;color:#212529;transition:all .2s ease;text-align:center;font-family:Monaco,Consolas,monospace;letter-spacing:.5px}.session-input::placeholder{color:#6c757d;font-family:inherit;letter-spacing:normal}.session-input:focus{outline:none;border-color:#1da4b1;box-shadow:0 0 0 3px #1da4b126}.session-input:hover:not(:focus){border-color:#6c757d}@keyframes fadeIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.idle{background:rgba(29,164,177,.1);border:2px dashed #1da4b1}.state-icon.idle i{color:#1da4b1}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.ready{background:linear-gradient(135deg,#1da4b1 0%,#38b89a 100%);box-shadow:0 4px 16px #1da4b14d}.state-icon.ready i{color:#fff}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.success{color:#1da4b1}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.progress-steps{display:flex;justify-content:center;gap:24px;margin:24px 0 32px}.step{display:flex;flex-direction:column;align-items:center;gap:8px}.step .step-indicator{width:32px;height:32px;border-radius:50%;background:#f8f9fa;border:2px solid #e9ecef;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.step .step-indicator i{font-size:12px;color:#fff}.step .step-label{font-size:11px;color:#6c757d;text-transform:uppercase;letter-spacing:.5px;font-weight:500}.step.active .step-indicator{background:rgba(29,164,177,.1);border-color:#1da4b1;animation:pulse-active 1.5s infinite}.step.active .step-label{color:#1da4b1;font-weight:600}.step.completed .step-indicator{background:#1da4b1;border-color:#1da4b1}.step.completed .step-label{color:#1da4b1;font-weight:600}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.create-btn{background:#0077b3;color:#fff;box-shadow:0 2px 8px #0077b340}.action-btn.create-btn:hover{background:#005c8a;box-shadow:0 4px 12px #0077b359}.action-btn.create-btn:active{transform:translateY(1px)}.action-btn.join-btn{background:#1da4b1;color:#fff;box-shadow:0 2px 8px #1da4b140;animation:pulse-ready 2s infinite}.action-btn.join-btn:hover{background:#17848e;box-shadow:0 4px 12px #1da4b159}.action-btn.join-btn:active{transform:translateY(1px)}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.waiting-room-footer{padding:16px 40px 24px;background:#ffffff;border-top:1px solid #e9ecef;display:flex;justify-content:center}.waiting-room-footer .cancel-btn{padding:10px 24px;font-size:14px;font-weight:600;border-radius:4px;background:transparent;color:#6c757d;border:none;cursor:pointer;transition:all .2s ease}.waiting-room-footer .cancel-btn:hover{background:#f8f9fa;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse-active{0%,to{box-shadow:0 0 #1da4b166}50%{box-shadow:0 0 0 8px #1da4b100}}@keyframes pulse-ready{0%,to{box-shadow:0 2px 8px #1da4b140}50%{box-shadow:0 4px 16px #1da4b166}}@media (max-width: 576px){.tas-waiting-room{min-height:380px}.waiting-room-header{padding:24px 24px 20px}.waiting-room-header .header-icon{width:56px;height:56px}.waiting-room-header .header-icon i{font-size:22px}.waiting-room-header .header-title{font-size:18px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.progress-steps{gap:12px}.step .step-indicator{width:28px;height:28px}.step .step-label{font-size:9px}.action-btn{padding:10px 24px;font-size:14px}.waiting-room-footer{padding:16px 24px 20px}}\n"] }]
|
|
1525
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1208
1526
|
type: Input
|
|
1209
1527
|
}], entityId: [{
|
|
1210
1528
|
type: Input
|
|
@@ -1217,18 +1535,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1217
1535
|
}] } });
|
|
1218
1536
|
|
|
1219
1537
|
class TasButtonComponent {
|
|
1220
|
-
constructor(modalService, tasService) {
|
|
1538
|
+
constructor(modalService, tasService, tasUtilityService) {
|
|
1221
1539
|
this.modalService = modalService;
|
|
1222
1540
|
this.tasService = tasService;
|
|
1541
|
+
this.tasUtilityService = tasUtilityService;
|
|
1223
1542
|
// Status endpoint params
|
|
1224
1543
|
this.roomType = TasRoomType.TAS;
|
|
1225
1544
|
this.businessRole = TasBusinessRole.USER;
|
|
1545
|
+
// Style customization
|
|
1546
|
+
this.variant = 'default';
|
|
1547
|
+
this.buttonLabel = 'Iniciar TAS';
|
|
1226
1548
|
this.isLoading = false;
|
|
1227
|
-
this.buttonText = 'Iniciar TAS';
|
|
1228
1549
|
// Status check state
|
|
1229
1550
|
this.isCheckingStatus = false;
|
|
1230
1551
|
this.isStatusError = false;
|
|
1231
1552
|
this.statusErrorMessage = '';
|
|
1553
|
+
this.isJoinable = false; // Tracks joinable field from status response
|
|
1554
|
+
this.shouldShowButton = true; // Controls button visibility
|
|
1232
1555
|
this.subscriptions = new Subscription();
|
|
1233
1556
|
this.currentModalRef = null;
|
|
1234
1557
|
this.videoCallModalRef = null;
|
|
@@ -1241,18 +1564,22 @@ class TasButtonComponent {
|
|
|
1241
1564
|
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
1242
1565
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
1243
1566
|
}
|
|
1244
|
-
/** Whether the button should be visible */
|
|
1245
|
-
get isVisible() {
|
|
1246
|
-
// Backoffice: always show (disabled if error)
|
|
1247
|
-
// Other roles: hide if status error
|
|
1248
|
-
if (this.isBackoffice) {
|
|
1249
|
-
return true;
|
|
1250
|
-
}
|
|
1251
|
-
return !this.isStatusError;
|
|
1252
|
-
}
|
|
1253
1567
|
/** Whether the button should be disabled */
|
|
1254
1568
|
get isDisabled() {
|
|
1255
|
-
return this.isLoading || this.
|
|
1569
|
+
return this.isLoading || this.isStatusError || !this.isJoinable;
|
|
1570
|
+
}
|
|
1571
|
+
/** Reason why the button is disabled (for tooltip) */
|
|
1572
|
+
get disabledReason() {
|
|
1573
|
+
if (this.isLoading) {
|
|
1574
|
+
return '';
|
|
1575
|
+
}
|
|
1576
|
+
if (this.isStatusError) {
|
|
1577
|
+
return this.statusErrorMessage || 'Error al verificar el estado';
|
|
1578
|
+
}
|
|
1579
|
+
if (!this.isJoinable) {
|
|
1580
|
+
return 'Todavía no es el horario de la llamada';
|
|
1581
|
+
}
|
|
1582
|
+
return '';
|
|
1256
1583
|
}
|
|
1257
1584
|
ngOnInit() {
|
|
1258
1585
|
// Subscribe to viewMode to handle PiP return
|
|
@@ -1295,71 +1622,51 @@ class TasButtonComponent {
|
|
|
1295
1622
|
checkStatus() {
|
|
1296
1623
|
// Skip if required inputs are not available
|
|
1297
1624
|
if (!this.tenant || !this.entityId) {
|
|
1298
|
-
console.log('[TAS DEBUG] checkStatus skipped - missing required inputs');
|
|
1299
1625
|
return;
|
|
1300
1626
|
}
|
|
1301
1627
|
this.isCheckingStatus = true;
|
|
1302
1628
|
this.statusErrorMessage = '';
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
entityId: this.entityId,
|
|
1306
|
-
tenant: this.tenant,
|
|
1307
|
-
businessRole: this.businessRole,
|
|
1308
|
-
});
|
|
1309
|
-
this.subscriptions.add(this.tasService.getProxyVideoStatus({
|
|
1629
|
+
this.subscriptions.add(this.tasService
|
|
1630
|
+
.getProxyVideoStatus({
|
|
1310
1631
|
roomType: this.roomType,
|
|
1311
1632
|
entityId: this.entityId,
|
|
1312
1633
|
tenant: this.tenant,
|
|
1313
1634
|
businessRole: this.businessRole,
|
|
1314
|
-
})
|
|
1635
|
+
})
|
|
1636
|
+
.subscribe({
|
|
1315
1637
|
next: (response) => {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
response?.error ||
|
|
1323
|
-
response?.name === 'HttpErrorResponse';
|
|
1324
|
-
if (isErrorResponse) {
|
|
1325
|
-
console.error('[TAS DEBUG] Status check returned error in response:', response);
|
|
1326
|
-
this.isCheckingStatus = false;
|
|
1327
|
-
this.isStatusError = true;
|
|
1328
|
-
this.statusErrorMessage = response?.error?.message || response?.message || 'Error checking status';
|
|
1329
|
-
}
|
|
1330
|
-
else {
|
|
1331
|
-
console.log('[TAS DEBUG] Status check successful:', response);
|
|
1332
|
-
this.isCheckingStatus = false;
|
|
1333
|
-
this.isStatusError = false;
|
|
1334
|
-
this.statusErrorMessage = '';
|
|
1335
|
-
}
|
|
1638
|
+
this.isCheckingStatus = false;
|
|
1639
|
+
this.isStatusError = false;
|
|
1640
|
+
this.statusErrorMessage = '';
|
|
1641
|
+
this.shouldShowButton = true;
|
|
1642
|
+
// Update joinable state from response
|
|
1643
|
+
this.isJoinable = response.content?.joinable ?? false;
|
|
1336
1644
|
},
|
|
1337
1645
|
error: (err) => {
|
|
1338
|
-
console.error('[TAS DEBUG] Status check failed:', err);
|
|
1339
1646
|
this.isCheckingStatus = false;
|
|
1340
|
-
this.
|
|
1341
|
-
this.statusErrorMessage =
|
|
1647
|
+
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1648
|
+
this.statusErrorMessage = errorMessage;
|
|
1649
|
+
// Use utility service to determine if button should be shown
|
|
1650
|
+
this.shouldShowButton = this.tasUtilityService.shouldShowButton(errorMessage);
|
|
1651
|
+
// Stop polling on error
|
|
1652
|
+
this.stopStatusPolling();
|
|
1653
|
+
// If button should be hidden, don't treat as error
|
|
1654
|
+
if (!this.shouldShowButton) {
|
|
1655
|
+
this.isStatusError = false;
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
this.isStatusError = true;
|
|
1659
|
+
}
|
|
1342
1660
|
},
|
|
1343
1661
|
}));
|
|
1344
1662
|
}
|
|
1345
1663
|
onClick() {
|
|
1346
|
-
console.log('[TAS DEBUG] onClick called');
|
|
1347
|
-
console.log('[TAS DEBUG] Inputs:', {
|
|
1348
|
-
tenant: this.tenant,
|
|
1349
|
-
entityId: this.entityId,
|
|
1350
|
-
roomType: this.roomType,
|
|
1351
|
-
businessRole: this.businessRole,
|
|
1352
|
-
currentUser: this.currentUser,
|
|
1353
|
-
});
|
|
1354
1664
|
if (!this.tenant || !this.currentUser?.name) {
|
|
1355
|
-
console.error('[TAS DEBUG] Tenant or current user not available');
|
|
1356
1665
|
return;
|
|
1357
1666
|
}
|
|
1358
1667
|
if (!this.entityId) {
|
|
1359
|
-
console.error('[TAS DEBUG] entityId is required');
|
|
1360
1668
|
return;
|
|
1361
1669
|
}
|
|
1362
|
-
console.log('[TAS DEBUG] Validation passed, opening waiting room modal');
|
|
1363
1670
|
this.openWaitingRoomModal();
|
|
1364
1671
|
}
|
|
1365
1672
|
openWaitingRoomModal() {
|
|
@@ -1400,12 +1707,12 @@ class TasButtonComponent {
|
|
|
1400
1707
|
});
|
|
1401
1708
|
}
|
|
1402
1709
|
}
|
|
1403
|
-
TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, deps: [{ token: i1.NgbModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1404
|
-
TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasButtonComponent, selector: "tas-btn", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<
|
|
1710
|
+
TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, deps: [{ token: i1.NgbModal }, { token: TasService }, { token: TasUtilityService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1711
|
+
TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasButtonComponent, selector: "tas-btn", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser", variant: "variant", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span\n *ngIf=\"shouldShowButton\"\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "triggers", "container", "disableTooltip", "tooltipClass", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }] });
|
|
1405
1712
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
|
|
1406
1713
|
type: Component,
|
|
1407
|
-
args: [{ selector: 'tas-btn', template: "<
|
|
1408
|
-
}], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }]; }, propDecorators: { roomType: [{
|
|
1714
|
+
args: [{ selector: 'tas-btn', template: "<span\n *ngIf=\"shouldShowButton\"\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"] }]
|
|
1715
|
+
}], ctorParameters: function () { return [{ type: i1.NgbModal }, { type: TasService }, { type: TasUtilityService }]; }, propDecorators: { roomType: [{
|
|
1409
1716
|
type: Input
|
|
1410
1717
|
}], entityId: [{
|
|
1411
1718
|
type: Input
|
|
@@ -1415,6 +1722,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1415
1722
|
type: Input
|
|
1416
1723
|
}], currentUser: [{
|
|
1417
1724
|
type: Input
|
|
1725
|
+
}], variant: [{
|
|
1726
|
+
type: Input
|
|
1727
|
+
}], buttonLabel: [{
|
|
1728
|
+
type: Input
|
|
1418
1729
|
}] } });
|
|
1419
1730
|
|
|
1420
1731
|
class TasFloatingCallComponent {
|
|
@@ -1563,6 +1874,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1563
1874
|
args: [{ selector: 'tas-floating-call', template: "<div class=\"tas-floating-container\" [class.visible]=\"isVisible\">\n <!-- Video content area - shows main video only -->\n <div class=\"floating-content\">\n <!-- Main video container (subscriber if available, otherwise publisher) -->\n <div id=\"pip-main-video\" class=\"pip-main-video\"></div>\n\n <!-- Bottom controls -->\n <div class=\"floating-controls\">\n <button\n class=\"action-btn expand-btn\"\n (click)=\"onExpand()\"\n title=\"Expand to fullscreen\"\n >\n <i class=\"fa fa-expand\"></i>\n </button>\n <button\n class=\"action-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Unmute microphone' : 'Mute microphone'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button class=\"action-btn hangup-btn\" (click)=\"onHangUp()\" title=\"Hang up call\">\n <i class=\"fa fa-phone\" style=\"transform: rotate(135deg)\"></i>\n </button>\n </div>\n </div>\n</div>\n", styles: [".tas-floating-container{position:fixed;bottom:20px;right:20px;width:280px;height:180px;background:#000;border-radius:12px;box-shadow:0 8px 32px #00000080;z-index:9999;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none;transition:opacity .3s ease,visibility .3s ease,box-shadow .2s ease;opacity:0;visibility:hidden;pointer-events:none}.tas-floating-container.visible{opacity:1;visibility:visible;pointer-events:auto}.tas-floating-container:hover{box-shadow:0 8px 32px #00000080,0 0 0 2px #ffffff4d}.floating-content{position:relative;width:100%;height:100%;overflow:hidden}.pip-main-video{position:absolute;top:0;left:0;width:100%;height:100%;background:#000}.pip-main-video ::ng-deep video{width:100%;height:100%;object-fit:cover}.pip-main-video ::ng-deep .OT_subscriber,.pip-main-video ::ng-deep .OT_publisher{width:100%!important;height:100%!important}.pip-main-video ::ng-deep .OT_edge-bar-item,.pip-main-video ::ng-deep .OT_mute,.pip-main-video ::ng-deep .OT_audio-level-meter,.pip-main-video ::ng-deep .OT_bar,.pip-main-video ::ng-deep .OT_name{display:none!important}.floating-controls{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:12px;padding:6px 14px;background:rgba(0,0,0,.7);border-radius:24px;backdrop-filter:blur(8px);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}.tas-floating-container:hover .floating-controls{opacity:1;visibility:visible}.action-btn{width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s ease}.action-btn.expand-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.expand-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn{background:rgba(255,255,255,.2);color:#fff}.action-btn.mute-btn:hover{background:rgba(255,255,255,.35);transform:scale(1.1)}.action-btn.mute-btn.muted{background:#f39c12;color:#fff}.action-btn.mute-btn.muted:hover{background:#e67e22}.action-btn.hangup-btn{background:#dc3545;color:#fff}.action-btn.hangup-btn:hover{background:#c82333;transform:scale(1.1)}\n"] }]
|
|
1564
1875
|
}], ctorParameters: function () { return [{ type: TasService }, { type: i1.NgbModal }]; } });
|
|
1565
1876
|
|
|
1877
|
+
class TasIncomingAppointmentComponent {
|
|
1878
|
+
constructor(tasService) {
|
|
1879
|
+
this.tasService = tasService;
|
|
1880
|
+
// Passthrough inputs for tas-btn
|
|
1881
|
+
this.roomType = TasRoomType.TAS;
|
|
1882
|
+
this.businessRole = TasBusinessRole.USER;
|
|
1883
|
+
this.enterCall = new EventEmitter();
|
|
1884
|
+
this.appointment = null;
|
|
1885
|
+
this.isLoading = true;
|
|
1886
|
+
this.hasError = false;
|
|
1887
|
+
this.subscriptions = new Subscription();
|
|
1888
|
+
}
|
|
1889
|
+
ngOnInit() {
|
|
1890
|
+
this.loadAppointments();
|
|
1891
|
+
}
|
|
1892
|
+
ngOnDestroy() {
|
|
1893
|
+
this.subscriptions.unsubscribe();
|
|
1894
|
+
}
|
|
1895
|
+
loadAppointments() {
|
|
1896
|
+
const today = new Date();
|
|
1897
|
+
const in7Days = new Date(today);
|
|
1898
|
+
in7Days.setDate(today.getDate() + 7);
|
|
1899
|
+
this.subscriptions.add(this.tasService
|
|
1900
|
+
.getAppointments({ fromDate: this.formatDate(today), toDate: this.formatDate(in7Days), entityId: this.entityId })
|
|
1901
|
+
.subscribe({
|
|
1902
|
+
next: (response) => {
|
|
1903
|
+
// Handle both array response and wrapped response (e.g., { content: [...] })
|
|
1904
|
+
const appointments = Array.isArray(response)
|
|
1905
|
+
? response
|
|
1906
|
+
: response?.content || [];
|
|
1907
|
+
// Filter for confirmed appointments and get the first one
|
|
1908
|
+
const confirmed = appointments.filter((a) => a.status === AppointmentStatus.CONFIRMED);
|
|
1909
|
+
this.appointment = confirmed.length > 0 ? confirmed[0] : null;
|
|
1910
|
+
this.isLoading = false;
|
|
1911
|
+
},
|
|
1912
|
+
error: () => {
|
|
1913
|
+
this.hasError = true;
|
|
1914
|
+
this.isLoading = false;
|
|
1915
|
+
},
|
|
1916
|
+
}));
|
|
1917
|
+
}
|
|
1918
|
+
onEnterCall() {
|
|
1919
|
+
if (this.appointment) {
|
|
1920
|
+
this.enterCall.emit(this.appointment);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Format date to Spanish format: "Lunes 8 de diciembre"
|
|
1925
|
+
*/
|
|
1926
|
+
get formattedDate() {
|
|
1927
|
+
if (!this.appointment)
|
|
1928
|
+
return '';
|
|
1929
|
+
const [year, month, day] = this.appointment.date.split('-').map(Number);
|
|
1930
|
+
const date = new Date(year, month - 1, day);
|
|
1931
|
+
const dayNames = [
|
|
1932
|
+
'Domingo', 'Lunes', 'Martes', 'Miércoles',
|
|
1933
|
+
'Jueves', 'Viernes', 'Sábado'
|
|
1934
|
+
];
|
|
1935
|
+
const monthNames = [
|
|
1936
|
+
'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
|
|
1937
|
+
'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
|
|
1938
|
+
];
|
|
1939
|
+
const dayName = dayNames[date.getDay()];
|
|
1940
|
+
const dayNum = date.getDate();
|
|
1941
|
+
const monthName = monthNames[date.getMonth()];
|
|
1942
|
+
return `${dayName} ${dayNum} de ${monthName}`;
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Format time range: "9:00 - 9:30"
|
|
1946
|
+
*/
|
|
1947
|
+
get formattedTimeRange() {
|
|
1948
|
+
if (!this.appointment)
|
|
1949
|
+
return '';
|
|
1950
|
+
return `${this.appointment.startTime} - ${this.appointment.endTime}`;
|
|
1951
|
+
}
|
|
1952
|
+
formatDate(date) {
|
|
1953
|
+
const year = date.getFullYear();
|
|
1954
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1955
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
1956
|
+
return `${year}-${month}-${day}`;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1960
|
+
TasIncomingAppointmentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasIncomingAppointmentComponent, selector: "tas-incoming-appointment", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, outputs: { enterCall: "enterCall" }, ngImport: i0, template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && !appointment\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointment state -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointment\">\n \n <div class=\"appointment-card\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"appointment.title\" [size]=\"48\"></tas-avatar>\n \n <span class=\"doctor-name\">{{ appointment.title }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formattedDate }}</span>\n <span class=\"time\">{{ formattedTimeRange }}</span>\n </div>\n <tas-btn\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appointment.roomType\"\n [entityId]=\"appointment.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }, { type: TasButtonComponent, selector: "tas-btn", inputs: ["roomType", "entityId", "tenant", "businessRole", "currentUser", "variant", "buttonLabel"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1961
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
1962
|
+
type: Component,
|
|
1963
|
+
args: [{ selector: 'tas-incoming-appointment', template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && !appointment\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointment state -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointment\">\n \n <div class=\"appointment-card\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"appointment.title\" [size]=\"48\"></tas-avatar>\n \n <span class=\"doctor-name\">{{ appointment.title }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formattedDate }}</span>\n <span class=\"time\">{{ formattedTimeRange }}</span>\n </div>\n <tas-btn\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appointment.roomType\"\n [entityId]=\"appointment.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"] }]
|
|
1964
|
+
}], ctorParameters: function () { return [{ type: TasService }]; }, propDecorators: { roomType: [{
|
|
1965
|
+
type: Input
|
|
1966
|
+
}], entityId: [{
|
|
1967
|
+
type: Input
|
|
1968
|
+
}], tenant: [{
|
|
1969
|
+
type: Input
|
|
1970
|
+
}], businessRole: [{
|
|
1971
|
+
type: Input
|
|
1972
|
+
}], currentUser: [{
|
|
1973
|
+
type: Input
|
|
1974
|
+
}], enterCall: [{
|
|
1975
|
+
type: Output
|
|
1976
|
+
}] } });
|
|
1977
|
+
|
|
1566
1978
|
class TasUellSdkModule {
|
|
1567
1979
|
/**
|
|
1568
1980
|
* Use forRoot() to configure the TAS SDK module with required dependencies.
|
|
@@ -1609,12 +2021,14 @@ TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", versio
|
|
|
1609
2021
|
TasVideocallComponent,
|
|
1610
2022
|
TasFloatingCallComponent,
|
|
1611
2023
|
TasWaitingRoomComponent,
|
|
1612
|
-
TasAvatarComponent
|
|
2024
|
+
TasAvatarComponent,
|
|
2025
|
+
TasIncomingAppointmentComponent], imports: [CommonModule, FormsModule, NgbTooltipModule], exports: [TasButtonComponent,
|
|
1613
2026
|
TasVideocallComponent,
|
|
1614
2027
|
TasFloatingCallComponent,
|
|
1615
2028
|
TasWaitingRoomComponent,
|
|
1616
|
-
TasAvatarComponent
|
|
1617
|
-
|
|
2029
|
+
TasAvatarComponent,
|
|
2030
|
+
TasIncomingAppointmentComponent] });
|
|
2031
|
+
TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, imports: [[CommonModule, FormsModule, NgbTooltipModule]] });
|
|
1618
2032
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, decorators: [{
|
|
1619
2033
|
type: NgModule,
|
|
1620
2034
|
args: [{
|
|
@@ -1624,14 +2038,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1624
2038
|
TasFloatingCallComponent,
|
|
1625
2039
|
TasWaitingRoomComponent,
|
|
1626
2040
|
TasAvatarComponent,
|
|
2041
|
+
TasIncomingAppointmentComponent,
|
|
1627
2042
|
],
|
|
1628
|
-
imports: [CommonModule, FormsModule],
|
|
2043
|
+
imports: [CommonModule, FormsModule, NgbTooltipModule],
|
|
1629
2044
|
exports: [
|
|
1630
2045
|
TasButtonComponent,
|
|
1631
2046
|
TasVideocallComponent,
|
|
1632
2047
|
TasFloatingCallComponent,
|
|
1633
2048
|
TasWaitingRoomComponent,
|
|
1634
2049
|
TasAvatarComponent,
|
|
2050
|
+
TasIncomingAppointmentComponent,
|
|
1635
2051
|
],
|
|
1636
2052
|
}]
|
|
1637
2053
|
}] });
|
|
@@ -1644,5 +2060,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1644
2060
|
* Generated bundle index. Do not edit.
|
|
1645
2061
|
*/
|
|
1646
2062
|
|
|
1647
|
-
export { CallState, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2063
|
+
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 };
|
|
1648
2064
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|