tas-uell-sdk 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +17 -15
- package/esm2020/lib/components/tas-feedback-modal/tas-feedback-modal.component.mjs +229 -0
- package/esm2020/lib/components/tas-floating-call/tas-floating-call.component.mjs +38 -3
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +46 -8
- package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +163 -43
- package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +150 -34
- package/esm2020/lib/icons/tas-icons.mjs +17 -0
- package/esm2020/lib/interfaces/tas.interfaces.mjs +13 -1
- package/esm2020/lib/services/tas.service.mjs +127 -26
- package/esm2020/lib/tas-uell-sdk.module.mjs +8 -3
- package/esm2020/public-api.mjs +2 -1
- package/fesm2015/tas-uell-sdk.mjs +794 -124
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +787 -123
- package/fesm2020/tas-uell-sdk.mjs.map +1 -1
- package/lib/components/tas-btn/tas-btn.component.d.ts +1 -0
- package/lib/components/tas-feedback-modal/tas-feedback-modal.component.d.ts +101 -0
- package/lib/components/tas-floating-call/tas-floating-call.component.d.ts +5 -0
- package/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.d.ts +12 -1
- package/lib/components/tas-videocall/tas-videocall.component.d.ts +49 -6
- package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +40 -12
- package/lib/icons/tas-icons.d.ts +8 -0
- package/lib/interfaces/tas.interfaces.d.ts +36 -3
- package/lib/services/tas.service.d.ts +27 -2
- package/lib/tas-uell-sdk.module.d.ts +5 -4
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
- package/src/lib/styles/tas-global.scss +17 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Inject, Component,
|
|
3
|
-
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
4
|
-
import { map, catchError } from 'rxjs/operators';
|
|
2
|
+
import { InjectionToken, Injectable, Inject, Component, Input, ChangeDetectionStrategy, ViewChild, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
|
4
|
+
import { map, catchError, finalize, shareReplay } from 'rxjs/operators';
|
|
5
5
|
import * as OT from '@opentok/client';
|
|
6
|
-
import interact from 'interactjs';
|
|
7
6
|
import * as i1 from '@ng-bootstrap/ng-bootstrap';
|
|
8
7
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
9
8
|
import * as i4 from '@angular/common';
|
|
10
9
|
import { CommonModule } from '@angular/common';
|
|
10
|
+
import * as i4$1 from '@angular/forms';
|
|
11
11
|
import { FormsModule } from '@angular/forms';
|
|
12
|
+
import interact from 'interactjs';
|
|
13
|
+
import * as i4$2 from '@angular/platform-browser';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Injection token for TAS configuration
|
|
@@ -66,7 +68,14 @@ var UserCallAction;
|
|
|
66
68
|
UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
|
|
67
69
|
UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
|
|
68
70
|
UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
|
|
71
|
+
UserCallAction["DENY_GEOLOCATION"] = "DENY_GEOLOCATION";
|
|
69
72
|
})(UserCallAction || (UserCallAction = {}));
|
|
73
|
+
var GeoStatus;
|
|
74
|
+
(function (GeoStatus) {
|
|
75
|
+
GeoStatus["PENDING"] = "PENDING";
|
|
76
|
+
GeoStatus["GRANTED"] = "GRANTED";
|
|
77
|
+
GeoStatus["DENIED"] = "DENIED";
|
|
78
|
+
})(GeoStatus || (GeoStatus = {}));
|
|
70
79
|
var RoomUserStatus;
|
|
71
80
|
(function (RoomUserStatus) {
|
|
72
81
|
RoomUserStatus["ASSIGNED"] = "ASSIGNED";
|
|
@@ -87,6 +96,11 @@ var ViewMode;
|
|
|
87
96
|
ViewMode["FULLSCREEN"] = "FULLSCREEN";
|
|
88
97
|
ViewMode["PIP"] = "PIP";
|
|
89
98
|
})(ViewMode || (ViewMode = {}));
|
|
99
|
+
var FeedbackMotiveType;
|
|
100
|
+
(function (FeedbackMotiveType) {
|
|
101
|
+
FeedbackMotiveType["TECHNICAL"] = "TECHNICAL";
|
|
102
|
+
FeedbackMotiveType["BUSINESS"] = "BUSINESS";
|
|
103
|
+
})(FeedbackMotiveType || (FeedbackMotiveType = {}));
|
|
90
104
|
// Appointment types
|
|
91
105
|
var AppointmentStatus;
|
|
92
106
|
(function (AppointmentStatus) {
|
|
@@ -197,6 +211,9 @@ class TasService {
|
|
|
197
211
|
// Observable for when all geo has been granted
|
|
198
212
|
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
199
213
|
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
214
|
+
// Observable for individual user geo status (for owner panel)
|
|
215
|
+
this.userGeoInfoSubject = new BehaviorSubject([]);
|
|
216
|
+
this.userGeoInfo$ = this.userGeoInfoSubject.asObservable();
|
|
200
217
|
this.statusPollingInterval = null;
|
|
201
218
|
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
202
219
|
this.wasOwnerPresent = false;
|
|
@@ -204,6 +221,10 @@ class TasService {
|
|
|
204
221
|
this.currentAppointmentId = null;
|
|
205
222
|
this.currentVideoCallId = null;
|
|
206
223
|
this.currentTenant = null;
|
|
224
|
+
// Status cache (1 second TTL)
|
|
225
|
+
this.STATUS_CACHE_TTL_MS = 1000;
|
|
226
|
+
this.statusCache = null;
|
|
227
|
+
this.inflightStatusRequests = new Map();
|
|
207
228
|
}
|
|
208
229
|
// ... (Getters and other methods remain unchanged)
|
|
209
230
|
/**
|
|
@@ -222,7 +243,6 @@ class TasService {
|
|
|
222
243
|
if (params.tenant) {
|
|
223
244
|
this.currentTenant = params.tenant;
|
|
224
245
|
}
|
|
225
|
-
console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
|
|
226
246
|
// Initial status fetch
|
|
227
247
|
this.fetchAndProcessStatus(params);
|
|
228
248
|
// Set up periodic polling
|
|
@@ -325,7 +345,6 @@ class TasService {
|
|
|
325
345
|
}
|
|
326
346
|
// Session Management
|
|
327
347
|
disconnectSession(clearStorage = true) {
|
|
328
|
-
console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
|
|
329
348
|
// Call finishSession before disconnecting if we have a sessionId
|
|
330
349
|
const sessionIdToFinish = this.currentSessionId;
|
|
331
350
|
// Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
|
|
@@ -356,11 +375,9 @@ class TasService {
|
|
|
356
375
|
businessRole: this.currentBusinessRole,
|
|
357
376
|
}).subscribe({
|
|
358
377
|
next: (response) => {
|
|
359
|
-
console.log('[TAS DEBUG] Session finished successfully:', response);
|
|
360
378
|
this.isFinishingSession = false;
|
|
361
379
|
},
|
|
362
380
|
error: (error) => {
|
|
363
|
-
console.error('[TAS DEBUG] Error finishing session:', error);
|
|
364
381
|
this.isFinishingSession = false;
|
|
365
382
|
},
|
|
366
383
|
});
|
|
@@ -395,7 +412,6 @@ class TasService {
|
|
|
395
412
|
// If so, don't restore it on page reload
|
|
396
413
|
const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
|
|
397
414
|
if (wasDisconnected === 'true') {
|
|
398
|
-
console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
|
|
399
415
|
this.clearSessionState();
|
|
400
416
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
401
417
|
return;
|
|
@@ -403,14 +419,12 @@ class TasService {
|
|
|
403
419
|
// Don't restore if we're already disconnected or if there's no active session
|
|
404
420
|
// This prevents restoring sessions that were properly disconnected
|
|
405
421
|
if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
|
|
406
|
-
console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
|
|
407
422
|
this.clearSessionState();
|
|
408
423
|
return;
|
|
409
424
|
}
|
|
410
425
|
try {
|
|
411
426
|
const state = JSON.parse(savedState);
|
|
412
427
|
if (state.sessionId && state.token) {
|
|
413
|
-
console.log('[TAS DEBUG] Restoring session from storage');
|
|
414
428
|
// Force PiP mode for restoration to ensure UI consistency
|
|
415
429
|
this.viewModeSubject.next(ViewMode.PIP);
|
|
416
430
|
if (state.businessRole) {
|
|
@@ -419,18 +433,15 @@ class TasService {
|
|
|
419
433
|
this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
|
|
420
434
|
containerId, this.currentBusinessRole)
|
|
421
435
|
.then(() => {
|
|
422
|
-
console.log('[TAS DEBUG] Session restored successfully');
|
|
423
436
|
// Clear the disconnected flag if restoration succeeds
|
|
424
437
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
425
438
|
})
|
|
426
439
|
.catch((err) => {
|
|
427
|
-
console.error('[TAS DEBUG] Failed to restore session:', err);
|
|
428
440
|
this.clearSessionState(); // Clear bad state
|
|
429
441
|
});
|
|
430
442
|
}
|
|
431
443
|
}
|
|
432
444
|
catch (e) {
|
|
433
|
-
console.error('[TAS DEBUG] Error parsing saved session state', e);
|
|
434
445
|
this.clearSessionState();
|
|
435
446
|
}
|
|
436
447
|
}
|
|
@@ -449,19 +460,77 @@ class TasService {
|
|
|
449
460
|
throw error;
|
|
450
461
|
}));
|
|
451
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Generate cache key from request payload
|
|
465
|
+
*/
|
|
466
|
+
getStatusCacheKey(payload) {
|
|
467
|
+
return `${payload.roomType}-${payload.entityId}-${payload.tenant}-${payload.sessionId || ''}`;
|
|
468
|
+
}
|
|
452
469
|
/**
|
|
453
470
|
* PROXY circuit status: /v2/proxy/video/status
|
|
471
|
+
* Uses a 1-second cache to avoid redundant API calls from multiple components
|
|
472
|
+
* Implements request deduplication for simultaneous calls
|
|
454
473
|
*/
|
|
455
474
|
getProxyVideoStatus(payload) {
|
|
456
|
-
|
|
475
|
+
const cacheKey = this.getStatusCacheKey(payload);
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
// 1. Check if we have a valid cached result (less than 1 second old)
|
|
478
|
+
if (this.statusCache &&
|
|
479
|
+
this.statusCache.key === cacheKey &&
|
|
480
|
+
(now - this.statusCache.timestamp) < this.STATUS_CACHE_TTL_MS) {
|
|
481
|
+
// Return cached error if present
|
|
482
|
+
if (this.statusCache.error) {
|
|
483
|
+
return new Observable(observer => {
|
|
484
|
+
observer.error(this.statusCache.error);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
// Return cached response
|
|
488
|
+
if (this.statusCache.response) {
|
|
489
|
+
return new Observable(observer => {
|
|
490
|
+
observer.next(this.statusCache.response);
|
|
491
|
+
observer.complete();
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// 2. Check if there is already an inflight request for this key
|
|
496
|
+
if (this.inflightStatusRequests.has(cacheKey)) {
|
|
497
|
+
return this.inflightStatusRequests.get(cacheKey);
|
|
498
|
+
}
|
|
499
|
+
// 3. Make the API call, share it, and cache the Observable
|
|
500
|
+
const request$ = this.httpClient
|
|
457
501
|
.post('v2/proxy/video/status', {
|
|
458
502
|
body: payload,
|
|
459
503
|
headers: {},
|
|
460
504
|
})
|
|
461
|
-
.pipe(map((response) =>
|
|
505
|
+
.pipe(map((response) => {
|
|
506
|
+
const typedResponse = response;
|
|
507
|
+
// Cache successful response
|
|
508
|
+
this.statusCache = {
|
|
509
|
+
key: cacheKey,
|
|
510
|
+
response: typedResponse,
|
|
511
|
+
error: null,
|
|
512
|
+
timestamp: Date.now(),
|
|
513
|
+
};
|
|
514
|
+
return typedResponse;
|
|
515
|
+
}), catchError((error) => {
|
|
462
516
|
console.error('TAS Service: getProxyVideoStatus failed', error);
|
|
517
|
+
// Cache error response
|
|
518
|
+
this.statusCache = {
|
|
519
|
+
key: cacheKey,
|
|
520
|
+
response: null,
|
|
521
|
+
error: error,
|
|
522
|
+
timestamp: Date.now(),
|
|
523
|
+
};
|
|
463
524
|
throw error;
|
|
464
|
-
})
|
|
525
|
+
}),
|
|
526
|
+
// Clean up inflight map when the observable completes or errors
|
|
527
|
+
finalize(() => {
|
|
528
|
+
this.inflightStatusRequests.delete(cacheKey);
|
|
529
|
+
}),
|
|
530
|
+
// Share the result with all subscribers (deduplication)
|
|
531
|
+
shareReplay(1));
|
|
532
|
+
this.inflightStatusRequests.set(cacheKey, request$);
|
|
533
|
+
return request$;
|
|
465
534
|
}
|
|
466
535
|
/**
|
|
467
536
|
* PROXY circuit user modification: /v2/proxy/video/user/modify
|
|
@@ -515,6 +584,42 @@ class TasService {
|
|
|
515
584
|
throw error;
|
|
516
585
|
}));
|
|
517
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Save video call feedback.
|
|
589
|
+
* POST /v2/proxy/video/save/feedback
|
|
590
|
+
* @param payload Feedback data including videoCallId, motiveId, motiveType, observation, rating (1-5), and tenant
|
|
591
|
+
* @returns Observable that completes on success (HTTP 200 OK, no response body)
|
|
592
|
+
*/
|
|
593
|
+
saveFeedback(payload) {
|
|
594
|
+
// Validate rating is between 1 and 5
|
|
595
|
+
if (payload.rating < 1 || payload.rating > 5) {
|
|
596
|
+
return new Observable(observer => {
|
|
597
|
+
observer.error(new Error('Rating must be between 1 and 5'));
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return this.httpClient
|
|
601
|
+
.post('v2/proxy/video/save/feedback', {
|
|
602
|
+
body: payload,
|
|
603
|
+
headers: {},
|
|
604
|
+
})
|
|
605
|
+
.pipe(catchError((error) => {
|
|
606
|
+
console.error('TAS Service: saveFeedback failed', error);
|
|
607
|
+
throw error;
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Get available feedback motives.
|
|
612
|
+
* GET /v2/proxy/video/motives
|
|
613
|
+
* @returns Observable of feedback motives array
|
|
614
|
+
*/
|
|
615
|
+
getMotives() {
|
|
616
|
+
return this.httpClient
|
|
617
|
+
.get('v2/proxy/video/motives', { headers: {} })
|
|
618
|
+
.pipe(catchError((error) => {
|
|
619
|
+
console.error('TAS Service: getMotives failed', error);
|
|
620
|
+
throw error;
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
518
623
|
/**
|
|
519
624
|
* Start automatic status polling for the current session.
|
|
520
625
|
* Status is polled every STATUS_POLL_INTERVAL_MS milliseconds.
|
|
@@ -531,7 +636,6 @@ class TasService {
|
|
|
531
636
|
this.processStatusResponse(response);
|
|
532
637
|
},
|
|
533
638
|
error: (err) => {
|
|
534
|
-
console.error('[TAS DEBUG] Status polling error:', err);
|
|
535
639
|
},
|
|
536
640
|
});
|
|
537
641
|
}
|
|
@@ -553,17 +657,18 @@ class TasService {
|
|
|
553
657
|
this.joinableSubject.next(content.joinable);
|
|
554
658
|
// Update activateGeo status
|
|
555
659
|
if (content.activateGeo !== undefined) {
|
|
556
|
-
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
557
660
|
this.activateGeoSubject.next(content.activateGeo);
|
|
558
661
|
}
|
|
559
662
|
// Update geoRequestActive status (owner waiting for user geo response)
|
|
560
|
-
if (content.geoRequestActive !== undefined) {
|
|
561
|
-
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
663
|
+
if (content.geoRequestActive !== undefined && content.geoRequestActive !== null) {
|
|
562
664
|
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
563
665
|
}
|
|
666
|
+
else if (content.geoRequestActive === null) {
|
|
667
|
+
// Reset to false when null
|
|
668
|
+
this.geoRequestActiveSubject.next(false);
|
|
669
|
+
}
|
|
564
670
|
// Update allGeoGranted status (all users responded with geo)
|
|
565
671
|
if (content.allGeoGranted !== undefined) {
|
|
566
|
-
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
567
672
|
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
568
673
|
}
|
|
569
674
|
// Check if owner has joined
|
|
@@ -571,7 +676,6 @@ class TasService {
|
|
|
571
676
|
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
572
677
|
// Detect if owner left: was present, now not present
|
|
573
678
|
if (this.wasOwnerPresent && !ownerJoined) {
|
|
574
|
-
console.log('[TAS DEBUG] Owner has left the session');
|
|
575
679
|
this.ownerHasLeftSubject.next(true);
|
|
576
680
|
}
|
|
577
681
|
if (ownerJoined) {
|
|
@@ -586,6 +690,16 @@ class TasService {
|
|
|
586
690
|
status: RoomUserStatus.WAITING,
|
|
587
691
|
}));
|
|
588
692
|
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
693
|
+
// Extract geo info for USER role participants (for owner panel)
|
|
694
|
+
const userGeoInfo = content.users
|
|
695
|
+
.filter((u) => u.rol === 'USER')
|
|
696
|
+
.map((u) => ({
|
|
697
|
+
userId: u.userId,
|
|
698
|
+
geoStatus: u.geoStatus,
|
|
699
|
+
latitude: u.latitude,
|
|
700
|
+
longitude: u.longitude,
|
|
701
|
+
}));
|
|
702
|
+
this.userGeoInfoSubject.next(userGeoInfo);
|
|
589
703
|
}
|
|
590
704
|
/**
|
|
591
705
|
* Admit a user from the waiting room by changing their status.
|
|
@@ -604,6 +718,9 @@ class TasService {
|
|
|
604
718
|
get videoCallId() {
|
|
605
719
|
return this.currentVideoCallId;
|
|
606
720
|
}
|
|
721
|
+
get tenant() {
|
|
722
|
+
return this.currentTenant;
|
|
723
|
+
}
|
|
607
724
|
/**
|
|
608
725
|
* Connects to a TokBox video session
|
|
609
726
|
*/
|
|
@@ -656,11 +773,9 @@ class TasService {
|
|
|
656
773
|
businessRole: this.currentBusinessRole,
|
|
657
774
|
}).subscribe({
|
|
658
775
|
next: (response) => {
|
|
659
|
-
console.log('[TAS DEBUG] Session finished on disconnect event:', response);
|
|
660
776
|
this.isFinishingSession = false;
|
|
661
777
|
},
|
|
662
778
|
error: (error) => {
|
|
663
|
-
console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
|
|
664
779
|
this.isFinishingSession = false;
|
|
665
780
|
},
|
|
666
781
|
});
|
|
@@ -818,6 +933,244 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
818
933
|
}]
|
|
819
934
|
}] });
|
|
820
935
|
|
|
936
|
+
class TasFeedbackModalComponent {
|
|
937
|
+
constructor(activeModal, tasService) {
|
|
938
|
+
this.activeModal = activeModal;
|
|
939
|
+
this.tasService = tasService;
|
|
940
|
+
this.businessRole = TasBusinessRole.USER;
|
|
941
|
+
this.motives = [];
|
|
942
|
+
this.filteredMotives = [];
|
|
943
|
+
this.selectedMotive = null;
|
|
944
|
+
this.rating = 0;
|
|
945
|
+
this.hoverRating = 0;
|
|
946
|
+
this.observation = '';
|
|
947
|
+
this.isSubmitting = false;
|
|
948
|
+
this.showToast = false;
|
|
949
|
+
this.isDropdownOpen = false;
|
|
950
|
+
this.subscriptions = new Subscription();
|
|
951
|
+
this.toastTimeout = null;
|
|
952
|
+
}
|
|
953
|
+
ngOnInit() {
|
|
954
|
+
this.loadMotives();
|
|
955
|
+
}
|
|
956
|
+
ngOnDestroy() {
|
|
957
|
+
this.subscriptions.unsubscribe();
|
|
958
|
+
if (this.toastTimeout) {
|
|
959
|
+
clearTimeout(this.toastTimeout);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Check if current user can see all motives (BUSINESS + TECHNICAL)
|
|
964
|
+
* Only BACKOFFICE, ADMIN_MANAGER, MANAGER roles see BUSINESS motives
|
|
965
|
+
*/
|
|
966
|
+
get canSeeBusinessMotives() {
|
|
967
|
+
return (this.businessRole === TasBusinessRole.BACKOFFICE ||
|
|
968
|
+
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
969
|
+
this.businessRole === TasBusinessRole.MANAGER);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Observation is required when "Otro problema tecnico" is selected
|
|
973
|
+
*/
|
|
974
|
+
get isObservationRequired() {
|
|
975
|
+
return this.selectedMotive?.description === 'Otro problema tecnico';
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Can submit if rating > 0 OR motive is selected
|
|
979
|
+
* If observation is required, it must not be empty
|
|
980
|
+
*/
|
|
981
|
+
get canSubmit() {
|
|
982
|
+
const hasRatingOrMotive = this.rating > 0 || this.selectedMotive !== null;
|
|
983
|
+
if (!hasRatingOrMotive)
|
|
984
|
+
return false;
|
|
985
|
+
if (this.isObservationRequired && !this.observation.trim())
|
|
986
|
+
return false;
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Progress percentage for the divider line
|
|
991
|
+
* 0 = nothing selected, 50 = one selected, 100 = both selected
|
|
992
|
+
*/
|
|
993
|
+
get feedbackProgress() {
|
|
994
|
+
const hasRating = this.rating > 0;
|
|
995
|
+
const hasMotive = this.selectedMotive !== null;
|
|
996
|
+
if (hasRating && hasMotive)
|
|
997
|
+
return 100;
|
|
998
|
+
if (hasRating || hasMotive)
|
|
999
|
+
return 50;
|
|
1000
|
+
return 0;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Set star rating
|
|
1004
|
+
*/
|
|
1005
|
+
setRating(value) {
|
|
1006
|
+
// Toggle off if clicking the same rating
|
|
1007
|
+
if (this.rating === value) {
|
|
1008
|
+
this.rating = 0;
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
this.rating = value;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Set hover preview rating
|
|
1016
|
+
*/
|
|
1017
|
+
setHoverRating(value) {
|
|
1018
|
+
this.hoverRating = value;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Clear hover preview
|
|
1022
|
+
*/
|
|
1023
|
+
clearHoverRating() {
|
|
1024
|
+
this.hoverRating = 0;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get the display rating (hover preview or actual)
|
|
1028
|
+
*/
|
|
1029
|
+
getDisplayRating() {
|
|
1030
|
+
return this.hoverRating > 0 ? this.hoverRating : this.rating;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Toggle dropdown open/close
|
|
1034
|
+
*/
|
|
1035
|
+
toggleDropdown() {
|
|
1036
|
+
this.isDropdownOpen = !this.isDropdownOpen;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Close dropdown
|
|
1040
|
+
*/
|
|
1041
|
+
closeDropdown() {
|
|
1042
|
+
this.isDropdownOpen = false;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Select a motive from dropdown
|
|
1046
|
+
*/
|
|
1047
|
+
selectMotive(motive) {
|
|
1048
|
+
this.selectedMotive = motive;
|
|
1049
|
+
this.closeDropdown();
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Clear selected motive
|
|
1053
|
+
*/
|
|
1054
|
+
clearMotive() {
|
|
1055
|
+
this.selectedMotive = null;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Submit feedback
|
|
1059
|
+
*/
|
|
1060
|
+
submit() {
|
|
1061
|
+
if (!this.canSubmit || this.isSubmitting)
|
|
1062
|
+
return;
|
|
1063
|
+
this.isSubmitting = true;
|
|
1064
|
+
const payload = {
|
|
1065
|
+
videoCallId: this.videoCallId,
|
|
1066
|
+
motiveId: this.selectedMotive?.id ?? 0,
|
|
1067
|
+
observation: this.observation.trim(),
|
|
1068
|
+
rating: this.rating || 1,
|
|
1069
|
+
tenant: this.tenant,
|
|
1070
|
+
};
|
|
1071
|
+
if (this.selectedMotive) {
|
|
1072
|
+
payload.motiveType = this.selectedMotive.motiveType;
|
|
1073
|
+
}
|
|
1074
|
+
this.subscriptions.add(this.tasService.saveFeedback(payload).subscribe({
|
|
1075
|
+
next: () => {
|
|
1076
|
+
this.isSubmitting = false;
|
|
1077
|
+
this.showToastNotification();
|
|
1078
|
+
},
|
|
1079
|
+
error: (err) => {
|
|
1080
|
+
console.error('Error saving feedback:', err);
|
|
1081
|
+
this.isSubmitting = false;
|
|
1082
|
+
// Still close modal on error, just don't show toast
|
|
1083
|
+
this.activeModal.close('submitted');
|
|
1084
|
+
},
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Close modal without saving
|
|
1089
|
+
*/
|
|
1090
|
+
dismiss() {
|
|
1091
|
+
this.activeModal.dismiss('dismissed');
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Load motives from API
|
|
1095
|
+
*/
|
|
1096
|
+
loadMotives() {
|
|
1097
|
+
this.subscriptions.add(this.tasService.getMotives().subscribe({
|
|
1098
|
+
next: (motives) => {
|
|
1099
|
+
this.motives = motives;
|
|
1100
|
+
this.filterMotives();
|
|
1101
|
+
},
|
|
1102
|
+
error: (err) => {
|
|
1103
|
+
console.error('Error loading motives:', err);
|
|
1104
|
+
// Fallback mock motives for development/testing
|
|
1105
|
+
this.motives = [
|
|
1106
|
+
{ id: 1, description: 'Problemas de audio', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1107
|
+
{ id: 2, description: 'Problemas de video', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1108
|
+
{ id: 3, description: 'Conexión inestable', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1109
|
+
{ id: 4, description: 'Otro problema tecnico', motiveType: FeedbackMotiveType.TECHNICAL },
|
|
1110
|
+
{ id: 5, description: 'Problema de negocio', motiveType: FeedbackMotiveType.BUSINESS },
|
|
1111
|
+
];
|
|
1112
|
+
this.filterMotives();
|
|
1113
|
+
},
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Filter motives based on user role
|
|
1118
|
+
* USERs see only TECHNICAL motives
|
|
1119
|
+
* Owners (BACKOFFICE, ADMIN_MANAGER, MANAGER) see all motives
|
|
1120
|
+
*/
|
|
1121
|
+
filterMotives() {
|
|
1122
|
+
if (this.canSeeBusinessMotives) {
|
|
1123
|
+
this.filteredMotives = this.motives;
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
this.filteredMotives = this.motives.filter((m) => !m.motiveType || m.motiveType === FeedbackMotiveType.TECHNICAL);
|
|
1127
|
+
}
|
|
1128
|
+
// If still empty after filtering, and we have motives, just show all as fallback
|
|
1129
|
+
if (this.filteredMotives.length === 0 && this.motives.length > 0) {
|
|
1130
|
+
this.filteredMotives = this.motives;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Show toast notification and auto-dismiss after 3s
|
|
1135
|
+
*/
|
|
1136
|
+
showToastNotification() {
|
|
1137
|
+
this.showToast = true;
|
|
1138
|
+
this.toastTimeout = setTimeout(() => {
|
|
1139
|
+
this.showToast = false;
|
|
1140
|
+
this.activeModal.close('submitted');
|
|
1141
|
+
}, 3000);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
TasFeedbackModalComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFeedbackModalComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1145
|
+
TasFeedbackModalComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasFeedbackModalComponent, selector: "tas-feedback-modal", inputs: { videoCallId: "videoCallId", tenant: "tenant", businessRole: "businessRole" }, ngImport: i0, template: "<div class=\"tas-feedback-modal\">\n <!-- Header with subtitle -->\n <div class=\"feedback-header\">\n <div class=\"header-text\">\n <span class=\"subtitle\">Cierre de la consulta</span>\n <h2>Calidad de la videollamada</h2>\n </div>\n <button\n type=\"button\"\n class=\"close-btn\"\n (click)=\"dismiss()\"\n aria-label=\"Cerrar\"\n >\n <i class=\"fa fa-times\"></i>\n </button>\n </div>\n\n <!-- Progress Divider -->\n <div class=\"progress-divider\">\n <div class=\"progress-fill\" [style.width.%]=\"feedbackProgress\"></div>\n </div>\n <div class=\"accent-line\"></div>\n\n <div class=\"feedback-content\">\n <!-- Star Rating with question -->\n <div class=\"rating-section\">\n <p class=\"rating-question\">\u00BFC\u00F3mo fue la calidad de la llamada?</p>\n <div\n class=\"stars-container\"\n (mouseleave)=\"clearHoverRating()\"\n >\n <button\n *ngFor=\"let star of [1, 2, 3, 4, 5]\"\n type=\"button\"\n class=\"star-btn\"\n [class.filled]=\"star <= getDisplayRating()\"\n (click)=\"setRating(star)\"\n (mouseenter)=\"setHoverRating(star)\"\n [attr.aria-label]=\"'Calificar ' + star + ' estrellas'\"\n >\n <i class=\"fa\" [class.fa-star]=\"star <= getDisplayRating()\" [class.fa-star-o]=\"star > getDisplayRating()\"></i>\n </button>\n </div>\n </div>\n\n <!-- Motive Dropdown -->\n <div class=\"motive-section\">\n <label class=\"motive-label\">Motivo (opcional)</label>\n <div class=\"custom-dropdown\" [class.open]=\"isDropdownOpen\">\n <button\n type=\"button\"\n class=\"dropdown-trigger\"\n (click)=\"toggleDropdown()\"\n [attr.aria-expanded]=\"isDropdownOpen\"\n aria-haspopup=\"listbox\"\n >\n <span class=\"dropdown-text\">\n {{ selectedMotive?.description || 'Seleccionar motivo' }}\n </span>\n <i class=\"fa fa-chevron-down dropdown-arrow\"></i>\n </button>\n <div\n class=\"dropdown-menu\"\n *ngIf=\"isDropdownOpen\"\n role=\"listbox\"\n >\n <button\n *ngFor=\"let motive of filteredMotives\"\n type=\"button\"\n class=\"dropdown-item\"\n [class.selected]=\"selectedMotive?.id === motive.id\"\n (click)=\"selectMotive(motive)\"\n role=\"option\"\n [attr.aria-selected]=\"selectedMotive?.id === motive.id\"\n >\n {{ motive.description }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Observation Textarea -->\n <div class=\"observation-section\">\n <label class=\"observation-label\" [class.required]=\"isObservationRequired\">\n {{ isObservationRequired ? 'Observacion (requerida)' : 'Observacion (opcional)' }}\n </label>\n <textarea\n class=\"observation-textarea\"\n [(ngModel)]=\"observation\"\n [placeholder]=\"isObservationRequired ? 'Por favor, describe el problema...' : 'Escribe tu comentario...'\"\n rows=\"3\"\n [attr.aria-required]=\"isObservationRequired\"\n ></textarea>\n </div>\n </div>\n\n <div class=\"feedback-footer\">\n <button\n type=\"button\"\n class=\"submit-btn\"\n [disabled]=\"!canSubmit || isSubmitting\"\n (click)=\"submit()\"\n >\n <span *ngIf=\"!isSubmitting\">Enviar</span>\n <span *ngIf=\"isSubmitting\">\n <i class=\"fa fa-spinner fa-spin\"></i>\n Enviando...\n </span>\n </button>\n </div>\n\n <!-- Toast Notification -->\n <div class=\"toast-notification\" *ngIf=\"showToast\" role=\"alert\" aria-live=\"polite\">\n <i class=\"fa fa-check-circle\"></i>\n <span>Gracias por tu feedback</span>\n </div>\n</div>\n\n<!-- Backdrop to close dropdown when clicking outside -->\n<div\n class=\"dropdown-backdrop\"\n *ngIf=\"isDropdownOpen\"\n (click)=\"closeDropdown()\"\n></div>\n", styles: [":host{display:block;width:100%}::ng-deep .tas-feedback-modal-wrapper .modal-content{overflow:visible!important;border:none;background:transparent}::ng-deep .tas-feedback-modal-wrapper .modal-dialog{max-width:450px}.tas-feedback-modal{width:100%;background:#fff;border-radius:16px;overflow:visible;position:relative;padding:.5rem;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.feedback-header{display:flex;justify-content:space-between;align-items:flex-start;padding:1.25rem 1.5rem 1rem;background:#fff;border-radius:16px 16px 0 0}.feedback-header .header-text{display:flex;flex-direction:column;gap:.25rem}.feedback-header .header-text .subtitle{font-size:.8125rem;color:#6b7280;font-weight:400}.feedback-header .header-text h2{margin:0;font-size:1.25rem;font-weight:600;color:#1f2937;letter-spacing:-.01em}.feedback-header .close-btn{background:transparent;border:none;padding:8px;cursor:pointer;color:#9ca3af;font-size:1.25rem;line-height:1;transition:all .2s ease;margin-top:-4px}.feedback-header .close-btn:hover{color:#4b5563}.feedback-header .close-btn:focus{outline:none;color:#4b5563}.progress-divider{height:4px;background:#e5e7eb;margin:0;position:relative;overflow:hidden}.progress-divider .progress-fill{height:100%;background:linear-gradient(90deg,var(--Primary-Uell, #1da4b1) 0%,#4fd1c5 100%);transition:width .4s cubic-bezier(.4,0,.2,1);border-radius:0 2px 2px 0}.feedback-content{padding:1.5rem;display:flex;flex-direction:column;gap:1.5rem}.rating-section{display:flex;flex-direction:column;align-items:flex-start;gap:1rem}.rating-section .rating-question{margin:0;font-size:.9375rem;color:#4b5563;font-weight:400;align-self:center}.rating-section .stars-container{display:flex;gap:8px;width:100%;justify-content:center}.rating-section .star-btn{background:transparent;border:none;padding:4px;cursor:pointer;font-size:2rem;color:#d1d5db;transition:all .15s ease;line-height:1}.rating-section .star-btn:hover{transform:scale(1.15)}.rating-section .star-btn.filled{color:#f5a623}.rating-section .star-btn.filled i.fa-star{text-shadow:0 2px 4px rgba(245,166,35,.3)}.rating-section .star-btn i.fa-star-o{color:#d1d5db}.rating-section .star-btn:focus{outline:none}.motive-section{display:flex;flex-direction:column;gap:.5rem;position:relative;overflow:visible}.motive-section .motive-label{font-size:.875rem;font-weight:500;color:#4b5563}.motive-section .custom-dropdown{position:relative;overflow:visible}.motive-section .custom-dropdown .dropdown-trigger{width:100%;display:flex;justify-content:space-between;align-items:center;padding:1rem;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;font-size:.9375rem;color:#4b5563;transition:all .2s ease;min-height:52px}.motive-section .custom-dropdown .dropdown-trigger:hover{border-color:#9ca3af}.motive-section .custom-dropdown .dropdown-trigger:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown .dropdown-trigger .dropdown-text{flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.motive-section .custom-dropdown .dropdown-trigger .dropdown-arrow{font-size:.875rem;color:#6b7280;transition:transform .2s ease;margin-left:.5rem}.motive-section .custom-dropdown.open .dropdown-trigger{border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown.open .dropdown-trigger .dropdown-arrow{transform:rotate(180deg)}.motive-section .custom-dropdown .dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;display:block;min-height:40px;min-width:100%;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 16px #0000001f;max-height:250px;overflow:hidden;overflow-y:auto;z-index:1050;visibility:visible;opacity:1;padding:0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item{width:100%;padding:1rem 1.25rem;background:transparent;border:none;border-bottom:1px dashed #e5e7eb;text-align:left;font-size:.9375rem;color:#4b5563;cursor:pointer;transition:all .15s ease;position:relative}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-bottom:none}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:hover{background:rgba(29,164,177,.05);color:#374151}.motive-section .custom-dropdown .dropdown-menu .dropdown-item.selected{background:rgba(29,164,177,.08);color:var(--Primary-Uell, #1da4b1);border-left:3px solid var(--Primary-Uell, #1da4b1);padding-left:calc(1.25rem - 3px)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:focus{outline:none;background:rgba(29,164,177,.05)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:first-child{border-radius:8px 8px 0 0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-radius:0 0 8px 8px}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:only-child{border-radius:8px}.motive-section .clear-motive-btn{position:absolute;right:40px;top:32px;background:#f3f4f6;border:none;padding:4px 6px;border-radius:4px;cursor:pointer;color:#6b7280;font-size:.625rem;transition:all .2s ease}.motive-section .clear-motive-btn:hover{background:#e5e7eb;color:#374151}.motive-section .clear-motive-btn:focus{outline:none}.observation-section{display:flex;flex-direction:column;gap:.5rem}.observation-section .observation-label{font-size:.875rem;font-weight:500;color:#4b5563}.observation-section .observation-label.required{color:#dc2626}.observation-section .observation-label.required:after{content:\" *\";font-weight:600}.observation-section .observation-textarea{width:100%;padding:.75rem 1rem;border:1px solid #d1d5db;border-radius:12px;font-size:.875rem;font-family:inherit;resize:vertical;min-height:80px;transition:all .2s ease;background:#fff}.observation-section .observation-textarea::placeholder{color:#9ca3af}.observation-section .observation-textarea:hover{border-color:#9ca3af}.observation-section .observation-textarea:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1);box-shadow:0 0 0 3px #1da4b126}.feedback-footer{padding:1rem 1.5rem 1.5rem;display:flex;justify-content:flex-end}.feedback-footer .submit-btn{padding:.75rem 2rem;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:.9375rem;font-weight:600;cursor:pointer;transition:all .2s ease;min-width:120px}.feedback-footer .submit-btn:hover:not(:disabled){background:#178e99}.feedback-footer .submit-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.feedback-footer .submit-btn:focus{outline:none;box-shadow:0 0 0 3px #1da4b126}.feedback-footer .submit-btn i.fa-spinner{margin-right:6px}.toast-notification{position:fixed;bottom:24px;left:24px;display:flex;align-items:center;gap:10px;padding:14px 20px;background:#383e52;color:#fff;border-radius:8px;box-shadow:0 4px 12px #00000040;font-size:.875rem;font-weight:500;z-index:1060;animation:slideIn .3s ease}.toast-notification i.fa-check-circle{color:#4ade80;font-size:1.125rem}@keyframes slideIn{0%{transform:translate(-100%);opacity:0}to{transform:translate(0);opacity:1}}.dropdown-backdrop{position:fixed;inset:0;z-index:99}\n"], directives: [{ type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i4$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1146
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasFeedbackModalComponent, decorators: [{
|
|
1147
|
+
type: Component,
|
|
1148
|
+
args: [{ selector: 'tas-feedback-modal', template: "<div class=\"tas-feedback-modal\">\n <!-- Header with subtitle -->\n <div class=\"feedback-header\">\n <div class=\"header-text\">\n <span class=\"subtitle\">Cierre de la consulta</span>\n <h2>Calidad de la videollamada</h2>\n </div>\n <button\n type=\"button\"\n class=\"close-btn\"\n (click)=\"dismiss()\"\n aria-label=\"Cerrar\"\n >\n <i class=\"fa fa-times\"></i>\n </button>\n </div>\n\n <!-- Progress Divider -->\n <div class=\"progress-divider\">\n <div class=\"progress-fill\" [style.width.%]=\"feedbackProgress\"></div>\n </div>\n <div class=\"accent-line\"></div>\n\n <div class=\"feedback-content\">\n <!-- Star Rating with question -->\n <div class=\"rating-section\">\n <p class=\"rating-question\">\u00BFC\u00F3mo fue la calidad de la llamada?</p>\n <div\n class=\"stars-container\"\n (mouseleave)=\"clearHoverRating()\"\n >\n <button\n *ngFor=\"let star of [1, 2, 3, 4, 5]\"\n type=\"button\"\n class=\"star-btn\"\n [class.filled]=\"star <= getDisplayRating()\"\n (click)=\"setRating(star)\"\n (mouseenter)=\"setHoverRating(star)\"\n [attr.aria-label]=\"'Calificar ' + star + ' estrellas'\"\n >\n <i class=\"fa\" [class.fa-star]=\"star <= getDisplayRating()\" [class.fa-star-o]=\"star > getDisplayRating()\"></i>\n </button>\n </div>\n </div>\n\n <!-- Motive Dropdown -->\n <div class=\"motive-section\">\n <label class=\"motive-label\">Motivo (opcional)</label>\n <div class=\"custom-dropdown\" [class.open]=\"isDropdownOpen\">\n <button\n type=\"button\"\n class=\"dropdown-trigger\"\n (click)=\"toggleDropdown()\"\n [attr.aria-expanded]=\"isDropdownOpen\"\n aria-haspopup=\"listbox\"\n >\n <span class=\"dropdown-text\">\n {{ selectedMotive?.description || 'Seleccionar motivo' }}\n </span>\n <i class=\"fa fa-chevron-down dropdown-arrow\"></i>\n </button>\n <div\n class=\"dropdown-menu\"\n *ngIf=\"isDropdownOpen\"\n role=\"listbox\"\n >\n <button\n *ngFor=\"let motive of filteredMotives\"\n type=\"button\"\n class=\"dropdown-item\"\n [class.selected]=\"selectedMotive?.id === motive.id\"\n (click)=\"selectMotive(motive)\"\n role=\"option\"\n [attr.aria-selected]=\"selectedMotive?.id === motive.id\"\n >\n {{ motive.description }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Observation Textarea -->\n <div class=\"observation-section\">\n <label class=\"observation-label\" [class.required]=\"isObservationRequired\">\n {{ isObservationRequired ? 'Observacion (requerida)' : 'Observacion (opcional)' }}\n </label>\n <textarea\n class=\"observation-textarea\"\n [(ngModel)]=\"observation\"\n [placeholder]=\"isObservationRequired ? 'Por favor, describe el problema...' : 'Escribe tu comentario...'\"\n rows=\"3\"\n [attr.aria-required]=\"isObservationRequired\"\n ></textarea>\n </div>\n </div>\n\n <div class=\"feedback-footer\">\n <button\n type=\"button\"\n class=\"submit-btn\"\n [disabled]=\"!canSubmit || isSubmitting\"\n (click)=\"submit()\"\n >\n <span *ngIf=\"!isSubmitting\">Enviar</span>\n <span *ngIf=\"isSubmitting\">\n <i class=\"fa fa-spinner fa-spin\"></i>\n Enviando...\n </span>\n </button>\n </div>\n\n <!-- Toast Notification -->\n <div class=\"toast-notification\" *ngIf=\"showToast\" role=\"alert\" aria-live=\"polite\">\n <i class=\"fa fa-check-circle\"></i>\n <span>Gracias por tu feedback</span>\n </div>\n</div>\n\n<!-- Backdrop to close dropdown when clicking outside -->\n<div\n class=\"dropdown-backdrop\"\n *ngIf=\"isDropdownOpen\"\n (click)=\"closeDropdown()\"\n></div>\n", styles: [":host{display:block;width:100%}::ng-deep .tas-feedback-modal-wrapper .modal-content{overflow:visible!important;border:none;background:transparent}::ng-deep .tas-feedback-modal-wrapper .modal-dialog{max-width:450px}.tas-feedback-modal{width:100%;background:#fff;border-radius:16px;overflow:visible;position:relative;padding:.5rem;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.feedback-header{display:flex;justify-content:space-between;align-items:flex-start;padding:1.25rem 1.5rem 1rem;background:#fff;border-radius:16px 16px 0 0}.feedback-header .header-text{display:flex;flex-direction:column;gap:.25rem}.feedback-header .header-text .subtitle{font-size:.8125rem;color:#6b7280;font-weight:400}.feedback-header .header-text h2{margin:0;font-size:1.25rem;font-weight:600;color:#1f2937;letter-spacing:-.01em}.feedback-header .close-btn{background:transparent;border:none;padding:8px;cursor:pointer;color:#9ca3af;font-size:1.25rem;line-height:1;transition:all .2s ease;margin-top:-4px}.feedback-header .close-btn:hover{color:#4b5563}.feedback-header .close-btn:focus{outline:none;color:#4b5563}.progress-divider{height:4px;background:#e5e7eb;margin:0;position:relative;overflow:hidden}.progress-divider .progress-fill{height:100%;background:linear-gradient(90deg,var(--Primary-Uell, #1da4b1) 0%,#4fd1c5 100%);transition:width .4s cubic-bezier(.4,0,.2,1);border-radius:0 2px 2px 0}.feedback-content{padding:1.5rem;display:flex;flex-direction:column;gap:1.5rem}.rating-section{display:flex;flex-direction:column;align-items:flex-start;gap:1rem}.rating-section .rating-question{margin:0;font-size:.9375rem;color:#4b5563;font-weight:400;align-self:center}.rating-section .stars-container{display:flex;gap:8px;width:100%;justify-content:center}.rating-section .star-btn{background:transparent;border:none;padding:4px;cursor:pointer;font-size:2rem;color:#d1d5db;transition:all .15s ease;line-height:1}.rating-section .star-btn:hover{transform:scale(1.15)}.rating-section .star-btn.filled{color:#f5a623}.rating-section .star-btn.filled i.fa-star{text-shadow:0 2px 4px rgba(245,166,35,.3)}.rating-section .star-btn i.fa-star-o{color:#d1d5db}.rating-section .star-btn:focus{outline:none}.motive-section{display:flex;flex-direction:column;gap:.5rem;position:relative;overflow:visible}.motive-section .motive-label{font-size:.875rem;font-weight:500;color:#4b5563}.motive-section .custom-dropdown{position:relative;overflow:visible}.motive-section .custom-dropdown .dropdown-trigger{width:100%;display:flex;justify-content:space-between;align-items:center;padding:1rem;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;font-size:.9375rem;color:#4b5563;transition:all .2s ease;min-height:52px}.motive-section .custom-dropdown .dropdown-trigger:hover{border-color:#9ca3af}.motive-section .custom-dropdown .dropdown-trigger:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown .dropdown-trigger .dropdown-text{flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.motive-section .custom-dropdown .dropdown-trigger .dropdown-arrow{font-size:.875rem;color:#6b7280;transition:transform .2s ease;margin-left:.5rem}.motive-section .custom-dropdown.open .dropdown-trigger{border-color:var(--Primary-Uell, #1da4b1)}.motive-section .custom-dropdown.open .dropdown-trigger .dropdown-arrow{transform:rotate(180deg)}.motive-section .custom-dropdown .dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;display:block;min-height:40px;min-width:100%;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 16px #0000001f;max-height:250px;overflow:hidden;overflow-y:auto;z-index:1050;visibility:visible;opacity:1;padding:0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item{width:100%;padding:1rem 1.25rem;background:transparent;border:none;border-bottom:1px dashed #e5e7eb;text-align:left;font-size:.9375rem;color:#4b5563;cursor:pointer;transition:all .15s ease;position:relative}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-bottom:none}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:hover{background:rgba(29,164,177,.05);color:#374151}.motive-section .custom-dropdown .dropdown-menu .dropdown-item.selected{background:rgba(29,164,177,.08);color:var(--Primary-Uell, #1da4b1);border-left:3px solid var(--Primary-Uell, #1da4b1);padding-left:calc(1.25rem - 3px)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:focus{outline:none;background:rgba(29,164,177,.05)}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:first-child{border-radius:8px 8px 0 0}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:last-child{border-radius:0 0 8px 8px}.motive-section .custom-dropdown .dropdown-menu .dropdown-item:only-child{border-radius:8px}.motive-section .clear-motive-btn{position:absolute;right:40px;top:32px;background:#f3f4f6;border:none;padding:4px 6px;border-radius:4px;cursor:pointer;color:#6b7280;font-size:.625rem;transition:all .2s ease}.motive-section .clear-motive-btn:hover{background:#e5e7eb;color:#374151}.motive-section .clear-motive-btn:focus{outline:none}.observation-section{display:flex;flex-direction:column;gap:.5rem}.observation-section .observation-label{font-size:.875rem;font-weight:500;color:#4b5563}.observation-section .observation-label.required{color:#dc2626}.observation-section .observation-label.required:after{content:\" *\";font-weight:600}.observation-section .observation-textarea{width:100%;padding:.75rem 1rem;border:1px solid #d1d5db;border-radius:12px;font-size:.875rem;font-family:inherit;resize:vertical;min-height:80px;transition:all .2s ease;background:#fff}.observation-section .observation-textarea::placeholder{color:#9ca3af}.observation-section .observation-textarea:hover{border-color:#9ca3af}.observation-section .observation-textarea:focus{outline:none;border-color:var(--Primary-Uell, #1da4b1);box-shadow:0 0 0 3px #1da4b126}.feedback-footer{padding:1rem 1.5rem 1.5rem;display:flex;justify-content:flex-end}.feedback-footer .submit-btn{padding:.75rem 2rem;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:.9375rem;font-weight:600;cursor:pointer;transition:all .2s ease;min-width:120px}.feedback-footer .submit-btn:hover:not(:disabled){background:#178e99}.feedback-footer .submit-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.feedback-footer .submit-btn:focus{outline:none;box-shadow:0 0 0 3px #1da4b126}.feedback-footer .submit-btn i.fa-spinner{margin-right:6px}.toast-notification{position:fixed;bottom:24px;left:24px;display:flex;align-items:center;gap:10px;padding:14px 20px;background:#383e52;color:#fff;border-radius:8px;box-shadow:0 4px 12px #00000040;font-size:.875rem;font-weight:500;z-index:1060;animation:slideIn .3s ease}.toast-notification i.fa-check-circle{color:#4ade80;font-size:1.125rem}@keyframes slideIn{0%{transform:translate(-100%);opacity:0}to{transform:translate(0);opacity:1}}.dropdown-backdrop{position:fixed;inset:0;z-index:99}\n"] }]
|
|
1149
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }]; }, propDecorators: { videoCallId: [{
|
|
1150
|
+
type: Input
|
|
1151
|
+
}], tenant: [{
|
|
1152
|
+
type: Input
|
|
1153
|
+
}], businessRole: [{
|
|
1154
|
+
type: Input
|
|
1155
|
+
}] } });
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* SVG icons used internally by TAS SDK components.
|
|
1159
|
+
* Icons are stored as strings to avoid asset bundling complexity.
|
|
1160
|
+
*/
|
|
1161
|
+
const TAS_ICONS = {
|
|
1162
|
+
home: `<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1163
|
+
<rect x="10" width="100" height="100" rx="50" fill="#44D8E8" fill-opacity="0.2"/>
|
|
1164
|
+
<path d="M45 70C43.625 70 42.4479 69.5104 41.4688 68.5313C40.4896 67.5521 40 66.375 40 65V46.5625L37.5 48.5C36.9583 48.9167 36.3438 49.0833 35.6562 49C34.9688 48.9167 34.4167 48.5833 34 48C33.5833 47.4583 33.4271 46.8542 33.5312 46.1875C33.6354 45.5208 33.9583 44.9792 34.5 44.5625L56.9375 27.3125C57.3958 26.9792 57.8854 26.7292 58.4062 26.5625C58.9271 26.3958 59.4583 26.3125 60 26.3125C60.5417 26.3125 61.0729 26.3958 61.5938 26.5625C62.1146 26.7292 62.6042 26.9792 63.0625 27.3125L85.5 44.5C86.0417 44.9167 86.3646 45.4583 86.4688 46.125C86.5729 46.7917 86.4167 47.4167 86 48C85.5833 48.5833 85.0417 48.9063 84.375 48.9688C83.7083 49.0313 83.0833 48.8542 82.5 48.4375L60 31.25L45 42.75V65H50.0625C50.7708 65 51.3542 65.2396 51.8125 65.7188C52.2708 66.1979 52.5 66.7917 52.5 67.5C52.5 68.2083 52.2604 68.8021 51.7812 69.2813C51.3021 69.7604 50.7083 70 50 70H45ZM67.3125 73.9375C66.9792 73.9375 66.6667 73.875 66.375 73.75C66.0833 73.625 65.8125 73.4375 65.5625 73.1875L58.5 66.125C58 65.625 57.75 65.0417 57.75 64.375C57.75 63.7083 58 63.125 58.5 62.625C59 62.125 59.5833 61.875 60.25 61.875C60.9167 61.875 61.5 62.125 62 62.625L67.3125 67.875L79.6875 55.5C80.1875 55 80.7812 54.7604 81.4688 54.7813C82.1562 54.8021 82.75 55.0625 83.25 55.5625C83.75 56.0625 84 56.6458 84 57.3125C84 57.9792 83.75 58.5625 83.25 59.0625L69.0625 73.1875C68.8125 73.4375 68.5417 73.625 68.25 73.75C67.9583 73.875 67.6458 73.9375 67.3125 73.9375Z" fill="white"/>
|
|
1165
|
+
<path d="M110 5L106.575 3.425L105 0L103.425 3.425L100 5L103.425 6.575L105 10L106.575 6.575L110 5Z" fill="#44D8E8"/>
|
|
1166
|
+
<path d="M10 51L6.575 49.425L5 46L3.425 49.425L0 51L3.425 52.575L5 56L6.575 52.575L10 51Z" fill="#44D8E8"/>
|
|
1167
|
+
<path d="M95 43.5L93.2875 42.7125L92.5 41L91.7125 42.7125L90 43.5L91.7125 44.2875L92.5 46L93.2875 44.2875L95 43.5Z" fill="#44D8E8"/>
|
|
1168
|
+
<path d="M42 4.5L40.2875 3.7125L39.5 2L38.7125 3.7125L37 4.5L38.7125 5.2875L39.5 7L40.2875 5.2875L42 4.5Z" fill="#44D8E8"/>
|
|
1169
|
+
<path d="M88 83.5L86.2875 82.7125L85.5 81L84.7125 82.7125L83 83.5L84.7125 84.2875L85.5 86L86.2875 84.2875L88 83.5Z" fill="#44D8E8"/>
|
|
1170
|
+
<path d="M120 97.5L118.287 96.7125L117.5 95L116.713 96.7125L115 97.5L116.713 98.2875L117.5 100L118.287 98.2875L120 97.5Z" fill="#44D8E8"/>
|
|
1171
|
+
</svg>`,
|
|
1172
|
+
};
|
|
1173
|
+
|
|
821
1174
|
class TasAvatarComponent {
|
|
822
1175
|
constructor() {
|
|
823
1176
|
this.name = '';
|
|
@@ -891,11 +1244,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
891
1244
|
type: Input
|
|
892
1245
|
}] } });
|
|
893
1246
|
|
|
1247
|
+
/** User geolocation panel view states */
|
|
1248
|
+
var UserGeoViewState;
|
|
1249
|
+
(function (UserGeoViewState) {
|
|
1250
|
+
UserGeoViewState["HIDDEN"] = "HIDDEN";
|
|
1251
|
+
UserGeoViewState["INITIAL"] = "INITIAL";
|
|
1252
|
+
UserGeoViewState["VERIFYING"] = "VERIFYING";
|
|
1253
|
+
UserGeoViewState["VERIFIED"] = "VERIFIED";
|
|
1254
|
+
UserGeoViewState["DENIED"] = "DENIED";
|
|
1255
|
+
})(UserGeoViewState || (UserGeoViewState = {}));
|
|
894
1256
|
class TasVideocallComponent {
|
|
895
|
-
constructor(activeModal, tasService, geolocationService) {
|
|
1257
|
+
constructor(activeModal, tasService, geolocationService, sanitizer, modalService) {
|
|
896
1258
|
this.activeModal = activeModal;
|
|
897
1259
|
this.tasService = tasService;
|
|
898
1260
|
this.geolocationService = geolocationService;
|
|
1261
|
+
this.sanitizer = sanitizer;
|
|
1262
|
+
this.modalService = modalService;
|
|
899
1263
|
this.participantName = '';
|
|
900
1264
|
this.tenant = '';
|
|
901
1265
|
this.businessRole = TasBusinessRole.USER;
|
|
@@ -913,11 +1277,23 @@ class TasVideocallComponent {
|
|
|
913
1277
|
// Geo panel states for owner
|
|
914
1278
|
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
915
1279
|
this.allGeoGranted = false; // All users responded with geo
|
|
1280
|
+
this.userGeoInfo = []; // Individual user geo status
|
|
1281
|
+
// User geo panel state (for owners)
|
|
1282
|
+
this.userGeoViewState = UserGeoViewState.HIDDEN;
|
|
1283
|
+
this.UserGeoViewState = UserGeoViewState; // Expose enum to template
|
|
1284
|
+
this.devModeEnabled = false; // Enable dev controls for testing
|
|
1285
|
+
this.geoPanelDismissed = false; // Track if owner manually closed the panel
|
|
1286
|
+
this.feedbackShown = false; // Track if feedback modal has been shown
|
|
916
1287
|
this.subscriptions = new Subscription();
|
|
1288
|
+
this.homeIcon = this.sanitizer.bypassSecurityTrustHtml(TAS_ICONS.home);
|
|
917
1289
|
}
|
|
918
1290
|
ngOnInit() {
|
|
919
1291
|
this.setupSubscriptions();
|
|
920
1292
|
this.initializeCall();
|
|
1293
|
+
// For owners: show the geo panel to request user location (unless dismissed)
|
|
1294
|
+
if (this.canAdmitUsers && !this.geoPanelDismissed) {
|
|
1295
|
+
this.userGeoViewState = UserGeoViewState.INITIAL;
|
|
1296
|
+
}
|
|
921
1297
|
}
|
|
922
1298
|
ngAfterViewInit() {
|
|
923
1299
|
this.initInteract();
|
|
@@ -935,6 +1311,37 @@ class TasVideocallComponent {
|
|
|
935
1311
|
hangUp() {
|
|
936
1312
|
this.tasService.disconnectSession();
|
|
937
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Open feedback modal when call ends
|
|
1316
|
+
*/
|
|
1317
|
+
openFeedbackModal() {
|
|
1318
|
+
// Prevent opening feedback modal multiple times
|
|
1319
|
+
if (this.feedbackShown) {
|
|
1320
|
+
this.activeModal.close('hangup');
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
// Get videoCallId from input or service
|
|
1324
|
+
const videoCallId = this.videoCallId ?? this.tasService.videoCallId;
|
|
1325
|
+
// If no videoCallId, skip feedback and close directly
|
|
1326
|
+
if (!videoCallId) {
|
|
1327
|
+
this.activeModal.close('hangup');
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.feedbackShown = true;
|
|
1331
|
+
const modalRef = this.modalService.open(TasFeedbackModalComponent, {
|
|
1332
|
+
centered: true,
|
|
1333
|
+
backdrop: true,
|
|
1334
|
+
keyboard: true,
|
|
1335
|
+
windowClass: 'tas-feedback-modal-wrapper',
|
|
1336
|
+
});
|
|
1337
|
+
modalRef.componentInstance.videoCallId = videoCallId;
|
|
1338
|
+
modalRef.componentInstance.tenant = this.tenant;
|
|
1339
|
+
modalRef.componentInstance.businessRole = this.businessRole;
|
|
1340
|
+
// Close videocall modal after feedback modal is closed/dismissed
|
|
1341
|
+
modalRef.result.finally(() => {
|
|
1342
|
+
this.activeModal.close('hangup');
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
938
1345
|
toggleMute() {
|
|
939
1346
|
this.tasService.toggleMute();
|
|
940
1347
|
}
|
|
@@ -959,6 +1366,43 @@ class TasVideocallComponent {
|
|
|
959
1366
|
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
960
1367
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
961
1368
|
}
|
|
1369
|
+
/** Users with pending geo status */
|
|
1370
|
+
get pendingUsers() {
|
|
1371
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.PENDING);
|
|
1372
|
+
}
|
|
1373
|
+
/** Users who granted geo */
|
|
1374
|
+
get grantedUsers() {
|
|
1375
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1376
|
+
}
|
|
1377
|
+
/** Users who denied geo */
|
|
1378
|
+
get deniedUsers() {
|
|
1379
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.DENIED);
|
|
1380
|
+
}
|
|
1381
|
+
/** Show location panel only if owner and there are users who haven't granted */
|
|
1382
|
+
get shouldShowLocationPanel() {
|
|
1383
|
+
if (!this.canAdmitUsers || this.userGeoInfo.length === 0) {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
// Hide if all users have granted
|
|
1387
|
+
const allGranted = this.userGeoInfo.every(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1388
|
+
return !allGranted;
|
|
1389
|
+
}
|
|
1390
|
+
/** Check if any user has denied geo */
|
|
1391
|
+
get hasAnyDenied() {
|
|
1392
|
+
return this.deniedUsers.length > 0;
|
|
1393
|
+
}
|
|
1394
|
+
/** Show user geo panel for owners when not hidden */
|
|
1395
|
+
get shouldShowUserGeoPanel() {
|
|
1396
|
+
return this.canAdmitUsers && this.userGeoViewState !== UserGeoViewState.HIDDEN;
|
|
1397
|
+
}
|
|
1398
|
+
/** Set user geo view state (for dev controls or close button) */
|
|
1399
|
+
setUserGeoViewState(state) {
|
|
1400
|
+
this.userGeoViewState = state;
|
|
1401
|
+
// Track if owner manually dismissed the panel
|
|
1402
|
+
if (state === UserGeoViewState.HIDDEN) {
|
|
1403
|
+
this.geoPanelDismissed = true;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
962
1406
|
/**
|
|
963
1407
|
* Admit a user from the waiting room
|
|
964
1408
|
*/
|
|
@@ -997,7 +1441,7 @@ class TasVideocallComponent {
|
|
|
997
1441
|
this.showLocationPanel = false;
|
|
998
1442
|
}
|
|
999
1443
|
/**
|
|
1000
|
-
* Request the user to share their location
|
|
1444
|
+
* Request the user to share their location (called by owner)
|
|
1001
1445
|
*/
|
|
1002
1446
|
requestUserLocation() {
|
|
1003
1447
|
if (!this.videoCallId) {
|
|
@@ -1008,9 +1452,12 @@ class TasVideocallComponent {
|
|
|
1008
1452
|
videoCallId: this.videoCallId,
|
|
1009
1453
|
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
1010
1454
|
};
|
|
1011
|
-
// TODO: Send location request action to backend when endpoint is ready
|
|
1012
1455
|
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1013
|
-
next: () =>
|
|
1456
|
+
next: () => {
|
|
1457
|
+
console.log('Location request sent');
|
|
1458
|
+
// Set panel to verifying state while waiting for user response
|
|
1459
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1460
|
+
},
|
|
1014
1461
|
error: (err) => console.error('Error requesting location:', err),
|
|
1015
1462
|
});
|
|
1016
1463
|
}
|
|
@@ -1020,7 +1467,7 @@ class TasVideocallComponent {
|
|
|
1020
1467
|
this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
|
|
1021
1468
|
this.callState = state;
|
|
1022
1469
|
if (state === CallState.DISCONNECTED) {
|
|
1023
|
-
this.
|
|
1470
|
+
this.openFeedbackModal();
|
|
1024
1471
|
}
|
|
1025
1472
|
// Track if we have an active video stream
|
|
1026
1473
|
this.hasVideoStream = state === CallState.CONNECTED;
|
|
@@ -1047,69 +1494,82 @@ class TasVideocallComponent {
|
|
|
1047
1494
|
// Owner left subscription - disconnect non-owners
|
|
1048
1495
|
this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
|
|
1049
1496
|
if (hasLeft && !this.canAdmitUsers) { // Non-owner user
|
|
1050
|
-
|
|
1497
|
+
// hangUp() triggers DISCONNECTED state, which opens feedback modal
|
|
1498
|
+
// then closes videocall modal after feedback is done
|
|
1051
1499
|
this.hangUp();
|
|
1052
|
-
this.activeModal.close('owner_left');
|
|
1053
1500
|
}
|
|
1054
1501
|
}));
|
|
1055
1502
|
// ActivateGeo subscription - only for non-owners (users)
|
|
1056
1503
|
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
1057
1504
|
if (activateGeo && !this.canAdmitUsers) {
|
|
1058
|
-
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
1059
1505
|
this.handleActivateGeo();
|
|
1060
1506
|
}
|
|
1061
1507
|
}));
|
|
1062
1508
|
// GeoRequestActive subscription - for owners
|
|
1063
1509
|
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
1064
|
-
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
1065
1510
|
this.geoRequestActive = active;
|
|
1066
1511
|
}));
|
|
1067
|
-
// AllGeoGranted subscription - for owners
|
|
1512
|
+
// AllGeoGranted subscription - for owners to update panel state
|
|
1068
1513
|
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1069
|
-
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1070
1514
|
this.allGeoGranted = granted;
|
|
1515
|
+
// For owners: update panel state based on geo status
|
|
1516
|
+
if (this.canAdmitUsers && granted) {
|
|
1517
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1518
|
+
}
|
|
1519
|
+
}));
|
|
1520
|
+
// UserGeoInfo subscription - for owner geo panel
|
|
1521
|
+
this.subscriptions.add(this.tasService.userGeoInfo$.subscribe((info) => {
|
|
1522
|
+
this.userGeoInfo = info;
|
|
1523
|
+
// Note: We don't auto-switch to DENIED here - that only happens
|
|
1524
|
+
// after owner requests and user denies (via geoRequestActive flow)
|
|
1071
1525
|
}));
|
|
1072
1526
|
}
|
|
1073
1527
|
/**
|
|
1074
1528
|
* Handle activateGeo request from backend (for non-owner users).
|
|
1075
|
-
*
|
|
1529
|
+
* Directly prompts browser for geolocation - no panel for users.
|
|
1076
1530
|
*/
|
|
1077
1531
|
async handleActivateGeo() {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1532
|
+
// Clear any cached position to force re-prompting
|
|
1533
|
+
this.geolocationService.clearCache();
|
|
1534
|
+
// Request geolocation from user (browser will prompt)
|
|
1535
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1536
|
+
if (position) {
|
|
1083
1537
|
this.geoLocationStatus = 'active';
|
|
1084
|
-
this.reportGeoStatus(
|
|
1085
|
-
|
|
1538
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
this.geoLocationStatus = 'denied';
|
|
1542
|
+
this.denyGeoLocation();
|
|
1086
1543
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1544
|
+
}
|
|
1545
|
+
/** Start geolocation verification (called from template button) */
|
|
1546
|
+
async startGeoVerification() {
|
|
1547
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1548
|
+
// Clear any cached position to force re-prompting
|
|
1549
|
+
this.geolocationService.clearCache();
|
|
1550
|
+
// Request geolocation from user (browser will prompt)
|
|
1089
1551
|
const position = await this.geolocationService.getCurrentPosition();
|
|
1090
1552
|
if (position) {
|
|
1091
|
-
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1092
1553
|
this.geoLocationStatus = 'active';
|
|
1554
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1093
1555
|
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1094
1556
|
}
|
|
1095
1557
|
else {
|
|
1096
|
-
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1097
1558
|
this.geoLocationStatus = 'denied';
|
|
1098
|
-
|
|
1099
|
-
this.
|
|
1559
|
+
this.userGeoViewState = UserGeoViewState.DENIED;
|
|
1560
|
+
this.denyGeoLocation();
|
|
1100
1561
|
}
|
|
1101
1562
|
}
|
|
1102
1563
|
/**
|
|
1103
|
-
* Report geolocation
|
|
1564
|
+
* Report granted geolocation to backend.
|
|
1565
|
+
* IMPORTANT: Only call with valid coordinates.
|
|
1104
1566
|
*/
|
|
1105
1567
|
reportGeoStatus(latitude, longitude) {
|
|
1106
1568
|
if (!this.videoCallId) {
|
|
1107
|
-
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1108
1569
|
return;
|
|
1109
1570
|
}
|
|
1110
|
-
//
|
|
1111
|
-
if (
|
|
1112
|
-
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1571
|
+
// Validate coordinates are present
|
|
1572
|
+
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
|
|
1113
1573
|
return;
|
|
1114
1574
|
}
|
|
1115
1575
|
const body = {
|
|
@@ -1119,11 +1579,21 @@ class TasVideocallComponent {
|
|
|
1119
1579
|
latitude,
|
|
1120
1580
|
longitude,
|
|
1121
1581
|
};
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1582
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Report denied geolocation to backend.
|
|
1586
|
+
*/
|
|
1587
|
+
denyGeoLocation() {
|
|
1588
|
+
if (!this.videoCallId) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const body = {
|
|
1592
|
+
userId: this.userId,
|
|
1593
|
+
videoCallId: this.videoCallId,
|
|
1594
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1595
|
+
};
|
|
1596
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1127
1597
|
}
|
|
1128
1598
|
initializeCall() {
|
|
1129
1599
|
if (this.isReturningFromPip) {
|
|
@@ -1212,12 +1682,12 @@ class TasVideocallComponent {
|
|
|
1212
1682
|
});
|
|
1213
1683
|
}
|
|
1214
1684
|
}
|
|
1215
|
-
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1216
|
-
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", userId: "userId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:transparent;cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px)}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1685
|
+
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }, { token: i4$2.DomSanitizer }, { token: i1.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
|
|
1686
|
+
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", userId: "userId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Owner geolocation panel (for owners to request user location) -->\n <div class=\"user-geo-panel\" *ngIf=\"shouldShowUserGeoPanel || devModeEnabled\">\n <div class=\"user-geo-header\">\n <div class=\"header-title-row\">\n <i class=\"fa fa-map-marker header-icon\" *ngIf=\"userGeoViewState !== UserGeoViewState.INITIAL\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n </div>\n <button class=\"close-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n\n <div class=\"user-geo-description\" *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n\n <div class=\"user-geo-content\">\n <!-- INITIAL state: just the button -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.INITIAL\">\n <!-- spacer -->\n </ng-container>\n\n <!-- VERIFYING state: spinner + loading message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFYING\">\n <div class=\"user-geo-verifying\">\n <div class=\"geo-spinner-large\"></div>\n <p class=\"verifying-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"verifying-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n </ng-container>\n\n <!-- VERIFIED state: home icon with sparkles -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFIED\">\n <div class=\"user-geo-verified\">\n <div class=\"verified-icon-container\">\n <span class=\"home-icon\" [innerHTML]=\"homeIcon\"></span>\n </div>\n <p class=\"verified-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n\n <!-- DENIED state: error icon and message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.DENIED\">\n <div class=\"user-geo-denied\">\n <div class=\"denied-icon-container\">\n <i class=\"fa fa-times-circle\"></i>\n </div>\n <p class=\"denied-title\">La ubicaci\u00F3n fue rechazada</p>\n <p class=\"denied-subtitle\">El colaborador no permiti\u00F3 el acceso a su ubicaci\u00F3n.</p>\n </div>\n </ng-container>\n </div>\n\n <!-- Button (hidden in verified and denied states) -->\n <button \n class=\"user-geo-btn\" \n *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\"\n [class.disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n [disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n\n <!-- Dev controls -->\n <div class=\"dev-controls\" *ngIf=\"devModeEnabled\">\n <span class=\"dev-label\">Dev:</span>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.INITIAL)\">Initial</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFYING)\">Verifying</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFIED)\">Verified</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.DENIED)\">Denied</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\">Hide</button>\n </div>\n </div>\n</div>\n\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:transparent;cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px)}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-user-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:200px;overflow-y:auto}.location-panel .location-user-list .user-geo-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:rgba(255,255,255,.05);border-radius:6px}.location-panel .location-user-list .user-geo-item .user-icon{color:#fff9;font-size:14px}.location-panel .location-user-list .user-geo-item .user-name{flex:1;font-size:14px;color:#fff}.location-panel .location-user-list .user-geo-item .geo-status{display:flex;align-items:center;gap:.35rem;font-size:12px;padding:2px 8px;border-radius:12px}.location-panel .location-user-list .user-geo-item .geo-status.pending{color:#ffc107;background:rgba(255,193,7,.15)}.location-panel .location-user-list .user-geo-item .geo-status.granted{color:#4caf50;background:rgba(76,175,80,.15)}.location-panel .location-user-list .user-geo-item .geo-status.denied{color:#f44336;background:rgba(244,67,54,.15)}.location-panel .location-user-list .user-geo-item .geo-status i{font-size:10px}.location-panel .geo-denied-warning{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(244,67,54,.15);border-radius:6px;margin-bottom:1rem;color:#f44336;font-size:13px}.location-panel .geo-denied-warning i{font-size:14px}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}.user-geo-panel{width:360px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.user-geo-panel .user-geo-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2.5rem;padding-top:2rem}.user-geo-panel .user-geo-header .header-title-row{display:flex;align-items:center;gap:.5rem}.user-geo-panel .user-geo-header .header-title-row .header-icon{font-size:16px;color:#fff}.user-geo-panel .user-geo-header .header-title-row h3{font-size:18px;font-weight:600;margin:0;color:#fff}.user-geo-panel .user-geo-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.user-geo-panel .user-geo-header .close-btn:hover{opacity:1}.user-geo-panel .user-geo-description{font-size:14px;color:#fffc;line-height:1.6;margin-bottom:1rem}.user-geo-panel .user-geo-description p{margin:0 0 .75rem}.user-geo-panel .user-geo-content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}.user-geo-panel .user-geo-verifying{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verifying .geo-spinner-large{width:80px;height:80px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite;margin-bottom:1.5rem}.user-geo-panel .user-geo-verifying .verifying-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .25rem}.user-geo-panel .user-geo-verifying .verifying-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-verified{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verified .verified-icon-container{margin-bottom:1rem}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon{display:block}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon svg{width:120px;height:100px}.user-geo-panel .user-geo-verified .verified-title{color:#fff;font-size:16px;font-weight:600;margin:0}.user-geo-panel .user-geo-denied{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-denied .denied-icon-container{width:100px;height:100px;border-radius:50%;background:rgba(244,67,54,.2);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.user-geo-panel .user-geo-denied .denied-icon-container i{font-size:48px;color:#f44336}.user-geo-panel .user-geo-denied .denied-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .5rem}.user-geo-panel .user-geo-denied .denied-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-btn{width:100%;padding:14px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-top:auto;margin-bottom:1rem}.user-geo-panel .user-geo-btn:hover:not(.disabled){background:#178e99}.user-geo-panel .user-geo-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.user-geo-panel .dev-controls{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(0,0,0,.3);border-radius:6px;margin-bottom:1rem;flex-wrap:wrap}.user-geo-panel .dev-controls .dev-label{font-size:12px;color:#fff9;font-weight:600}.user-geo-panel .dev-controls .dev-btn{padding:4px 8px;font-size:11px;background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.user-geo-panel .dev-controls .dev-btn:hover{background:rgba(255,255,255,.2)}.user-geo-panel .user-geo-footer{display:flex;justify-content:center;gap:.75rem;padding-top:.5rem}.user-geo-panel .user-geo-footer .footer-icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff9;font-size:16px}.user-geo-panel .user-geo-footer .footer-icon.active{background:var(--Primary-Uell, #1da4b1);color:#fff}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1217
1687
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
1218
1688
|
type: Component,
|
|
1219
|
-
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:transparent;cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px)}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
1220
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
1689
|
+
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Owner geolocation panel (for owners to request user location) -->\n <div class=\"user-geo-panel\" *ngIf=\"shouldShowUserGeoPanel || devModeEnabled\">\n <div class=\"user-geo-header\">\n <div class=\"header-title-row\">\n <i class=\"fa fa-map-marker header-icon\" *ngIf=\"userGeoViewState !== UserGeoViewState.INITIAL\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n </div>\n <button class=\"close-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n\n <div class=\"user-geo-description\" *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n\n <div class=\"user-geo-content\">\n <!-- INITIAL state: just the button -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.INITIAL\">\n <!-- spacer -->\n </ng-container>\n\n <!-- VERIFYING state: spinner + loading message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFYING\">\n <div class=\"user-geo-verifying\">\n <div class=\"geo-spinner-large\"></div>\n <p class=\"verifying-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"verifying-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n </ng-container>\n\n <!-- VERIFIED state: home icon with sparkles -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFIED\">\n <div class=\"user-geo-verified\">\n <div class=\"verified-icon-container\">\n <span class=\"home-icon\" [innerHTML]=\"homeIcon\"></span>\n </div>\n <p class=\"verified-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n\n <!-- DENIED state: error icon and message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.DENIED\">\n <div class=\"user-geo-denied\">\n <div class=\"denied-icon-container\">\n <i class=\"fa fa-times-circle\"></i>\n </div>\n <p class=\"denied-title\">La ubicaci\u00F3n fue rechazada</p>\n <p class=\"denied-subtitle\">El colaborador no permiti\u00F3 el acceso a su ubicaci\u00F3n.</p>\n </div>\n </ng-container>\n </div>\n\n <!-- Button (hidden in verified and denied states) -->\n <button \n class=\"user-geo-btn\" \n *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\"\n [class.disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n [disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n\n <!-- Dev controls -->\n <div class=\"dev-controls\" *ngIf=\"devModeEnabled\">\n <span class=\"dev-label\">Dev:</span>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.INITIAL)\">Initial</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFYING)\">Verifying</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFIED)\">Verified</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.DENIED)\">Denied</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\">Hide</button>\n </div>\n </div>\n</div>\n\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:transparent;cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px)}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-user-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:200px;overflow-y:auto}.location-panel .location-user-list .user-geo-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:rgba(255,255,255,.05);border-radius:6px}.location-panel .location-user-list .user-geo-item .user-icon{color:#fff9;font-size:14px}.location-panel .location-user-list .user-geo-item .user-name{flex:1;font-size:14px;color:#fff}.location-panel .location-user-list .user-geo-item .geo-status{display:flex;align-items:center;gap:.35rem;font-size:12px;padding:2px 8px;border-radius:12px}.location-panel .location-user-list .user-geo-item .geo-status.pending{color:#ffc107;background:rgba(255,193,7,.15)}.location-panel .location-user-list .user-geo-item .geo-status.granted{color:#4caf50;background:rgba(76,175,80,.15)}.location-panel .location-user-list .user-geo-item .geo-status.denied{color:#f44336;background:rgba(244,67,54,.15)}.location-panel .location-user-list .user-geo-item .geo-status i{font-size:10px}.location-panel .geo-denied-warning{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(244,67,54,.15);border-radius:6px;margin-bottom:1rem;color:#f44336;font-size:13px}.location-panel .geo-denied-warning i{font-size:14px}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}.user-geo-panel{width:360px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.user-geo-panel .user-geo-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2.5rem;padding-top:2rem}.user-geo-panel .user-geo-header .header-title-row{display:flex;align-items:center;gap:.5rem}.user-geo-panel .user-geo-header .header-title-row .header-icon{font-size:16px;color:#fff}.user-geo-panel .user-geo-header .header-title-row h3{font-size:18px;font-weight:600;margin:0;color:#fff}.user-geo-panel .user-geo-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.user-geo-panel .user-geo-header .close-btn:hover{opacity:1}.user-geo-panel .user-geo-description{font-size:14px;color:#fffc;line-height:1.6;margin-bottom:1rem}.user-geo-panel .user-geo-description p{margin:0 0 .75rem}.user-geo-panel .user-geo-content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}.user-geo-panel .user-geo-verifying{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verifying .geo-spinner-large{width:80px;height:80px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite;margin-bottom:1.5rem}.user-geo-panel .user-geo-verifying .verifying-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .25rem}.user-geo-panel .user-geo-verifying .verifying-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-verified{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verified .verified-icon-container{margin-bottom:1rem}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon{display:block}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon svg{width:120px;height:100px}.user-geo-panel .user-geo-verified .verified-title{color:#fff;font-size:16px;font-weight:600;margin:0}.user-geo-panel .user-geo-denied{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-denied .denied-icon-container{width:100px;height:100px;border-radius:50%;background:rgba(244,67,54,.2);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.user-geo-panel .user-geo-denied .denied-icon-container i{font-size:48px;color:#f44336}.user-geo-panel .user-geo-denied .denied-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .5rem}.user-geo-panel .user-geo-denied .denied-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-btn{width:100%;padding:14px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-top:auto;margin-bottom:1rem}.user-geo-panel .user-geo-btn:hover:not(.disabled){background:#178e99}.user-geo-panel .user-geo-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.user-geo-panel .dev-controls{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(0,0,0,.3);border-radius:6px;margin-bottom:1rem;flex-wrap:wrap}.user-geo-panel .dev-controls .dev-label{font-size:12px;color:#fff9;font-weight:600}.user-geo-panel .dev-controls .dev-btn{padding:4px 8px;font-size:11px;background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.user-geo-panel .dev-controls .dev-btn:hover{background:rgba(255,255,255,.2)}.user-geo-panel .user-geo-footer{display:flex;justify-content:center;gap:.75rem;padding-top:.5rem}.user-geo-panel .user-geo-footer .footer-icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff9;font-size:16px}.user-geo-panel .user-geo-footer .footer-icon.active{background:var(--Primary-Uell, #1da4b1);color:#fff}\n"] }]
|
|
1690
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i4$2.DomSanitizer }, { type: i1.NgbModal }]; }, propDecorators: { sessionId: [{
|
|
1221
1691
|
type: Input
|
|
1222
1692
|
}], token: [{
|
|
1223
1693
|
type: Input
|
|
@@ -1266,6 +1736,7 @@ class TasWaitingRoomComponent {
|
|
|
1266
1736
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1267
1737
|
this.WaitingRoomState = WaitingRoomState; // Expose enum to template
|
|
1268
1738
|
this.errorMessage = '';
|
|
1739
|
+
this.showErrorDetails = false;
|
|
1269
1740
|
this.isJoinable = false;
|
|
1270
1741
|
// Session data from status response
|
|
1271
1742
|
this.resolvedSessionId = '';
|
|
@@ -1277,19 +1748,40 @@ class TasWaitingRoomComponent {
|
|
|
1277
1748
|
this.videoCallModalRef = null;
|
|
1278
1749
|
// Geolocation
|
|
1279
1750
|
this.geoPosition = null;
|
|
1751
|
+
this.geoWasDenied = false; // Tracks if user denied geo permission
|
|
1752
|
+
this.geoResultSent = false; // Tracks if geo result was already sent
|
|
1753
|
+
// Permission tracking for auto-join
|
|
1754
|
+
this.mediaPermissionsGranted = false;
|
|
1755
|
+
this.geoPermissionsResolved = false;
|
|
1756
|
+
// Auto-retry tracking
|
|
1757
|
+
this.retryCount = 0;
|
|
1758
|
+
this.MAX_RETRIES = 1;
|
|
1280
1759
|
}
|
|
1281
1760
|
/** Whether current user is an owner */
|
|
1282
1761
|
get isOwner() {
|
|
1283
1762
|
return this.currentUser?.role === TasUserRole.OWNER;
|
|
1284
1763
|
}
|
|
1764
|
+
/** Whether we're still requesting permissions (for template) */
|
|
1765
|
+
get isRequestingPermissions() {
|
|
1766
|
+
return !this.mediaPermissionsGranted || (!this.geoPermissionsResolved && !this.isOwner && !this.isBackoffice);
|
|
1767
|
+
}
|
|
1768
|
+
/** Whether we're waiting for admission after permissions granted (for template) */
|
|
1769
|
+
get isWaitingForAdmission() {
|
|
1770
|
+
return this.mediaPermissionsGranted &&
|
|
1771
|
+
(this.geoPermissionsResolved || this.isOwner || this.isBackoffice) &&
|
|
1772
|
+
!this.isJoinable;
|
|
1773
|
+
}
|
|
1285
1774
|
ngOnInit() {
|
|
1286
1775
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1287
1776
|
this.requestMediaPermissions();
|
|
1777
|
+
// Request geolocation permission and cache it (but don't send to backend yet)
|
|
1778
|
+
// The cached position will be sent when owner requests it via activateGeo
|
|
1288
1779
|
this.requestGeolocation();
|
|
1289
1780
|
this.checkStatus();
|
|
1290
1781
|
}
|
|
1291
1782
|
/**
|
|
1292
1783
|
* Request camera and microphone permissions.
|
|
1784
|
+
* Triggers auto-join when permissions are resolved.
|
|
1293
1785
|
*/
|
|
1294
1786
|
async requestMediaPermissions() {
|
|
1295
1787
|
console.log('[TAS DEBUG] Requesting media permissions...');
|
|
@@ -1298,37 +1790,71 @@ class TasWaitingRoomComponent {
|
|
|
1298
1790
|
// Stop tracks immediately - we just needed the permission
|
|
1299
1791
|
stream.getTracks().forEach(track => track.stop());
|
|
1300
1792
|
console.log('[TAS DEBUG] Media permissions granted');
|
|
1793
|
+
this.mediaPermissionsGranted = true;
|
|
1301
1794
|
}
|
|
1302
1795
|
catch (error) {
|
|
1303
1796
|
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
1797
|
+
// Still allow joining - some users may not have camera/mic
|
|
1798
|
+
this.mediaPermissionsGranted = true;
|
|
1304
1799
|
}
|
|
1800
|
+
this.tryAutoJoin();
|
|
1801
|
+
this.cdr.detectChanges();
|
|
1305
1802
|
}
|
|
1306
1803
|
/**
|
|
1307
|
-
* Request geolocation
|
|
1804
|
+
* Request geolocation permission and cache result.
|
|
1308
1805
|
* Only for regular users (not owners/backoffice).
|
|
1309
|
-
*
|
|
1806
|
+
* Actual send to backend happens after we have videoCallId.
|
|
1807
|
+
* Triggers auto-join when geolocation is resolved.
|
|
1310
1808
|
*/
|
|
1311
1809
|
async requestGeolocation() {
|
|
1312
1810
|
// Only request geolocation for regular users, not owners/backoffice
|
|
1313
1811
|
if (this.isOwner || this.isBackoffice) {
|
|
1314
1812
|
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1813
|
+
this.geoPermissionsResolved = true;
|
|
1315
1814
|
return;
|
|
1316
1815
|
}
|
|
1317
|
-
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1816
|
+
console.log('[TAS DEBUG] Requesting geolocation permission...');
|
|
1318
1817
|
const position = await this.geolocationService.getCurrentPosition();
|
|
1319
1818
|
if (position) {
|
|
1320
|
-
console.log('[TAS DEBUG] Geolocation
|
|
1819
|
+
console.log('[TAS DEBUG] Geolocation granted:', position);
|
|
1321
1820
|
this.geoPosition = position;
|
|
1322
|
-
|
|
1323
|
-
this.sendGeolocationToBackend();
|
|
1821
|
+
this.geoWasDenied = false;
|
|
1324
1822
|
}
|
|
1325
1823
|
else {
|
|
1326
1824
|
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1825
|
+
this.geoPosition = null;
|
|
1826
|
+
this.geoWasDenied = true;
|
|
1327
1827
|
}
|
|
1828
|
+
this.geoPermissionsResolved = true;
|
|
1829
|
+
// If we already have videoCallId, send now. Otherwise, it will be sent after status response.
|
|
1830
|
+
this.sendGeoResultToBackend();
|
|
1831
|
+
this.tryAutoJoin();
|
|
1832
|
+
this.cdr.detectChanges();
|
|
1328
1833
|
}
|
|
1329
1834
|
/**
|
|
1330
|
-
* Send
|
|
1331
|
-
*
|
|
1835
|
+
* Send geo result to backend (granted or denied).
|
|
1836
|
+
* Only sends if videoCallId is available and not already sent.
|
|
1837
|
+
*/
|
|
1838
|
+
sendGeoResultToBackend() {
|
|
1839
|
+
if (!this.videoCallId) {
|
|
1840
|
+
console.log('[TAS DEBUG] Cannot send geo result: videoCallId not available yet');
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (this.geoResultSent) {
|
|
1844
|
+
console.log('[TAS DEBUG] Geo result already sent, skipping');
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
if (this.geoPosition) {
|
|
1848
|
+
this.sendGeolocationToBackend();
|
|
1849
|
+
this.geoResultSent = true;
|
|
1850
|
+
}
|
|
1851
|
+
else if (this.geoWasDenied) {
|
|
1852
|
+
this.sendGeoDenialToBackend();
|
|
1853
|
+
this.geoResultSent = true;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Send granted geolocation to backend via modify user endpoint.
|
|
1332
1858
|
*/
|
|
1333
1859
|
sendGeolocationToBackend() {
|
|
1334
1860
|
if (!this.geoPosition || !this.videoCallId) {
|
|
@@ -1336,7 +1862,6 @@ class TasWaitingRoomComponent {
|
|
|
1336
1862
|
}
|
|
1337
1863
|
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
1338
1864
|
const body = {
|
|
1339
|
-
userId: this.currentUser?.id,
|
|
1340
1865
|
videoCallId: this.videoCallId,
|
|
1341
1866
|
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1342
1867
|
latitude: this.geoPosition.latitude,
|
|
@@ -1347,6 +1872,23 @@ class TasWaitingRoomComponent {
|
|
|
1347
1872
|
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1348
1873
|
});
|
|
1349
1874
|
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Send geolocation denial to backend via modify user endpoint.
|
|
1877
|
+
*/
|
|
1878
|
+
sendGeoDenialToBackend() {
|
|
1879
|
+
if (!this.videoCallId) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
console.log('[TAS DEBUG] Sending geo denial to backend...');
|
|
1883
|
+
const body = {
|
|
1884
|
+
videoCallId: this.videoCallId,
|
|
1885
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1886
|
+
};
|
|
1887
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1888
|
+
next: () => console.log('[TAS DEBUG] Geo denial sent successfully'),
|
|
1889
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geo denial:', err),
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1350
1892
|
ngOnDestroy() {
|
|
1351
1893
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1352
1894
|
this.subscriptions.unsubscribe();
|
|
@@ -1380,8 +1922,8 @@ class TasWaitingRoomComponent {
|
|
|
1380
1922
|
this.resolvedAppointmentId = content.appointmentId;
|
|
1381
1923
|
this.videoCallId = content.videoCallId;
|
|
1382
1924
|
console.log('[TAS DEBUG] Status response:', content);
|
|
1383
|
-
//
|
|
1384
|
-
this.
|
|
1925
|
+
// Now that we have videoCallId, send geo result if not already sent
|
|
1926
|
+
this.sendGeoResultToBackend();
|
|
1385
1927
|
// Start polling for status updates
|
|
1386
1928
|
this.tasService.startStatusPolling(statusParams);
|
|
1387
1929
|
// Subscribe to joinable status
|
|
@@ -1397,7 +1939,8 @@ class TasWaitingRoomComponent {
|
|
|
1397
1939
|
}));
|
|
1398
1940
|
}
|
|
1399
1941
|
/**
|
|
1400
|
-
* Handle changes to joinable status
|
|
1942
|
+
* Handle changes to joinable status.
|
|
1943
|
+
* Triggers auto-join when joinable becomes true.
|
|
1401
1944
|
*/
|
|
1402
1945
|
handleJoinableChange(joinable) {
|
|
1403
1946
|
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
@@ -1413,10 +1956,11 @@ class TasWaitingRoomComponent {
|
|
|
1413
1956
|
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
1414
1957
|
return;
|
|
1415
1958
|
}
|
|
1416
|
-
//
|
|
1959
|
+
// Update state and attempt auto-join
|
|
1417
1960
|
if (joinable) {
|
|
1418
|
-
console.log('[TAS DEBUG] Joinable is true,
|
|
1961
|
+
console.log('[TAS DEBUG] Joinable is true, attempting auto-join');
|
|
1419
1962
|
this.state = WaitingRoomState.READY;
|
|
1963
|
+
this.tryAutoJoin();
|
|
1420
1964
|
}
|
|
1421
1965
|
else {
|
|
1422
1966
|
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
@@ -1425,11 +1969,33 @@ class TasWaitingRoomComponent {
|
|
|
1425
1969
|
this.cdr.detectChanges();
|
|
1426
1970
|
}
|
|
1427
1971
|
/**
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1972
|
+
* Attempt to auto-join the session when all conditions are met.
|
|
1973
|
+
* - For owners: just need joinable + sessionId
|
|
1974
|
+
* - For users: need media permissions + geo resolved + joinable + sessionId
|
|
1430
1975
|
*/
|
|
1431
|
-
|
|
1432
|
-
|
|
1976
|
+
tryAutoJoin() {
|
|
1977
|
+
// Don't try if already joining or in error
|
|
1978
|
+
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
1979
|
+
this.state === WaitingRoomState.JOINING) {
|
|
1980
|
+
console.log('[TAS DEBUG] tryAutoJoin skipped - already in state:', this.state);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
// For owners: just need joinable
|
|
1984
|
+
if (this.isOwner) {
|
|
1985
|
+
if (this.isJoinable && this.resolvedSessionId) {
|
|
1986
|
+
console.log('[TAS DEBUG] Owner auto-joining...');
|
|
1987
|
+
this.startSessionAndJoin();
|
|
1988
|
+
}
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
// For users: need media + geo resolved + joinable
|
|
1992
|
+
if (this.mediaPermissionsGranted &&
|
|
1993
|
+
this.geoPermissionsResolved &&
|
|
1994
|
+
this.isJoinable &&
|
|
1995
|
+
this.resolvedSessionId) {
|
|
1996
|
+
console.log('[TAS DEBUG] User auto-joining...');
|
|
1997
|
+
this.startSessionAndJoin();
|
|
1998
|
+
}
|
|
1433
1999
|
}
|
|
1434
2000
|
/**
|
|
1435
2001
|
* Check if user has owner/backoffice role
|
|
@@ -1482,30 +2048,50 @@ class TasWaitingRoomComponent {
|
|
|
1482
2048
|
},
|
|
1483
2049
|
error: (err) => {
|
|
1484
2050
|
console.error('[TAS DEBUG] /start request failed:', err);
|
|
1485
|
-
|
|
1486
|
-
this.
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
2051
|
+
// Auto-retry on first failure
|
|
2052
|
+
if (this.retryCount < this.MAX_RETRIES) {
|
|
2053
|
+
this.retryCount++;
|
|
2054
|
+
console.log('[TAS DEBUG] Auto-retrying... attempt:', this.retryCount);
|
|
2055
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
2056
|
+
this.cdr.detectChanges();
|
|
2057
|
+
setTimeout(() => this.startSessionAndJoin(), 2000);
|
|
2058
|
+
}
|
|
2059
|
+
else {
|
|
2060
|
+
// Show error after retry fails
|
|
2061
|
+
this.state = WaitingRoomState.ERROR;
|
|
2062
|
+
this.errorMessage = err?.error?.message || err?.message || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
|
|
2063
|
+
this.tasService.stopStatusPolling();
|
|
2064
|
+
console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
|
|
2065
|
+
this.cdr.detectChanges();
|
|
2066
|
+
}
|
|
1490
2067
|
},
|
|
1491
2068
|
}));
|
|
1492
2069
|
}
|
|
1493
2070
|
/**
|
|
1494
|
-
*
|
|
1495
|
-
|
|
1496
|
-
cancel() {
|
|
1497
|
-
this.tasService.stopStatusPolling();
|
|
1498
|
-
this.activeModal.dismiss('cancel');
|
|
1499
|
-
}
|
|
1500
|
-
/**
|
|
1501
|
-
* Retry after an error
|
|
2071
|
+
* Retry after an error.
|
|
2072
|
+
* Resets retry count and restarts the flow.
|
|
1502
2073
|
*/
|
|
1503
2074
|
retry() {
|
|
1504
2075
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1505
2076
|
this.errorMessage = '';
|
|
2077
|
+
this.showErrorDetails = false;
|
|
1506
2078
|
this.token = '';
|
|
2079
|
+
this.retryCount = 0; // Reset retry count for auto-retry
|
|
1507
2080
|
this.checkStatus();
|
|
1508
2081
|
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Toggle the error details dropdown visibility.
|
|
2084
|
+
*/
|
|
2085
|
+
toggleErrorDetails() {
|
|
2086
|
+
this.showErrorDetails = !this.showErrorDetails;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Close the waiting room modal.
|
|
2090
|
+
*/
|
|
2091
|
+
close() {
|
|
2092
|
+
this.tasService.stopStatusPolling();
|
|
2093
|
+
this.activeModal.dismiss('close');
|
|
2094
|
+
}
|
|
1509
2095
|
openVideoCallModal() {
|
|
1510
2096
|
this.videoCallModalRef = this.modalService.open(TasVideocallComponent, {
|
|
1511
2097
|
size: 'xl',
|
|
@@ -1529,10 +2115,10 @@ class TasWaitingRoomComponent {
|
|
|
1529
2115
|
}
|
|
1530
2116
|
}
|
|
1531
2117
|
TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }, { token: i1.NgbModal }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1532
|
-
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n
|
|
2118
|
+
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n <div class=\"waiting-room-content\">\n <div class=\"state-container\">\n <!-- Loading states (show spinner) -->\n <ng-container *ngIf=\"state !== WaitingRoomState.ERROR\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n \n <!-- Requesting permissions -->\n <ng-container *ngIf=\"isRequestingPermissions\">\n <p class=\"state-message\">Solicitando permisos...</p>\n <p class=\"state-submessage\">Por favor, acept\u00E1 los permisos de c\u00E1mara y micr\u00F3fono.</p>\n </ng-container>\n \n <!-- Waiting for admission (permissions granted, not joinable yet) -->\n <ng-container *ngIf=\"isWaitingForAdmission\">\n <p class=\"state-message\">Medicina laboral va a admitirte</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </ng-container>\n \n <!-- Getting token / Joining -->\n <ng-container *ngIf=\"state === WaitingRoomState.GETTING_TOKEN || state === WaitingRoomState.JOINING\">\n <p class=\"state-message\">Conectando...</p>\n </ng-container>\n \n <!-- Checking status (when not requesting permissions) -->\n <ng-container *ngIf=\"state === WaitingRoomState.CHECKING_STATUS && !isRequestingPermissions\">\n <p class=\"state-message\">Verificando estado...</p>\n </ng-container>\n </ng-container>\n\n <!-- Error state (show error icon and message) -->\n <ng-container *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n \n <!-- Collapsible error details -->\n <div class=\"error-dropdown\" *ngIf=\"errorMessage\">\n <button \n type=\"button\" \n class=\"error-toggle-btn\"\n (click)=\"toggleErrorDetails()\"\n [attr.aria-expanded]=\"showErrorDetails\"\n aria-controls=\"error-details-content\"\n >\n <span>Ver detalles del error</span>\n <i class=\"fa\" [class.fa-chevron-down]=\"!showErrorDetails\" [class.fa-chevron-up]=\"showErrorDetails\"></i>\n </button>\n <div \n id=\"error-details-content\"\n class=\"error-details-content\"\n [class.expanded]=\"showErrorDetails\"\n >\n <p class=\"error-details-text\">{{ errorMessage }}</p>\n </div>\n </div>\n \n <div class=\"error-actions\">\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n <button type=\"button\" class=\"btn action-btn close-btn\" (click)=\"close()\">\n Cerrar\n </button>\n </div>\n </ng-container>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:300px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-dropdown{margin:16px 0;width:100%}.error-toggle-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:10px 16px;font-size:13px;font-weight:500;color:#6c757d;background:transparent;border:1px solid #e9ecef;border-radius:6px;cursor:pointer;transition:all .2s ease}.error-toggle-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.error-toggle-btn i{font-size:12px;transition:transform .2s ease}.error-details-content{max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease}.error-details-content.expanded{max-height:200px;padding-top:12px}.error-details-text{font-size:12px;color:#ee316b;margin:0;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2);text-align:left;word-break:break-word;max-height:150px;overflow-y:auto}.error-actions{display:flex;justify-content:center;gap:12px;margin-top:16px}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.action-btn.close-btn{background:#ee316b;color:#fff;border:none}.action-btn.close-btn:hover{background:#da124f}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 576px){.tas-waiting-room{min-height:250px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.action-btn{padding:10px 24px;font-size:14px}}\n"], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1533
2119
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
1534
2120
|
type: Component,
|
|
1535
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n
|
|
2121
|
+
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n <div class=\"waiting-room-content\">\n <div class=\"state-container\">\n <!-- Loading states (show spinner) -->\n <ng-container *ngIf=\"state !== WaitingRoomState.ERROR\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n \n <!-- Requesting permissions -->\n <ng-container *ngIf=\"isRequestingPermissions\">\n <p class=\"state-message\">Solicitando permisos...</p>\n <p class=\"state-submessage\">Por favor, acept\u00E1 los permisos de c\u00E1mara y micr\u00F3fono.</p>\n </ng-container>\n \n <!-- Waiting for admission (permissions granted, not joinable yet) -->\n <ng-container *ngIf=\"isWaitingForAdmission\">\n <p class=\"state-message\">Medicina laboral va a admitirte</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </ng-container>\n \n <!-- Getting token / Joining -->\n <ng-container *ngIf=\"state === WaitingRoomState.GETTING_TOKEN || state === WaitingRoomState.JOINING\">\n <p class=\"state-message\">Conectando...</p>\n </ng-container>\n \n <!-- Checking status (when not requesting permissions) -->\n <ng-container *ngIf=\"state === WaitingRoomState.CHECKING_STATUS && !isRequestingPermissions\">\n <p class=\"state-message\">Verificando estado...</p>\n </ng-container>\n </ng-container>\n\n <!-- Error state (show error icon and message) -->\n <ng-container *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n \n <!-- Collapsible error details -->\n <div class=\"error-dropdown\" *ngIf=\"errorMessage\">\n <button \n type=\"button\" \n class=\"error-toggle-btn\"\n (click)=\"toggleErrorDetails()\"\n [attr.aria-expanded]=\"showErrorDetails\"\n aria-controls=\"error-details-content\"\n >\n <span>Ver detalles del error</span>\n <i class=\"fa\" [class.fa-chevron-down]=\"!showErrorDetails\" [class.fa-chevron-up]=\"showErrorDetails\"></i>\n </button>\n <div \n id=\"error-details-content\"\n class=\"error-details-content\"\n [class.expanded]=\"showErrorDetails\"\n >\n <p class=\"error-details-text\">{{ errorMessage }}</p>\n </div>\n </div>\n \n <div class=\"error-actions\">\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n <button type=\"button\" class=\"btn action-btn close-btn\" (click)=\"close()\">\n Cerrar\n </button>\n </div>\n </ng-container>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:300px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-dropdown{margin:16px 0;width:100%}.error-toggle-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:10px 16px;font-size:13px;font-weight:500;color:#6c757d;background:transparent;border:1px solid #e9ecef;border-radius:6px;cursor:pointer;transition:all .2s ease}.error-toggle-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.error-toggle-btn i{font-size:12px;transition:transform .2s ease}.error-details-content{max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease}.error-details-content.expanded{max-height:200px;padding-top:12px}.error-details-text{font-size:12px;color:#ee316b;margin:0;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2);text-align:left;word-break:break-word;max-height:150px;overflow-y:auto}.error-actions{display:flex;justify-content:center;gap:12px;margin-top:16px}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}.action-btn.close-btn{background:#ee316b;color:#fff;border:none}.action-btn.close-btn:hover{background:#da124f}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 576px){.tas-waiting-room{min-height:250px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.action-btn{padding:10px 24px;font-size:14px}}\n"] }]
|
|
1536
2122
|
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1537
2123
|
type: Input
|
|
1538
2124
|
}], entityId: [{
|
|
@@ -1646,6 +2232,11 @@ class TasButtonComponent {
|
|
|
1646
2232
|
})
|
|
1647
2233
|
.subscribe({
|
|
1648
2234
|
next: (response) => {
|
|
2235
|
+
// Validate response structure
|
|
2236
|
+
if (!response || !response.content) {
|
|
2237
|
+
this.handleStatusError('Invalid response');
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
1649
2240
|
this.isCheckingStatus = false;
|
|
1650
2241
|
this.isStatusError = false;
|
|
1651
2242
|
this.statusErrorMessage = '';
|
|
@@ -1654,23 +2245,20 @@ class TasButtonComponent {
|
|
|
1654
2245
|
this.isJoinable = response.content?.joinable ?? false;
|
|
1655
2246
|
},
|
|
1656
2247
|
error: (err) => {
|
|
1657
|
-
this.
|
|
1658
|
-
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1659
|
-
this.statusErrorMessage = errorMessage;
|
|
1660
|
-
// Use utility service to determine if button should be shown
|
|
1661
|
-
this.shouldShowButton = this.tasUtilityService.shouldShowButton(errorMessage);
|
|
1662
|
-
// Stop polling on error
|
|
1663
|
-
this.stopStatusPolling();
|
|
1664
|
-
// If button should be hidden, don't treat as error
|
|
1665
|
-
if (!this.shouldShowButton) {
|
|
1666
|
-
this.isStatusError = false;
|
|
1667
|
-
}
|
|
1668
|
-
else {
|
|
1669
|
-
this.isStatusError = true;
|
|
1670
|
-
}
|
|
2248
|
+
this.handleStatusError(err);
|
|
1671
2249
|
},
|
|
1672
2250
|
}));
|
|
1673
2251
|
}
|
|
2252
|
+
handleStatusError(err) {
|
|
2253
|
+
this.isCheckingStatus = false;
|
|
2254
|
+
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
2255
|
+
this.statusErrorMessage = errorMessage;
|
|
2256
|
+
// On any status error, hide the button
|
|
2257
|
+
this.shouldShowButton = false;
|
|
2258
|
+
this.isStatusError = false; // We don't show error UI, just hide the button
|
|
2259
|
+
// Stop polling on error
|
|
2260
|
+
this.stopStatusPolling();
|
|
2261
|
+
}
|
|
1674
2262
|
onClick() {
|
|
1675
2263
|
if (!this.tenant || !this.currentUser?.name) {
|
|
1676
2264
|
return;
|
|
@@ -1747,6 +2335,7 @@ class TasFloatingCallComponent {
|
|
|
1747
2335
|
this.isMuted = false;
|
|
1748
2336
|
this.subscriptions = new Subscription();
|
|
1749
2337
|
this.videoCallModalRef = null;
|
|
2338
|
+
this.feedbackShown = false; // Track if feedback modal has been shown
|
|
1750
2339
|
// Margin from screen edges (in pixels)
|
|
1751
2340
|
this.PIP_MARGIN = 20;
|
|
1752
2341
|
}
|
|
@@ -1768,12 +2357,45 @@ class TasFloatingCallComponent {
|
|
|
1768
2357
|
toggleMute() {
|
|
1769
2358
|
this.tasService.toggleMute();
|
|
1770
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Open feedback modal when call ends from PiP mode
|
|
2362
|
+
*/
|
|
2363
|
+
openFeedbackModal() {
|
|
2364
|
+
// Prevent opening feedback modal multiple times
|
|
2365
|
+
if (this.feedbackShown) {
|
|
2366
|
+
this.isVisible = false;
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
// Get videoCallId from service
|
|
2370
|
+
const videoCallId = this.tasService.videoCallId;
|
|
2371
|
+
// If no videoCallId, skip feedback and hide directly
|
|
2372
|
+
if (!videoCallId) {
|
|
2373
|
+
this.isVisible = false;
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
this.feedbackShown = true;
|
|
2377
|
+
const modalRef = this.modalService.open(TasFeedbackModalComponent, {
|
|
2378
|
+
centered: true,
|
|
2379
|
+
backdrop: true,
|
|
2380
|
+
keyboard: true,
|
|
2381
|
+
windowClass: 'tas-feedback-modal-wrapper',
|
|
2382
|
+
});
|
|
2383
|
+
modalRef.componentInstance.videoCallId = videoCallId;
|
|
2384
|
+
modalRef.componentInstance.tenant = this.tasService.tenant ?? '';
|
|
2385
|
+
modalRef.componentInstance.businessRole = this.tasService.businessRole;
|
|
2386
|
+
// Hide floating call after feedback modal is closed/dismissed
|
|
2387
|
+
modalRef.result.finally(() => {
|
|
2388
|
+
this.isVisible = false;
|
|
2389
|
+
this.feedbackShown = false; // Reset for potential future calls
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
1771
2392
|
// Private Methods
|
|
1772
2393
|
setupSubscriptions() {
|
|
1773
2394
|
// Call state subscription
|
|
1774
2395
|
this.subscriptions.add(this.tasService.callState$.subscribe((state) => {
|
|
1775
|
-
if
|
|
1776
|
-
|
|
2396
|
+
// Only handle disconnect feedback if we're in PiP mode (isVisible)
|
|
2397
|
+
if (state === CallState.DISCONNECTED && this.isVisible) {
|
|
2398
|
+
this.openFeedbackModal();
|
|
1777
2399
|
}
|
|
1778
2400
|
}));
|
|
1779
2401
|
// View mode subscription
|
|
@@ -1895,10 +2517,13 @@ class TasIncomingAppointmentComponent {
|
|
|
1895
2517
|
this.appointments = [];
|
|
1896
2518
|
this.isLoading = true;
|
|
1897
2519
|
this.hasError = false;
|
|
2520
|
+
// The appointmentId from status API - only this appointment shows tas-btn
|
|
2521
|
+
this.activeAppointmentId = null;
|
|
1898
2522
|
this.subscriptions = new Subscription();
|
|
1899
2523
|
}
|
|
1900
2524
|
ngOnInit() {
|
|
1901
2525
|
this.loadAppointments();
|
|
2526
|
+
this.checkStatus();
|
|
1902
2527
|
}
|
|
1903
2528
|
ngOnDestroy() {
|
|
1904
2529
|
this.subscriptions.unsubscribe();
|
|
@@ -1916,8 +2541,9 @@ class TasIncomingAppointmentComponent {
|
|
|
1916
2541
|
const appointments = Array.isArray(response)
|
|
1917
2542
|
? response
|
|
1918
2543
|
: response?.content || [];
|
|
1919
|
-
//
|
|
1920
|
-
|
|
2544
|
+
// Deduplicate by id and sort by date and startTime ascending (earliest first)
|
|
2545
|
+
const uniqueAppointments = appointments.filter((appt, index, self) => index === self.findIndex((a) => a.id === appt.id));
|
|
2546
|
+
this.appointments = uniqueAppointments.sort((a, b) => {
|
|
1921
2547
|
const dateTimeA = `${a.date}T${a.startTime}`;
|
|
1922
2548
|
const dateTimeB = `${b.date}T${b.startTime}`;
|
|
1923
2549
|
return dateTimeB.localeCompare(dateTimeA);
|
|
@@ -1930,15 +2556,49 @@ class TasIncomingAppointmentComponent {
|
|
|
1930
2556
|
},
|
|
1931
2557
|
}));
|
|
1932
2558
|
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Check status endpoint to get the active appointmentId
|
|
2561
|
+
*/
|
|
2562
|
+
checkStatus() {
|
|
2563
|
+
if (!this.tenant || !this.entityId) {
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
this.subscriptions.add(this.tasService
|
|
2567
|
+
.getProxyVideoStatus({
|
|
2568
|
+
roomType: this.roomType,
|
|
2569
|
+
entityId: this.entityId,
|
|
2570
|
+
tenant: this.tenant,
|
|
2571
|
+
businessRole: this.businessRole,
|
|
2572
|
+
})
|
|
2573
|
+
.subscribe({
|
|
2574
|
+
next: (response) => {
|
|
2575
|
+
// Store the appointmentId from status - tas-btn only shows for this one
|
|
2576
|
+
this.activeAppointmentId = response?.content?.appointmentId ?? null;
|
|
2577
|
+
},
|
|
2578
|
+
error: () => {
|
|
2579
|
+
this.activeAppointmentId = null;
|
|
2580
|
+
},
|
|
2581
|
+
}));
|
|
2582
|
+
}
|
|
1933
2583
|
onEnterCall(appointment) {
|
|
1934
2584
|
this.enterCall.emit(appointment);
|
|
1935
2585
|
}
|
|
1936
2586
|
/**
|
|
1937
|
-
* Check if tas-btn should be shown for an appointment
|
|
2587
|
+
* Check if tas-btn should be shown for an appointment.
|
|
2588
|
+
* Only shows when appointment.id matches the activeAppointmentId from status API.
|
|
2589
|
+
* tas-btn handles its own polling for joinable state.
|
|
1938
2590
|
*/
|
|
1939
2591
|
shouldShowTasBtn(appointment) {
|
|
1940
|
-
|
|
2592
|
+
const hasValidStatus = appointment.status === AppointmentStatus.CONFIRMED ||
|
|
1941
2593
|
appointment.status === AppointmentStatus.ACTIVE;
|
|
2594
|
+
// Only show for the appointment that matches status API response
|
|
2595
|
+
return hasValidStatus && appointment.id === this.activeAppointmentId;
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* TrackBy function for ngFor
|
|
2599
|
+
*/
|
|
2600
|
+
trackByAppointmentId(index, appointment) {
|
|
2601
|
+
return appointment.id;
|
|
1942
2602
|
}
|
|
1943
2603
|
/**
|
|
1944
2604
|
* Format date to Spanish format: "Lunes 8 de diciembre"
|
|
@@ -1970,17 +2630,17 @@ class TasIncomingAppointmentComponent {
|
|
|
1970
2630
|
*/
|
|
1971
2631
|
getOtherParticipant(appointment) {
|
|
1972
2632
|
if (!appointment.participants || appointment.participants.length === 0) {
|
|
1973
|
-
return appointment.title;
|
|
2633
|
+
return appointment.title;
|
|
1974
2634
|
}
|
|
1975
2635
|
const otherParticipant = appointment.participants.find(p => p.userId !== this.currentUser?.id);
|
|
1976
2636
|
return otherParticipant?.name || appointment.title;
|
|
1977
2637
|
}
|
|
1978
2638
|
}
|
|
1979
2639
|
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1980
|
-
TasIncomingAppointmentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasIncomingAppointmentComponent, selector: "tas-incoming-appointment", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser", fromDate: "fromDate", toDate: "toDate" }, outputs: { enterCall: "enterCall" }, ngImport: i0, template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && appointments.length === 0\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch;gap:16px}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }, { type: TasButtonComponent, selector: "tas-btn", inputs: ["roomType", "entityId", "tenant", "businessRole", "currentUser", "variant", "buttonLabel"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
2640
|
+
TasIncomingAppointmentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasIncomingAppointmentComponent, selector: "tas-incoming-appointment", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser", fromDate: "fromDate", toDate: "toDate" }, outputs: { enterCall: "enterCall" }, ngImport: i0, template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && appointments.length === 0\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments; trackBy: trackByAppointmentId\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch;gap:16px}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }, { type: TasButtonComponent, selector: "tas-btn", inputs: ["roomType", "entityId", "tenant", "businessRole", "currentUser", "variant", "buttonLabel"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
1981
2641
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
1982
2642
|
type: Component,
|
|
1983
|
-
args: [{ selector: 'tas-incoming-appointment', template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && appointments.length === 0\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch;gap:16px}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"] }]
|
|
2643
|
+
args: [{ selector: 'tas-incoming-appointment', template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && appointments.length === 0\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments; trackBy: trackByAppointmentId\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch;gap:16px}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"] }]
|
|
1984
2644
|
}], ctorParameters: function () { return [{ type: TasService }]; }, propDecorators: { roomType: [{
|
|
1985
2645
|
type: Input
|
|
1986
2646
|
}], entityId: [{
|
|
@@ -2046,12 +2706,14 @@ TasUellSdkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", versio
|
|
|
2046
2706
|
TasFloatingCallComponent,
|
|
2047
2707
|
TasWaitingRoomComponent,
|
|
2048
2708
|
TasAvatarComponent,
|
|
2049
|
-
TasIncomingAppointmentComponent
|
|
2709
|
+
TasIncomingAppointmentComponent,
|
|
2710
|
+
TasFeedbackModalComponent], imports: [CommonModule, FormsModule, NgbTooltipModule], exports: [TasButtonComponent,
|
|
2050
2711
|
TasVideocallComponent,
|
|
2051
2712
|
TasFloatingCallComponent,
|
|
2052
2713
|
TasWaitingRoomComponent,
|
|
2053
2714
|
TasAvatarComponent,
|
|
2054
|
-
TasIncomingAppointmentComponent
|
|
2715
|
+
TasIncomingAppointmentComponent,
|
|
2716
|
+
TasFeedbackModalComponent] });
|
|
2055
2717
|
TasUellSdkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, imports: [[CommonModule, FormsModule, NgbTooltipModule]] });
|
|
2056
2718
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasUellSdkModule, decorators: [{
|
|
2057
2719
|
type: NgModule,
|
|
@@ -2063,6 +2725,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2063
2725
|
TasWaitingRoomComponent,
|
|
2064
2726
|
TasAvatarComponent,
|
|
2065
2727
|
TasIncomingAppointmentComponent,
|
|
2728
|
+
TasFeedbackModalComponent,
|
|
2066
2729
|
],
|
|
2067
2730
|
imports: [CommonModule, FormsModule, NgbTooltipModule],
|
|
2068
2731
|
exports: [
|
|
@@ -2072,6 +2735,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2072
2735
|
TasWaitingRoomComponent,
|
|
2073
2736
|
TasAvatarComponent,
|
|
2074
2737
|
TasIncomingAppointmentComponent,
|
|
2738
|
+
TasFeedbackModalComponent,
|
|
2075
2739
|
],
|
|
2076
2740
|
}]
|
|
2077
2741
|
}] });
|
|
@@ -2084,5 +2748,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2084
2748
|
* Generated bundle index. Do not edit.
|
|
2085
2749
|
*/
|
|
2086
2750
|
|
|
2087
|
-
export { AppointmentStatus, CallState, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2751
|
+
export { AppointmentStatus, CallState, FeedbackMotiveType, GeoStatus, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFeedbackModalComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserGeoViewState, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2088
2752
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|