tas-uell-sdk 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/esm2020/lib/components/tas-btn/tas-btn.component.mjs +30 -17
- package/esm2020/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.mjs +41 -28
- package/esm2020/lib/components/tas-videocall/tas-videocall.component.mjs +126 -41
- package/esm2020/lib/components/tas-waiting-room/tas-waiting-room.component.mjs +135 -34
- package/esm2020/lib/icons/tas-icons.mjs +17 -0
- package/esm2020/lib/interfaces/tas.interfaces.mjs +9 -1
- package/esm2020/lib/services/tas.service.mjs +99 -27
- package/fesm2015/tas-uell-sdk.mjs +449 -140
- package/fesm2015/tas-uell-sdk.mjs.map +1 -1
- package/fesm2020/tas-uell-sdk.mjs +447 -139
- package/fesm2020/tas-uell-sdk.mjs.map +1 -1
- package/lib/components/tas-btn/tas-btn.component.d.ts +3 -1
- package/lib/components/tas-incoming-appointment/tas-incoming-appointment.component.d.ts +15 -6
- package/lib/components/tas-videocall/tas-videocall.component.d.ts +42 -5
- package/lib/components/tas-waiting-room/tas-waiting-room.component.d.ts +32 -13
- package/lib/icons/tas-icons.d.ts +8 -0
- package/lib/interfaces/tas.interfaces.d.ts +26 -4
- package/lib/services/tas.service.d.ts +13 -2
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, Injectable, Inject, Component, ChangeDetectionStrategy, Input, ViewChild, EventEmitter, Output, NgModule } from '@angular/core';
|
|
3
|
-
import { BehaviorSubject, Subscription } from 'rxjs';
|
|
4
|
-
import { map, catchError } from 'rxjs/operators';
|
|
3
|
+
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
|
4
|
+
import { map, catchError, finalize, shareReplay } from 'rxjs/operators';
|
|
5
5
|
import * as OT from '@opentok/client';
|
|
6
6
|
import interact from 'interactjs';
|
|
7
7
|
import * as i1 from '@ng-bootstrap/ng-bootstrap';
|
|
8
8
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
9
|
-
import * as i4 from '@angular/
|
|
9
|
+
import * as i4 from '@angular/platform-browser';
|
|
10
|
+
import * as i4$1 from '@angular/common';
|
|
10
11
|
import { CommonModule } from '@angular/common';
|
|
11
12
|
import { FormsModule } from '@angular/forms';
|
|
12
13
|
|
|
@@ -66,7 +67,14 @@ var UserCallAction;
|
|
|
66
67
|
UserCallAction["CHANGE_STATUS"] = "CHANGE_STATUS";
|
|
67
68
|
UserCallAction["REQUEST_GEOLOCALIZATION"] = "REQUEST_GEOLOCALIZATION";
|
|
68
69
|
UserCallAction["ACTIVATE_GEOLOCATION"] = "ACTIVATE_GEOLOCATION";
|
|
70
|
+
UserCallAction["DENY_GEOLOCATION"] = "DENY_GEOLOCATION";
|
|
69
71
|
})(UserCallAction || (UserCallAction = {}));
|
|
72
|
+
var GeoStatus;
|
|
73
|
+
(function (GeoStatus) {
|
|
74
|
+
GeoStatus["PENDING"] = "PENDING";
|
|
75
|
+
GeoStatus["GRANTED"] = "GRANTED";
|
|
76
|
+
GeoStatus["DENIED"] = "DENIED";
|
|
77
|
+
})(GeoStatus || (GeoStatus = {}));
|
|
70
78
|
var RoomUserStatus;
|
|
71
79
|
(function (RoomUserStatus) {
|
|
72
80
|
RoomUserStatus["ASSIGNED"] = "ASSIGNED";
|
|
@@ -93,6 +101,7 @@ var AppointmentStatus;
|
|
|
93
101
|
AppointmentStatus["CONFIRMED"] = "CONFIRMED";
|
|
94
102
|
AppointmentStatus["CANCELLED"] = "CANCELLED";
|
|
95
103
|
AppointmentStatus["RESCHEDULED"] = "RESCHEDULED";
|
|
104
|
+
AppointmentStatus["ACTIVE"] = "ACTIVE";
|
|
96
105
|
})(AppointmentStatus || (AppointmentStatus = {}));
|
|
97
106
|
|
|
98
107
|
class TasUtilityService {
|
|
@@ -196,6 +205,9 @@ class TasService {
|
|
|
196
205
|
// Observable for when all geo has been granted
|
|
197
206
|
this.allGeoGrantedSubject = new BehaviorSubject(false);
|
|
198
207
|
this.allGeoGranted$ = this.allGeoGrantedSubject.asObservable();
|
|
208
|
+
// Observable for individual user geo status (for owner panel)
|
|
209
|
+
this.userGeoInfoSubject = new BehaviorSubject([]);
|
|
210
|
+
this.userGeoInfo$ = this.userGeoInfoSubject.asObservable();
|
|
199
211
|
this.statusPollingInterval = null;
|
|
200
212
|
this.DEFAULT_POLL_INTERVAL_MS = 30000; // Default 30s
|
|
201
213
|
this.wasOwnerPresent = false;
|
|
@@ -203,6 +215,10 @@ class TasService {
|
|
|
203
215
|
this.currentAppointmentId = null;
|
|
204
216
|
this.currentVideoCallId = null;
|
|
205
217
|
this.currentTenant = null;
|
|
218
|
+
// Status cache (1 second TTL)
|
|
219
|
+
this.STATUS_CACHE_TTL_MS = 1000;
|
|
220
|
+
this.statusCache = null;
|
|
221
|
+
this.inflightStatusRequests = new Map();
|
|
206
222
|
}
|
|
207
223
|
// ... (Getters and other methods remain unchanged)
|
|
208
224
|
/**
|
|
@@ -221,7 +237,6 @@ class TasService {
|
|
|
221
237
|
if (params.tenant) {
|
|
222
238
|
this.currentTenant = params.tenant;
|
|
223
239
|
}
|
|
224
|
-
console.log(`[TAS DEBUG] Starting status polling with interval ${intervalMs}ms`);
|
|
225
240
|
// Initial status fetch
|
|
226
241
|
this.fetchAndProcessStatus(params);
|
|
227
242
|
// Set up periodic polling
|
|
@@ -324,7 +339,6 @@ class TasService {
|
|
|
324
339
|
}
|
|
325
340
|
// Session Management
|
|
326
341
|
disconnectSession(clearStorage = true) {
|
|
327
|
-
console.log('[TAS DEBUG] TasService.disconnectSession called. clearStorage:', clearStorage);
|
|
328
342
|
// Call finishSession before disconnecting if we have a sessionId
|
|
329
343
|
const sessionIdToFinish = this.currentSessionId;
|
|
330
344
|
// Clear storage FIRST to prevent any race conditions where state might be saved after disconnect
|
|
@@ -355,11 +369,9 @@ class TasService {
|
|
|
355
369
|
businessRole: this.currentBusinessRole,
|
|
356
370
|
}).subscribe({
|
|
357
371
|
next: (response) => {
|
|
358
|
-
console.log('[TAS DEBUG] Session finished successfully:', response);
|
|
359
372
|
this.isFinishingSession = false;
|
|
360
373
|
},
|
|
361
374
|
error: (error) => {
|
|
362
|
-
console.error('[TAS DEBUG] Error finishing session:', error);
|
|
363
375
|
this.isFinishingSession = false;
|
|
364
376
|
},
|
|
365
377
|
});
|
|
@@ -394,7 +406,6 @@ class TasService {
|
|
|
394
406
|
// If so, don't restore it on page reload
|
|
395
407
|
const wasDisconnected = sessionStorage.getItem(this.DISCONNECTED_FLAG_KEY);
|
|
396
408
|
if (wasDisconnected === 'true') {
|
|
397
|
-
console.log('[TAS DEBUG] Session was intentionally disconnected, skipping restore');
|
|
398
409
|
this.clearSessionState();
|
|
399
410
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
400
411
|
return;
|
|
@@ -402,14 +413,12 @@ class TasService {
|
|
|
402
413
|
// Don't restore if we're already disconnected or if there's no active session
|
|
403
414
|
// This prevents restoring sessions that were properly disconnected
|
|
404
415
|
if (this.callStateSubject.getValue() === CallState.DISCONNECTED) {
|
|
405
|
-
console.log('[TAS DEBUG] Call state is DISCONNECTED, skipping restore');
|
|
406
416
|
this.clearSessionState();
|
|
407
417
|
return;
|
|
408
418
|
}
|
|
409
419
|
try {
|
|
410
420
|
const state = JSON.parse(savedState);
|
|
411
421
|
if (state.sessionId && state.token) {
|
|
412
|
-
console.log('[TAS DEBUG] Restoring session from storage');
|
|
413
422
|
// Force PiP mode for restoration to ensure UI consistency
|
|
414
423
|
this.viewModeSubject.next(ViewMode.PIP);
|
|
415
424
|
if (state.businessRole) {
|
|
@@ -418,18 +427,15 @@ class TasService {
|
|
|
418
427
|
this.connectSession(state.sessionId, state.token, containerId, // Use the same container for both since we are in PiP
|
|
419
428
|
containerId, this.currentBusinessRole)
|
|
420
429
|
.then(() => {
|
|
421
|
-
console.log('[TAS DEBUG] Session restored successfully');
|
|
422
430
|
// Clear the disconnected flag if restoration succeeds
|
|
423
431
|
sessionStorage.removeItem(this.DISCONNECTED_FLAG_KEY);
|
|
424
432
|
})
|
|
425
433
|
.catch((err) => {
|
|
426
|
-
console.error('[TAS DEBUG] Failed to restore session:', err);
|
|
427
434
|
this.clearSessionState(); // Clear bad state
|
|
428
435
|
});
|
|
429
436
|
}
|
|
430
437
|
}
|
|
431
438
|
catch (e) {
|
|
432
|
-
console.error('[TAS DEBUG] Error parsing saved session state', e);
|
|
433
439
|
this.clearSessionState();
|
|
434
440
|
}
|
|
435
441
|
}
|
|
@@ -448,19 +454,77 @@ class TasService {
|
|
|
448
454
|
throw error;
|
|
449
455
|
}));
|
|
450
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Generate cache key from request payload
|
|
459
|
+
*/
|
|
460
|
+
getStatusCacheKey(payload) {
|
|
461
|
+
return `${payload.roomType}-${payload.entityId}-${payload.tenant}-${payload.sessionId || ''}`;
|
|
462
|
+
}
|
|
451
463
|
/**
|
|
452
464
|
* PROXY circuit status: /v2/proxy/video/status
|
|
465
|
+
* Uses a 1-second cache to avoid redundant API calls from multiple components
|
|
466
|
+
* Implements request deduplication for simultaneous calls
|
|
453
467
|
*/
|
|
454
468
|
getProxyVideoStatus(payload) {
|
|
455
|
-
|
|
469
|
+
const cacheKey = this.getStatusCacheKey(payload);
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
// 1. Check if we have a valid cached result (less than 1 second old)
|
|
472
|
+
if (this.statusCache &&
|
|
473
|
+
this.statusCache.key === cacheKey &&
|
|
474
|
+
(now - this.statusCache.timestamp) < this.STATUS_CACHE_TTL_MS) {
|
|
475
|
+
// Return cached error if present
|
|
476
|
+
if (this.statusCache.error) {
|
|
477
|
+
return new Observable(observer => {
|
|
478
|
+
observer.error(this.statusCache.error);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
// Return cached response
|
|
482
|
+
if (this.statusCache.response) {
|
|
483
|
+
return new Observable(observer => {
|
|
484
|
+
observer.next(this.statusCache.response);
|
|
485
|
+
observer.complete();
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 2. Check if there is already an inflight request for this key
|
|
490
|
+
if (this.inflightStatusRequests.has(cacheKey)) {
|
|
491
|
+
return this.inflightStatusRequests.get(cacheKey);
|
|
492
|
+
}
|
|
493
|
+
// 3. Make the API call, share it, and cache the Observable
|
|
494
|
+
const request$ = this.httpClient
|
|
456
495
|
.post('v2/proxy/video/status', {
|
|
457
496
|
body: payload,
|
|
458
497
|
headers: {},
|
|
459
498
|
})
|
|
460
|
-
.pipe(map((response) =>
|
|
499
|
+
.pipe(map((response) => {
|
|
500
|
+
const typedResponse = response;
|
|
501
|
+
// Cache successful response
|
|
502
|
+
this.statusCache = {
|
|
503
|
+
key: cacheKey,
|
|
504
|
+
response: typedResponse,
|
|
505
|
+
error: null,
|
|
506
|
+
timestamp: Date.now(),
|
|
507
|
+
};
|
|
508
|
+
return typedResponse;
|
|
509
|
+
}), catchError((error) => {
|
|
461
510
|
console.error('TAS Service: getProxyVideoStatus failed', error);
|
|
511
|
+
// Cache error response
|
|
512
|
+
this.statusCache = {
|
|
513
|
+
key: cacheKey,
|
|
514
|
+
response: null,
|
|
515
|
+
error: error,
|
|
516
|
+
timestamp: Date.now(),
|
|
517
|
+
};
|
|
462
518
|
throw error;
|
|
463
|
-
})
|
|
519
|
+
}),
|
|
520
|
+
// Clean up inflight map when the observable completes or errors
|
|
521
|
+
finalize(() => {
|
|
522
|
+
this.inflightStatusRequests.delete(cacheKey);
|
|
523
|
+
}),
|
|
524
|
+
// Share the result with all subscribers (deduplication)
|
|
525
|
+
shareReplay(1));
|
|
526
|
+
this.inflightStatusRequests.set(cacheKey, request$);
|
|
527
|
+
return request$;
|
|
464
528
|
}
|
|
465
529
|
/**
|
|
466
530
|
* PROXY circuit user modification: /v2/proxy/video/user/modify
|
|
@@ -493,7 +557,17 @@ class TasService {
|
|
|
493
557
|
* @returns Observable of appointment array
|
|
494
558
|
*/
|
|
495
559
|
getAppointments(params) {
|
|
496
|
-
|
|
560
|
+
// Normalize dates to YYYY-MM-DD format (handles ISO timestamps like 2026-01-15T03:00:00.000+0000)
|
|
561
|
+
const normalizeDate = (date) => date.split('T')[0];
|
|
562
|
+
const fromDate = normalizeDate(params.fromDate);
|
|
563
|
+
const toDate = normalizeDate(params.toDate);
|
|
564
|
+
let url;
|
|
565
|
+
// If dates are the same, use only initDate
|
|
566
|
+
// if (fromDate === toDate) {
|
|
567
|
+
// url = `v2/proxy/appointment/agendas/user/appointments?fromDate=${fromDate}&roomType=TAS`;
|
|
568
|
+
// } else {
|
|
569
|
+
url = `v2/proxy/appointment/agendas/user/appointments?fromDate=${fromDate}&toDate=${toDate}&roomType=TAS`;
|
|
570
|
+
// }
|
|
497
571
|
if (params.entityId !== undefined) {
|
|
498
572
|
url += `&entityId=${params.entityId}`;
|
|
499
573
|
}
|
|
@@ -520,7 +594,6 @@ class TasService {
|
|
|
520
594
|
this.processStatusResponse(response);
|
|
521
595
|
},
|
|
522
596
|
error: (err) => {
|
|
523
|
-
console.error('[TAS DEBUG] Status polling error:', err);
|
|
524
597
|
},
|
|
525
598
|
});
|
|
526
599
|
}
|
|
@@ -542,17 +615,18 @@ class TasService {
|
|
|
542
615
|
this.joinableSubject.next(content.joinable);
|
|
543
616
|
// Update activateGeo status
|
|
544
617
|
if (content.activateGeo !== undefined) {
|
|
545
|
-
console.log('[TAS DEBUG] activateGeo received:', content.activateGeo);
|
|
546
618
|
this.activateGeoSubject.next(content.activateGeo);
|
|
547
619
|
}
|
|
548
620
|
// Update geoRequestActive status (owner waiting for user geo response)
|
|
549
|
-
if (content.geoRequestActive !== undefined) {
|
|
550
|
-
console.log('[TAS DEBUG] geoRequestActive received:', content.geoRequestActive);
|
|
621
|
+
if (content.geoRequestActive !== undefined && content.geoRequestActive !== null) {
|
|
551
622
|
this.geoRequestActiveSubject.next(content.geoRequestActive);
|
|
552
623
|
}
|
|
624
|
+
else if (content.geoRequestActive === null) {
|
|
625
|
+
// Reset to false when null
|
|
626
|
+
this.geoRequestActiveSubject.next(false);
|
|
627
|
+
}
|
|
553
628
|
// Update allGeoGranted status (all users responded with geo)
|
|
554
629
|
if (content.allGeoGranted !== undefined) {
|
|
555
|
-
console.log('[TAS DEBUG] allGeoGranted received:', content.allGeoGranted);
|
|
556
630
|
this.allGeoGrantedSubject.next(content.allGeoGranted);
|
|
557
631
|
}
|
|
558
632
|
// Check if owner has joined
|
|
@@ -560,7 +634,6 @@ class TasService {
|
|
|
560
634
|
this.ownerHasJoinedSubject.next(ownerJoined);
|
|
561
635
|
// Detect if owner left: was present, now not present
|
|
562
636
|
if (this.wasOwnerPresent && !ownerJoined) {
|
|
563
|
-
console.log('[TAS DEBUG] Owner has left the session');
|
|
564
637
|
this.ownerHasLeftSubject.next(true);
|
|
565
638
|
}
|
|
566
639
|
if (ownerJoined) {
|
|
@@ -575,6 +648,16 @@ class TasService {
|
|
|
575
648
|
status: RoomUserStatus.WAITING,
|
|
576
649
|
}));
|
|
577
650
|
this.waitingRoomUsersSubject.next(waitingUsers);
|
|
651
|
+
// Extract geo info for USER role participants (for owner panel)
|
|
652
|
+
const userGeoInfo = content.users
|
|
653
|
+
.filter((u) => u.rol === 'USER')
|
|
654
|
+
.map((u) => ({
|
|
655
|
+
userId: u.userId,
|
|
656
|
+
geoStatus: u.geoStatus,
|
|
657
|
+
latitude: u.latitude,
|
|
658
|
+
longitude: u.longitude,
|
|
659
|
+
}));
|
|
660
|
+
this.userGeoInfoSubject.next(userGeoInfo);
|
|
578
661
|
}
|
|
579
662
|
/**
|
|
580
663
|
* Admit a user from the waiting room by changing their status.
|
|
@@ -645,11 +728,9 @@ class TasService {
|
|
|
645
728
|
businessRole: this.currentBusinessRole,
|
|
646
729
|
}).subscribe({
|
|
647
730
|
next: (response) => {
|
|
648
|
-
console.log('[TAS DEBUG] Session finished on disconnect event:', response);
|
|
649
731
|
this.isFinishingSession = false;
|
|
650
732
|
},
|
|
651
733
|
error: (error) => {
|
|
652
|
-
console.error('[TAS DEBUG] Error finishing session on disconnect:', error);
|
|
653
734
|
this.isFinishingSession = false;
|
|
654
735
|
},
|
|
655
736
|
});
|
|
@@ -807,6 +888,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
807
888
|
}]
|
|
808
889
|
}] });
|
|
809
890
|
|
|
891
|
+
/**
|
|
892
|
+
* SVG icons used internally by TAS SDK components.
|
|
893
|
+
* Icons are stored as strings to avoid asset bundling complexity.
|
|
894
|
+
*/
|
|
895
|
+
const TAS_ICONS = {
|
|
896
|
+
home: `<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
897
|
+
<rect x="10" width="100" height="100" rx="50" fill="#44D8E8" fill-opacity="0.2"/>
|
|
898
|
+
<path d="M45 70C43.625 70 42.4479 69.5104 41.4688 68.5313C40.4896 67.5521 40 66.375 40 65V46.5625L37.5 48.5C36.9583 48.9167 36.3438 49.0833 35.6562 49C34.9688 48.9167 34.4167 48.5833 34 48C33.5833 47.4583 33.4271 46.8542 33.5312 46.1875C33.6354 45.5208 33.9583 44.9792 34.5 44.5625L56.9375 27.3125C57.3958 26.9792 57.8854 26.7292 58.4062 26.5625C58.9271 26.3958 59.4583 26.3125 60 26.3125C60.5417 26.3125 61.0729 26.3958 61.5938 26.5625C62.1146 26.7292 62.6042 26.9792 63.0625 27.3125L85.5 44.5C86.0417 44.9167 86.3646 45.4583 86.4688 46.125C86.5729 46.7917 86.4167 47.4167 86 48C85.5833 48.5833 85.0417 48.9063 84.375 48.9688C83.7083 49.0313 83.0833 48.8542 82.5 48.4375L60 31.25L45 42.75V65H50.0625C50.7708 65 51.3542 65.2396 51.8125 65.7188C52.2708 66.1979 52.5 66.7917 52.5 67.5C52.5 68.2083 52.2604 68.8021 51.7812 69.2813C51.3021 69.7604 50.7083 70 50 70H45ZM67.3125 73.9375C66.9792 73.9375 66.6667 73.875 66.375 73.75C66.0833 73.625 65.8125 73.4375 65.5625 73.1875L58.5 66.125C58 65.625 57.75 65.0417 57.75 64.375C57.75 63.7083 58 63.125 58.5 62.625C59 62.125 59.5833 61.875 60.25 61.875C60.9167 61.875 61.5 62.125 62 62.625L67.3125 67.875L79.6875 55.5C80.1875 55 80.7812 54.7604 81.4688 54.7813C82.1562 54.8021 82.75 55.0625 83.25 55.5625C83.75 56.0625 84 56.6458 84 57.3125C84 57.9792 83.75 58.5625 83.25 59.0625L69.0625 73.1875C68.8125 73.4375 68.5417 73.625 68.25 73.75C67.9583 73.875 67.6458 73.9375 67.3125 73.9375Z" fill="white"/>
|
|
899
|
+
<path d="M110 5L106.575 3.425L105 0L103.425 3.425L100 5L103.425 6.575L105 10L106.575 6.575L110 5Z" fill="#44D8E8"/>
|
|
900
|
+
<path d="M10 51L6.575 49.425L5 46L3.425 49.425L0 51L3.425 52.575L5 56L6.575 52.575L10 51Z" fill="#44D8E8"/>
|
|
901
|
+
<path d="M95 43.5L93.2875 42.7125L92.5 41L91.7125 42.7125L90 43.5L91.7125 44.2875L92.5 46L93.2875 44.2875L95 43.5Z" fill="#44D8E8"/>
|
|
902
|
+
<path d="M42 4.5L40.2875 3.7125L39.5 2L38.7125 3.7125L37 4.5L38.7125 5.2875L39.5 7L40.2875 5.2875L42 4.5Z" fill="#44D8E8"/>
|
|
903
|
+
<path d="M88 83.5L86.2875 82.7125L85.5 81L84.7125 82.7125L83 83.5L84.7125 84.2875L85.5 86L86.2875 84.2875L88 83.5Z" fill="#44D8E8"/>
|
|
904
|
+
<path d="M120 97.5L118.287 96.7125L117.5 95L116.713 96.7125L115 97.5L116.713 98.2875L117.5 100L118.287 98.2875L120 97.5Z" fill="#44D8E8"/>
|
|
905
|
+
</svg>`,
|
|
906
|
+
};
|
|
907
|
+
|
|
810
908
|
class TasAvatarComponent {
|
|
811
909
|
constructor() {
|
|
812
910
|
this.name = '';
|
|
@@ -880,11 +978,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
880
978
|
type: Input
|
|
881
979
|
}] } });
|
|
882
980
|
|
|
981
|
+
/** User geolocation panel view states */
|
|
982
|
+
var UserGeoViewState;
|
|
983
|
+
(function (UserGeoViewState) {
|
|
984
|
+
UserGeoViewState["HIDDEN"] = "HIDDEN";
|
|
985
|
+
UserGeoViewState["INITIAL"] = "INITIAL";
|
|
986
|
+
UserGeoViewState["VERIFYING"] = "VERIFYING";
|
|
987
|
+
UserGeoViewState["VERIFIED"] = "VERIFIED";
|
|
988
|
+
UserGeoViewState["DENIED"] = "DENIED";
|
|
989
|
+
})(UserGeoViewState || (UserGeoViewState = {}));
|
|
883
990
|
class TasVideocallComponent {
|
|
884
|
-
constructor(activeModal, tasService, geolocationService) {
|
|
991
|
+
constructor(activeModal, tasService, geolocationService, sanitizer) {
|
|
885
992
|
this.activeModal = activeModal;
|
|
886
993
|
this.tasService = tasService;
|
|
887
994
|
this.geolocationService = geolocationService;
|
|
995
|
+
this.sanitizer = sanitizer;
|
|
888
996
|
this.participantName = '';
|
|
889
997
|
this.tenant = '';
|
|
890
998
|
this.businessRole = TasBusinessRole.USER;
|
|
@@ -902,11 +1010,22 @@ class TasVideocallComponent {
|
|
|
902
1010
|
// Geo panel states for owner
|
|
903
1011
|
this.geoRequestActive = false; // Owner sent request, waiting for user
|
|
904
1012
|
this.allGeoGranted = false; // All users responded with geo
|
|
1013
|
+
this.userGeoInfo = []; // Individual user geo status
|
|
1014
|
+
// User geo panel state (for owners)
|
|
1015
|
+
this.userGeoViewState = UserGeoViewState.HIDDEN;
|
|
1016
|
+
this.UserGeoViewState = UserGeoViewState; // Expose enum to template
|
|
1017
|
+
this.devModeEnabled = false; // Enable dev controls for testing
|
|
1018
|
+
this.geoPanelDismissed = false; // Track if owner manually closed the panel
|
|
905
1019
|
this.subscriptions = new Subscription();
|
|
1020
|
+
this.homeIcon = this.sanitizer.bypassSecurityTrustHtml(TAS_ICONS.home);
|
|
906
1021
|
}
|
|
907
1022
|
ngOnInit() {
|
|
908
1023
|
this.setupSubscriptions();
|
|
909
1024
|
this.initializeCall();
|
|
1025
|
+
// For owners: show the geo panel to request user location (unless dismissed)
|
|
1026
|
+
if (this.canAdmitUsers && !this.geoPanelDismissed) {
|
|
1027
|
+
this.userGeoViewState = UserGeoViewState.INITIAL;
|
|
1028
|
+
}
|
|
910
1029
|
}
|
|
911
1030
|
ngAfterViewInit() {
|
|
912
1031
|
this.initInteract();
|
|
@@ -948,6 +1067,43 @@ class TasVideocallComponent {
|
|
|
948
1067
|
this.businessRole === TasBusinessRole.ADMIN_MANAGER ||
|
|
949
1068
|
this.businessRole === TasBusinessRole.MANAGER);
|
|
950
1069
|
}
|
|
1070
|
+
/** Users with pending geo status */
|
|
1071
|
+
get pendingUsers() {
|
|
1072
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.PENDING);
|
|
1073
|
+
}
|
|
1074
|
+
/** Users who granted geo */
|
|
1075
|
+
get grantedUsers() {
|
|
1076
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1077
|
+
}
|
|
1078
|
+
/** Users who denied geo */
|
|
1079
|
+
get deniedUsers() {
|
|
1080
|
+
return this.userGeoInfo.filter(u => u.geoStatus === GeoStatus.DENIED);
|
|
1081
|
+
}
|
|
1082
|
+
/** Show location panel only if owner and there are users who haven't granted */
|
|
1083
|
+
get shouldShowLocationPanel() {
|
|
1084
|
+
if (!this.canAdmitUsers || this.userGeoInfo.length === 0) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
// Hide if all users have granted
|
|
1088
|
+
const allGranted = this.userGeoInfo.every(u => u.geoStatus === GeoStatus.GRANTED);
|
|
1089
|
+
return !allGranted;
|
|
1090
|
+
}
|
|
1091
|
+
/** Check if any user has denied geo */
|
|
1092
|
+
get hasAnyDenied() {
|
|
1093
|
+
return this.deniedUsers.length > 0;
|
|
1094
|
+
}
|
|
1095
|
+
/** Show user geo panel for owners when not hidden */
|
|
1096
|
+
get shouldShowUserGeoPanel() {
|
|
1097
|
+
return this.canAdmitUsers && this.userGeoViewState !== UserGeoViewState.HIDDEN;
|
|
1098
|
+
}
|
|
1099
|
+
/** Set user geo view state (for dev controls or close button) */
|
|
1100
|
+
setUserGeoViewState(state) {
|
|
1101
|
+
this.userGeoViewState = state;
|
|
1102
|
+
// Track if owner manually dismissed the panel
|
|
1103
|
+
if (state === UserGeoViewState.HIDDEN) {
|
|
1104
|
+
this.geoPanelDismissed = true;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
951
1107
|
/**
|
|
952
1108
|
* Admit a user from the waiting room
|
|
953
1109
|
*/
|
|
@@ -986,7 +1142,7 @@ class TasVideocallComponent {
|
|
|
986
1142
|
this.showLocationPanel = false;
|
|
987
1143
|
}
|
|
988
1144
|
/**
|
|
989
|
-
* Request the user to share their location
|
|
1145
|
+
* Request the user to share their location (called by owner)
|
|
990
1146
|
*/
|
|
991
1147
|
requestUserLocation() {
|
|
992
1148
|
if (!this.videoCallId) {
|
|
@@ -997,9 +1153,12 @@ class TasVideocallComponent {
|
|
|
997
1153
|
videoCallId: this.videoCallId,
|
|
998
1154
|
action: UserCallAction.REQUEST_GEOLOCALIZATION,
|
|
999
1155
|
};
|
|
1000
|
-
// TODO: Send location request action to backend when endpoint is ready
|
|
1001
1156
|
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1002
|
-
next: () =>
|
|
1157
|
+
next: () => {
|
|
1158
|
+
console.log('Location request sent');
|
|
1159
|
+
// Set panel to verifying state while waiting for user response
|
|
1160
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1161
|
+
},
|
|
1003
1162
|
error: (err) => console.error('Error requesting location:', err),
|
|
1004
1163
|
});
|
|
1005
1164
|
}
|
|
@@ -1036,7 +1195,6 @@ class TasVideocallComponent {
|
|
|
1036
1195
|
// Owner left subscription - disconnect non-owners
|
|
1037
1196
|
this.subscriptions.add(this.tasService.ownerHasLeft$.subscribe((hasLeft) => {
|
|
1038
1197
|
if (hasLeft && !this.canAdmitUsers) { // Non-owner user
|
|
1039
|
-
console.log('[TAS DEBUG] Owner left, disconnecting user');
|
|
1040
1198
|
this.hangUp();
|
|
1041
1199
|
this.activeModal.close('owner_left');
|
|
1042
1200
|
}
|
|
@@ -1044,61 +1202,74 @@ class TasVideocallComponent {
|
|
|
1044
1202
|
// ActivateGeo subscription - only for non-owners (users)
|
|
1045
1203
|
this.subscriptions.add(this.tasService.activateGeo$.subscribe((activateGeo) => {
|
|
1046
1204
|
if (activateGeo && !this.canAdmitUsers) {
|
|
1047
|
-
console.log('[TAS DEBUG] activateGeo=true, checking geo status for user...');
|
|
1048
1205
|
this.handleActivateGeo();
|
|
1049
1206
|
}
|
|
1050
1207
|
}));
|
|
1051
1208
|
// GeoRequestActive subscription - for owners
|
|
1052
1209
|
this.subscriptions.add(this.tasService.geoRequestActive$.subscribe((active) => {
|
|
1053
|
-
console.log('[TAS DEBUG] geoRequestActive changed:', active);
|
|
1054
1210
|
this.geoRequestActive = active;
|
|
1055
1211
|
}));
|
|
1056
|
-
// AllGeoGranted subscription - for owners
|
|
1212
|
+
// AllGeoGranted subscription - for owners to update panel state
|
|
1057
1213
|
this.subscriptions.add(this.tasService.allGeoGranted$.subscribe((granted) => {
|
|
1058
|
-
console.log('[TAS DEBUG] allGeoGranted changed:', granted);
|
|
1059
1214
|
this.allGeoGranted = granted;
|
|
1215
|
+
// For owners: update panel state based on geo status
|
|
1216
|
+
if (this.canAdmitUsers && granted) {
|
|
1217
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1218
|
+
}
|
|
1219
|
+
}));
|
|
1220
|
+
// UserGeoInfo subscription - for owner geo panel
|
|
1221
|
+
this.subscriptions.add(this.tasService.userGeoInfo$.subscribe((info) => {
|
|
1222
|
+
this.userGeoInfo = info;
|
|
1223
|
+
// Note: We don't auto-switch to DENIED here - that only happens
|
|
1224
|
+
// after owner requests and user denies (via geoRequestActive flow)
|
|
1060
1225
|
}));
|
|
1061
1226
|
}
|
|
1062
1227
|
/**
|
|
1063
1228
|
* Handle activateGeo request from backend (for non-owner users).
|
|
1064
|
-
*
|
|
1229
|
+
* Directly prompts browser for geolocation - no panel for users.
|
|
1065
1230
|
*/
|
|
1066
1231
|
async handleActivateGeo() {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1232
|
+
// Clear any cached position to force re-prompting
|
|
1233
|
+
this.geolocationService.clearCache();
|
|
1234
|
+
// Request geolocation from user (browser will prompt)
|
|
1235
|
+
const position = await this.geolocationService.getCurrentPosition();
|
|
1236
|
+
if (position) {
|
|
1072
1237
|
this.geoLocationStatus = 'active';
|
|
1073
|
-
this.reportGeoStatus(
|
|
1074
|
-
|
|
1238
|
+
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
this.geoLocationStatus = 'denied';
|
|
1242
|
+
this.denyGeoLocation();
|
|
1075
1243
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1244
|
+
}
|
|
1245
|
+
/** Start geolocation verification (called from template button) */
|
|
1246
|
+
async startGeoVerification() {
|
|
1247
|
+
this.userGeoViewState = UserGeoViewState.VERIFYING;
|
|
1248
|
+
// Clear any cached position to force re-prompting
|
|
1249
|
+
this.geolocationService.clearCache();
|
|
1250
|
+
// Request geolocation from user (browser will prompt)
|
|
1078
1251
|
const position = await this.geolocationService.getCurrentPosition();
|
|
1079
1252
|
if (position) {
|
|
1080
|
-
console.log('[TAS DEBUG] Geolocation obtained:', position);
|
|
1081
1253
|
this.geoLocationStatus = 'active';
|
|
1254
|
+
this.userGeoViewState = UserGeoViewState.VERIFIED;
|
|
1082
1255
|
this.reportGeoStatus(position.latitude, position.longitude);
|
|
1083
1256
|
}
|
|
1084
1257
|
else {
|
|
1085
|
-
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1086
1258
|
this.geoLocationStatus = 'denied';
|
|
1087
|
-
|
|
1088
|
-
this.
|
|
1259
|
+
this.userGeoViewState = UserGeoViewState.DENIED;
|
|
1260
|
+
this.denyGeoLocation();
|
|
1089
1261
|
}
|
|
1090
1262
|
}
|
|
1091
1263
|
/**
|
|
1092
|
-
* Report geolocation
|
|
1264
|
+
* Report granted geolocation to backend.
|
|
1265
|
+
* IMPORTANT: Only call with valid coordinates.
|
|
1093
1266
|
*/
|
|
1094
1267
|
reportGeoStatus(latitude, longitude) {
|
|
1095
1268
|
if (!this.videoCallId) {
|
|
1096
|
-
console.error('[TAS DEBUG] Cannot report geo status: videoCallId not set');
|
|
1097
1269
|
return;
|
|
1098
1270
|
}
|
|
1099
|
-
//
|
|
1100
|
-
if (
|
|
1101
|
-
console.error('[TAS DEBUG] Cannot report geo status: geoLocationStatus not set');
|
|
1271
|
+
// Validate coordinates are present
|
|
1272
|
+
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
|
|
1102
1273
|
return;
|
|
1103
1274
|
}
|
|
1104
1275
|
const body = {
|
|
@@ -1108,11 +1279,21 @@ class TasVideocallComponent {
|
|
|
1108
1279
|
latitude,
|
|
1109
1280
|
longitude,
|
|
1110
1281
|
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1282
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Report denied geolocation to backend.
|
|
1286
|
+
*/
|
|
1287
|
+
denyGeoLocation() {
|
|
1288
|
+
if (!this.videoCallId) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const body = {
|
|
1292
|
+
userId: this.userId,
|
|
1293
|
+
videoCallId: this.videoCallId,
|
|
1294
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1295
|
+
};
|
|
1296
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({});
|
|
1116
1297
|
}
|
|
1117
1298
|
initializeCall() {
|
|
1118
1299
|
if (this.isReturningFromPip) {
|
|
@@ -1201,12 +1382,12 @@ class TasVideocallComponent {
|
|
|
1201
1382
|
});
|
|
1202
1383
|
}
|
|
1203
1384
|
}
|
|
1204
|
-
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1205
|
-
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", userId: "userId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1385
|
+
TasVideocallComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }, { token: i4.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
|
|
1386
|
+
TasVideocallComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasVideocallComponent, selector: "tas-videocall", inputs: { sessionId: "sessionId", token: "token", appointmentId: "appointmentId", videoCallId: "videoCallId", userId: "userId", participantName: "participantName", tenant: "tenant", businessRole: "businessRole", isReturningFromPip: "isReturningFromPip" }, viewQueries: [{ propertyName: "publisherContainer", first: true, predicate: ["publisherContainer"], descendants: true }, { propertyName: "subscriberContainer", first: true, predicate: ["subscriberContainer"], descendants: true }], ngImport: i0, template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Owner geolocation panel (for owners to request user location) -->\n <div class=\"user-geo-panel\" *ngIf=\"shouldShowUserGeoPanel || devModeEnabled\">\n <div class=\"user-geo-header\">\n <div class=\"header-title-row\">\n <i class=\"fa fa-map-marker header-icon\" *ngIf=\"userGeoViewState !== UserGeoViewState.INITIAL\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n </div>\n <button class=\"close-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n\n <div class=\"user-geo-description\" *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n\n <div class=\"user-geo-content\">\n <!-- INITIAL state: just the button -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.INITIAL\">\n <!-- spacer -->\n </ng-container>\n\n <!-- VERIFYING state: spinner + loading message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFYING\">\n <div class=\"user-geo-verifying\">\n <div class=\"geo-spinner-large\"></div>\n <p class=\"verifying-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"verifying-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n </ng-container>\n\n <!-- VERIFIED state: home icon with sparkles -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFIED\">\n <div class=\"user-geo-verified\">\n <div class=\"verified-icon-container\">\n <span class=\"home-icon\" [innerHTML]=\"homeIcon\"></span>\n </div>\n <p class=\"verified-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n\n <!-- DENIED state: error icon and message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.DENIED\">\n <div class=\"user-geo-denied\">\n <div class=\"denied-icon-container\">\n <i class=\"fa fa-times-circle\"></i>\n </div>\n <p class=\"denied-title\">La ubicaci\u00F3n fue rechazada</p>\n <p class=\"denied-subtitle\">El colaborador no permiti\u00F3 el acceso a su ubicaci\u00F3n.</p>\n </div>\n </ng-container>\n </div>\n\n <!-- Button (hidden in verified and denied states) -->\n <button \n class=\"user-geo-btn\" \n *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\"\n [class.disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n [disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n\n <!-- Dev controls -->\n <div class=\"dev-controls\" *ngIf=\"devModeEnabled\">\n <span class=\"dev-label\">Dev:</span>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.INITIAL)\">Initial</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFYING)\">Verifying</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFIED)\">Verified</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.DENIED)\">Denied</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\">Hide</button>\n </div>\n </div>\n</div>\n\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:transparent;cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px)}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-user-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:200px;overflow-y:auto}.location-panel .location-user-list .user-geo-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:rgba(255,255,255,.05);border-radius:6px}.location-panel .location-user-list .user-geo-item .user-icon{color:#fff9;font-size:14px}.location-panel .location-user-list .user-geo-item .user-name{flex:1;font-size:14px;color:#fff}.location-panel .location-user-list .user-geo-item .geo-status{display:flex;align-items:center;gap:.35rem;font-size:12px;padding:2px 8px;border-radius:12px}.location-panel .location-user-list .user-geo-item .geo-status.pending{color:#ffc107;background:rgba(255,193,7,.15)}.location-panel .location-user-list .user-geo-item .geo-status.granted{color:#4caf50;background:rgba(76,175,80,.15)}.location-panel .location-user-list .user-geo-item .geo-status.denied{color:#f44336;background:rgba(244,67,54,.15)}.location-panel .location-user-list .user-geo-item .geo-status i{font-size:10px}.location-panel .geo-denied-warning{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(244,67,54,.15);border-radius:6px;margin-bottom:1rem;color:#f44336;font-size:13px}.location-panel .geo-denied-warning i{font-size:14px}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}.user-geo-panel{width:360px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.user-geo-panel .user-geo-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2.5rem;padding-top:2rem}.user-geo-panel .user-geo-header .header-title-row{display:flex;align-items:center;gap:.5rem}.user-geo-panel .user-geo-header .header-title-row .header-icon{font-size:16px;color:#fff}.user-geo-panel .user-geo-header .header-title-row h3{font-size:18px;font-weight:600;margin:0;color:#fff}.user-geo-panel .user-geo-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.user-geo-panel .user-geo-header .close-btn:hover{opacity:1}.user-geo-panel .user-geo-description{font-size:14px;color:#fffc;line-height:1.6;margin-bottom:1rem}.user-geo-panel .user-geo-description p{margin:0 0 .75rem}.user-geo-panel .user-geo-content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}.user-geo-panel .user-geo-verifying{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verifying .geo-spinner-large{width:80px;height:80px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite;margin-bottom:1.5rem}.user-geo-panel .user-geo-verifying .verifying-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .25rem}.user-geo-panel .user-geo-verifying .verifying-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-verified{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verified .verified-icon-container{margin-bottom:1rem}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon{display:block}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon svg{width:120px;height:100px}.user-geo-panel .user-geo-verified .verified-title{color:#fff;font-size:16px;font-weight:600;margin:0}.user-geo-panel .user-geo-denied{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-denied .denied-icon-container{width:100px;height:100px;border-radius:50%;background:rgba(244,67,54,.2);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.user-geo-panel .user-geo-denied .denied-icon-container i{font-size:48px;color:#f44336}.user-geo-panel .user-geo-denied .denied-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .5rem}.user-geo-panel .user-geo-denied .denied-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-btn{width:100%;padding:14px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-top:auto;margin-bottom:1rem}.user-geo-panel .user-geo-btn:hover:not(.disabled){background:#178e99}.user-geo-panel .user-geo-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.user-geo-panel .dev-controls{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(0,0,0,.3);border-radius:6px;margin-bottom:1rem;flex-wrap:wrap}.user-geo-panel .dev-controls .dev-label{font-size:12px;color:#fff9;font-weight:600}.user-geo-panel .dev-controls .dev-btn{padding:4px 8px;font-size:11px;background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.user-geo-panel .dev-controls .dev-btn:hover{background:rgba(255,255,255,.2)}.user-geo-panel .user-geo-footer{display:flex;justify-content:center;gap:.75rem;padding-top:.5rem}.user-geo-panel .user-geo-footer .footer-icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff9;font-size:16px}.user-geo-panel .user-geo-footer .footer-icon.active{background:var(--Primary-Uell, #1da4b1);color:#fff}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1206
1387
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasVideocallComponent, decorators: [{
|
|
1207
1388
|
type: Component,
|
|
1208
|
-
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-video-camera\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n <button\n class=\"btn control-btn more-btn\"\n title=\"M\u00E1s opciones\"\n aria-label=\"M\u00E1s opciones\"\n >\n <i class=\"fa fa-ellipsis-v\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Location panel (shown for owners when user hasn't allowed location) -->\n <div class=\"location-panel\" *ngIf=\"showLocationPanel && canAdmitUsers\">\n <div class=\"location-header\">\n <i class=\"fa fa-map-marker header-icon\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n <button class=\"close-btn\" (click)=\"closeLocationPanel()\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n <div class=\"location-description\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n <div class=\"location-content\">\n <!-- Initial state: Show verify button -->\n <ng-container *ngIf=\"!geoRequestActive && !allGeoGranted\">\n <button class=\"verify-location-btn\" (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Loading state: Spinner while waiting for user response -->\n <ng-container *ngIf=\"geoRequestActive && !allGeoGranted\">\n <div class=\"geo-loading-container\">\n <div class=\"geo-spinner\"></div>\n <p class=\"loading-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"loading-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n <button class=\"verify-location-btn disabled\" disabled>\n Verificar ubicaci\u00F3n\n </button>\n </ng-container>\n\n <!-- Success state: Location verified -->\n <ng-container *ngIf=\"allGeoGranted\">\n <div class=\"geo-success-container\">\n <div class=\"success-icon\">\n <i class=\"fa fa-check\"></i>\n </div>\n <p class=\"success-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n </div>\n <div class=\"location-footer\">\n <span class=\"footer-icon\"><i class=\"fa fa-clock-o\"></i></span>\n <span class=\"footer-icon location-icon\"><i class=\"fa fa-map-marker\"></i></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:var(--Primary-Uell, #1da4b1);cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .control-btn.muted{background:#f39c12}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn{background:var(--Primary-Uell, #1da4b1)}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover{background:#178e99}.tas-videocall-container .controls-container .more-btn{background:rgba(255,255,255,.2)}.tas-videocall-container .controls-container .more-btn:hover{background:rgba(255,255,255,.35)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px);box-shadow:0 4px 12px #0003}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
1209
|
-
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }]; }, propDecorators: { sessionId: [{
|
|
1389
|
+
args: [{ selector: 'tas-videocall', template: "<div class=\"tas-videocall-wrapper\">\n <div class=\"tas-videocall-container\">\n <!-- Subscriber video (large, background) -->\n <div\n id=\"subscriber-container\"\n [class.subscriber-view]=\"isPublisherSmall\"\n [class.publisher-view]=\"!isPublisherSmall\"\n #subscriberContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Publisher video (small, overlay) -->\n <div\n id=\"publisher-container\"\n [class.publisher-view]=\"isPublisherSmall\"\n [class.subscriber-view]=\"!isPublisherSmall\"\n #publisherContainer\n (dblclick)=\"onDoubleClick()\"\n ></div>\n\n <!-- Centered avatar (shown when no video stream) -->\n <div class=\"avatar-container\" *ngIf=\"!hasVideoStream\">\n <tas-avatar [name]=\"participantName\" [size]=\"80\"></tas-avatar>\n </div>\n\n <!-- Controls -->\n <div class=\"controls-container\">\n <button\n class=\"btn control-btn mute-btn\"\n [class.muted]=\"isMuted\"\n (click)=\"toggleMute()\"\n [title]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n [attr.aria-label]=\"isMuted ? 'Activar micr\u00F3fono' : 'Silenciar micr\u00F3fono'\"\n >\n <i\n class=\"fa\"\n [class.fa-microphone]=\"!isMuted\"\n [class.fa-microphone-slash]=\"isMuted\"\n ></i>\n </button>\n <button\n class=\"btn control-btn swap-btn\"\n (click)=\"toggleSwap()\"\n title=\"Intercambiar vista\"\n aria-label=\"Intercambiar vista\"\n >\n <i class=\"fa fa-refresh\"></i>\n </button>\n <button\n class=\"btn control-btn pip-btn\"\n (click)=\"minimize()\"\n title=\"Minimizar (Picture in Picture)\"\n aria-label=\"Minimizar videollamada\"\n >\n <i class=\"fa fa-compress\"></i>\n </button>\n <button\n class=\"btn control-btn hangup-btn\"\n (click)=\"hangUp()\"\n title=\"Finalizar llamada\"\n aria-label=\"Finalizar llamada\"\n >\n <i class=\"fa fa-phone\"></i>\n </button>\n </div>\n\n <!-- Waiting room notification (shown for OWNER/BACKOFFICE only) -->\n <div\n class=\"waiting-notification\"\n *ngIf=\"waitingRoomUsers.length > 0 && canAdmitUsers\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <span class=\"waiting-text\">\n {{ waitingRoomUsers[0].name }} est\u00E1 en la sala de espera.\n </span>\n <button\n class=\"admit-btn\"\n (click)=\"admitUser(waitingRoomUsers[0].userId)\"\n aria-label=\"Admitir usuario\"\n >\n Admitir\n </button>\n <button\n class=\"dismiss-btn\"\n (click)=\"dismissWaitingNotification(waitingRoomUsers[0].userId)\"\n aria-label=\"Cerrar notificaci\u00F3n\"\n >\n \u00D7\n </button>\n </div>\n </div>\n\n <!-- Owner geolocation panel (for owners to request user location) -->\n <div class=\"user-geo-panel\" *ngIf=\"shouldShowUserGeoPanel || devModeEnabled\">\n <div class=\"user-geo-header\">\n <div class=\"header-title-row\">\n <i class=\"fa fa-map-marker header-icon\" *ngIf=\"userGeoViewState !== UserGeoViewState.INITIAL\"></i>\n <h3>Ubicaci\u00F3n del colaborador</h3>\n </div>\n <button class=\"close-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\" aria-label=\"Cerrar\">\u00D7</button>\n </div>\n\n <div class=\"user-geo-description\" *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\">\n <p>El colaborador tiene la ubicaci\u00F3n desactivada, solicita que la active.</p>\n <p>Esta acci\u00F3n nos permitir\u00E1 disponibilizar algunas alertas.</p>\n </div>\n\n <div class=\"user-geo-content\">\n <!-- INITIAL state: just the button -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.INITIAL\">\n <!-- spacer -->\n </ng-container>\n\n <!-- VERIFYING state: spinner + loading message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFYING\">\n <div class=\"user-geo-verifying\">\n <div class=\"geo-spinner-large\"></div>\n <p class=\"verifying-title\">Verificando ubicaci\u00F3n...</p>\n <p class=\"verifying-subtitle\">Esto puede tardar unos segundos.</p>\n </div>\n </ng-container>\n\n <!-- VERIFIED state: home icon with sparkles -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.VERIFIED\">\n <div class=\"user-geo-verified\">\n <div class=\"verified-icon-container\">\n <span class=\"home-icon\" [innerHTML]=\"homeIcon\"></span>\n </div>\n <p class=\"verified-title\">La ubicaci\u00F3n fue verificada</p>\n </div>\n </ng-container>\n\n <!-- DENIED state: error icon and message -->\n <ng-container *ngIf=\"userGeoViewState === UserGeoViewState.DENIED\">\n <div class=\"user-geo-denied\">\n <div class=\"denied-icon-container\">\n <i class=\"fa fa-times-circle\"></i>\n </div>\n <p class=\"denied-title\">La ubicaci\u00F3n fue rechazada</p>\n <p class=\"denied-subtitle\">El colaborador no permiti\u00F3 el acceso a su ubicaci\u00F3n.</p>\n </div>\n </ng-container>\n </div>\n\n <!-- Button (hidden in verified and denied states) -->\n <button \n class=\"user-geo-btn\" \n *ngIf=\"userGeoViewState !== UserGeoViewState.VERIFIED && userGeoViewState !== UserGeoViewState.DENIED\"\n [class.disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n [disabled]=\"userGeoViewState === UserGeoViewState.VERIFYING\"\n (click)=\"requestUserLocation()\">\n Verificar ubicaci\u00F3n\n </button>\n\n <!-- Dev controls -->\n <div class=\"dev-controls\" *ngIf=\"devModeEnabled\">\n <span class=\"dev-label\">Dev:</span>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.INITIAL)\">Initial</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFYING)\">Verifying</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.VERIFIED)\">Verified</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.DENIED)\">Denied</button>\n <button class=\"dev-btn\" (click)=\"setUserGeoViewState(UserGeoViewState.HIDDEN)\">Hide</button>\n </div>\n </div>\n</div>\n\n", styles: ["@charset \"UTF-8\";:host{display:flex;width:100vw;height:100vh;box-sizing:border-box;padding:2rem;background:linear-gradient(281deg,rgba(29,164,177,.2) 6.96%,rgba(0,0,0,0) 70.44%),#212532}.tas-videocall-wrapper{display:flex;flex:1;gap:1rem;height:100%}.tas-videocall-container{position:relative;flex:1;height:100%;overflow:hidden;border-radius:8px;border:1px solid var(--Neutral-GreyLight, #dadfe9);background:linear-gradient(180deg,#e5f1f7 0%,#0072ac 100%)}.tas-videocall-container ::ng-deep .OT_edge-bar-item,.tas-videocall-container ::ng-deep .OT_mute,.tas-videocall-container ::ng-deep .OT_audio-level-meter,.tas-videocall-container ::ng-deep .OT_bar,.tas-videocall-container ::ng-deep .OT_name{display:none!important}.tas-videocall-container .subscriber-view{width:100%;height:100%;z-index:1}.tas-videocall-container .publisher-view{position:absolute;top:20px;right:20px;width:200px;height:150px;z-index:2;border:2px solid #fff;border-radius:8px;background-color:#0000004d;overflow:hidden}.tas-videocall-container .avatar-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1;display:flex;align-items:center;justify-content:center}.tas-videocall-container .controls-container{display:flex;flex-direction:row;gap:12px;position:absolute;bottom:30px;left:50%;transform:translate(-50%);z-index:3;background-color:#33475bb3;padding:12px 20px;border-radius:30px;backdrop-filter:blur(8px)}.tas-videocall-container .controls-container .control-btn{width:44px;height:44px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:18px;border:none;background:transparent;cursor:pointer;transition:all .2s ease}.tas-videocall-container .controls-container .control-btn i{color:#fff}.tas-videocall-container .controls-container .control-btn:hover{transform:scale(1.05);filter:brightness(1.1)}.tas-videocall-container .controls-container .control-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .controls-container .hangup-btn{background:#f44336}.tas-videocall-container .controls-container .hangup-btn i{transform:rotate(135deg)}.tas-videocall-container .controls-container .hangup-btn:hover{background:#d32f2f}.tas-videocall-container .controls-container .swap-btn,.tas-videocall-container .controls-container .pip-btn,.tas-videocall-container .controls-container .mute-btn{background:transparent}.tas-videocall-container .controls-container .swap-btn:hover,.tas-videocall-container .controls-container .pip-btn:hover,.tas-videocall-container .controls-container .mute-btn:hover{background:rgba(255,255,255,.15)}.tas-videocall-container .waiting-notification{position:absolute;bottom:100px;left:16px;display:flex;align-items:center;gap:12px;background-color:#33475be6;padding:10px 16px;border-radius:8px;z-index:4;backdrop-filter:blur(4px);max-width:calc(100% - 32px)}.tas-videocall-container .waiting-notification .waiting-text{color:#fff;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tas-videocall-container .waiting-notification .admit-btn{background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:4px;padding:6px 16px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;white-space:nowrap}.tas-videocall-container .waiting-notification .admit-btn:hover{background:#178e99}.tas-videocall-container .waiting-notification .admit-btn:focus{outline:2px solid #fff;outline-offset:2px}.tas-videocall-container .waiting-notification .dismiss-btn{background:transparent;color:#fff;border:none;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;opacity:.7;transition:opacity .2s ease}.tas-videocall-container .waiting-notification .dismiss-btn:hover{opacity:1}.tas-videocall-container .waiting-notification .dismiss-btn:focus{outline:2px solid #fff;outline-offset:2px}.location-panel{width:280px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.location-panel .location-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.location-panel .location-header h3{font-size:16px;font-weight:600;margin:0;color:#fff}.location-panel .location-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.location-panel .location-header .close-btn:hover{opacity:1}.location-panel .location-description{font-size:14px;color:#fffc;line-height:1.5;margin-bottom:.5rem}.location-panel .location-description p{margin:0 0 .5rem}.location-panel .location-user-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;max-height:200px;overflow-y:auto}.location-panel .location-user-list .user-geo-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:rgba(255,255,255,.05);border-radius:6px}.location-panel .location-user-list .user-geo-item .user-icon{color:#fff9;font-size:14px}.location-panel .location-user-list .user-geo-item .user-name{flex:1;font-size:14px;color:#fff}.location-panel .location-user-list .user-geo-item .geo-status{display:flex;align-items:center;gap:.35rem;font-size:12px;padding:2px 8px;border-radius:12px}.location-panel .location-user-list .user-geo-item .geo-status.pending{color:#ffc107;background:rgba(255,193,7,.15)}.location-panel .location-user-list .user-geo-item .geo-status.granted{color:#4caf50;background:rgba(76,175,80,.15)}.location-panel .location-user-list .user-geo-item .geo-status.denied{color:#f44336;background:rgba(244,67,54,.15)}.location-panel .location-user-list .user-geo-item .geo-status i{font-size:10px}.location-panel .geo-denied-warning{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(244,67,54,.15);border-radius:6px;margin-bottom:1rem;color:#f44336;font-size:13px}.location-panel .geo-denied-warning i{font-size:14px}.location-panel .location-content{flex:1;display:flex;flex-direction:column;justify-content:flex-end}.location-panel .verify-location-btn{width:100%;padding:12px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s ease;margin-bottom:1rem}.location-panel .verify-location-btn:hover{background:#178e99}.location-panel .verify-location-btn:disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.location-panel .location-footer{display:flex;justify-content:flex-end;gap:.5rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.1)}.location-panel .location-footer .footer-icon{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff;font-size:14px}.location-panel .location-footer .footer-icon.location-icon{background:var(--Primary-Uell, #1da4b1)}.location-panel .header-icon{color:#fff;font-size:16px}.location-panel .geo-loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-loading-container .geo-spinner{width:64px;height:64px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite}.location-panel .geo-loading-container .loading-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 .25rem}.location-panel .geo-loading-container .loading-subtitle{color:#ffffffb3;font-size:14px;margin:0}.location-panel .geo-success-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 0}.location-panel .geo-success-container .success-icon{width:80px;height:80px;border-radius:50%;background:var(--Primary-Uell, #1da4b1);display:flex;align-items:center;justify-content:center;position:relative}.location-panel .geo-success-container .success-icon i{color:#fff;font-size:32px}.location-panel .geo-success-container .success-icon:before,.location-panel .geo-success-container .success-icon:after{content:\"\\2726\";position:absolute;color:#fff;font-size:10px}.location-panel .geo-success-container .success-icon:before{top:-8px;right:-4px}.location-panel .geo-success-container .success-icon:after{bottom:-4px;left:-8px}.location-panel .geo-success-container .success-title{color:#fff;font-size:16px;font-weight:600;margin:1.5rem 0 0}.location-panel .verify-location-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}@keyframes geo-spin{to{transform:rotate(360deg)}}.user-geo-panel{width:360px;height:100%;background:#212532;border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;color:#fff}.user-geo-panel .user-geo-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:2.5rem;padding-top:2rem}.user-geo-panel .user-geo-header .header-title-row{display:flex;align-items:center;gap:.5rem}.user-geo-panel .user-geo-header .header-title-row .header-icon{font-size:16px;color:#fff}.user-geo-panel .user-geo-header .header-title-row h3{font-size:18px;font-weight:600;margin:0;color:#fff}.user-geo-panel .user-geo-header .close-btn{background:transparent;border:none;color:#fff;font-size:20px;cursor:pointer;padding:0;line-height:1;opacity:.7}.user-geo-panel .user-geo-header .close-btn:hover{opacity:1}.user-geo-panel .user-geo-description{font-size:14px;color:#fffc;line-height:1.6;margin-bottom:1rem}.user-geo-panel .user-geo-description p{margin:0 0 .75rem}.user-geo-panel .user-geo-content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}.user-geo-panel .user-geo-verifying{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verifying .geo-spinner-large{width:80px;height:80px;border:4px solid rgba(29,164,177,.2);border-top-color:var(--Primary-Uell, #1da4b1);border-radius:50%;animation:geo-spin 1s linear infinite;margin-bottom:1.5rem}.user-geo-panel .user-geo-verifying .verifying-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .25rem}.user-geo-panel .user-geo-verifying .verifying-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-verified{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-verified .verified-icon-container{margin-bottom:1rem}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon{display:block}.user-geo-panel .user-geo-verified .verified-icon-container .home-icon svg{width:120px;height:100px}.user-geo-panel .user-geo-verified .verified-title{color:#fff;font-size:16px;font-weight:600;margin:0}.user-geo-panel .user-geo-denied{display:flex;flex-direction:column;align-items:center;text-align:center}.user-geo-panel .user-geo-denied .denied-icon-container{width:100px;height:100px;border-radius:50%;background:rgba(244,67,54,.2);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.user-geo-panel .user-geo-denied .denied-icon-container i{font-size:48px;color:#f44336}.user-geo-panel .user-geo-denied .denied-title{color:#fff;font-size:16px;font-weight:600;margin:0 0 .5rem}.user-geo-panel .user-geo-denied .denied-subtitle{color:#fff9;font-size:14px;margin:0}.user-geo-panel .user-geo-btn{width:100%;padding:14px 24px;background:var(--Primary-Uell, #1da4b1);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-top:auto;margin-bottom:1rem}.user-geo-panel .user-geo-btn:hover:not(.disabled){background:#178e99}.user-geo-panel .user-geo-btn.disabled{background:rgba(29,164,177,.5);cursor:not-allowed}.user-geo-panel .dev-controls{display:flex;align-items:center;gap:.5rem;padding:.75rem;background:rgba(0,0,0,.3);border-radius:6px;margin-bottom:1rem;flex-wrap:wrap}.user-geo-panel .dev-controls .dev-label{font-size:12px;color:#fff9;font-weight:600}.user-geo-panel .dev-controls .dev-btn{padding:4px 8px;font-size:11px;background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.user-geo-panel .dev-controls .dev-btn:hover{background:rgba(255,255,255,.2)}.user-geo-panel .user-geo-footer{display:flex;justify-content:center;gap:.75rem;padding-top:.5rem}.user-geo-panel .user-geo-footer .footer-icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);color:#fff9;font-size:16px}.user-geo-panel .user-geo-footer .footer-icon.active{background:var(--Primary-Uell, #1da4b1);color:#fff}\n"] }]
|
|
1390
|
+
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i4.DomSanitizer }]; }, propDecorators: { sessionId: [{
|
|
1210
1391
|
type: Input
|
|
1211
1392
|
}], token: [{
|
|
1212
1393
|
type: Input
|
|
@@ -1266,19 +1447,40 @@ class TasWaitingRoomComponent {
|
|
|
1266
1447
|
this.videoCallModalRef = null;
|
|
1267
1448
|
// Geolocation
|
|
1268
1449
|
this.geoPosition = null;
|
|
1450
|
+
this.geoWasDenied = false; // Tracks if user denied geo permission
|
|
1451
|
+
this.geoResultSent = false; // Tracks if geo result was already sent
|
|
1452
|
+
// Permission tracking for auto-join
|
|
1453
|
+
this.mediaPermissionsGranted = false;
|
|
1454
|
+
this.geoPermissionsResolved = false;
|
|
1455
|
+
// Auto-retry tracking
|
|
1456
|
+
this.retryCount = 0;
|
|
1457
|
+
this.MAX_RETRIES = 1;
|
|
1269
1458
|
}
|
|
1270
1459
|
/** Whether current user is an owner */
|
|
1271
1460
|
get isOwner() {
|
|
1272
1461
|
return this.currentUser?.role === TasUserRole.OWNER;
|
|
1273
1462
|
}
|
|
1463
|
+
/** Whether we're still requesting permissions (for template) */
|
|
1464
|
+
get isRequestingPermissions() {
|
|
1465
|
+
return !this.mediaPermissionsGranted || (!this.geoPermissionsResolved && !this.isOwner && !this.isBackoffice);
|
|
1466
|
+
}
|
|
1467
|
+
/** Whether we're waiting for admission after permissions granted (for template) */
|
|
1468
|
+
get isWaitingForAdmission() {
|
|
1469
|
+
return this.mediaPermissionsGranted &&
|
|
1470
|
+
(this.geoPermissionsResolved || this.isOwner || this.isBackoffice) &&
|
|
1471
|
+
!this.isJoinable;
|
|
1472
|
+
}
|
|
1274
1473
|
ngOnInit() {
|
|
1275
1474
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnInit');
|
|
1276
1475
|
this.requestMediaPermissions();
|
|
1476
|
+
// Request geolocation permission and cache it (but don't send to backend yet)
|
|
1477
|
+
// The cached position will be sent when owner requests it via activateGeo
|
|
1277
1478
|
this.requestGeolocation();
|
|
1278
1479
|
this.checkStatus();
|
|
1279
1480
|
}
|
|
1280
1481
|
/**
|
|
1281
1482
|
* Request camera and microphone permissions.
|
|
1483
|
+
* Triggers auto-join when permissions are resolved.
|
|
1282
1484
|
*/
|
|
1283
1485
|
async requestMediaPermissions() {
|
|
1284
1486
|
console.log('[TAS DEBUG] Requesting media permissions...');
|
|
@@ -1287,37 +1489,71 @@ class TasWaitingRoomComponent {
|
|
|
1287
1489
|
// Stop tracks immediately - we just needed the permission
|
|
1288
1490
|
stream.getTracks().forEach(track => track.stop());
|
|
1289
1491
|
console.log('[TAS DEBUG] Media permissions granted');
|
|
1492
|
+
this.mediaPermissionsGranted = true;
|
|
1290
1493
|
}
|
|
1291
1494
|
catch (error) {
|
|
1292
1495
|
console.warn('[TAS DEBUG] Media permissions denied or unavailable:', error);
|
|
1496
|
+
// Still allow joining - some users may not have camera/mic
|
|
1497
|
+
this.mediaPermissionsGranted = true;
|
|
1293
1498
|
}
|
|
1499
|
+
this.tryAutoJoin();
|
|
1500
|
+
this.cdr.detectChanges();
|
|
1294
1501
|
}
|
|
1295
1502
|
/**
|
|
1296
|
-
* Request geolocation
|
|
1503
|
+
* Request geolocation permission and cache result.
|
|
1297
1504
|
* Only for regular users (not owners/backoffice).
|
|
1298
|
-
*
|
|
1505
|
+
* Actual send to backend happens after we have videoCallId.
|
|
1506
|
+
* Triggers auto-join when geolocation is resolved.
|
|
1299
1507
|
*/
|
|
1300
1508
|
async requestGeolocation() {
|
|
1301
1509
|
// Only request geolocation for regular users, not owners/backoffice
|
|
1302
1510
|
if (this.isOwner || this.isBackoffice) {
|
|
1303
1511
|
console.log('[TAS DEBUG] Skipping geolocation for owner/backoffice');
|
|
1512
|
+
this.geoPermissionsResolved = true;
|
|
1304
1513
|
return;
|
|
1305
1514
|
}
|
|
1306
|
-
console.log('[TAS DEBUG] Requesting geolocation...');
|
|
1515
|
+
console.log('[TAS DEBUG] Requesting geolocation permission...');
|
|
1307
1516
|
const position = await this.geolocationService.getCurrentPosition();
|
|
1308
1517
|
if (position) {
|
|
1309
|
-
console.log('[TAS DEBUG] Geolocation
|
|
1518
|
+
console.log('[TAS DEBUG] Geolocation granted:', position);
|
|
1310
1519
|
this.geoPosition = position;
|
|
1311
|
-
|
|
1312
|
-
this.sendGeolocationToBackend();
|
|
1520
|
+
this.geoWasDenied = false;
|
|
1313
1521
|
}
|
|
1314
1522
|
else {
|
|
1315
1523
|
console.log('[TAS DEBUG] Geolocation denied or unavailable');
|
|
1524
|
+
this.geoPosition = null;
|
|
1525
|
+
this.geoWasDenied = true;
|
|
1316
1526
|
}
|
|
1527
|
+
this.geoPermissionsResolved = true;
|
|
1528
|
+
// If we already have videoCallId, send now. Otherwise, it will be sent after status response.
|
|
1529
|
+
this.sendGeoResultToBackend();
|
|
1530
|
+
this.tryAutoJoin();
|
|
1531
|
+
this.cdr.detectChanges();
|
|
1317
1532
|
}
|
|
1318
1533
|
/**
|
|
1319
|
-
* Send
|
|
1320
|
-
*
|
|
1534
|
+
* Send geo result to backend (granted or denied).
|
|
1535
|
+
* Only sends if videoCallId is available and not already sent.
|
|
1536
|
+
*/
|
|
1537
|
+
sendGeoResultToBackend() {
|
|
1538
|
+
if (!this.videoCallId) {
|
|
1539
|
+
console.log('[TAS DEBUG] Cannot send geo result: videoCallId not available yet');
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (this.geoResultSent) {
|
|
1543
|
+
console.log('[TAS DEBUG] Geo result already sent, skipping');
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (this.geoPosition) {
|
|
1547
|
+
this.sendGeolocationToBackend();
|
|
1548
|
+
this.geoResultSent = true;
|
|
1549
|
+
}
|
|
1550
|
+
else if (this.geoWasDenied) {
|
|
1551
|
+
this.sendGeoDenialToBackend();
|
|
1552
|
+
this.geoResultSent = true;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Send granted geolocation to backend via modify user endpoint.
|
|
1321
1557
|
*/
|
|
1322
1558
|
sendGeolocationToBackend() {
|
|
1323
1559
|
if (!this.geoPosition || !this.videoCallId) {
|
|
@@ -1325,7 +1561,6 @@ class TasWaitingRoomComponent {
|
|
|
1325
1561
|
}
|
|
1326
1562
|
console.log('[TAS DEBUG] Sending geolocation to backend...');
|
|
1327
1563
|
const body = {
|
|
1328
|
-
userId: this.currentUser?.id,
|
|
1329
1564
|
videoCallId: this.videoCallId,
|
|
1330
1565
|
action: UserCallAction.ACTIVATE_GEOLOCATION,
|
|
1331
1566
|
latitude: this.geoPosition.latitude,
|
|
@@ -1336,6 +1571,23 @@ class TasWaitingRoomComponent {
|
|
|
1336
1571
|
error: (err) => console.error('[TAS DEBUG] Failed to send geolocation:', err),
|
|
1337
1572
|
});
|
|
1338
1573
|
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Send geolocation denial to backend via modify user endpoint.
|
|
1576
|
+
*/
|
|
1577
|
+
sendGeoDenialToBackend() {
|
|
1578
|
+
if (!this.videoCallId) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
console.log('[TAS DEBUG] Sending geo denial to backend...');
|
|
1582
|
+
const body = {
|
|
1583
|
+
videoCallId: this.videoCallId,
|
|
1584
|
+
action: UserCallAction.DENY_GEOLOCATION,
|
|
1585
|
+
};
|
|
1586
|
+
this.tasService.modifyProxyVideoUser(body).subscribe({
|
|
1587
|
+
next: () => console.log('[TAS DEBUG] Geo denial sent successfully'),
|
|
1588
|
+
error: (err) => console.error('[TAS DEBUG] Failed to send geo denial:', err),
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1339
1591
|
ngOnDestroy() {
|
|
1340
1592
|
console.log('[TAS DEBUG] TasWaitingRoomComponent.ngOnDestroy');
|
|
1341
1593
|
this.subscriptions.unsubscribe();
|
|
@@ -1369,8 +1621,8 @@ class TasWaitingRoomComponent {
|
|
|
1369
1621
|
this.resolvedAppointmentId = content.appointmentId;
|
|
1370
1622
|
this.videoCallId = content.videoCallId;
|
|
1371
1623
|
console.log('[TAS DEBUG] Status response:', content);
|
|
1372
|
-
//
|
|
1373
|
-
this.
|
|
1624
|
+
// Now that we have videoCallId, send geo result if not already sent
|
|
1625
|
+
this.sendGeoResultToBackend();
|
|
1374
1626
|
// Start polling for status updates
|
|
1375
1627
|
this.tasService.startStatusPolling(statusParams);
|
|
1376
1628
|
// Subscribe to joinable status
|
|
@@ -1386,7 +1638,8 @@ class TasWaitingRoomComponent {
|
|
|
1386
1638
|
}));
|
|
1387
1639
|
}
|
|
1388
1640
|
/**
|
|
1389
|
-
* Handle changes to joinable status
|
|
1641
|
+
* Handle changes to joinable status.
|
|
1642
|
+
* Triggers auto-join when joinable becomes true.
|
|
1390
1643
|
*/
|
|
1391
1644
|
handleJoinableChange(joinable) {
|
|
1392
1645
|
console.log('[TAS DEBUG] handleJoinableChange called', {
|
|
@@ -1402,10 +1655,11 @@ class TasWaitingRoomComponent {
|
|
|
1402
1655
|
console.log('[TAS DEBUG] Skipping state update - already in:', this.state);
|
|
1403
1656
|
return;
|
|
1404
1657
|
}
|
|
1405
|
-
//
|
|
1658
|
+
// Update state and attempt auto-join
|
|
1406
1659
|
if (joinable) {
|
|
1407
|
-
console.log('[TAS DEBUG] Joinable is true,
|
|
1660
|
+
console.log('[TAS DEBUG] Joinable is true, attempting auto-join');
|
|
1408
1661
|
this.state = WaitingRoomState.READY;
|
|
1662
|
+
this.tryAutoJoin();
|
|
1409
1663
|
}
|
|
1410
1664
|
else {
|
|
1411
1665
|
console.log('[TAS DEBUG] Waiting for joinable...');
|
|
@@ -1414,11 +1668,33 @@ class TasWaitingRoomComponent {
|
|
|
1414
1668
|
this.cdr.detectChanges();
|
|
1415
1669
|
}
|
|
1416
1670
|
/**
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1671
|
+
* Attempt to auto-join the session when all conditions are met.
|
|
1672
|
+
* - For owners: just need joinable + sessionId
|
|
1673
|
+
* - For users: need media permissions + geo resolved + joinable + sessionId
|
|
1419
1674
|
*/
|
|
1420
|
-
|
|
1421
|
-
|
|
1675
|
+
tryAutoJoin() {
|
|
1676
|
+
// Don't try if already joining or in error
|
|
1677
|
+
if (this.state === WaitingRoomState.GETTING_TOKEN ||
|
|
1678
|
+
this.state === WaitingRoomState.JOINING) {
|
|
1679
|
+
console.log('[TAS DEBUG] tryAutoJoin skipped - already in state:', this.state);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
// For owners: just need joinable
|
|
1683
|
+
if (this.isOwner) {
|
|
1684
|
+
if (this.isJoinable && this.resolvedSessionId) {
|
|
1685
|
+
console.log('[TAS DEBUG] Owner auto-joining...');
|
|
1686
|
+
this.startSessionAndJoin();
|
|
1687
|
+
}
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
// For users: need media + geo resolved + joinable
|
|
1691
|
+
if (this.mediaPermissionsGranted &&
|
|
1692
|
+
this.geoPermissionsResolved &&
|
|
1693
|
+
this.isJoinable &&
|
|
1694
|
+
this.resolvedSessionId) {
|
|
1695
|
+
console.log('[TAS DEBUG] User auto-joining...');
|
|
1696
|
+
this.startSessionAndJoin();
|
|
1697
|
+
}
|
|
1422
1698
|
}
|
|
1423
1699
|
/**
|
|
1424
1700
|
* Check if user has owner/backoffice role
|
|
@@ -1471,28 +1747,34 @@ class TasWaitingRoomComponent {
|
|
|
1471
1747
|
},
|
|
1472
1748
|
error: (err) => {
|
|
1473
1749
|
console.error('[TAS DEBUG] /start request failed:', err);
|
|
1474
|
-
|
|
1475
|
-
this.
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1750
|
+
// Auto-retry on first failure
|
|
1751
|
+
if (this.retryCount < this.MAX_RETRIES) {
|
|
1752
|
+
this.retryCount++;
|
|
1753
|
+
console.log('[TAS DEBUG] Auto-retrying... attempt:', this.retryCount);
|
|
1754
|
+
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1755
|
+
this.cdr.detectChanges();
|
|
1756
|
+
setTimeout(() => this.startSessionAndJoin(), 2000);
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
// Show error after retry fails
|
|
1760
|
+
this.state = WaitingRoomState.ERROR;
|
|
1761
|
+
this.errorMessage = err?.error?.message || err?.message || 'Error al iniciar la sesión. Por favor, intente nuevamente.';
|
|
1762
|
+
this.tasService.stopStatusPolling();
|
|
1763
|
+
console.log('[TAS DEBUG] State set to ERROR, errorMessage:', this.errorMessage);
|
|
1764
|
+
this.cdr.detectChanges();
|
|
1765
|
+
}
|
|
1479
1766
|
},
|
|
1480
1767
|
}));
|
|
1481
1768
|
}
|
|
1482
1769
|
/**
|
|
1483
|
-
*
|
|
1484
|
-
|
|
1485
|
-
cancel() {
|
|
1486
|
-
this.tasService.stopStatusPolling();
|
|
1487
|
-
this.activeModal.dismiss('cancel');
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Retry after an error
|
|
1770
|
+
* Retry after an error.
|
|
1771
|
+
* Resets retry count and restarts the flow.
|
|
1491
1772
|
*/
|
|
1492
1773
|
retry() {
|
|
1493
1774
|
this.state = WaitingRoomState.CHECKING_STATUS;
|
|
1494
1775
|
this.errorMessage = '';
|
|
1495
1776
|
this.token = '';
|
|
1777
|
+
this.retryCount = 0; // Reset retry count for auto-retry
|
|
1496
1778
|
this.checkStatus();
|
|
1497
1779
|
}
|
|
1498
1780
|
openVideoCallModal() {
|
|
@@ -1518,10 +1800,10 @@ class TasWaitingRoomComponent {
|
|
|
1518
1800
|
}
|
|
1519
1801
|
}
|
|
1520
1802
|
TasWaitingRoomComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, deps: [{ token: i1.NgbActiveModal }, { token: TasService }, { token: GeolocationService }, { token: i1.NgbModal }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1521
|
-
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n
|
|
1803
|
+
TasWaitingRoomComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasWaitingRoomComponent, selector: "tas-waiting-room", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, ngImport: i0, template: "<div class=\"tas-waiting-room\">\n <div class=\"waiting-room-content\">\n <div class=\"state-container\">\n <!-- Loading states (show spinner) -->\n <ng-container *ngIf=\"state !== WaitingRoomState.ERROR\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n \n <!-- Requesting permissions -->\n <ng-container *ngIf=\"isRequestingPermissions\">\n <p class=\"state-message\">Solicitando permisos...</p>\n <p class=\"state-submessage\">Por favor, acept\u00E1 los permisos de c\u00E1mara y micr\u00F3fono.</p>\n </ng-container>\n \n <!-- Waiting for admission (permissions granted, not joinable yet) -->\n <ng-container *ngIf=\"isWaitingForAdmission\">\n <p class=\"state-message\">Medicina laboral va a admitirte</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </ng-container>\n \n <!-- Getting token / Joining -->\n <ng-container *ngIf=\"state === WaitingRoomState.GETTING_TOKEN || state === WaitingRoomState.JOINING\">\n <p class=\"state-message\">Conectando...</p>\n </ng-container>\n \n <!-- Checking status (when not requesting permissions) -->\n <ng-container *ngIf=\"state === WaitingRoomState.CHECKING_STATUS && !isRequestingPermissions\">\n <p class=\"state-message\">Verificando estado...</p>\n </ng-container>\n </ng-container>\n\n <!-- Error state (show error icon and message) -->\n <ng-container *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n <p class=\"error-details\" *ngIf=\"errorMessage\">{{ errorMessage }}</p>\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n </ng-container>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:300px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 576px){.tas-waiting-room{min-height:250px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.action-btn{padding:10px 24px;font-size:14px}}\n"], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1522
1804
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasWaitingRoomComponent, decorators: [{
|
|
1523
1805
|
type: Component,
|
|
1524
|
-
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n
|
|
1806
|
+
args: [{ selector: 'tas-waiting-room', template: "<div class=\"tas-waiting-room\">\n <div class=\"waiting-room-content\">\n <div class=\"state-container\">\n <!-- Loading states (show spinner) -->\n <ng-container *ngIf=\"state !== WaitingRoomState.ERROR\">\n <div class=\"state-icon loading\">\n <div class=\"spinner\"></div>\n </div>\n \n <!-- Requesting permissions -->\n <ng-container *ngIf=\"isRequestingPermissions\">\n <p class=\"state-message\">Solicitando permisos...</p>\n <p class=\"state-submessage\">Por favor, acept\u00E1 los permisos de c\u00E1mara y micr\u00F3fono.</p>\n </ng-container>\n \n <!-- Waiting for admission (permissions granted, not joinable yet) -->\n <ng-container *ngIf=\"isWaitingForAdmission\">\n <p class=\"state-message\">Medicina laboral va a admitirte</p>\n <p class=\"state-submessage\">Por favor, permanec\u00E9 en esta pantalla.</p>\n </ng-container>\n \n <!-- Getting token / Joining -->\n <ng-container *ngIf=\"state === WaitingRoomState.GETTING_TOKEN || state === WaitingRoomState.JOINING\">\n <p class=\"state-message\">Conectando...</p>\n </ng-container>\n \n <!-- Checking status (when not requesting permissions) -->\n <ng-container *ngIf=\"state === WaitingRoomState.CHECKING_STATUS && !isRequestingPermissions\">\n <p class=\"state-message\">Verificando estado...</p>\n </ng-container>\n </ng-container>\n\n <!-- Error state (show error icon and message) -->\n <ng-container *ngIf=\"state === WaitingRoomState.ERROR\">\n <div class=\"state-icon error\">\n <i class=\"fa fa-exclamation-triangle\"></i>\n </div>\n <p class=\"state-message error\">Ocurri\u00F3 un error</p>\n <p class=\"error-details\" *ngIf=\"errorMessage\">{{ errorMessage }}</p>\n <button type=\"button\" class=\"btn action-btn retry-btn\" (click)=\"retry()\">\n <i class=\"fa fa-refresh\"></i>\n Reintentar\n </button>\n </ng-container>\n </div>\n </div>\n</div>\n", styles: [".tas-waiting-room{display:flex;flex-direction:column;min-height:300px;background:#ffffff;border-radius:5px;overflow:hidden}.waiting-room-content{flex:1;display:flex;align-items:center;justify-content:center;padding:32px 40px;background:#ffffff}.state-container{text-align:center;max-width:400px;width:100%}.state-icon{width:80px;height:80px;margin:0 auto 24px;border-radius:50%;display:flex;align-items:center;justify-content:center}.state-icon i{font-size:36px}.state-icon.loading{background:rgba(29,164,177,.1);border:2px solid #1da4b1}.state-icon.error{background:rgba(238,49,107,.1);border:2px solid #ee316b}.state-icon.error i{color:#ee316b}.spinner{width:40px;height:40px;border:3px solid #e9ecef;border-top-color:#1da4b1;border-radius:50%;animation:spin 1s linear infinite}.state-message{font-size:16px;font-weight:600;margin:0 0 8px;color:#212529;line-height:24px}.state-message.error{color:#ee316b}.state-submessage{font-size:14px;color:#6c757d;margin:0 0 24px;font-weight:400}.error-details{font-size:13px;color:#ee316b;margin:0 0 24px;padding:12px 16px;background:rgba(238,49,107,.08);border-radius:8px;border:1px solid rgba(238,49,107,.2)}.action-btn{padding:12px 32px;font-size:16px;font-weight:600;border-radius:4px;border:none;cursor:pointer;transition:all .2s ease;display:inline-flex;align-items:center;gap:10px}.action-btn i{font-size:16px}.action-btn.retry-btn{background:transparent;color:#6c757d;border:1px solid #e9ecef}.action-btn.retry-btn:hover{background:#f8f9fa;border-color:#6c757d;color:#212529}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 576px){.tas-waiting-room{min-height:250px}.waiting-room-content{padding:24px}.state-icon{width:64px;height:64px}.state-icon i{font-size:28px}.spinner{width:32px;height:32px}.action-btn{padding:10px 24px;font-size:14px}}\n"] }]
|
|
1525
1807
|
}], ctorParameters: function () { return [{ type: i1.NgbActiveModal }, { type: TasService }, { type: GeolocationService }, { type: i1.NgbModal }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { roomType: [{
|
|
1526
1808
|
type: Input
|
|
1527
1809
|
}], entityId: [{
|
|
@@ -1545,13 +1827,16 @@ class TasButtonComponent {
|
|
|
1545
1827
|
// Style customization
|
|
1546
1828
|
this.variant = 'default';
|
|
1547
1829
|
this.buttonLabel = 'Iniciar TAS';
|
|
1830
|
+
// Skip status check - when true, button shows immediately without API call
|
|
1831
|
+
// Useful when parent component already knows the appointment is valid
|
|
1832
|
+
this.skipStatusCheck = false;
|
|
1548
1833
|
this.isLoading = false;
|
|
1549
1834
|
// Status check state
|
|
1550
1835
|
this.isCheckingStatus = false;
|
|
1551
1836
|
this.isStatusError = false;
|
|
1552
1837
|
this.statusErrorMessage = '';
|
|
1553
1838
|
this.isJoinable = false; // Tracks joinable field from status response
|
|
1554
|
-
this.shouldShowButton =
|
|
1839
|
+
this.shouldShowButton = false; // Hidden by default, shown after status check confirms visibility
|
|
1555
1840
|
this.subscriptions = new Subscription();
|
|
1556
1841
|
this.currentModalRef = null;
|
|
1557
1842
|
this.videoCallModalRef = null;
|
|
@@ -1589,6 +1874,12 @@ class TasButtonComponent {
|
|
|
1589
1874
|
this.videoCallModalRef = null;
|
|
1590
1875
|
}
|
|
1591
1876
|
}));
|
|
1877
|
+
// If skipStatusCheck is true, show button immediately without polling
|
|
1878
|
+
if (this.skipStatusCheck) {
|
|
1879
|
+
this.shouldShowButton = true;
|
|
1880
|
+
this.isJoinable = true;
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1592
1883
|
// Start status checking
|
|
1593
1884
|
this.startStatusPolling();
|
|
1594
1885
|
}
|
|
@@ -1635,6 +1926,11 @@ class TasButtonComponent {
|
|
|
1635
1926
|
})
|
|
1636
1927
|
.subscribe({
|
|
1637
1928
|
next: (response) => {
|
|
1929
|
+
// Validate response structure
|
|
1930
|
+
if (!response || !response.content) {
|
|
1931
|
+
this.handleStatusError('Invalid response');
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1638
1934
|
this.isCheckingStatus = false;
|
|
1639
1935
|
this.isStatusError = false;
|
|
1640
1936
|
this.statusErrorMessage = '';
|
|
@@ -1643,23 +1939,20 @@ class TasButtonComponent {
|
|
|
1643
1939
|
this.isJoinable = response.content?.joinable ?? false;
|
|
1644
1940
|
},
|
|
1645
1941
|
error: (err) => {
|
|
1646
|
-
this.
|
|
1647
|
-
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1648
|
-
this.statusErrorMessage = errorMessage;
|
|
1649
|
-
// Use utility service to determine if button should be shown
|
|
1650
|
-
this.shouldShowButton = this.tasUtilityService.shouldShowButton(errorMessage);
|
|
1651
|
-
// Stop polling on error
|
|
1652
|
-
this.stopStatusPolling();
|
|
1653
|
-
// If button should be hidden, don't treat as error
|
|
1654
|
-
if (!this.shouldShowButton) {
|
|
1655
|
-
this.isStatusError = false;
|
|
1656
|
-
}
|
|
1657
|
-
else {
|
|
1658
|
-
this.isStatusError = true;
|
|
1659
|
-
}
|
|
1942
|
+
this.handleStatusError(err);
|
|
1660
1943
|
},
|
|
1661
1944
|
}));
|
|
1662
1945
|
}
|
|
1946
|
+
handleStatusError(err) {
|
|
1947
|
+
this.isCheckingStatus = false;
|
|
1948
|
+
const errorMessage = this.tasUtilityService.extractErrorMessage(err, 'Error checking status');
|
|
1949
|
+
this.statusErrorMessage = errorMessage;
|
|
1950
|
+
// On any status error, hide the button
|
|
1951
|
+
this.shouldShowButton = false;
|
|
1952
|
+
this.isStatusError = false; // We don't show error UI, just hide the button
|
|
1953
|
+
// Stop polling on error
|
|
1954
|
+
this.stopStatusPolling();
|
|
1955
|
+
}
|
|
1663
1956
|
onClick() {
|
|
1664
1957
|
if (!this.tenant || !this.currentUser?.name) {
|
|
1665
1958
|
return;
|
|
@@ -1708,7 +2001,7 @@ class TasButtonComponent {
|
|
|
1708
2001
|
}
|
|
1709
2002
|
}
|
|
1710
2003
|
TasButtonComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, deps: [{ token: i1.NgbModal }, { token: TasService }, { token: TasUtilityService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1711
|
-
TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasButtonComponent, selector: "tas-btn", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser", variant: "variant", buttonLabel: "buttonLabel" }, ngImport: i0, template: "<span\n *ngIf=\"shouldShowButton\"\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"], directives: [{ type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "triggers", "container", "disableTooltip", "tooltipClass", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }] });
|
|
2004
|
+
TasButtonComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasButtonComponent, selector: "tas-btn", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser", variant: "variant", buttonLabel: "buttonLabel", skipStatusCheck: "skipStatusCheck" }, ngImport: i0, template: "<span\n *ngIf=\"shouldShowButton\"\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "triggers", "container", "disableTooltip", "tooltipClass", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }] });
|
|
1712
2005
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasButtonComponent, decorators: [{
|
|
1713
2006
|
type: Component,
|
|
1714
2007
|
args: [{ selector: 'tas-btn', template: "<span\n *ngIf=\"shouldShowButton\"\n [ngbTooltip]=\"isDisabled && disabledReason ? disabledReason : null\"\n container=\"body\"\n placement=\"top\"\n tooltipClass=\"tas-btn-tooltip\"\n>\n <button\n type=\"button\"\n class=\"btn btn-primary tas-btn\"\n [class.tas-btn--teal]=\"variant === 'teal'\"\n (click)=\"onClick()\"\n [disabled]=\"isDisabled\"\n >\n <i class=\"fa fa-video-camera\" aria-hidden=\"true\" *ngIf=\"!isLoading\"></i>\n <span *ngIf=\"!isLoading\">{{ buttonLabel }}</span>\n <span *ngIf=\"isLoading\">Processing...</span>\n </button>\n</span>\n", styles: [":host{display:inline-block}.tas-btn{background-color:#ee316b!important;color:#fff!important;border-color:#ee316b!important;margin-right:24px;display:flex;padding:6px 14px;justify-content:center;align-items:center;gap:7px;flex-shrink:0;flex-grow:0}.tas-btn:disabled{background-color:#ccc!important;border-color:#ccc!important;cursor:not-allowed}.tas-btn:hover:not(:disabled){background-color:#d62a5f!important;border-color:#d62a5f!important}.tas-btn i{margin-right:5px}.tas-btn--teal{background-color:#0097a7!important;border-color:#0097a7!important;border-radius:24px;font-weight:500;margin-right:0;padding:6px 14px}.tas-btn--teal:hover:not(:disabled){background-color:#00838f!important;border-color:#00838f!important}\n"] }]
|
|
@@ -1726,6 +2019,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1726
2019
|
type: Input
|
|
1727
2020
|
}], buttonLabel: [{
|
|
1728
2021
|
type: Input
|
|
2022
|
+
}], skipStatusCheck: [{
|
|
2023
|
+
type: Input
|
|
1729
2024
|
}] } });
|
|
1730
2025
|
|
|
1731
2026
|
class TasFloatingCallComponent {
|
|
@@ -1881,7 +2176,7 @@ class TasIncomingAppointmentComponent {
|
|
|
1881
2176
|
this.roomType = TasRoomType.TAS;
|
|
1882
2177
|
this.businessRole = TasBusinessRole.USER;
|
|
1883
2178
|
this.enterCall = new EventEmitter();
|
|
1884
|
-
this.
|
|
2179
|
+
this.appointments = [];
|
|
1885
2180
|
this.isLoading = true;
|
|
1886
2181
|
this.hasError = false;
|
|
1887
2182
|
this.subscriptions = new Subscription();
|
|
@@ -1893,20 +2188,24 @@ class TasIncomingAppointmentComponent {
|
|
|
1893
2188
|
this.subscriptions.unsubscribe();
|
|
1894
2189
|
}
|
|
1895
2190
|
loadAppointments() {
|
|
1896
|
-
const today = new Date();
|
|
1897
|
-
const in7Days = new Date(today);
|
|
1898
|
-
in7Days.setDate(today.getDate() + 7);
|
|
1899
2191
|
this.subscriptions.add(this.tasService
|
|
1900
|
-
.getAppointments({
|
|
2192
|
+
.getAppointments({
|
|
2193
|
+
fromDate: this.fromDate,
|
|
2194
|
+
toDate: this.toDate,
|
|
2195
|
+
entityId: this.entityId
|
|
2196
|
+
})
|
|
1901
2197
|
.subscribe({
|
|
1902
2198
|
next: (response) => {
|
|
1903
2199
|
// Handle both array response and wrapped response (e.g., { content: [...] })
|
|
1904
2200
|
const appointments = Array.isArray(response)
|
|
1905
2201
|
? response
|
|
1906
2202
|
: response?.content || [];
|
|
1907
|
-
//
|
|
1908
|
-
|
|
1909
|
-
|
|
2203
|
+
// Sort by date and startTime descending (most recent first)
|
|
2204
|
+
this.appointments = appointments.sort((a, b) => {
|
|
2205
|
+
const dateTimeA = `${a.date}T${a.startTime}`;
|
|
2206
|
+
const dateTimeB = `${b.date}T${b.startTime}`;
|
|
2207
|
+
return dateTimeB.localeCompare(dateTimeA);
|
|
2208
|
+
});
|
|
1910
2209
|
this.isLoading = false;
|
|
1911
2210
|
},
|
|
1912
2211
|
error: () => {
|
|
@@ -1915,18 +2214,21 @@ class TasIncomingAppointmentComponent {
|
|
|
1915
2214
|
},
|
|
1916
2215
|
}));
|
|
1917
2216
|
}
|
|
1918
|
-
onEnterCall() {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2217
|
+
onEnterCall(appointment) {
|
|
2218
|
+
this.enterCall.emit(appointment);
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Check if tas-btn should be shown for an appointment (CONFIRMED or ACTIVE status)
|
|
2222
|
+
*/
|
|
2223
|
+
shouldShowTasBtn(appointment) {
|
|
2224
|
+
return appointment.status === AppointmentStatus.CONFIRMED ||
|
|
2225
|
+
appointment.status === AppointmentStatus.ACTIVE;
|
|
1922
2226
|
}
|
|
1923
2227
|
/**
|
|
1924
2228
|
* Format date to Spanish format: "Lunes 8 de diciembre"
|
|
1925
2229
|
*/
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
return '';
|
|
1929
|
-
const [year, month, day] = this.appointment.date.split('-').map(Number);
|
|
2230
|
+
formatAppointmentDate(appointment) {
|
|
2231
|
+
const [year, month, day] = appointment.date.split('-').map(Number);
|
|
1930
2232
|
const date = new Date(year, month - 1, day);
|
|
1931
2233
|
const dayNames = [
|
|
1932
2234
|
'Domingo', 'Lunes', 'Martes', 'Miércoles',
|
|
@@ -1944,23 +2246,25 @@ class TasIncomingAppointmentComponent {
|
|
|
1944
2246
|
/**
|
|
1945
2247
|
* Format time range: "9:00 - 9:30"
|
|
1946
2248
|
*/
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
return '';
|
|
1950
|
-
return `${this.appointment.startTime} - ${this.appointment.endTime}`;
|
|
2249
|
+
formatTimeRange(appointment) {
|
|
2250
|
+
return `${appointment.startTime} - ${appointment.endTime}`;
|
|
1951
2251
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2252
|
+
/**
|
|
2253
|
+
* Get the other participant in the call (not the current user)
|
|
2254
|
+
*/
|
|
2255
|
+
getOtherParticipant(appointment) {
|
|
2256
|
+
if (!appointment.participants || appointment.participants.length === 0) {
|
|
2257
|
+
return appointment.title; // Fallback to title if no participants
|
|
2258
|
+
}
|
|
2259
|
+
const otherParticipant = appointment.participants.find(p => p.userId !== this.currentUser?.id);
|
|
2260
|
+
return otherParticipant?.name || appointment.title;
|
|
1957
2261
|
}
|
|
1958
2262
|
}
|
|
1959
2263
|
TasIncomingAppointmentComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, deps: [{ token: TasService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1960
|
-
TasIncomingAppointmentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasIncomingAppointmentComponent, selector: "tas-incoming-appointment", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser" }, outputs: { enterCall: "enterCall" }, ngImport: i0, template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading &&
|
|
2264
|
+
TasIncomingAppointmentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.12", type: TasIncomingAppointmentComponent, selector: "tas-incoming-appointment", inputs: { roomType: "roomType", entityId: "entityId", tenant: "tenant", businessRole: "businessRole", currentUser: "currentUser", fromDate: "fromDate", toDate: "toDate" }, outputs: { enterCall: "enterCall" }, ngImport: i0, template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && appointments.length === 0\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch;gap:16px}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"], components: [{ type: TasAvatarComponent, selector: "tas-avatar", inputs: ["name", "size"] }, { type: TasButtonComponent, selector: "tas-btn", inputs: ["roomType", "entityId", "tenant", "businessRole", "currentUser", "variant", "buttonLabel", "skipStatusCheck"] }], directives: [{ type: i4$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i4$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
1961
2265
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImport: i0, type: TasIncomingAppointmentComponent, decorators: [{
|
|
1962
2266
|
type: Component,
|
|
1963
|
-
args: [{ selector: 'tas-incoming-appointment', template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading &&
|
|
2267
|
+
args: [{ selector: 'tas-incoming-appointment', template: "<div class=\"incoming-appointment-card\">\n <h3 class=\"card-title\">Pr\u00F3ximo turno</h3>\n\n <!-- Loading state -->\n <div class=\"card-content\" *ngIf=\"isLoading\">\n <div class=\"loading-spinner\"></div>\n </div>\n\n <!-- Empty state -->\n <div class=\"card-content empty-state\" *ngIf=\"!isLoading && appointments.length === 0\">\n <div class=\"icon-container\">\n <div class=\"icon-circle\">\n <i class=\"fa fa-calendar\" aria-hidden=\"true\"></i>\n </div>\n <span class=\"sparkle sparkle-1\">\u2726</span>\n <span class=\"sparkle sparkle-2\">\u2726</span>\n <span class=\"sparkle sparkle-3\">\u2726</span>\n <span class=\"sparkle sparkle-4\">\u2726</span>\n </div>\n <h4 class=\"empty-title\">Todav\u00EDa no ten\u00E9s turnos agendados</h4>\n <p class=\"empty-subtitle\">\n En caso de que Medicina Laboral requiera una consulta, lo ver\u00E1s en esta secci\u00F3n.\n </p>\n </div>\n\n <!-- Appointments list -->\n <div class=\"card-content appointment-state\" *ngIf=\"!isLoading && appointments.length > 0\">\n <div class=\"appointment-card\" *ngFor=\"let appt of appointments\">\n <div class=\"appointment-header\">\n <tas-avatar [name]=\"getOtherParticipant(appt)\" [size]=\"48\"></tas-avatar>\n <span class=\"doctor-name\">{{ getOtherParticipant(appt) }}</span>\n </div>\n <div class=\"appointment-details\">\n <div class=\"date-time\">\n <span class=\"date\">{{ formatAppointmentDate(appt) }}</span>\n <span class=\"time\">{{ formatTimeRange(appt) }}</span>\n </div>\n <tas-btn\n *ngIf=\"shouldShowTasBtn(appt)\"\n variant=\"teal\"\n buttonLabel=\"Ingresar\"\n [roomType]=\"appt.roomType\"\n [entityId]=\"appt.entityId\"\n [tenant]=\"tenant\"\n [businessRole]=\"businessRole\"\n [currentUser]=\"currentUser\"\n ></tas-btn>\n </div>\n </div>\n </div>\n</div>\n\n", styles: [":host{display:block}.incoming-appointment-card{background:#ffffff;border-radius:12px;box-shadow:0 2px 8px #00000014;padding:24px;min-width:360px}.card-title{font-size:16px;font-weight:400;color:#6b7280;margin:0 0 24px}.card-content{display:flex;flex-direction:column;align-items:center}.loading-spinner{width:40px;height:40px;border:3px solid #e0f7fa;border-top-color:#0097a7;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:20px 0}.icon-container{position:relative;width:120px;height:120px;margin-bottom:24px}.icon-circle{width:100%;height:100%;background:#e0f7fa;border-radius:50%;display:flex;align-items:center;justify-content:center}.icon-circle i{font-size:40px;color:#0097a7}.sparkle{position:absolute;color:#0097a7;font-size:12px}.sparkle-1{top:10px;right:5px}.sparkle-2{top:0;right:30px}.sparkle-3{top:25px;left:0}.sparkle-4{bottom:20px;right:0}.empty-title{font-size:18px;font-weight:600;color:#1f2937;margin:0 0 12px}.empty-subtitle{font-size:14px;color:#6b7280;margin:0;max-width:320px;line-height:1.5}.appointment-state{align-items:stretch;gap:16px}.appointment-card{border-radius:12px;border:1px solid var(--Primary-White-Uell50, #8ED1D8);background:#F1FAFA;padding:1.5rem}.appointment-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}.doctor-name{overflow:hidden;color:var(--Neutral-GreyDark, #383E52);text-overflow:ellipsis;font-size:16px;font-style:normal;font-weight:600;line-height:24px;letter-spacing:.016px}.appointment-details{display:flex;justify-content:space-between;align-items:flex-end}.date-time{display:flex;flex-direction:column;gap:4px}.date{color:var(--Neutral-GreyDark, #383E52);font-size:12px;font-style:normal;font-weight:600;line-height:16px;letter-spacing:.06px}.time{font-size:14px;color:var(--Neutral-GreyDark, #383E52);font-family:Inter;font-size:10px;font-style:normal;font-weight:400;line-height:14px;letter-spacing:.04px}\n"] }]
|
|
1964
2268
|
}], ctorParameters: function () { return [{ type: TasService }]; }, propDecorators: { roomType: [{
|
|
1965
2269
|
type: Input
|
|
1966
2270
|
}], entityId: [{
|
|
@@ -1971,6 +2275,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
1971
2275
|
type: Input
|
|
1972
2276
|
}], currentUser: [{
|
|
1973
2277
|
type: Input
|
|
2278
|
+
}], fromDate: [{
|
|
2279
|
+
type: Input
|
|
2280
|
+
}], toDate: [{
|
|
2281
|
+
type: Input
|
|
1974
2282
|
}], enterCall: [{
|
|
1975
2283
|
type: Output
|
|
1976
2284
|
}] } });
|
|
@@ -2060,5 +2368,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.12", ngImpo
|
|
|
2060
2368
|
* Generated bundle index. Do not edit.
|
|
2061
2369
|
*/
|
|
2062
2370
|
|
|
2063
|
-
export { AppointmentStatus, CallState, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2371
|
+
export { AppointmentStatus, CallState, GeoStatus, GeolocationService, RoomUserStatus, TAS_CONFIG, TAS_HTTP_CLIENT, TasAvatarComponent, TasBusinessRole, TasButtonComponent, TasFloatingCallComponent, TasIncomingAppointmentComponent, TasRoomType, TasService, TasSessionType, TasUellSdkModule, TasUserRole, TasUtilityService, TasVideocallComponent, TasWaitingRoomComponent, UserCallAction, UserGeoViewState, UserStatus, VideoSessionStatus, ViewMode, WaitingRoomState };
|
|
2064
2372
|
//# sourceMappingURL=tas-uell-sdk.mjs.map
|