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