tas-uell-sdk 0.1.3 → 0.3.0

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