PageSourceSearch

https://redone.med.br/assets/controllers/satisfaction_survey_controller-qGINlso.js

js redone.med.br collected 2026-10-07 17:07:22 UTC 10,011 bytes, 241 lines download raw bytes

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.