1import { Controller } from '@hotwired/stimulus'; 2 3/** 4 * Satisfaction Survey Controller 5 * 6 * Displays a satisfaction survey modal using SweetAlert2 after a delay. 7 * Handles user actions: submit (opens form in new tab), remind later, close. 8 * Stars can be clicked to submit the survey directly. 9 * 10 * Usage in Twig: 11 * <div data-controller="satisfaction-survey" 12 * data-satisfaction-survey-check-url-value="{{ path('api_satisfaction_check') }}" 13 * data-satisfaction-survey-submit-url-value="{{ path('api_satisfaction_submit') }}" 14 * data-satisfaction-survey-remind-url-value="{{ path('api_satisfaction_remind_later') }}" 15 * data-satisfaction-survey-close-url-value="{{ path('api_satisfaction_close') }}"> 16 * </div> 17 */ 18export default class extends Controller { 19 static values = { 20 checkUrl: String, 21 submitUrl: String, 22 remindUrl: String, 23 closeUrl: String, 24 }; 25 26 connect() { 27 this.selectedStars = 0; 28 this.submittedViaStars = false; // Flag to prevent race condition with Swal.close() 29 // Check if survey should be shown 30 this.checkAndShowSurvey(); 31 } 32 33 async checkAndShowSurvey() { 34 try { 35 const response = await fetch(this.checkUrlValue, { 36 method: 'GET', 37 headers: { 38 'Accept': 'application/json', 39 'X-Requested-With': 'XMLHttpRequest', 40 }, 41 credentials: 'same-origin', 42 }); 43 44 if (!response.ok) { 45 return; 46 } 47 48 const data = await response.json(); 49 50 if (data.shouldShow) { 51 this.surveyId = data.surveyId; 52 this.surveyUrl = data.surveyUrl; 53 this.delay = data.delay || 4000; 54 this.title = data.title || 'Como está sua experiência?'; 55 this.description = data.description || 'Sua opinião ajuda a tornarmos a gestão da sua pesquisa mais eficiente.'; 56 this.buttonText = data.buttonText || 'Avaliar'; 57 58 // Show modal after delay 59 setTimeout(() => this.showModal(), this.delay); 60 } 61 } catch (error) { 62 console.error('Error checking satisfaction survey:', error); 63 } 64 } 65 66 showModal() { 67 // Check if SweetAlert2 is available 68 if (typeof Swal === 'undefined') { 69 console.error('SweetAlert2 not loaded'); 70 return; 71 } 72 73 const self = this; 74 75 Swal.fire({ 76 title: this.title, 77 html: ` 78 <p class="survey-description"> 79 ${this.description} 80 </p> 81 <div class="fb-rating"> 82 <input type="radio" name="star" id="st5" value="5"> 83 <label for="st5" title="Excelente" data-stars="5">â </label> 84 <input type="radio" name="star" id="st4" value="4"> 85 <label for="st4" title="Muito bom" data-stars="4">â </label> 86 <input type="radio" name="star" id="st3" value="3"> 87 <label for="st3" title="Bom" data-stars="3">â </label> 88 <input type="radio" name="star" id="st2" value="2"> 89 <label for="st2" title="Ruim" data-stars="2">â </label> 90 <input type="radio" name="star" id="st1" value="1"> 91 <label for="st1" title="Péssimo" data-stars="1">â </label> 92 </div> 93 <p class="star-hint">Clique em uma estrela para avaliar</p> 94 <style> 95 .survey-description { 96 color: #64748b; 97 font-size: 14px; 98 line-height: 1.5; 99 margin-bottom: 20px; 100 } 101 .fb-rating { 102 display: flex; 103 justify-content: center; 104 flex-direction: row-reverse; 105 gap: 5px; 106 margin-bottom: 15px; 107 } 108 .fb-rating input { 109 display: none; 110 } 111 .fb-rating label { 112 font-size: 32px; 113 color: #e2e8f0; 114 cursor: pointer; 115 transition: color 0.2s; 116 } 117 .fb-rating input:checked ~ label, 118 .fb-rating label:hover, 119 .fb-rating label:hover ~ label { 120 color: #fbbf24 !important; 121 } 122 .fb-rating label.selected, 123 .fb-rating label.selected ~ label { 124 color: #fbbf24 !important; 125 } 126 .star-hint { 127 color: #94a3b8; 128 font-size: 12px; 129 margin-top: 5px; 130 } 131 </style> 132 `, 133 showConfirmButton: true, 134 confirmButtonText: this.buttonText, 135 confirmButtonColor: '#0056b3', 136 showDenyButton: true, 137 denyButtonText: 'Lembrar depois', 138 denyButtonColor: '#94a3b8', 139 showCloseButton: true, 140 allowOutsideClick: false, 141 customClass: { 142 popup: 'satisfaction-survey-popup', 143 title: 'satisfaction-survey-title', 144 htmlContainer: 'satisfaction-survey-content', 145 }, 146 didOpen: () => { 147 // Add star rating interaction with click-to-submit 148 const labels = document.querySelectorAll('.fb-rating label'); 149 const inputs = document.querySelectorAll('.fb-rating input'); 150
151 labels.forEach((label) => { 152 label.addEventListener('click', (e) => { 153 const stars = parseInt(label.dataset.stars); 154 self.selectedStars = stars; 155 156 // Mark as selected visually 157 labels.forEach(l => l.classList.remove('selected')); 158 label.classList.add('selected'); 159 // Also select all labels after this one (lower star values) 160 let sibling = label.nextElementSibling; 161 while (sibling) { 162 if (sibling.tagName === 'LABEL') { 163 sibling.classList.add('selected'); 164 } 165 sibling = sibling.nextElementSibling; 166 } 167 168 // Check the corresponding radio 169 const input = document.getElementById(`st${stars}`); 170 if (input) input.checked = true; 171 172 // Submit immediately when clicking stars 173 setTimeout(() => { 174 // Set flag BEFORE Swal.close() to prevent .then() from firing close action 175 self.submittedViaStars = true; 176 // Record action with stars first (before Swal.close triggers .then handler) 177 self.recordAction(self.submitUrlValue, 'submitted', stars); 178 // Open survey URL in new tab 179 window.open(self.surveyUrl, '_blank', 'noopener,noreferrer'); 180 // Close modal (this triggers .then() with DismissReason.close, but flag prevents double-action) 181 Swal.close(); 182 }, 300); // Small delay for visual feedback 183 }); 184 }); 185 }, 186 preConfirm: () => { 187 // Get selected stars if any 188 const checkedInput = document.querySelector('.fb-rating input:checked'); 189 if (checkedInput) { 190 self.selectedStars = parseInt(checkedInput.value); 191 } 192 // Open survey URL in new tab 193 window.open(this.surveyUrl, '_blank', 'noopener,noreferrer'); 194 return true; 195 }, 196 }).then((result) => { 197 if (result.isConfirmed) {
198 // User clicked "Avaliar" button - mark as submitted with stars 199 this.recordAction(this.submitUrlValue, 'submitted', this.selectedStars); 200 } else if (result.isDenied) { 201 // User clicked "Lembrar depois" 202 this.recordAction(this.remindUrlValue, 'remind_later'); 203 } else if (result.dismiss === Swal.DismissReason.close) { 204 // User clicked X â but only if NOT already submitted via star click 205 if (!this.submittedViaStars) { 206 this.recordAction(this.closeUrlValue, 'closed'); 207 } 208 } 209 }); 210 } 211 212 async recordAction(url, action, stars = null) { 213 try { 214 const body = { 215 surveyId: this.surveyId, 216 action: action, 217 }; 218 219 if (stars && stars >= 1 && stars <= 5) { 220 body.stars = stars; 221 } 222 223 const response = await fetch(url, { 224 method: 'POST', 225 headers: { 226 'Content-Type': 'application/json', 227 'Accept': 'application/json', 228 'X-Requested-With': 'XMLHttpRequest', 229 }, 230 credentials: 'same-origin', 231 body: JSON.stringify(body), 232 }); 233 234 if (!response.ok) { 235 console.error('Error recording survey action:', action); 236 } 237 } catch (error) { 238 console.error('Error recording survey action:', error); 239 } 240 } 241}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.