PageSourceSearch

https://cteasy.com/js/riot-tags-build/riot-generated.js?v=1791407280000

js cteasy.com collected 2026-10-09 00:03:06 UTC 242,224 bytes, 4,456 lines download raw bytes

1riot.tag2('ctrl-html', '', 'ctrl-html,[data-is="ctrl-html"]{ overflow:hidden; }', '', function(opts) {
2        var tag = this;
3        tag.refresh = function () {
4            $(tag.root).html(tag.opts.content);
5        }
6        tag.on('mount', function () {
7            tag.refresh();
8        });
9
10        tag.on('update', function () {
11            tag.refresh();
12        });
13});
14
15
16riot.tag2('ctrl-locality', '<div ref="main" class="dropdown has-feedback"> <input ref="searchInput" type="text" class="form-control {checked: selection != null}" data-toggle="dropdown" placeholder="{dictionary.TCC_SEARCH_PANEL_LABEL_LOCALITY}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" onfocus="{this.searchInputFocus}" onblur="{this.searchInputBlur}" onkeydown="{this.searchInputKeydown}" oninput="{this.searchInputWrite}" riot-value="{this.opts.search_place_name}"> <span class="material-icons">location_on</span> <div class="clear-button" if="{this.butResetVisible}" onclick="{this.reset}"> <span class="material-icons">clear</span> </div> <i ref="searchIcon" class="fas fa-spinner fa-spin form-control-feedback {searching: searching}"></i> <div ref="dropdown_menu" class="dropdown-menu" aria-labelledby="searchLocality"> <div ref="list"> <div if="{items.length>0}" ref="listTable"> <table class="table" style="position:relative;"> <tr data-id-locality="{item.id}" each="{item, index in items}" onmouseover="{this.localityOnMouseEnter}" class="line {selected : index == selectionIndex}" onclick="{this.dropdownSelect}"> <td onclick="{this.dropdownSelect}"> <div> <span class="material-icons">location_on</span> <span class="zip">{item.zipCode}</span> <span class="commune">{item.name}</span> </div> </td> </tr> </table> </div> <hr if="{items.length > 0}"> <div class="padding-small" if="{items.length == 0}"> <span class="help">{dictionary.TCC_SEARCH_PANEL_LOCALITY_HELP1}</span> </div> <div class="padding-small" if="{items.length > 0}"> <span class="help">{dictionary.TCC_SEARCH_PANEL_LOCALITY_LIST_HELP1}</span> </div> <div class="padding-small" if="{items.length > 20}"> <span class="help">{dictionary.TCC_SEARCH_PANEL_LOCALITY_LIST_TIP2}</span> </div> </div> </div> </div>', '', '', function(opts) {
17        var tag = this;
18        tag.mixin(dcMixin);
19
20        tag.searching = false;
21        tag.items = [];
22        tag.selection = null;
23        tag.selectionIndex = -1;
24        tag.selectionId = -1;
25        tag.isReadOnly = false;
26		tag.butResetVisible = false;
27
28        tag.localityOnMouseEnter = function (e) {
29            var ua = window.navigator.userAgent;
30            var iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i);
31            var webkit = !!ua.match(/WebKit/i);
32            var iOSSafari = iOS && webkit && !ua.match(/CriOS/i);
33            if (e.item && e.item.item) {
34                if (e.item.item) {
35                    tag.selectItem(e.item.item, null, iOSSafari);
36                    if(iOSSafari){
37                      tag.dropdownClose();
38
39                    }
40                }
41            }
42
43        };
44
45        tag.searchInputFocus = function (e) {
46            e = e || event;
47            $(tag.refs.searchInput).select();
48            tag.butResetVisible = tag.selectionId > 0;
49            if(e && e.type == 'focus' && !$(tag.refs.dropdown_menu).hasClass('show')) {
50              window._trk && window._trk({
51                event: 'ux_event',
52                event_category: 'search',
53                event_action: 'Locality Open',
54                event_label: tag.opts.locality
55              });
56            }
57            tag.trigger('onDropdownOpen');
58        };
59
60        tag.searchInputBlur = function (e) {
61            e = e || event;
62            window.setTimeout(function () {
63                if (document.activeElement != tag.refs.searchInput) {
64                    tag.dropdownClose();
65
66                }
67
68                tag.butResetVisible = false;
69                tag.update();
70
71            }, 200);
72
73        };
74
75        tag.searchInputKeydown = function (e) {
76            e = e || event;
77            if (  e.keyCode == 13) {
78
79                if (!tag.searching) {
80                    if (tag.selection) {
81                        tag.selectItem(tag.selectionIndex, null, true);
82                        tag.dropdownClose();
83                        e.stopPropagation();
84                        window._trk && window._trk({
85                          event: 'ux_event',
86                          event_category: 'search',
87                          event_action: 'Locality Enter',
88                          event_label: tag.opts.locality
89                        });
90                    }
91                }
92            }
93            if (  e.keyCode == 8 || e.keyCode == 46) {
94                tag.selectItem(null);
95                return false;
96            }
97            if (  e.keyCode == 27) {
98                tag.selectItem(null);
99                e.preventDefault();
100                return false;
101            }
102            if (  e.keyCode == 38) {
103                tag.dropdownOpen();
104                tag.selectItem(tag.selectionIndex - 1);
105                e.preventDefault();
106                return false;
107            }
108            if (  e.keyCode == 40) {
109                tag.dropdownOpen();
110                tag.selectItem(tag.selectionIndex + 1);
111                e.preventDefault();
112                return false;
113            }
114        };
115
116         tag.searchInputWrite = $.debounce(function (e) {
117            e = e || event;
118            var value = $(tag.refs.searchInput).val();
119            tag.search(value);
120        }, 300);
121
122        tag.dropdownSelect = function (e) {
123            e = e || event;
124            tag.selectItem(e.item.item, null, true);
125            tag.dropdownClose();
126            e.stopPropagation();
127            window._trk && window._trk({
128              event: 'ux_event',
129              event_category: 'search',
130              event_action: 'Locality Select',
131              event_label: tag.opts.locality
132            });
133        };
134
135        tag.reset = function (e) {
136
137            e.stopPropagation();
138
139            tag.dropdownClose();
140            tag.selectItem(null, false, false);
141            tag.selectionId = -1;
142            tag.butResetVisible = false;
143            tag.update();
144
145        };
146
147        tag.dropdownOpen = function () {
148        	$(tag.refs.dropdown_menu).addClass('show');
149        };
150
151        tag.dropdownClose = function () {
152        	$(tag.refs.dropdown_menu).removeClass('show');
153        };
154
155        tag.search = function (value, autoselect) {
156            if (value == null || value == '') return;
157            tag.searching = true;
158            $.ajax({
159            	url: tag.opts.context + tag.opts.url + '?action=searchLocality',
160                method: 'post',
161                data: {
162                    search: value
163                },
164                cache: false
165            }).done(function (data) {
166                tag.searching = false;
167                tag.items = data.items;
168                if (autoselect && tag.items.length > 0) {
169                    tag.selectItem(0);
170                }
171                else {
172                    tag.dropdownOpen();
173                    $(tag.refs.listTable).scrollTop(0);
174                }
175                tag.update();
176            }).fail(function () {
177                tag.searching = false;
178                tag.update();
179            });
180            tag.selectItem(null, true);
181            tag.update();
182        };
183
184        tag.selectItem = function (item, keepCaption, withAutomaticSearch) {
185
186            if (typeof (item) == 'number') {
187                if (item < 0) return;
188                if (item >= tag.items.length) return;
189                item = tag.items[item];
190            }
191            var oldselection = tag.selection;
192            if (oldselection != item || withAutomaticSearch) {
193                tag.selection = item
194                if (item) {
195                    tag.selection = item;
196                    if (item != true) {
197                        $(tag.refs.searchInput).val(item.name + ' (' + item.zipCode + ')');
198                         tag.selectionIndex = tag.items.indexOf(item);
199						 tag.selectionId = item.idDev10;
200
201                    }
202                }
203                else {
204                    if (keepCaption != true) $(tag.refs.searchInput).val(null);
205                    tag.selection = null;
206                    tag.selectionIndex = -1;
207                    $(tag.refs.listTable).scrollTop(0);
208                }
209
210                tag.update();
211                tag.trigger('onchanged', item, oldselection, withAutomaticSearch);
212            }
213
214            tag.butResetVisible = false;
215        }
216
217        tag.on('mount', function () {
218            if (tag.opts.search_place_name && tag.opts.search_place_name != '') {
219                tag.selectItem(true);
220            } else if (tag.opts.locality && tag.opts.locality != '') {
221
222            	const searchString = tag.opts.locality.replace(/&#039;/g,' ');
223
224                tag.search(searchString, true);
225            }
226
227        });
228
229});
230
231riot.tag2('ctrl-month', '<div ref="main" class="dropdown has-feedback"> <div class="zone-sensitive" onclick="{this.searchInputFocus}" data-toggle="dropdown"> <input type="text" class="form-control {checked: selection != null}" placeholder="{dictionary.TCC_SEARCH_PANEL_LABEL_MONTH}" autocomplete="off" readonly="readonly" onfocus="{this.searchInputFocus}" onkeydown="{this.onkeydown}" riot-value="{this.selectionDisplay}" id="searchInputMonth"> <span class="material-icons">date_range</span> <i class="fas fa-chevron-down"></i> </div> <div ref="dropdown_menu" class="dropdown-menu" aria-labelledby="searchInputMonth"> <ul class="month-list row"> <li each="{item in this.items}" class="month {selected: this.selection == item}" onclick="{this.dropdownSelect}"> <span class="month-name">{item.MonthName}</span> <span class="month-year">{item.Year}</span> </li> </ul> </div> </div>', '', '', function(opts) {
232        var tag = this;
233        tag.mixin(dcMixin);
234        tag.selection = null;
235        tag.selectionDisplay = null;
236
237        tag.items = null;
238
239        tag.searchInputFocus = function (e) {
240            if (e && e.type == 'focus' && !$(tag.refs.dropdown_menu).hasClass('show')) {
241                window._trk && window._trk({
242                    event: 'ux_event',
243                    event_category: 'search',
244                    event_action: 'Date Open',
245                    event_label: tag.selectionDisplay
246                });
247            }
248            tag.trigger('onDropdownOpen');
249        };
250
251        tag.dropdownOpen = function () {
252            $(tag.refs.dropdown_menu).addClass('show');
253        };
254
255        tag.dropdownClose = function () {
256            $(tag.refs.dropdown_menu).removeClass('show');
257        };
258
259        tag.onkeydown = function (e) {
260            e = e || event;
261            if (tag.selection == null) {
262                tag.dropdownOpen();
263            }
264            e.preventDefault();
265            return false;
266        };
267
268        tag.selectItem = function (item, withAutomaticSearch) {
269            var oldselection = tag.selection;
270
271            if (typeof (item) == 'string') {
272                item = tag.items[parseInt(item)];
273            }
274
275            if (item) {
276                tag.selection = item;
277            } else {
278                tag.selection = null;
279            }
280
281            if (tag.selection) {
282                tag.selectionDisplay = tag.selection.MonthName + ' ' + tag.selection.Year;
283            } else {
284                tag.selectionDisplay = null;
285            }
286            tag.dropdownClose();
287
288            tag.update();
289
290            tag.trigger('onchanged', tag.selection, oldselection, withAutomaticSearch);
291        };
292
293        tag.dropdownSelect = function (e) {
294            e = e || event;
295            tag.selectItem(e.item.item, true);
296            e.stopPropagation();
297            window._trk && window._trk({
298                event: 'ux_event',
299                event_category: 'search',
300                event_action: 'Date Select',
301                event_label: tag.selectionDisplay
302            });
303        };
304
305        tag.load = function () {
306            $.ajax({
307                url: tag.opts.context + tag.opts.url + '?action=getMonthList',
308                type: 'POST',
309                cache: false
310            }).done(function (data) {
311                tag.items = data;
312                if (tag.opts.yearmonth) {
313                    tag.selectItem(tag.opts.yearmonth, false);
314                }
315
316                tag.update();
317            });
318        };
319
320        tag.load();
321
322});
323riot.tag2('ctrl-resplan-day', '<virtual if="{this.visible}"> <div class="header"> <div class="navigation-container previous"> <span if={this.canGoLeftDay()}" onclick={this.goLeftDay} class="material-icons-outlined">chevron_left</span> </div> <div class="header-best-offer-list clearfix"> <div class="header-best-offer-item clearfix {(day.isPast || day.isOutOfSell) ? \'not-open\' : \'open\'}" riot-style="width: {this.widthHeader}" each="{day in this.visibleDays}" onclick="{this.selectDayEvent(day)}" data-year="{day.Year}" data-month="{day.Month + 1}" data-day="{day.DayNum}"> <div class="header-best-offer {day == this.selectedDay ? \'header-best-offer-selected\' : \'\'}"> <div class="header-best-offer-container" data-price-value="{day.BestPrice}"> <div class="header-best-offer-year">{day.MonthName}</div> <div class="header-best-offer-month hide-on-mobile">{day.DayName} {day.DayNum}</div> <div class="header-best-offer-month dayname hide-on-desktop">{day.DayName}</div> <div class="header-best-offer-month daynum hide-on-desktop">{day.DayNum}</div> <div class="header-best-offer-price"> <div if="{day.isOpen && !this.data.forUpdateAppointment}">{day.BestPriceString}<span> {day.BestPrice}</span>&euro;</div> <div class="message" if="{day.isOpen && this.data.forUpdateAppointment}">{dictionary.PLANNING2_UPDATE_OPEN}</div> <div class="message" if="{!day.isOpen && !day.isOutOfSell}">{dictionary.PLANNING2_SOLD_OUT}</div> <div class="message" if="{!day.isOpen && day.isOutOfSell}">{dictionary.PLANNING2_CLOSED}</div> </div> </div> </div> </div> </div> <div class="navigation-container next"> <span if="{this.canGoRightDay()}" onclick="{this.goRightDay}" class="material-icons-outlined">chevron_right</span> </div> </div> <div id="content"> <div class="content-error" if="{this.showMsgSlotBurned}"> <div class="alert alert-warning" role="alert"> <span class="material-icons-outlined">error</span> <span class="msg">Le cr&eacute;neau que vous avez choisi ne semble plus disponible dans le centre de contr&ocirc;le technique. Merci de choisir un cr&eacute;neau diff&eacute;rent. Si le probl&egrave;me persiste, merci de contacter le service client<span if="{window.cteasyCustomerServicePhone}"> au <a href="{window.cteasyCustomerServicePhoneHref}">{window.cteasyCustomerServicePhone}</a></span>.</span> </div> </div> <div if="{!this.data.forUpdateAppointment}" class="legend"> <ctrl-resplan-legend context="{opts.context}"></ctrl-resplan-legend> </div> <div class="content-wrapper"> <div class="slot-container{this.showMsgSlotBurned?\' msg-slot-burned\':\'\'} day-hours"> <div each="{slot, index in this.selectedDay.Slots}" class="slot-element {slot === this.selectedSlot?\'selected-slot\':\'\'} {index + 1 >
323 (this.selectedDay.Slots.length / 2)?\'col2\':\'col1\'}" riot-style="grid-row:{index + 1 > (this.selectedDay.Slots.length / 2) ? index - (this.selectedDay.Slots.length / 2) +1 : index + 1}"> <div class="slot-price-container {slot.burned?\'burned\':\'available\'} {this.data.forUpdateAppointment ? \'for-update-appointment\' : \'\'}" onclick="{this.takeAppointmentEvent(slot)}"> <div class="slot-hours"> <span>{slot.MinHours}</span> </div> <virtual if="{!this.data.forUpdateAppointment && !slot.burned}"> <div class="slot-price {slot.FrontPrice == this.selectedDay.BestPrice ? \'best-price-of-day\' : \'\'}"> <span>{slot.FrontPrice}</span>&euro; </div> <div class="slot-reduction reduc {slot.FrontPrice == this.selectedDay.BestPrice ? \'best-price-of-day\' : \'\'}"> <span class="r-{slot.RoundedReduction}">-{slot.RoundedReduction}%</span> </div> </virtual> <span class="info-slot-burned" if="{slot.burned}">Horaire non disponible</span> </div> </div> <div if="{this.selectedDay.Slots.length === 0}" class="content-error"> <div class="alert alert-warning" role="alert"> <span class="material-icons-outlined">error</span> <span class="msg msg-short">{dictionary.PLANNING2_MSG_NOSLOT}</span> </div> <div class="prev-day-with-slot" if="{this.prevDayWithSlot}">{dictionary.PLANNING2_MSG_NOSLOT_PREVIOUS} <a if="{this.prevDayWithSlot}" onclick="{this.selectDayEvent(this.prevDayWithSlot)}"> {this.prevDayWithSlot.DayName}&nbsp;{this.prevDayWithSlot.DayNum}&nbsp;{this.prevDayWithSlot.MonthName} </a> </div> <div class="next-day-with-slot" if="{this.nextDayWithSlot}">{dictionary.PLANNING2_MSG_NOSLOT_NEXT} <a if="{this.nextDayWithSlot}" onclick="{this.selectDayEvent(this.nextDayWithSlot)}"> {this.nextDayWithSlot.DayName}&nbsp;{this.nextDayWithSlot.DayNum}&nbsp;{this.nextDayWithSlot.MonthName} </a> </div> </div> </div> </div> <div class="action-buttons hh-mm"> <div class="btn retour" onclick="{this.parent.returnToMonth}"> <span class="material-icons" style="color: white;">keyboard_backspace</span> Retour </div> </div> </div> </virtual>', 'ctrl-resplan-day,[data-is="ctrl-resplan-day"]{ ctrl-resplan-day .header,[data-is="ctrl-resplan-day"] .header{ display: flex; flex-flow: row; } }', '', function(opts) {
324        var tag = this;
325        tag.widthHeader = tag.parent.widthHeader;
326        tag.visible = false;
327        tag.showMsgSlotBurned = false;
328        tag.selectedDay = null;
329        tag.visibleDays = new Array();
330        tag.allDays = null;
331        tag.prevDayWithSlot = null;
332        tag.nextDayWithSlot = null;
333        tag.selectedSlot = null;
334
335        tag.data = {
336            locationId: tag.opts.locationid,
337            year: tag.opts.year,
338            month: tag.opts.month,
339            prestationType: tag.opts.prestationtype,
340            vehicleType: tag.opts.vehicletype,
341            energyType: tag.opts.energytype,
342            forUpdateAppointment: tag.opts.forupdateappointment,
343            lotteryCode: tag.opts.lotterycode,
344        };
345
346        tag.loadDay = function (daySelected, months) {
347            tag.selectedDay = daySelected;
348
349            tag.allDays = new Array();
350            $(months).each(function (i, month) {
351                $(month.Days).each(function (i, day) {
352                    if (!day.isFake) {
353                        tag.allDays.push(day);
354                    }
355                });
356            });
357
358            tag.loadVisibleDay();
359            tag.trigger('ready');
360        }
361
362        tag.canGoLeftDay = function () {
363            if (tag.selectedDay && tag.parent.endLoading) {
364                var idx = tag.allDays.indexOf(tag.selectedDay);
365                var previous = tag.allDays[idx - ((tag.parent.pas * 2) + 1)];
366                if (previous) {
367                    if (!previous.IsPast) {
368                        return true;
369                    }
370                }
371            }
372            return false;
373        };
374
375        tag.canGoRightDay = function () {
376            if (tag.selectedDay && tag.parent.endLoading) {
377                var idx = tag.allDays.indexOf(tag.selectedDay);
378                var next = tag.allDays[idx + ((tag.parent.pas * 2) + 1)];
379                if (next) {
380                    if (!next.IsOutOfSell) {
381                        return true;
382                    }
383                }
384            }
385            return false;
386        };
387
388        tag.goLeftDay = function () {
389
390            if (tag.selectedDay) {
391                var idx = tag.allDays.indexOf(tag.selectedDay);
392                var previous = tag.allDays[idx - ((tag.parent.pas * 2) + 1)];
393                if (previous) {
394                    if (!previous.IsPast) {
395                        tag.selectedDay = previous;
396                        tag.loadVisibleDay();
397                    }
398                }
399            }
400            window._trk && window._trk({
401                event: 'ux_event',
402                event_category: 'planning',
403                event_action: 'previous'
404            });
405        };
406
407        tag.goRightDay = function () {
408
409            if (tag.selectedDay) {
410                var idx = tag.allDays.indexOf(tag.selectedDay);
411                var next = tag.allDays[idx + ((tag.parent.pas * 2) + 1)];
412                if (next) {
413                    if (!next.IsOutOfSell) {
414                        tag.selectedDay = next;
415                        tag.loadVisibleDay();
416                    }
417                }
418            }
419            window._trk && window._trk({
420                event: 'ux_event',
421                event_category: 'planning',
422                event_action: 'next'
423            });
424        };
425
426        tag.selectDayEvent = function (day) {
427            return function (e) {
428                tag.showMsgSlotBurned = false;
429                tag.selectedDay = day;
430                tag.loadVisibleDay();
431
432                tag.prevDayWithSlot = tag.getPreviousWithSlots();
433                tag.nextDayWithSlot = tag.getNextWithSlots();
434
435                window._trk && window._trk({
436                    event: 'ux_event',
437                    event_category: 'planning',
438                    event_action: 'Date Select'
439                });
440            }
441        };
442
443        tag.takeAppointmentEvent = function (slot) {
444            return function (e) {
445                if (slot && !slot.burned) {
446
447                    if (tag.data.forUpdateAppointment) {
448                        myConfirm("Êtes-vous sûr de vouloir modifier votre rendez-vous ?", "Confirmation", 'lg').then(function () {
449                            tag.selectedSlot = slot;
450                            tag.takeAppointmentEventValidate();
451                        });
452                    } else {
453                        tag.selectedSlot = slot;
454                        tag.takeAppointmentEventValidate();
455                    }
456
457                }
458            }
459        };
460
461        tag.takeAppointmentEventValidate = function () {
462
463            if (tag.selectedSlot) {
464                tag.trigger('takeAppointment', tag.selectedSlot);
465                window._trk && window._trk({
466                    event: 'ux_event',
467                    event_category: 'planning',
468                    event_action: 'select_time'
469                });
470            }
471            tag.selectedSlot = null;
472        };
473
474        tag.loadVisibleDay = function () {
475
476            tag.visibleDays = new Array();
477            var pas = tag.parent.pas;
478            if (tag.selectedDay) {
479                var selectedIdx = tag.allDays.indexOf(tag.selectedDay);
480                for (var i = -pas; i <= pas; i++) {
481                    var currentDay = tag.allDays[selectedIdx + i];
482                    if (currentDay) {
483                        tag.visibleDays.push(currentDay);
484                    }
485                }
486            }
487            tag.update();
488        };
489
490        tag.burnSlot = function (slot) {
491            tag.showMsgSlotBurned = true;
492            slot.burned = true;
493            tag.update();
494        };
495
496        tag.getPreviousWithSlots = function () {
497
498            var tt = tag.visibleDays;
499            var indexJ = -1;
500            for (var index in tt) {
501                var day = tt[index];
502
503                if (day == tag.selectedDay) {
504
505                    indexJ = index;
506                }
507            }
508
509            if (indexJ > -1 && tt.length > indexJ) {
510                for (var i = indexJ - 1; i > -1; i--) {
511                    var day = tt[i];
512
513                    if (day.Slots && day.Slots.length > 0) {
514
515                        return day;
516                    }
517                }
518            }
519
520            return;
521        };
522
523        tag.getNextWithSlots = function () {
524
525            var tt = tag.visibleDays;
526            var indexJ = -1;
527            for (var index in tt) {
528
529                var day = tt[index];
530
531                if (day == tag.selectedDay) {
532
533                    indexJ = index;
534                }
535            }
536
537            if (indexJ > -1 && tt.length > indexJ) {
538
539                for (var i = (1 * indexJ + 1); i < tt.length; i++) {
540
541                    var day = tt[i];
542
543                    if (day.Slots && day.Slots.length > 0) {
544
545                        tag.nextDayWithSlot = day;
546                        return day;
547                    }
548                }
549            }
550
551            tag.nextDayWithSlot = null;
552            return;
553        };
554
555});
556riot.tag2('ctrl-resplan-legend', '<div class="legend-container"> <span class="moins-cher-icon"> </span> <span class="r-meilleure-promo"> Moins cher </span> </div>', '', '', function(opts) {
557});
558riot.tag2('ctrl-resplan-modal', '<div class="modal" tabindex="-1" role="dialog" id="planning-modal"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-header"> <div class="cteasy-controle-technique-ct-row"> <div class="cteasy-controle-technique-ct-row-header"> <div class="titre">Centre de contr&ocirc;le technique : <b>{opts.locationCity}</b></div> <div class="sous-titre"> <h5 class="modal-title">{this.getTitle()} <i if="{!this.endLoading}" class="fas fa-spinner fa-pulse"></i></h5> </div> </div> <button if="{this.currentMode != this.modes.Loading}" type="button" class="close" data-dismiss="modal" aria-label="Close"> <span class="material-icons">highlight_off</span> </button> </div> </div> <div class="modal-body"> <ctrl-resplan-month ref="tagmonth" context="{opts.context}" url="{opts.url}" locationid="{opts.locationId}" year="{opts.year}" month="{opts.month}" day="{opts.day}" prestationtype="{opts.prestationType}" vehicletype="{opts.vehicleType}" energytype="{opts.energyType}" forupdateappointment="{opts.forUpdateAppointment}" lotterycode="{opts.lotteryCode}" bypasscache="{opts.bypassCache}" ismobile="{this.isMobile}"></ctrl-resplan-month> <ctrl-resplan-day ref="tagday" context="{opts.context}" url="{opts.url}" locationid="{opts.locationId}" year="{opts.year}" month="{opts.month}" prestationtype="{opts.prestationType}" vehicletype="{opts.vehicleType}" energytype="{opts.energyType}" forupdateappointment="{opts.forUpdateAppointment}" lotterycode="{opts.lotteryCode}" ismobile="{this.isMobile}"></ctrl-resplan-day> <virtual if="{this.currentMode === this.modes.Loading || this.currentMode === this.modes.BeforeFunnel}"> <div class="content-loading"> <i class="fas fa-spinner fa-pulse"></i> <div class="msg-waiting-funnel">{this.msgLoading}</div> </div> </virtual> <virtual if="{this.currentMode === this.modes.Error}"> <div class="content-error"> <div class="alert alert-danger" role="alert"> {dictionary.PLANNING2_ALERT_ERROR} </div> <button class="btn btn-block btn-clic-to-action" onclick="{this.init}"> {dictionary.PLANNING2_BTN_RETRY} </button> </div> </virtual> </div> </div> </div> </div>', 'ctrl-resplan-modal,[data-is="ctrl-resplan-modal"]{ }', '', function(opts) {
559	 	var tag = this;
560
561	 	tag.modes = {
562	            Month: 1,
563	            Day: 2,
564	            Error: 3,
565	            Loading: 4,
566	            Expired: 5,
567	            BeforeFunnel: 6,
568	        };
569
570	 	tag.isMobile = $(window).width() <= 992;
571	 	//tag.nbHeadersShow = tag.isMobile ? 3 : 7;
572	 	tag.nbHeadersShow = 3;
573	    tag.pas = (tag.nbHeadersShow - 1) / 2;
574	    tag.widthHeader = (100 / tag.nbHeadersShow).toString() + '%';
575
576        tag.currentMode = tag.modes.Loading;
577        tag.data = {};
578
579        tag.endLoading = false; // fin du chargement de l'ensemble des datas
580        tag.headerDays = [];
581        tag.isStructureLoaded = false;
582
583        tag.tagManagerEventName = 'results_planning:popup';
584        tag.EVENT_OPEN = {'name':'OPEN','value':1}; // // ouverture de la popup
585        tag.EVENT_DAY_SELECTED = {'name':'DAY_SELECTED','value':2}; // s�lection du jour
586        tag.EVENT_TAKE_APPOINTMENT = {'name':'TAKE_APPOINTMENT','value':3};  // s�lection du RDV
587        tag.EVENT_TAKE_APPOINTMENT_VALIDATION = {'name':'TAKE_APPOINTMENT_VALIDATION','value':4}; // s�lection du RDV valid�e (car encore disponible apr�s v�rification en DB)
588        tag.EVENT_TAKE_APPOINTMENT_FAILED = {'name':'TAKE_APPOINTMENT_FAILED','value':5}; // s�lection du RDV refus�e (car plus disponible apr�s v�rification en DB)
589
590     	tag.mixin(dcMixin);
591
592     	this.on('mount',function(){
593
594     		 $.xhrPool = [];
595             $.xhrPool.abortAll = function() {
596                 $(this).each(function(i, jqXHR) {   //  cycle through list of recorded connection
597                     jqXHR.abort();  //  aborts connection
598                     $.xhrPool.splice(i, 1); //  removes from list by index
599                 });
600             }
601             $.ajaxSetup({
602                 beforeSend: function(jqXHR) { $.xhrPool.push(jqXHR); }, //  annd connection to list
603                 complete: function(jqXHR) {
604                     var i = $.xhrPool.indexOf(jqXHR);   //  get index for current connection completed
605                     if (i > -1) $.xhrPool.splice(i, 1); //  removes from list by index
606                 }
607             });
608
609             tag.data = {
610           		locationId: tag.opts.locationId,
611           		year: tag.opts.year,
612           		month: tag.opts.month,
613           		prestationType: tag.opts.prestationType,
614           		vehicleType: tag.opts.vehicleType,
615           		energyType: tag.opts.energyType,
616           		forUpdateAppointment: tag.opts.forupdateappointment,
617           		lotteryCode: tag.opts.lotterycode,
618             };
619
620     		$("#planning-modal").modal('show');
621
622     		$('#planning-modal').on('hidden.bs.modal', function (e) {
623     			 $.xhrPool.abortAll();
624     			$('#planning-modal').remove();
625     		});
626
627     		// Tracking Google Tag Manager
628     		tag.triggerEventTagManager(tag.EVENT_OPEN, tag.data, undefined);
629
630     		tag.init();
631     	});
632
633     	 tag.init = function(){
634
635             tag.showLoading();
636
637 			if (tag.refs.tagmonth && tag.refs.tagday) {
638
639             	tag.refs.tagmonth.loadStructure();
640
641             	// TRIGGERS sur le TAG MONTH
642             	tag.refs.tagmonth.on('showError',function(message){
643             		 tag.showError(message);
644             	});
645
646             	tag.refs.tagmonth.on('ready',function(){
647             		tag.currentMode = tag.modes.Month;
648             		tag.refs.tagmonth.visible = true;
649             		tag.refs.tagday.visible = false;
650             		tag.update();
651 	           	});
652
653             	tag.refs.tagmonth.on('daySelected',function(day){
654             		tag.currentMode = tag.modes.Loading;
655             		tag.refs.tagmonth.visible = false;
656             		tag.refs.tagday.visible = false;
657             		tag.refs.tagday.loadDay(day,tag.refs.tagmonth.months);
658             		tag.update();
659
660             		// Tracking Google Tag Manager
661             		tag.triggerEventTagManager(tag.EVENT_DAY_SELECTED, tag.data, undefined);
662
663 	           	});
664
665             	// TRIGGERS sur le TAG DAY
666             	tag.refs.tagday.on('ready',function(){
667             		tag.currentMode = tag.modes.Day;
668             		tag.refs.tagmonth.visible = false;
669             		tag.refs.tagday.visible = true;
670             		tag.update();
671 	           	});
672
673             	// -> 'Retour au calendrier du mois'
674             	tag.refs.tagday.on('returnToMonth',function(){
675             		tag.returnToMonth();
676 	           	});
677
678            	// -> 'Take appointment'
679             	tag.refs.tagday.on('takeAppointment',function(slot){
680
681             		//devlog("Take APPOINTMENT "+slot.MinHours);
682             		tag.takeAppointment(slot);
683 	           	});
684
685 			}
686
687         };
688
689     	 tag.getTitle = function(){
690            switch (tag.currentMode) {
691              case tag.modes.Month:
692                return dictionary.PLANNING2_TITLE;
693              case tag.modes.Day:
694                return dictionary.PLANNING2_TITLE_WEEK;
695              case tag.modes.Error:
696                return dictionary.PLANNING2_TITLE_ERROR;
697              case tag.modes.Loading:
698                return dictionary.PLANNING2_TITLE_LOADING;
699              case tag.modes.Expired:
700                return dictionary.PLANNING2_TITLE_EXPIRED;
701            case tag.modes.BeforeFunnel:
702                return "";
703              default:
704                break;
705            }
706        };
707
708        tag.showError = function(error){
709            tag.msgError = error;
710            tag.currentMode = tag.modes.Error;
711            tag.refs.tagmonth.visible = false;
712     		tag.refs.tagday.visible = false;
713            tag.update();
714          };
715          tag.showLoading = function(msgLoading){
716            tag.msgLoading = msgLoading;
717            tag.currentMode = tag.modes.Loading;
718            tag.refs.tagmonth.visible = false;
719     		tag.refs.tagday.visible = false;
720            tag.update();
721          };
722          tag.showBeforeFunnel = function (msgLoading) {
723              tag.msgLoading = msgLoading;
724              tag.currentMode = tag.modes.BeforeFunnel;
725              tag.refs.tagmonth.visible = false;
726       		  tag.refs.tagday.visible = false;
727              tag.update();
728          };
729          tag.showExpired = function(slot){
730            tag.currentMode = tag.modes.Day;
731            tag.refs.tagday.burnSlot(slot);
732            tag.refs.tagmonth.visible = false;
733     		tag.refs.tagday.visible = true;
734            tag.update();
735          };
736          tag.isExpired = function(){
737            return tag.limitFreshingData < new Date();
738          };
739
740        tag.takeAppointment = function(slot){
741
742        	// Tracking Google Tag Manager
743     		tag.triggerEventTagManager(tag.EVENT_TAKE_APPOINTMENT, tag.data, slot);
744
745            if(!tag.isExpired()){
746              tag.showBeforeFunnel(dictionary.PLANNING2_WAITING_FOR_FUNNEL);
747              tag.update();
748
749                //on viens verifier la dispo r�elle du cr�neau
750              $.ajax({
751            	  url: tag.opts.context + tag.opts.url + '?action=checkAvailability',
752                  method: 'post',
753                  dataType: "json",
754                  data: {
755                      date: slot.AppointmentDate,
756                      debut: slot.DebutTimeStamp,
757                      endDate: slot.AppointmentEnd,
758                      duree: slot.Duree,
759                      ligne: slot.Ligne,
760                      locationId: tag.data.locationId,
761                      prestationType: tag.data.prestationType,
762                      vehicleType: tag.data.vehicleType,
763                      energyType: tag.data.energyType,
764                      lotteryCode: tag.opts.lotteryCode,
765                      reservationCode: tag.data.reservationCode,
766                      forUpdateAppointment: tag.data.forUpdateAppointment
767                  },
768                  cache: false
769              }).done(function (result, textStatus, jqXHR) {
770                  if (result.availability) {
771
772	                	// Tracking Google Tag Manager
773	               		tag.triggerEventTagManager(tag.EVENT_TAKE_APPOINTMENT_VALIDATION, tag.data, slot);
774
775                      //on met un timeout car sinon le loading ne s'affiche pas sur safari
776                      //setTimeout(function () { window.location.href = slot.PurchaseUrl }, 100);
777
778                      window.location.href = slot.PurchaseUrl;
779
780                  } else {
781
782                	// Tracking Google Tag Manager
783                	tag.triggerEventTagManager(tag.EVENT_TAKE_APPOINTMENT_FAILED, tag.data, slot);
784
785                    tag.showExpired(slot);
786
787                  }
788
789              }).fail(function (jqXHR, textStatus, errorThrown) {
790                  tag.showError(errorThrown);
791              }).always(function () {
792                  tag.update();
793              });
794            }
795
796        };
797
798     	// -> 'Retour au calendrier du mois'
799     	 tag.returnToMonth = function(){
800     		tag.currentMode = tag.modes.Month;
801     		tag.refs.tagmonth.visible = true;
802     		tag.refs.tagday.visible = false;
803     		tag.update();
804     	 }
805
806     	 tag.triggerEventTagManager = function(tagEvent, params, slot){
807
808     		 var event = jQuery.Event( tag.tagManagerEventName );
809     		 event.trigger = tagEvent;
810     		 event.params = params;
811     		 event.slot = slot;
812
813     		$(window).trigger(event);
814     	 }
815
816});
817riot.tag2('ctrl-resplan-month', '<virtual if="{this.visible}"> <div class="header"> <div class="navigation-container previous"> <span riot-style="visibility : {this.canGoLeftMonth() ? \'visible\': \'hidden\'}" onclick="{this.goLeftMonth}" class="material-icons-outlined">chevron_left</span> </div> <div class="header-best-offer-list clearfix"> <div class="header-best-offer-item clearfix {(month.isOutOfSell || month.isPast) ? \'not-open\' : \'open\'}" riot-style="width: {this.widthHeader}" each="{month in this.getVisibleMonths()}" onclick="{this.selectMonthEvent(month)}" data-isloaded="{month.isLoaded}" data-year="{month.Year}" data-month="{month.Month}"> <div class="header-best-offer {month.isSelected ? \'header-best-offer-selected\' : \'\'}"> <div class="header-best-offer-container" data-price-value="{month.BestPrice}"> <div class="header-best-offer-year">{month.Year}</div> <div class="header-best-offer-month">{month.MonthName}</div> <div class="header-best-offer-month daynum hide-on-desktop">{month.MonthName}</div> <div class="header-best-offer-price"> <virtual if="{month.isLoaded}"> <div if="{month.isOpen && !this.data.forUpdateAppointment}"><span>{month.BestPrice}</span>&euro;</div> <div class="message" if="{month.isOpen && this.data.forUpdateAppointment}">{dictionary.PLANNING2_UPDATE_OPEN}</div> <div class="message" if="{!month.isOpen && !month.isOutOfSell}">{dictionary.PLANNING2_SOLD_OUT}</div> <div class="message" if="{!month.isOpen && month.isOutOfSell}">{dictionary.PLANNING2_CLOSED}</div> </virtual> <div if="{!month.isLoaded}"> <div class="header-best-offer-async fas fa-spinner fa-pulse"></div> </div> </div> </div> </div> </div> </div> <div class="navigation-container next"> <span if="{this.canGoRightMonth()}" onclick="{this.goRightMonth}" class="material-icons-outlined">chevron_right</span> </div> </div> <div id="content"> <div if="{!this.data.forUpdateAppointment}" class="legend"> <ctrl-resplan-legend context="{opts.context}"></ctrl-resplan-legend> </div> <div if="{this.data.forUpdateAppointment}" class="legend"> <div class="legend-container"> ✅ <span class="r-meilleure-promo"> Disponible </span> </div> </div> <div class="day-header-container"> <div class="day-header" each="{headerDay in this.headerDays}" riot-style="width : {100 / this.headerDays.length}%"> <span if="{this.isMobile}">{headerDay.AbbreviatedDayName}</span> <span if="{!this.isMobile}">{headerDay.DayName}</span> </div> </div> <div class="content-wrapper"> <div class="day-container"> <virtual if="{getSelectedMonth()}">
817 <div each="{day in this.getSelectedMonth().Days}" onclick="{this.selectDayEvent(day, true)}" class="{this.getDayClassName(day)}" riot-style="width: {100 / this.headerDays.length}%"> <div class="day"> <div class="day-element-header">{new Date(day.Date).getDate()}</div> <div class="day-element-content" style="text-align: center;"> <virtual if="{day.isOpen && !this.data.forUpdateAppointment}"> <div class="day-element-price {day.BestPrice === this.getSelectedMonth().BestPrice ? \'best-price-of-month\' : \'\'}"> <span>{getFancyPriceInteger(day.BestPrice2Dec)}</span><span class="small-devise">{getFancyPriceDecimal(day.BestPrice2Dec)}&euro;</span> </div> <div class="day-element-reduction reduc {day.BestPrice === this.getSelectedMonth().BestPrice ? \'best-price-of-month\' : \'\'}"> <span class="r-{day.BestReduction}">-{day.BestReduction}%</span> </div> </virtual> <virtual if="{day.isOpen && this.data.forUpdateAppointment}"> <div class="message">{dictionary.PLANNING2_UPDATE_OPEN}</div> </virtual> <virtual if="{!day.isOpen && !this.isDayInPast(day) && !day.isOutOfSell}"> <div class="day-complete-message">COMPLET</div> </virtual> </div> </div> </div> </virtual> </div> <div class="action-buttons"> <div class="btn retour" onclick="{this.closeModal}"> <span class="material-icons" style="color: white;">keyboard_backspace</span> Retour </div> </div> </div> </div> </virtual>', 'ctrl-resplan-month,[data-is="ctrl-resplan-month"]{ }', '', function(opts) {
818
819        var tag = this;
820
821        tag.visible = false;
822        tag.widthHeader = tag.parent.widthHeader;
823        tag.isMobile = tag.parent.isMobile;
824        tag.daySelected = null;
825
826        tag.data = {
827            locationId: tag.opts.locationid,
828            year: tag.opts.year,
829            month: tag.opts.month,
830            day: tag.opts.day,
831            prestationType: tag.opts.prestationtype,
832            vehicleType: tag.opts.vehicletype,
833            energyType: tag.opts.energytype,
834            forUpdateAppointment: tag.opts.forupdateappointment,
835            lotteryCode: tag.opts.lotterycode,
836            bypassCache: tag.opts.bypasscache === 'true' || tag.opts.bypasscache === true,
837        };
838
839        tag.getFancyPriceInteger = function (priceStr) {
840            if (!priceStr) return '';
841            var commaIndex = priceStr.indexOf(',');
842            if (commaIndex > -1) {
843                return priceStr.substring(0, commaIndex);
844            }
845            return priceStr;
846        }
847
848        tag.isDayInPast = function (day) {
849            if (!day || !day.Date) return false;
850            var today = new Date();
851            today.setHours(0, 0, 0, 0);
852            var dayDate = new Date(day.Date);
853            dayDate.setHours(0, 0, 0, 0);
854            return dayDate < today;
855        }
856
857        tag.getFancyPriceDecimal = function (priceStr) {
858            if (!priceStr) return '';
859            var commaIndex = priceStr.indexOf(',');
860            if (commaIndex > -1) {
861                return priceStr.substring(commaIndex);
862            }
863            return '';
864        }
865
866        tag.getSelectedMonth = function () {
867            if (tag.months) {
868                return tag.months.find(function (m) {
869                    return m.isSelected
870                });
871            }
872        }
873
874        tag.getSelectedMonthIndex = function () {
875            if (tag.months) {
876                return tag.months.indexOf(tag.getSelectedMonth());
877            }
878            return -1;
879        }
880
881        /*
882            @return la liste des mois visibles
883        */
884        tag.getVisibleMonths = function () {
885
886            if (tag.months) {
887                var pas = tag.parent.pas;
888                var visibleMonths = new Array();
889                var selectedMonthIndex = tag.getSelectedMonthIndex();
890                for (var i = -pas; i <= pas; i++) {
891
892                    var currentMonth = tag.months[selectedMonthIndex + i];
893                    if (currentMonth) {
894                        visibleMonths.push(currentMonth);
895                    }
896                }
897                return visibleMonths;
898            }
899
900        }
901
902        /*
903        charge une liste de mois proposés à la vente ou non, dont l'ensemble des données pour le mois demandé
904       */
905        tag.loadStructure = function () {
906            tag.loadDays();
907            tag._loadStructureWithRetry(0);
908        };
909
910        var MAX_RETRIES = 3;
911        var RETRY_DELAY_MS = 2000;
912
913        tag._loadStructureWithRetry = function (retryCount) {
914            $.ajax({
915                url: tag.opts.context + tag.opts.url + '?action=getMonthStructure',
916                method: 'post',
917                data: tag.data,
918                dataType: "json",
919                cache: false
920            }).done(function (months, textStatus, jqXHR) {
921                tag.months = months;
922
923                // Marquer tous les mois passés comme "loaded" pour éviter d'afficher le spinner
924                // (on ne les chargera jamais puisqu'ils sont dans le passé)
925                tag.months.forEach(function(month) {
926                    if (month.isPast) {
927                        month.isLoaded = true;
928                    }
929                });
930
931                // Sélectionner le mois demandé (depuis le meilleur prix ou autre)
932                // Le serveur renvoie par défaut le mois courant, mais on veut afficher
933                // le mois passé en paramètre (year/month)
934                if (tag.data.year && tag.data.month) {
935                    var targetYear = parseInt(tag.data.year);
936                    var targetMonth = parseInt(tag.data.month);
937                    tag.months.forEach(function(month) {
938                        month.isSelected = (month.Year === targetYear && month.Month === targetMonth);
939                    });
940                }
941
942                tag.trigger('ready'); // le tag parent fait l'update
943                tag.loadMonthsNotYetLoaded();
944
945            }).fail(function (jqXHR, textStatus, errorThrown) {
946                // Retry si session pas encore initialisée (race condition à l'arrivée sur la page centre)
947                if (jqXHR.status === 501 && retryCount < MAX_RETRIES) {
948                    setTimeout(function () {
949                        tag._loadStructureWithRetry(retryCount + 1);
950                    }, RETRY_DELAY_MS);
951                } else {
952                    tag.trigger('showError', errorThrown);
953                }
954            });
955        };
956
957        /*
958        Fonction appelée à la première ouverture : charge TOUS les mois visibles en parallèle
959        Y compris le mois initial (même s'il vient du cache de la pré-recherche)
960        */
961        tag.loadMonthsNotYetLoaded = function () {
962
963            // Charge TOUS les mois visibles à l'écran en parallèle (même ceux déjà en cache)
964            var monthsToLoad = tag.getVisibleMonths().filter(function (month) {
965                return !month.isPast
966            });
967
968            if (monthsToLoad.length > 0) {
969                // Marquer tous les mois à charger comme non chargés pour forcer le refresh
970                monthsToLoad.forEach(function(month) {
971                    month.isLoaded = false;
972                });
973                tag.update();
974
975                // Charger tous les mois en parallèle
976                monthsToLoad.forEach(function(month) {
977                    tag.loadMonth(month, false);
978                });
979            } else {
980                // Tous les mois visibles sont chargés
981                tag.parent.endLoading = true;
982                tag.parent.update();
983            }
984            return;
985        }
986
987        /*
988        Vérifie si tous les mois visibles sont chargés et met à jour endLoading
989        */
990        tag.checkAllVisibleMonthsLoaded = function() {
991            var visibleMonths = tag.getVisibleMonths();
992            if (!visibleMonths) return;
993
994            var allLoaded = visibleMonths.every(function(month) {
995                return month.isLoaded || month.isPast;
996            });
997
998            if (allLoaded && !tag.parent.endLoading) {
999                tag.parent.endLoading = true;
1000                tag.parent.update();
1001            }
1002        }
1003
1004        //chargement des jours pour la barre du haut
1005        tag.loadDays = function () {
1006            $.ajax({
1007                url: tag.opts.context + tag.opts.url + '?action=getDays',
1008                method: 'get',
1009                dataType: "json",
1010                cache: false
1011            }).done(function (days, textStatus, jqXHR) {
1012                tag.headerDays = days;
1013                tag.isDaysLoaded = true;
1014
1015            }).fail(function (jqXHR, textStatus, errorThrown) {
1016
1017                tag.trigger('showError', errorThrown);
1018
1019            }).always(function () {
1020                tag.update();
1021            });
1022        };
1023
1024        tag.loadMonth = function (month, loadOtherMonths) {
1025            //debugger;
1026            //devlog('LOADMONTH year=' + month.Year + ' month='+(month.Month -1));
1027
1028            $.ajax({
1029                url: tag.opts.context + tag.opts.url + '?action=loadOneMonth',
1030                method: 'post',
1031                dataType: "json",
1032                data: {
1033                    year: month.Year,
1034                    //  month : month.Month,
1035                    month: (month.Month - 1),
1036                    locationId: tag.data.locationId,
1037                    prestationType: tag.data.prestationType,
1038                    vehicleType: tag.data.vehicleType,
1039                    energyType: tag.data.energyType,
1040                    lotteryCode: tag.data.lotteryCode,
1041                    reservationCode: tag.data.reservationCode,
1042                    forUpdateAppointment: tag.data.forUpdateAppointment,
1043                    bypassCache: tag.data.bypassCache ? 'true' : 'false'
1044                },
1045                cache: false
1046            }).done(function (data, textStatus, jqXHR) {
1047                // pour test
1048                // debugger;
1049                // if(data.Days == undefined || data.Days == null){
1050                // 	devlog('year=' + month.Year + ' BestPrice='+(month.BestPrice));
1051                // }
1052                month.BestPrice = data.BestPrice;
1053                month.DataTime = data.DataTime;
1054                //month.isLoaded = data.isLoaded;
1055                month.Days = data.Days;
1056                month.isOpen = data.isOpen;
1057                //tag.fillAllDays(tag.Days);
1058                month.isLoaded = true;
1059
1060                // Vérifier si tous les mois visibles sont maintenant chargés
1061                tag.checkAllVisibleMonthsLoaded();
1062
1063                //cas du month selected charge avec loadCenterNextResult
1064                if (month.Month == tag.data.month) {
1065                    tag.selectMonth(month);
1066
1067                    // Auto-sélection du jour si spécifié (depuis le meilleur prix)
1068                    if (tag.data.day && month.Days) {
1069                        var targetDay = parseInt(tag.data.day);
1070                        var dayToSelect = month.Days.find(function(d) {
1071                            return new Date(d.Date).getDate() === targetDay && d.isOpen;
1072                        });
1073                        if (dayToSelect) {
1074                            tag.selectDay(dayToSelect, true);
1075                        }
1076                        // Reset pour ne pas re-sélectionner si on navigue
1077                        tag.data.day = null;
1078                    }
1079                }
1080
1081            }).fail(function (jqXHR, textStatus, errorThrown) {
1082                tag.trigger('showError', errorThrown);
1083            }).always(function () {
1084                tag.update();
1085            });
1086
1087        };
1088
1089        tag.canGoLeftMonth = function () {
1090            // Désactiver les flèches si des mois sont en cours de chargement
1091            if (tag.isLoadingVisibleMonths()) {
1092                return false;
1093            }
1094
1095            if (tag.getSelectedMonth()) {
1096                var previous = tag.months.getPrevious(tag.getSelectedMonth());
1097                if (previous && !previous.isPast) {
1098                    return true;
1099                }
1100            }
1101            return false;
1102        };
1103
1104        tag.canGoRightMonth = function () {
1105            // Désactiver les flèches si des mois sont en cours de chargement
1106            if (tag.isLoadingVisibleMonths()) {
1107                return false;
1108            }
1109
1110            if (tag.getSelectedMonth()) {
1111                var next = tag.months.getNext(tag.getSelectedMonth());
1112                if (next && !next.isOutOfSell) {
1113                    return true;
1114                }
1115            }
1116            return false;
1117        };
1118
1119        tag.goLeftMonth = function () {
1120            // Empêcher la navigation si des mois visibles sont en cours de chargement
1121            if (tag.isLoadingVisibleMonths()) {
1122                return;
1123            }
1124
1125            if (tag.getSelectedMonth()) {
1126                var previous = tag.months.getPrevious(tag.getSelectedMonth());
1127                if (previous && !previous.isPast) {
1128                    tag.selectMonth(previous);
1129                    // loadVisibleMonthsIfNeeded() est déjà appelé dans selectMonth()
1130                }
1131            }
1132        };
1133
1134        tag.goRightMonth = function () {
1135            // Empêcher la navigation si des mois visibles sont en cours de chargement
1136            if (tag.isLoadingVisibleMonths()) {
1137                return;
1138            }
1139
1140            if (tag.getSelectedMonth()) {
1141                var next = tag.months.getNext(tag.getSelectedMonth());
1142                if (next && !next.isOutOfSell) {
1143                    tag.selectMonth(next);
1144                    // loadVisibleMonthsIfNeeded() est déjà appelé dans selectMonth()
1145                }
1146            }
1147        };
1148
1149        /*
1150        Vérifie si des mois visibles sont en cours de chargement
1151        Utilisé pour empêcher les clics rapides sur les flèches
1152        */
1153        tag.isLoadingVisibleMonths = function() {
1154            var visibleMonths = tag.getVisibleMonths();
1155            if (!visibleMonths) return false;
1156
1157            return visibleMonths.some(function(month) {
1158                return !month.isLoaded && !month.isPast;
1159            });
1160        };
1161
1162        /*
1163        Charge tous les mois visibles à l'écran qui ne sont pas encore chargés
1164        Appelé automatiquement par selectMonth() lors de toute navigation (flèches ou clic direct)
1165        */
1166        tag.loadVisibleMonthsIfNeeded = function() {
1167            var hasLoadedAny = false;
1168            tag.getVisibleMonths().forEach(function(month) {
1169                if (!month.isLoaded && !month.isPast) {
1170                    tag.loadMonth(month, false);
1171                    hasLoadedAny = true;
1172                }
1173            });
1174            // Forcer l'update pour cacher les flèches pendant le chargement
1175            if (hasLoadedAny) {
1176                tag.update();
1177            }
1178        };
1179
1180        tag.selectMonthEvent = function (month) {
1181            return function (e) {
1182                // Empêcher la sélection si des mois visibles sont en cours de chargement
1183                if (tag.isLoadingVisibleMonths()) {
1184                    return;
1185                }
1186                tag.selectMonth(month);
1187                tag.daySelected = null;
1188            }
1189        };
1190
1191        tag.selectDayEvent = function (day, fromMonth) {
1192            return function (e) {
1193                tag.selectDay(day, fromMonth);
1194            }
1195        };
1196
1197        tag.selectMonth = function (month) {
1198            if (month.isOutOfSell || month.isPast)
1199                return;
1200            tag.months.forEach(function (m) {
1201                m.isSelected = (m == month);
1202            });
1203            // Charger les mois visibles si nécessaire après la sélection
1204            tag.loadVisibleMonthsIfNeeded();
1205            tag.update();
1206        };
1207
1208        tag.selectDay = function (day, fromMonth) {
1209
1210            if (fromMonth && !day.isOpen)
1211                return;
1212            if (day.isOutOfSell || day.isPast)
1213                return;
1214
1215            // changement de mode : affichage des slots du jour
1216            //tag.trigger('daySelected', day);
1217            tag.daySelected = day;
1218            // pas de bouton valider, passe directement � la s�lection des heures
1219            tag.validateSelectDay();
1220            window._trk && window._trk({
1221                event: 'ux_event',
1222                event_category: 'planning',
1223                event_action: 'select_day'
1224            });
1225        };
1226
1227        tag.validateSelectDay = function () {
1228            // changement de mode : affichage des slots du jour
1229            //debugger;
1230            if (tag.daySelected) {
1231                tag.trigger('daySelected', tag.daySelected);
1232            }
1233
1234        }
1235
1236        tag.closeModal = function () {
1237            $("#planning-modal").modal('hide');
1238        }
1239
1240        tag.getDayClassName = function (day) {
1241
1242            var dayClassName = 'day-element';
1243            if (day === tag.daySelected) dayClassName += ' day-selected';
1244            if (day.isFake) dayClassName += ' fake-day';
1245            dayClassName += day.isOpen ? ' open-day' : ' close-day';
1246
1247            // si le jour est fermé, on test les autres jours de la semaine pour voir si toute la semaine est fermée
1248            // alors on rajoute le className 'closed-week'
1249            if (!day.isOpen) {
1250
1251                const wom = day.WeekOfTheMonth;
1252                const lstDays = tag.getSelectedMonth().Days;
1253                const lstDaysOfTheWeek = lstDays.filter(d => d.WeekOfTheMonth === wom);
1254                const lstDaysOfTheWeekClosed = lstDaysOfTheWeek.filter(d => !d.isOpen);
1255
1256                if (lstDaysOfTheWeekClosed.length === lstDaysOfTheWeek.length) {
1257                    dayClassName += ' closed-week';
1258                }
1259
1260            }
1261
1262            return dayClassName;
1263
1264        }
1265
1266});
1267riot.tag2('ctrl-search-panel-confirm', '<div class="search-panel-global"> <div riot-style="{searching ? \'opacity : 0.1\' : \'opacity : 1\'}"> <div class="row"> <div class="col col-search-vehicle-type"> <ctrl-vehicle-type ref="vehicleType" type="{opts.vehicle_type}" items="{opts.vehicle_type_items}" context="{opts.context}" url="{opts.url}"></ctrl-vehicle-type> </div> <div class="col col-search-vehicle-energy"> <ctrl-vehicle-energy ref="vehicleEnergyType" type="{opts.vehicle_energy}" items="{opts.vehicle_energy_items}" context="{opts.context}" url="{opts.url}"></ctrl-vehicle-energy> </div> <div class="col col-btn-search"> <button ref="btnSearch" class="form-control btn btn-clic-to-action"> <span>{opts.search_label}</span> </button> </div> </div> </div> </div>', '', '', function(opts) {
1268        var tag = this;
1269        tag.mixin(dcMixin);
1270
1271        tag.searching = false;
1272        tag.selectedVehicleType = null;
1273        tag.selectedVehicleEnergyType = null;
1274        tag.goToNextFieldAfterSelection = opts.goToNextFieldAfterSelection === true;
1275
1276        tag.initialValues = opts.vehicle_type + "_" + opts.vehicle_energy;
1277
1278        tag.isReadyForSearch = function () {
1279
1280            if (tag.selectedVehicleType && tag.selectedVehicleEnergyType) {
1281
1282                var actualValues = tag.selectedVehicleType.code + "_" + tag.selectedVehicleEnergyType.code;
1283
1284                return tag.initialValues !== actualValues;
1285
1286            } else {
1287                return false;
1288            }
1289
1290        }
1291
1292        tag.refreshButtonState = function (withResearch) {
1293            if (tag.isReadyForSearch()) {
1294                $(tag.refs.btnSearch).prop('disabled', '');
1295                $(tag.refs.btnSearch).addClass('ready-for-search');
1296
1297                if (withResearch) {
1298
1299                    console.log("refreshButtonState " + withResearch);
1300
1301                } else {
1302                    if (!tag.isReadyForSearch()) {
1303                        $(tag.refs.btnSearch).prop('disabled', true);
1304                    }
1305                }
1306            } else {
1307                $(tag.refs.btnSearch).removeClass('ready-for-search');
1308            }
1309        }
1310
1311        tag.launchResearch = function () {
1312
1313            if (!tag.isReadyForSearch()) return;
1314
1315            var newurl = null;
1316            if (tag.selectedLocality.idDev10) {
1317                newurl = tag.opts.context + tag.selectedLocality.url
1318                    + 'yearMonth=' + tag.selectedMonth.Year + tag.selectedMonth.Month
1319                    + '&vehicleType=' + tag.selectedVehicleType.code
1320                    + '&energyType=' + tag.selectedVehicleEnergyType.code
1321            } else {
1322                newurl = tag.opts.context + tag.opts.url;
1323            }
1324
1325            if (newurl) {
1326                searching = true;
1327                showLoading(true, dictionary.SEARCH_ENGINE_LOADING_TITLE);
1328                $("input", tag.root)
1329                    .addClass('disabled')
1330                    .prop('disabled', true);
1331                $("button", tag.root)
1332                    .addClass('disabled')
1333                    .prop('disabled', true);
1334
1335                $(".promohome").css("visibility", "hidden");
1336
1337                tag.update();
1338
1339                const params = {
1340                    'context': webAppContext,
1341                    'url': currentNavNodeUrl,
1342                    'locationId': opts.locality,
1343                    'locationCity': locationCity,
1344                    'locationName': locationName,
1345                    'year': year,
1346                    'month': month,
1347                    'prestationType': 'CT',
1348                    'vehicleType': vehicleType,
1349                    'energyType': energyType
1350                };
1351                mount('ctrl-resplan-modal', params);
1352
1353                return true;
1354            }
1355        }
1356
1357        tag.on('mount', function () {
1358
1359            $(tag.refs.btnSearch).on('click', function () {
1360                var trkLabel = '';
1361                if (tag.selectedVehicleType && tag.selectedVehicleType.code) {
1362                    trkLabel = trkLabel + "_" + tag.selectedVehicleType.code;
1363                } else {
1364                    trkLabel = trkLabel + "_notype";
1365                }
1366                if (tag.selectedVehicleEnergyType && tag.selectedVehicleEnergyType.code) {
1367                    trkLabel = trkLabel + "_" + tag.selectedVehicleEnergyType.code;
1368                } else {
1369                    trkLabel = trkLabel + "_noenergy";
1370                }
1371
1372                if (!tag.isReadyForSearch()) {
1373                    if (tag.selectedVehicleType == null) {
1374                        $(tag.refs.vehicleType.refs.searchInput).effect("highlight", {color: '#f60'}, 1500);
1375                    } else if (tag.selectedVehicleEnergyType == null) {
1376                        $(tag.refs.vehicleEnergyType.refs.searchInput).effect("highlight", {color: '#f60'}, 1500);
1377                    }
1378                    window._trk && window._trk({
1379                        event: 'ux_event',
1380                        event_category: 'search_confirm',
1381                        event_action: 'Search confirm Not Ready',
1382                        event_label: trkLabel
1383                    });
1384                } else {
1385                    window._trk && window._trk({
1386                        event: 'ux_event',
1387                        event_category: 'search_confirm',
1388                        event_action: 'Search confirm Launch',
1389                        event_label: trkLabel
1390                    });
1391                }
1392                tag.launchResearch();
1393            });
1394
1395            tag.refs.locality.on('onDropdownOpen', function () {
1396                if (tag.refs.vehicleType) {
1397                    tag.refs.vehicleType.dropdownClose();
1398                }
1399                if (tag.refs.vehicleEnergyType) {
1400                    tag.refs.vehicleEnergyType.dropdownClose();
1401                }
1402
1403            });
1404
1405            if (tag.refs.vehicleType) {
1406                tag.refs.vehicleType.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1407
1408                    tag.selectedVehicleType = newvalue;
1409                    if (!tag.refreshButtonState(withAutomaticSearch)) {
1410
1411                        if (newvalue) {
1412                            if (withAutomaticSearch && tag.goToNextFieldAfterSelection) {
1413                                window.setTimeout(function () {
1414
1415                                    tag.refs.vehicleEnergyType.dropdownOpen();
1416                                }, 200);
1417
1418                            }
1419                        }
1420                    }
1421                });
1422                tag.refs.vehicleType.on('onDropdownOpen', function () {
1423
1424                    if (tag.refs.locality) {
1425                        tag.refs.locality.dropdownClose();
1426                    }
1427                    if (tag.refs.month) {
1428                        tag.refs.month.dropdownClose();
1429                    }
1430                    if (tag.refs.vehicleEnergyType) {
1431                        tag.refs.vehicleEnergyType.dropdownClose();
1432                    }
1433
1434                });
1435            }
1436
1437            if (tag.refs.vehicleEnergyType) {
1438                tag.refs.vehicleEnergyType.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1439
1440                    tag.selectedVehicleEnergyType = newvalue;
1441                    tag.refreshButtonState(withAutomaticSearch);
1442                });
1443                tag.refs.vehicleEnergyType.on('onDropdownOpen', function () {
1444                    if (tag.refs.locality) {
1445                        tag.refs.locality.dropdownClose();
1446                    }
1447                    if (tag.refs.month) {
1448                        tag.refs.month.dropdownClose();
1449                    }
1450                    if (tag.refs.vehicleType) {
1451                        tag.refs.vehicleType.dropdownClose();
1452                    }
1453
1454                });
1455            }
1456
1457            tag.refreshButtonState(false);
1458
1459            $(window).bind("pageshow", function (event) {
1460                if (event.originalEvent.persisted) {
1461                    window.location.reload();
1462                }
1463            });
1464
1465        });
1466});
1467riot.tag2('ctrl-search-panel-cteasy', '<div class="row {searchLocalityVisibleSM?\'d-sm-locality-input-block\':\'d-sm-locality-input-none\'}"> <div class="col col-12 col-md-2"> <div class="search-info search-info-locality">{dictionary.TCC_SEARCH_PANEL_LOCALITY}</div> </div> <div class="col col-12 col-md-10"> <div class="row"> <div class="col col-12 col-md-6"> <div class="search-locality-affichage-panel background-primary"> <div ref="searchLocalityAffichage" onclick="{this.toggleSearchLocalityVisibleSM}"> <p><i class="fas fa-map-marker-alt fa-placeholder"></i> {this.searchPlaceName}</p> <div class="arrow"></div> </div> </div> </div> <div class="col col-12 col-md-6"> <div class="search-locality-panel"> <div ref="searchLocalityPanel"> <ctrl-locality ref="locality" locality="" search_place_name="" context="{opts.context}" url="{opts.url}"></ctrl-locality> </div> </div> </div> </div> </div> </div> <hr> <div class="row"> <div class="col col-12 col-md-2"> <div class="search-info search-info-month">{dictionary.TCC_SEARCH_PANEL_MONTH}</div> </div> <div class="col col-12 col-md-10"> <div class="row"> <div class="col col-12 col-md col-item-month" each="{item in this.monthItems}" onclick="{this.changeMonth}"> <div class="btn {this.monthSelected == item.YearMonth? \'btn-primary\':\'btn-default\'} ">
1467 <i if="{this.monthSelected != item.YearMonth}" class="far fa-circle"></i> <i if="{this.monthSelected == item.YearMonth}" class="fas fa-check-circle"></i> <p class="month">{item.MonthName}</p> <p class="year">{item.Year}</p> </div> </div> </div> </div> </div> <hr> <div class="row"> <div class="col col-12 col-md-2"> <div class="search-info search-info-vehicle-type">{dictionary.TCC_SEARCH_PANEL_VEHICLE_TYPE}</div> </div> <div class="col col-12 col-md-10 cont-type-vehicule"> <div class="row"> <div class="col col-4 col-md col-type-vehicule" each="{item in this.opts.vehicle_type_items}" onclick="{this.changeType}"> <div class="btn {this.typeSelected == item.code? \'btn-primary\':\'btn-default\'} "> <p><img riot-src="{opts.context}/pict/riot-img/vehicle-type/ico-{item.code}.png" alt="{item.code}"></p> <p><span>{item.name}</span></p> </div> </div> </div> </div> </div> <hr> <div class="row"> <div class="col col-12 col-md-2"> <div class="search-info search-info-energy-type">{dictionary.TCC_SEARCH_PANEL_ENERGY_TYPE}</div> </div> <div class="col col-12 col-md-10 cont-type-energy"> <div class="row"> <div class="col col-4 col-md col-type-energy" each="{item in this.opts.vehicle_energy_items}" onclick="{this.changeEnergy}"> <div class="btn {this.energySelected == item.code? \'btn-primary\':\'btn-default\'} "> <i if="{this.energySelected != item.code}" class="far fa-circle"></i> <i if="{this.energySelected == item.code}" class="fas fa-check-circle"></i> <p class="energy"><span>{item.name}</span></p> </div> </div> </div> </div> </div>', 'ctrl-search-panel-cteasy .btn,[data-is="ctrl-search-panel-cteasy"] .btn{ width: 100%; } ctrl-search-panel-cteasy .btn p,[data-is="ctrl-search-panel-cteasy"] .btn p{ margin: 0; padding: 0; } ctrl-search-panel-cteasy .btn p.annee,[data-is="ctrl-search-panel-cteasy"] .btn p.annee{ line-height: 10px; }', '', function(opts) {
1468        this.mixin(dcMixin);
1469        this.monthItems = null;
1470        this.localitySelected = this.opts.locality;
1471        this.zipCodeSelected = this.opts.zipCode;
1472        this.nbLocalityFound = this.opts.nb_locality_found;
1473        this.searchPlaceName = this.opts.search_place_name;
1474        this.monthSelected = this.opts.year_month_selected;
1475        this.typeSelected = this.opts.vehicle_type_selected;
1476        this.energySelected = this.opts.vehicle_energy_selected;
1477
1478        this.searchLocalityVisibleSM = false;
1479
1480        this.loadMonthItems = function () {
1481            var tag = this;
1482            $.ajax({
1483                url: this.opts.context + this.opts.url + '?action=getMonthList',
1484                type: 'POST',
1485                cache: false
1486            }).done(function (data) {
1487                tag.monthItems = data;
1488                tag.update();
1489            });
1490        };
1491
1492        this.toggleSearchLocalityVisibleSM = function () {
1493            tag.searchLocalityVisibleSM = !tag.searchLocalityVisibleSM;
1494            tag.update();
1495            if (tag.searchLocalityVisibleSM) {
1496                var el = tag.refs.locality;
1497                el.searchInputFocus();
1498
1499                $('html, body').animate({
1500                    scrollTop: $(".search-locality-affichage-panel").offset().top
1501                }, 500);
1502            }
1503        }
1504
1505        var tag = this;
1506
1507        tag.on('mount', function () {
1508            devlog("localitySelected 1 : " + tag.localitySelected);
1509
1510            if (tag.refs.locality) {
1511
1512                tag.refs.locality.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1513
1514                    if (newvalue) {
1515                        if (withAutomaticSearch) {
1516                            window.setTimeout(function () {
1517                                tag.localitySelected = newvalue.codeCityInsee;
1518                                tag.zipCodeSelected = newvalue.zipCode;
1519                                tag.searchPlaceName = newvalue.name;
1520                                $(".search-locality-panel input").val("");
1521                                tag.search();
1522                            }, 200);
1523                        }
1524                    }
1525                });
1526            }
1527
1528            this.changeMonth = function (e) {
1529                tag.monthSelected = e.item.item.YearMonth;
1530                tag.search();
1531                tag.update();
1532            }
1533
1534            this.changeType = function (e) {
1535                tag.typeSelected = e.item.item.code;
1536                tag.search();
1537                tag.update();
1538            }
1539
1540            this.changeEnergy = function (e) {
1541                tag.energySelected = e.item.item.code;
1542                tag.search();
1543                tag.update();
1544            }
1545
1546            this.search = function () {
1547                reloadSearchResults(this.searchPlaceName, this.localitySelected, this.zipCodeSelected, this.monthSelected,
1548                    this.typeSelected, this.energySelected);
1549                tag.searchLocalityVisibleSM = false;
1550            }
1551
1552            this.loadMonthItems();
1553
1554        });
1555}); 
1556riot.tag2('ctrl-search-panel-global', '<div class="search-panel-global"> <div riot-style="{searching ? \'opacity : 0.1\' : \'opacity : 1\'}"> <div class="row"> <div class="col col-search-locality"> <ctrl-locality ref="locality" locality="{opts.locality}" search_place_name="{opts.search_place_name}" context="{opts.context}" url="{opts.url}"></ctrl-locality> </div> <div class="col col-search-month"> <ctrl-month ref="month" yearmonth="{opts.yearmonth}" context="{opts.context}" url="{opts.url}"></ctrl-month> </div> <div class="col col-search-vehicle-type"> <ctrl-vehicle-type ref="vehicleType" type="{opts.vehicle_type}" items="{opts.vehicle_type_items}" context="{opts.context}" url="{opts.url}"></ctrl-vehicle-type> </div> <div class="col col-search-vehicle-energy"> <ctrl-vehicle-energy ref="vehicleEnergyType" type="{opts.vehicle_energy}" items="{opts.vehicle_energy_items}" context="{opts.context}" url="{opts.url}"></ctrl-vehicle-energy> </div> <div class="col col-btn-search"> <button ref="btnSearch" class="form-control btn btn-clic-to-action"> <span>{opts.search_label}</span> </button> </div> </div> </div> </div>', '', '', function(opts) {
1557        var tag = this;
1558        tag.mixin(dcMixin);
1559
1560        tag.searching = false;
1561        tag.selectedLocality = null;
1562        tag.selectedMonth = null;
1563        tag.selectedVehicleType = null;
1564        tag.selectedVehicleEnergyType = null;
1565        tag.goToNextFieldAfterSelection = opts.goToNextFieldAfterSelection === true;
1566
1567        tag.initialValues = opts.locality + "_" + opts.yearmonth + "_" + opts.vehicle_type + "_" + opts.vehicle_energy;
1568
1569        tag.isReadyForSearch = function () {
1570
1571            if (tag.selectedLocality && tag.selectedMonth && tag.selectedVehicleType && tag.selectedVehicleEnergyType) {
1572
1573                var actualValues = tag.selectedLocality.name + "_" + tag.selectedMonth.YearMonth + "_" + tag.selectedVehicleType.code + "_" + tag.selectedVehicleEnergyType.code;
1574
1575                return tag.initialValues !== actualValues;
1576
1577            } else {
1578                return false;
1579            }
1580
1581        }
1582
1583        tag.refreshButtonState = function (withResearch) {
1584            if (tag.isReadyForSearch()) {
1585                $(tag.refs.btnSearch).prop('disabled', '');
1586                $(tag.refs.btnSearch).addClass('ready-for-search');
1587
1588                if (withResearch) {
1589
1590                    console.log("refreshButtonState " + withResearch);
1591
1592                } else {
1593                    if (!tag.isReadyForSearch()) {
1594                        $(tag.refs.btnSearch).prop('disabled', true);
1595                    }
1596                }
1597            } else {
1598                $(tag.refs.btnSearch).removeClass('ready-for-search');
1599            }
1600        }
1601
1602        tag.launchResearch = function () {
1603
1604            if (!tag.isReadyForSearch()) return;
1605
1606            var newurl = null;
1607            if (tag.selectedLocality.idDev10) {
1608                newurl = tag.opts.context + tag.selectedLocality.url
1609                    + 'yearMonth=' + tag.selectedMonth.Year + tag.selectedMonth.Month
1610                    + '&vehicleType=' + tag.selectedVehicleType.code
1611                    + '&energyType=' + tag.selectedVehicleEnergyType.code
1612            } else {
1613                newurl = tag.opts.context + tag.opts.url;
1614            }
1615
1616            if (newurl) {
1617                searching = true;
1618                showLoading(true, dictionary.SEARCH_ENGINE_LOADING_TITLE);
1619                $("input", tag.root)
1620                    .addClass('disabled')
1621                    .prop('disabled', true);
1622                $("button", tag.root)
1623                    .addClass('disabled')
1624                    .prop('disabled', true);
1625
1626                $(".promohome").css("visibility", "hidden");
1627
1628                tag.update();
1629                window.setTimeout(function () {
1630                    window.location.href = newurl;
1631                }, 500);
1632                return true;
1633            }
1634        }
1635
1636        tag.on('mount', function () {
1637
1638            $(tag.refs.btnSearch).on('click', function () {
1639                var trkLabel = '';
1640                if (tag.selectedLocality && tag.selectedLocality.name) {
1641                    trkLabel = tag.selectedLocality.name;
1642                } else {
1643                    trkLabel = 'noloc';
1644                }
1645                if (tag.selectedMonth && tag.selectedMonth.YearMonth) {
1646                    trkLabel = trkLabel + "_" + tag.selectedMonth.YearMonth;
1647                } else {
1648                    trkLabel = trkLabel + "_month";
1649                }
1650                if (tag.selectedVehicleType && tag.selectedVehicleType.code) {
1651                    trkLabel = trkLabel + "_" + tag.selectedVehicleType.code;
1652                } else {
1653                    trkLabel = trkLabel + "_notype";
1654                }
1655                if (tag.selectedVehicleEnergyType && tag.selectedVehicleEnergyType.code) {
1656                    trkLabel = trkLabel + "_" + tag.selectedVehicleEnergyType.code;
1657                } else {
1658                    trkLabel = trkLabel + "_noenergy";
1659                }
1660
1661                if (!tag.isReadyForSearch()) {
1662                    if (tag.selectedLocality == null) {
1663                        $(tag.refs.locality.refs.searchInput).effect("highlight", {color: '#f60'}, 1500);
1664                    } else if (tag.selectedMonth == null) {
1665                        $(tag.refs.month.refs.searchInput).effect("highlight", {color: '#f60'}, 1500);
1666                    } else if (tag.selectedVehicleType == null) {
1667                        $(tag.refs.vehicleType.refs.searchInput).effect("highlight", {color: '#f60'}, 1500);
1668                    } else if (tag.selectedVehicleEnergyType == null) {
1669                        $(tag.refs.vehicleEnergyType.refs.searchInput).effect("highlight", {color: '#f60'}, 1500);
1670                    }
1671                    window._trk && window._trk({
1672                        event: 'ux_event',
1673                        event_category: 'search',
1674                        event_action: 'Search Not Ready',
1675                        event_label: trkLabel
1676                    });
1677                } else {
1678                    window._trk && window._trk({
1679                        event: 'ux_event',
1680                        event_category: 'search',
1681                        event_action: 'Search Launch',
1682                        event_label: trkLabel
1683                    });
1684                }
1685                tag.launchResearch();
1686            });
1687
1688            if (tag.refs.locality) {
1689
1690                tag.refs.locality.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1691                    tag.selectedLocality = newvalue;
1692
1693                    if (!tag.refreshButtonState(withAutomaticSearch)) {
1694                        if (newvalue) {
1695                            if (withAutomaticSearch && tag.goToNextFieldAfterSelection) {
1696                                if (tag.refs.month.selection == null) {
1697                                    window.setTimeout(function () {
1698                                        if (tag.refs.month) {
1699                                            tag.refs.month.dropdownOpen();
1700                                            $("#searchInputMonth").focus();
1701                                        }
1702                                    }, 200);
1703                                }
1704                            }
1705                        }
1706                    }
1707
1708                });
1709                tag.refs.locality.on('onDropdownOpen', function () {
1710                    if (tag.refs.month) {
1711                        tag.refs.month.dropdownClose();
1712                    }
1713                    if (tag.refs.vehicleType) {
1714                        tag.refs.vehicleType.dropdownClose();
1715                    }
1716                    if (tag.refs.vehicleEnergyType) {
1717                        tag.refs.vehicleEnergyType.dropdownClose();
1718                    }
1719
1720                });
1721
1722            }
1723
1724            if (tag.refs.month) {
1725                tag.refs.month.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1726
1727                    tag.selectedMonth = newvalue;
1728                    if (!tag.refreshButtonState(withAutomaticSearch)) {
1729                        if (newvalue) {
1730                            if (withAutomaticSearch && tag.goToNextFieldAfterSelection) {
1731                                window.setTimeout(function () {
1732
1733                                    tag.refs.vehicleType.dropdownOpen();
1734                                }, 200);
1735                            }
1736                        }
1737
1738                    }
1739                });
1740                tag.refs.month.on('onDropdownOpen', function () {
1741                    if (tag.refs.locality) {
1742                        tag.refs.locality.dropdownClose();
1743                    }
1744                    if (tag.refs.vehicleType) {
1745                        tag.refs.vehicleType.dropdownClose();
1746                    }
1747                    if (tag.refs.vehicleEnergyType) {
1748                        tag.refs.vehicleEnergyType.dropdownClose();
1749                    }
1750
1751                });
1752            }
1753
1754            if (tag.refs.vehicleType) {
1755                tag.refs.vehicleType.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1756
1757                    tag.selectedVehicleType = newvalue;
1758                    if (!tag.refreshButtonState(withAutomaticSearch)) {
1759
1760                        if (newvalue) {
1761                            if (withAutomaticSearch && tag.goToNextFieldAfterSelection) {
1762                                window.setTimeout(function () {
1763
1764                                    tag.refs.vehicleEnergyType.dropdownOpen();
1765                                }, 200);
1766
1767                            }
1768                        }
1769                    }
1770                });
1771                tag.refs.vehicleType.on('onDropdownOpen', function () {
1772
1773                    if (tag.refs.locality) {
1774                        tag.refs.locality.dropdownClose();
1775                    }
1776                    if (tag.refs.month) {
1777                        tag.refs.month.dropdownClose();
1778                    }
1779                    if (tag.refs.vehicleEnergyType) {
1780                        tag.refs.vehicleEnergyType.dropdownClose();
1781                    }
1782
1783                });
1784            }
1785
1786            if (tag.refs.vehicleEnergyType) {
1787                tag.refs.vehicleEnergyType.on('onchanged', function (newvalue, oldvalue, withAutomaticSearch) {
1788
1789                    tag.selectedVehicleEnergyType = newvalue;
1790                    tag.refreshButtonState(withAutomaticSearch);
1791                });
1792                tag.refs.vehicleEnergyType.on('onDropdownOpen', function () {
1793                    if (tag.refs.locality) {
1794                        tag.refs.locality.dropdownClose();
1795                    }
1796                    if (tag.refs.month) {
1797                        tag.refs.month.dropdownClose();
1798                    }
1799                    if (tag.refs.vehicleType) {
1800                        tag.refs.vehicleType.dropdownClose();
1801                    }
1802
1803                });
1804            }
1805
1806            tag.refreshButtonState(false);
1807
1808            $(window).bind("pageshow", function (event) {
1809                if (event.originalEvent.persisted) {
1810                    window.location.reload();
1811                }
1812            });
1813
1814        });
1815});
1816riot.tag2('ctrl-search-result-2021-order-buttons', '<ul class="nav nav-order"> <li class="search-order border-right {this.sortMode == 1?\'selected\':\'\'} device-all best-price" onclick="{this.updateOrder}" data-value="1" data-trk="search_result_order_best_price"> <span class="order-button" data-value="1">Le moins<br> cher</span> <span class="material-icons">euro</span> </li> <li class="search-order border-right {this.sortMode == 2?\'selected\':\'\'} device-all best-distance" onclick="{this.updateOrder}" data-value="2" style="border-radius: 0;" data-trk="search_result_order_best_distance"> <span class="order-button" data-value="2">Le plus<br> proche</span> <span class="material-icons">directions_car</span> </li> <li if="{this.opts.ranker != null && this.opts.ranker == \'true\'}" class="search-order {this.sortMode == 3?\'selected\':\'\'} device-all best-rank" onclick="{this.updateOrder}" data-value="3" data-trk="search_result_order_best_rank"> <span class="order-button" data-value="3">Le mieux<br> not&eacute;</span> <span class="material-icons">star_rate</span> </li> <li if="{this.opts.availability != null && this.opts.availability == \'true\'}" class="search-order {this.sortMode == 5?\'selected\':\'\'} device-all best-availability" onclick="{this.updateOrder}" data-value="5" data-trk="search_result_order_best_availability"> <span class="order-button" data-value="5">Le plus vite<br> disponible</span> <span class="material-icons">calendar_month</span> </li> </ul> <div class="popup-filter popup-filter-availability" style="display: none;"> <form id="form-availability" class="form-availability"> <fieldset class="popup-filter-content popup-filter-availability-content"> <div class="options-right" data-trk="search_result_order_availability_today"> <input type="radio" name="availability" id="availability-today" value="availability-today" onchange="{this.selectRadioButton}" data-value="availability-today"> <label for="availability-today">Aujourd\'hui</label> </div> <div class="options-right" data-trk="search_result_order_availability_3days"> <input type="radio" name="availability" id="availability-3days" value="availability-3days" onchange="{this.selectRadioButton}" data-value="availability-3days"> <label for="availability-3days">Dans les trois prochains jours</label> </div> <input type="submit" class="btn" value="Afficher les résultats" onclick="{this.selectAvailibility}" data-trk="search_result_order_availability_submit"> </fieldset> </form> </div> <div class="popup-filter popup-distance-price" style="display: none;"> <form id="form-distance" class="form-distance"> <fieldset class="popup-filter-content popup-filter-distance-content"> <div class="options-right" data-trk="search_result_order_distance_less_15"> <input type="radio" name="distance" id="distance-moins-15" value="distance-moins-15" onchange="{this.selectRadioButton}" data-value="distance-moins-15"> <label for="distance-moins-15">A moins de 15 kms</label> </div> <div class="options-right" data-trk="search_result_order_distance_more_15"> <input type="radio" name="distance" id="distance-plus-15" value="distance-plus-15" onchange="{this.selectRadioButton}" data-value="distance-plus-15"> <label for="distance-plus-15">A plus de 15 kms</label> </div> <input type="submit" class="btn" value="Afficher les résultats" onclick="{this.selectDistance}" data-trk="search_result_order_distance_submit"> </fieldset> </form> </div>', 'ctrl-search-result-2021-order-buttons .pulsate-border,[data-is="ctrl-search-result-2021-order-buttons"] .pulsate-border{ animation: pulsate-border-animation 1s ease-in-out infinite; } @keyframes pulsate-border-animation { 0% { border: 1px solid transparent; } 50% { border: 1px solid #ff6600; } 100% { border: 1px solid transparent; } }', '', function(opts) {
1817
1818        this.selectAvailibility = function (e) {
1819            e.preventDefault();
1820
1821            if ($("input[name='availability']:checked").length === 0) {
1822                $(".options-right").addClass("pulsate-border");
1823                setTimeout(function() {
1824                    $(".options-right").removeClass("pulsate-border");
1825                }, 1000);
1826                return;
1827            }
1828
1829            this.sortMode = 5;
1830            orderList(5, $("input[name='availability']:checked").val());
1831            $(".popup-filter-availability").hide();
1832        };
1833
1834        this.selectDistance = function (e) {
1835            e.preventDefault();
1836
1837            if ($("input[name='distance']:checked").length === 0) {
1838                $(".options-right").addClass("pulsate-border");
1839                setTimeout(function() {
1840                    $(".options-right").removeClass("pulsate-border");
1841                }, 1000);
1842                return;
1843            }
1844
1845            this.sortMode = 1;
1846            orderList(1, $("input[name='distance']:checked").val());
1847            $(".popup-distance-price").hide();
1848        };
1849
1850        this.selectRadioButton = function (e) {
1851            var val = $(e.srcElement).data("value");
1852            $("input#" + val).prop('checked', true);
1853        };
1854
1855        this.on('mount', function () {
1856            orderList(this.sortMode);
1857            if (this.opts.rdv_today == "0") {
1858                $("input#availability-today").parent().hide();
1859            }
1860
1861            $(document).on("click", function (event) {
1862
1863                if (!$(event.target).closest(".popup-filter-availability, .nav-order, .popup-distance-price").length) {
1864                    $(".popup-distance-price").hide();
1865                    $(".popup-filter-availability").hide();
1866                }
1867            });
1868
1869        });
1870
1871        this.mixin(dcMixin);
1872        this.sortMode = this.opts.sortMode;
1873
1874        this.updateOrder = function (e) {
1875            var val = $(e.srcElement).data("value");
1876            if (val == 5) {
1877                this.sortMode = val;
1878                $(".popup-distance-price").hide();
1879                $(".popup-filter-availability").show();
1880                return;
1881            } else if (val == 1) {
1882                this.sortMode = val;
1883                $(".popup-filter-availability").hide();
1884                $(".popup-distance-price").show();
1885                return;
1886            } else {
1887                $(".popup-filter-availability").hide();
1888                $(".popup-distance-price").hide();
1889            }
1890
1891            if (val != this.sortMode) {
1892                this.sortMode = val;
1893                if (val == 5) {
1894                    orderList(val, $("input[name='availability']:checked").val());
1895                } else {
1896                    orderList(val);
1897                }
1898            }
1899        }
1900
1901});
1902riot.tag2('ctrl-search-result-panel-tris', '<div class="row"> <div if="{this.showList}" class="col col-5"> <div class="btn btn-primary btn-map" onclick="{this.toggleMapPanel}"> <p><i class="far fa-map"></i> Afficher la carte <span class="d-none d-sm-inline">des meilleurs prix</span></p> </div> </div> <div if="{!this.showList}" class="col col-5"> <div class="btn btn-primary btn-list" onclick="{this.toggleMapPanel}"> <p><i class="fas fa-list"></i> Afficher la liste des meilleures offres</p> </div> </div> <div class="col col-7"> <div if="{this.showList && !this.rechercheParCentre}" class="panel-choose-tri"> <label for="ctls777" class="title">Trier par&nbsp;:</label> <select id="ctls777" class="cteasy-selector cteasy-sort-list-results cteasy-sort-{this.sortMode}" onchange="{this.handleSortOrderChange}"> <option value="1" class="opt-sort-1" selected="{this.sortMode == 1}">Prix</option> <option value="2" class="opt-sort-2" selected="{this.sortMode == 2}">Distance</option> <option value="3" class="opt-sort-3" selected="{this.sortMode == 3}">Avis</option> </select> </div> </div> </div> <div class="cteasy-meilleurs-offres"> <div class="row"> <div class="col-4"> <div class="offres offres-1 offre-tri-container {this.sortMode == 1?\'selected\':\'\'}" onclick="{this.modifSortList}" data-value="1"> <p><span class="but but-sort-1">LE MOINS CHER</span></p> <p><span class="price" id="le-moins-cher-price">X</span>&euro;</p> <p class="comment" id="le-moins-cher-comment">X% de remise</p> </div> </div> <div class="col-4"> <div class="offres offres-2 offre-tri-container {this.sortMode == 2?\'selected\':\'\'}" onclick="{this.modifSortList}" data-value="2"> <p><span class="but but-sort-2">LE PLUS PROCHE</span></p> <p><span class="price" id="le-plus-proche-price">X</span>&euro;</p> <p class="comment" id="le-plus-proche-comment">&agrave; X km de vous</p> </div> </div> <div class="col-4"> <div class="offres offres-3 offre-tri-container {this.sortMode == 3?\'selected\':\'\'}" onclick="{this.modifSortList}" data-value="3"> <p><span class="but but-sort-3">LE PLUS APPR&Eacute;CI&Eacute;</span></p> <p><span class="price" id="le-plus-apprecie-price">X</span>&euro;</p> <p class="comment" id="le-plus-apprecie-comment">Not&eacute; X avec X avis clients</p> </div> </div> </div> </div>', '', '', function(opts) {
1903
1904
1905	     this.on('mount', function() {
1906	    	 orderList(this.sortMode);
1907	     	this.fillDatasBestRankings();
1908	    });
1909
1910		this.mixin(dcMixin);
1911		this.searchPanelVisible =  this.opts.searchPanelVisible;
1912		this.showList = true;
1913		this.sortMode = this.opts.sortMode == undefined ? 2 : this.opts.sortMode;
1914		this.rechercheParCentre = this.opts.rechercheParCentre;
1915
1916		this.modifSortList = function(e){
1917			console.log("modifSortList");
1918
1919			var val = $(e.srcElement).parents(".offre-tri-container").data("value");
1920			console.log("val = " + val);
1921			if(val != this.sortMode){
1922				this.sortMode = val;
1923				orderList(val);
1924			}
1925
1926			this.fillDatasBestRankings();
1927		}
1928
1929		showPanelMap(!this.showList);
1930
1931		this.toggleMapPanel = function(e){
1932			this.showList = !this.showList;
1933			showPanelMap(!this.showList);
1934		}
1935
1936		this.handleSortOrderChange = function(e){
1937			var val =  e.srcElement.value;
1938
1939			if(val != this.sortMode){
1940				this.sortMode = val;
1941				orderList(val);
1942			}
1943		}
1944
1945		this.fillDatasBestRankings = function(e){
1946
1947			var bestPriceValue = 1000;
1948			var bestPriceComment = "";
1949
1950			var bestDistanceValue = 0;
1951			var bestDistance = 1000;
1952
1953			var bestEvalValue = 0;
1954			var bestEval = 0;
1955			var bestEvalComment = "";
1956
1957			$(".result-location").each(function(index,el){
1958
1959				var elPrice = +($(el).data("price"));
1960
1961				if(elPrice==0){
1962
1963				}else{
1964
1965					var elDistance = +($(el).data("distance"));
1966					var elEval = +($(el).data("trustpilot-rank"));
1967
1968					if(elPrice < bestPriceValue){
1969						bestPriceValue = elPrice;
1970						bestPriceComment = $(el).find(".economie-prct").html();
1971					}
1972
1973					if(elDistance < bestDistance){
1974						bestDistance = elDistance;
1975						bestDistanceValue = $(el).data("price");
1976					}
1977
1978					if(elEval > bestEval){
1979						bestEval = elEval;
1980						bestEvalValue = $(el).data("price");
1981
1982						var note = $(el).find(".trust-pilot .trust-pilot-note .note").html();
1983						var nbAvis = $(el).find(".trust-pilot .trust-pilot-avis .like-a-link").html();
1984
1985						bestEvalComment =  "Note "+ note + " avec " + nbAvis;
1986					}
1987
1988				}
1989			});
1990
1991			if(Math.round(bestPriceValue)==1000){
1992				$("#cteasy-result-list .cteasy-meilleurs-offres").hide();
1993			}
1994
1995			$("#le-moins-cher-price").html(Math.round(bestPriceValue));
1996			$("#le-moins-cher-comment").html(bestPriceComment+" de remise");
1997
1998			$("#le-plus-proche-price").html(Math.round(bestDistanceValue));
1999			$("#le-plus-proche-comment").html("&agrave; "+bestDistance+"Km de vous");
2000
2001			$("#le-plus-apprecie-price").html(Math.round(bestEvalValue));
2002			$("#le-plus-apprecie-comment").html(bestEvalComment);
2003
2004		}
2005
2006});
2007riot.tag2('ctrl-search-result-panel-verif', '<div class="row"> <div class="col col-sm-12"> <div class="search-title search-title-params"> <div class="part part-1"> <p class="titre"><strong>&Eacute;TAPE 1</strong> <i class="fas fa-caret-right"></i> V&eacute;rifier votre recherche</p> </div> <div class="part part-2"> <p if="{!this.searchPanelVisible}" onclick="{this.toggleSearchPanel}"> <span class="search-resume"><b>{this.ville}</b> - <b>{this.date}</b> - <b>{this.typeVehicule}</b> - <b>{this.typeEnergy}</b> </span> <span class="btn btn-dark btn-sm">Modifier <i class="fas fa-pencil-alt"></i></span> </p> <p if="{this.searchPanelVisible}" onclick="{this.toggleSearchPanel}" class="search-close"> <span class="btn btn-dark btn-sm">Fermer la recherche <i class="fas fa-times"></i></span> </p> </div> </div> </div> </div>', '', '', function(opts) {
2008		this.mixin(dcMixin);
2009		this.searchPanelVisible =  this.opts.searchPanelVisible;
2010		this.showList = true;
2011		this.orderMode = 2;
2012		this.ville = this.opts.search_place_name;
2013		this.typeVehicule = this.opts.vehicle_type_selected;
2014		this.typeEnergy = this.opts.vehicle_energy_selected;
2015		this.date = this.opts.year_month_selected;
2016		this.rechercheParCentre = this.opts.rechercheParCentre;
2017
2018		showPanelSearch(this.searchPanelVisible);
2019		this.toggleSearchPanel = function(e){
2020			this.searchPanelVisible = !this.searchPanelVisible;
2021			showPanelSearch(this.searchPanelVisible);
2022		}
2023
2024});
2025riot.tag2('ctrl-tunnel-2021', '<div class="navigation-wrapper navigation-wrapper-main"> <ol class="navigation-tunnel-stepper stepper"> <virtual if="{this.purchase.withOptions}"> <li class="step-options first active"> Options </li> <li class="step-coordinates"> Coordonnées <span class="stepper-delimiter first"></span> </li> </virtual> <virtual if="{!this.purchase.withOptions}"> <li class="step-coordinates first active"> Coordonnées </li> </virtual> <li class="step-payment "> Paiement <span class="stepper-delimiter second without-options"></span> </li> </ol> <virtual if="{this.purchase.stepActif == \'OPTIONS\'}"> <a if="{this.purchase.urlModificationRdv != null}" class="btn btn-go-back" href="{this.purchase.urlModificationRdv}"><span class="material-icons">chevron_left</span> Retour</a> </virtual> <virtual if="{this.purchase.stepActif == \'COORDINATES\'}"> <span if="{this.purchase.withOptions}" onclick="{this.goBackOptions}" class="btn btn-go-back"><span class="material-icons">chevron_left</span> Retour <virtual if="{!this.isMobile}">aux options</virtual></span> <a if="{this.purchase.urlModificationRdv != null && !this.purchase.withOptions}" class="btn btn-go-back" href="{this.purchase.urlModificationRdv}"><span class="material-icons">chevron_left</span> Retour</a> </virtual> </div> <div class="row ctrl-tunnel-wrapper"> <div class="tunnel-wrapper-left"> <virtual if="{this.purchase.stepActif == \'OPTIONS\'}"> <button if="{!this.isMobile}" onclick="{this.submitOptions}" type="button" class="btn btn-passer-etape">Passer cette &eacute;tape <span class="material-icons">chevron_right</span></button> <div class="step step-options" ref="form_options"></div> <p class="form-coordonnees-button"> <button onclick="{this.submitOptions}" type="button" class="btn btn-primary btn-click-to-action">Continuer <span class="material-icons">chevron_right</span></button> </p> </virtual> <virtual if="{this.purchase.stepActif == \'COORDINATES\'}"> <div class="step step-coordinates" ref="form_coordinates"></div> <div class="form-coordonnees-button"> <div class="wrapper-go-to-payment-button"> <virtual if="{this.purchase.reservationPrixFinalDouble > 0}"> <button onclick="{this.submitCoordinates}" ref="btn-validate-coordinates" type="button" class="btn btn-primary btn-click-to-action">Aller au paiement <span class="material-icons">chevron_right</span></button> <div class="go-to-payment-footer"> <div class="moyens-de-paiement"> <img class="payment-logo" riot-src="{this.opts.context}/pict/card-logo-visa.png" alt="VISA"> <img class="payment-logo" riot-src="{this.opts.context}/pict/card-logo-cb.png" alt="Logo CB"> <img class="payment-logo" riot-src="{this.opts.context}/pict/card-logo-mastercard.png" alt="Mastercard"> <img class="payment-logo" riot-src="{this.opts.context}/pict/card-logo-maestro.png" alt="Maestro"> <img class="payment-logo" riot-src="{this.opts.context}/pict/apple-pay.png" alt="Apple Pay" style="height: 18px;width: unset;"> <img class="payment-logo" riot-src="{this.opts.context}/pict/google-pay.png" alt="Google Pay" style="height: 18px;width: unset;"> <img class="payment-logo" riot-src="{this.opts.context}/pict/paiement/paiement-paypal.png" alt="Paypal" style="height: 18px;width: unset;"> </div> <p>Moyens de paiement adaptés et sécurisés</p> </div> </virtual> <virtual if="{this.purchase.reservationPrixFinalDouble == 0}"> <button onclick="{this.submitCoordinates}" ref="btn-validate-coordinates" type="button" class="btn btn-primary btn-click-to-action">Continuer <span class="material-icons">chevron_right</span></button> </virtual> </div> </div> <div class="alert alert-primary" if="{this.purchase.reservationPrixFinalDouble > 0}"> <p> <b>Détendez vous, on a tout prévu !</b> <br> Le paiement pour confirmer votre réservation s\'effectue en 3DSecure, vous recevrez ensuite un mail instantané de confirmation de paiement, votre réservation et un SMS 24H avant votre rendez-vous pour ne pas oublier. </p> </div> <virtual if="{this.isMobile}"> <div class="alert alert-primary my-4 my-0"> <virtual if="{this.purchase.stepActif != \'OPTIONS\' && !this.purchase.reservation.modifyAppointment && !this.purchase.modificationDeadlinePassed}"> <virtual if="{!this.purchase.reservationInMoreThan14Days}"> <div class="recap recap-gestion"> <p class="title font-weight-bold my-0"> <span>Rendez-vous non modifiable et non remboursable</span> </p> </div> </virtual> <virtual if="{this.purchase.reservationInMoreThan14Days}"> <div class="recap recap-gestion"> <p class="title"> <span><span class="font-weight-bold">Annulation gratuite</span><br>
2025avant le <span class="font-weight-bold">{this.purchase.reservationDateAnnulation}</span></span> </p> <p style="margin-top: 3px;"><a href="#" data-toggle="modal" data-target="#popup-gestion-reservation" style="font-weight: normal; text-decoration: underline;">Consultez les conditions complètes</a></p> </div> </virtual> </virtual> <virtual if="{this.purchase.stepActif != \'OPTIONS\' && this.purchase.reservation.modifyAppointment}"> <div class="recap recap-gestion"> <p class="title"> <span>Annulation ou modification avant le <span class="font-weight-bold">{this.purchase.reservationDateHeureAnnulation}</span></span> </p> <p style="margin-top: 3px;"><a href="#" data-toggle="modal" data-target="#popup-gestion-reservation" style="font-weight: normal; text-decoration: underline;">Consultez les conditions complètes</a></p> </div> </virtual> <virtual if="{this.purchase.modificationDeadlinePassed && !this.purchase.reservationInMoreThan14Days}"> <div class="recap recap-gestion recap-gestion-non-remboursable"> <p class="title" style="color: black;"> <span class="font-weight-bold">Rendez-vous non annulable, non remboursable</span> </p> <p style="margin-top: 3px;"><a href="#" data-toggle="modal" data-target="#popup-gestion-reservation" style="font-weight: normal; text-decoration: underline;">Consultez les conditions complètes</a></p> <p style="margin-top: 3px;"><a href="{this.purchase.urlModificationRdv}" style="font-weight: normal; text-decoration: underline;">Choisir un autre rendez-vous</a></p> </div> </virtual> </div> </virtual> </virtual> <virtual if="{this.purchase.stepActif == \'PAYMENT\' || this.purchase.stepActif == \'CONFIRMATION_WITHOUT_PAYMENT\'}"> <div class="step step-payment" ref="form_payment"></div> </virtual> </div> <virtual if="{!this.isMobile}"> <div class="tunnel-wrapper-right"> <ctrl-tunnel-recap ref="recap" purchase="{this.purchase}" context="{this.opts.context}" url="{this.opts.url}"></ctrl-tunnel-recap> </div> </virtual> </div> <virtual if="{this.isMobile}"> <footer class="footer-tunnel" data-toggle="modal" data-target="#tunnelRecapPopup"> <div class="details-reservation-wrapper"> <div class="details-reservation-banner"> <div> <span class="material-icons icone-cercle">keyboard_arrow_up</span> <a href="#" data-toggle="modal" data-target="#tunnelRecapPopup">Afficher votre réservation</a> </div> <div class="details-reservation-price-wrapper"> <virtual if="{this.purchase.stepActif == \'PAYMENT\' || this.purchase.stepActif == \'CONFIRMATION_WITHOUT_PAYMENT\'}"> <p>PRIX FINAL: <span>{this.purchase.reservationPrixFinal}&euro;</span></p> </virtual> <virtual if="{this.purchase.stepActif != \'PAYMENT\' && this.purchase.stepActif != \'CONFIRMATION_WITHOUT_PAYMENT\'}"> <p>PRIX TOTAL: <span>{this.purchase.reservationPrixFinal}&euro;</span></p> </virtual> <virtual if="{!this.purchase.reservation.promoCode && !this.purchase.reservation.fromPartenaire}"> <p class="code-promo-wrapper"> <a href="#" class="code-promo-link" onclick="{this.openModalWithPromoCode}">Vous avez un code promo ?</a> </p> </virtual> </div> </div> </div> </footer> </virtual> <virtual if="{this.isMobile}"> <div class="modal fade" id="tunnelRecapPopup" tabindex="-1" role="dialog" aria-labelledby="tunnelRecapPopupTitle" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-header sticky-top"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span class="material-icons">highlight_off</span> </button> </div> <div class="modal-body"> <ctrl-tunnel-recap ref="recapModal" purchase="{this.purchase}" context="{this.opts.context}" url="{this.opts.url}"></ctrl-tunnel-recap> </div> </div> </div> </div> </virtual> <div class="modal fade" id="popinLogin" tabindex="-1" role="dialog" aria-labelledby="popinLoginTitle" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="exampleModalLongTitle">Vous devez vous connecter</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span class="material-icons">highlight_off</span> </button> </div> <div class="modal-body"> <p id="popinLoginMsg"> Le compte ... </p> <p> <input class="form-control" placeholder="Votre mot de passe" type="password" name="password"> </p> <p class="forgotten-pwd"> <a href="{this.opts.context}/fr/reset-password/">Mot de passe oubli&eacute; ?</a> </p> </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" onclick="{this.submitPwd}"><i class="fas fa-check"></i> Valider</button> </div> </div> </div> </div> <div class="modal fade" id="popup-option-modification-reminder" tabindex="-1" role="dialog" aria-labelledby="popup-option-modification-reminder" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-header border-0"> <h2>Protégez votre rendez-vous</h2> <button type="button" class="close" data-dismiss="modal" aria-label="Fermer"> <span aria-hidden="true">&times;</span> </button> </div> <div class="modal-body"> <p>Vous êtes sur le point d\'économiser <span class="font-weight-bold amount">{this.purchase.reservationPrixEconomie}&euro;</span> </p> <p> Êtes-vous certain de vouloir vous passer de l\'option <b>modification & annulation</b> de rendez-vous à <b>{this.purchase.montantOptionModifAppointment}&euro;</b> pour gérer vos imprévus jusqu\'à la veille de votre rendez-vous ? </p> <p if="{this.purchase.reservationPrixAvecOptionModification != null}"> Vous économiserez quand même <span class="font-weight-bold amount">{this.purchase.reservationPrixAvecOptionModification}&euro;</span> </p> </div> <div class="modal-footer"> <div class="cta"> <button type="button" class="btn btn-secondary" onclick="{this.addOptionModificationRdv}" data-dismiss="modal"> <i class="fas fa-check"></i> Oui, j\'ajoute l\'option (2,99 &euro;) </button> <button type="button" class="btn btn-cancel btn-outline-secondary" onclick="{this.submitOptions}" data-dismiss="modal"> JE CONTINUE SANS L\'OPTION </button> </div> </div> </div> </div> </div>', 'ctrl-tunnel-2021,[data-is="ctrl-tunnel-2021"]{ } ctrl-tunnel-2021 .details-reservation-price-wrapper,[data-is="ctrl-tunnel-2021"] .details-reservation-price-wrapper{ flex-direction: column !important; align-items: flex-end !important; gap: 0 !important; } ctrl-tunnel-2021 .code-promo-wrapper,[data-is="ctrl-tunnel-2021"] .code-promo-wrapper{ margin: 0; } ctrl-tunnel-2021 .code-promo-link,[data-is="ctrl-tunnel-2021"] .code-promo-link{ font-size: 16px; text-decoration: underline;
2025 color: #007bff; } ctrl-tunnel-2021 .code-promo-link:hover,[data-is="ctrl-tunnel-2021"] .code-promo-link:hover{ color: #0056b3; text-decoration: underline; }', '', function(opts) {
2026
2027        function wait(ms) {
2028            return new Promise(resolve => setTimeout(resolve, ms));
2029        }
2030
2031        async function asyncWait(seconds) {
2032            await wait(seconds * 1000);
2033        }
2034
2035        function call3dsReturn(threeDSMethodNotificationURL) {
2036            $.ajax({
2037                url: threeDSMethodNotificationURL,
2038                method: 'post',
2039                cache: false
2040            }).done(function (html) {
2041                $('body').append(html);
2042            });
2043        }
2044
2045        var tag = this;
2046        tag.mixin(dcMixin);
2047        tag.purchase = opts.purchase;
2048        tag.reservation = tag.purchase.reservation;
2049        // Detect if we are on a mobile device
2050        if (window.matchMedia('(max-width: 991px)').matches) {
2051            tag.isMobile = true;
2052        } else {
2053            tag.isMobile = false;
2054        }
2055
2056        tag.steps = {
2057            COORDINATES: 0,
2058            OPTION: 1,
2059            PAYMENT: 2,
2060            CONFIRMATION_WITHOUT_PAYMENT: 3,
2061            CONFIRMATION: 4,
2062        };
2063
2064        tag.on('mount', function () {
2065            window.riotTagCtrlTunnel2021 = tag;
2066
2067            console.log("[Tunnel] mount - search='" + window.location.search + "'");
2068
2069            // Si on arrive sur la page après un retour Monext (redirect depuis le widget),
2070            // afficher immédiatement le loader pendant que le serveur traite validatePaymentStatus.
2071            // La page est rechargée par le serveur après validation, ce qui masquera le loader.
2072            if (window.location.search.indexOf('action=checkMonextPayment') !== -1) {
2073                console.log("[Tunnel] checkMonextPayment detected - showing loader");
2074                showLoading(true, "Validation du paiement en cours...");
2075                return; // ne pas charger le formulaire, le serveur va rediriger
2076            }
2077
2078            tag.loadForm();
2079        });
2080
2081        tag.on('update', function () {
2082            console.log("updating tag ctrl-tunnel-2021");
2083            tag.refreshPurchase()
2084            console.log("updating tag ctrl-tunnel-2021 with price: ", tag.purchase.reservationPrixFinal);
2085            $(".btn-validate-payment-montant").html(tag.purchase.reservationPrixFinal + "&euro;");
2086            if (tag.purchase.reservation.reservationInsurance) {
2087                $(".purchase-options-content input[name='pocv']").prop("checked", true);
2088            } else {
2089                $(".purchase-options-content input[name='pocv']").prop("checked", false);
2090            }
2091            if (tag.purchase.reservation.modifyAppointment) {
2092                $(".purchase-options-content input[name='poma']").prop("checked", true);
2093            } else {
2094                $(".purchase-options-content input[name='poma']").prop("checked", false);
2095            }
2096        })
2097
2098        //
2099        tag.loadForm = function (e) {
2100            if (tag.purchase.stepActif == 'OPTIONS') {
2101                if (tag.purchase.withOptions) {
2102                    tag.loadFormOptions();
2103                } else {
2104                    tag.loadFormCoordinates();
2105                }
2106            }
2107
2108            if (tag.purchase.stepActif == 'COORDINATES') {
2109                tag.loadFormCoordinates();
2110            }
2111
2112            if (tag.purchase.stepActif == 'PAYMENT') {
2113                tag.loadFormPayment();
2114            }
2115
2116            if (tag.purchase.stepActif == 'CONFIRMATION_WITHOUT_PAYMENT') {
2117                tag.loadConfirmationWithoutPayment();
2118            }
2119
2120        }
2121
2122        tag.refreshPurchase = function (e) {
2123            $.ajax({
2124                url: tag.opts.context + tag.opts.url + '?action=refreshPurchase',
2125                method: 'post',
2126                cache: false
2127            }).done(function (json) {
2128
2129                tag.purchase = json.purchase;
2130
2131                // Mettre à jour le composant de code promo dans les coordonnées si il existe
2132                if (tag.promoCodeTag) {
2133                    tag.promoCodeTag.purchase = json.purchase;
2134                    tag.promoCodeTag.update();
2135                }
2136
2137                window.funnel = {
2138                    data: json,
2139                    steps: tag.steps,
2140                };
2141
2142                $(window).trigger('funnel:changed', json);
2143
2144            }).fail(function (e, m) {
2145                showLoading(false);
2146                myAlert("Erreur " + e.status, e.statusText)
2147            });
2148        }
2149
2150        tag.updatePurchase = function() {
2151            // Mettre à jour le composant de code promo dans les coordonnées si il existe
2152            if (tag.promoCodeTag) {
2153                tag.promoCodeTag.purchase = tag.purchase;
2154                tag.promoCodeTag.update();
2155            }
2156            tag.update();
2157            tag.refreshPurchase();
2158        }
2159
2160        // Chargement du formulaire COORDONNEES
2161        tag.loadFormCoordinates = function (e) {
2162            $.ajax({
2163                url: tag.opts.context + tag.opts.url + '?action=loadFormCoordinates',
2164                method: 'post',
2165                cache: false
2166            }).done(function (html) {
2167                tag.purchase.stepActif = 'COORDINATES';
2168                $(".faq-tunnel").hide();
2169                tag.loadFaqFragment('coordinates');
2170                $(".main-sticky-header .navbar-full-width").removeClass("options-tunnel");
2171                tag.setNavigationActive('step-coordinates');
2172                $(".help-center-footer").html('<a href="#" data-toggle="modal" data-target="#popup-faq">Besoin d\'aide pour votre réservation ?</a>')
2173                $(".cgv-footer").html('<a href="#" data-toggle="modal" data-target="#popup-cgv">Conditions générales de vente</a>')
2174                $(".confidentialite-footer").html('<a href="#" data-toggle="modal" data-target="#popup-confidentialite">Politique de confidentialité</a>')
2175                tag.update();
2176                $(tag.refs.form_coordinates).html(html);
2177                tag.setTriggersOnFormCoordinates();
2178                activeTooltips();
2179                $('html, body').animate({
2180                    scrollTop: $("a[name='top']").offset().top
2181                }, 500);
2182
2183                tag.refreshPurchase();
2184                // tag.setNavigationActive("step-coordinates")
2185
2186            }).fail(function () {
2187                myAlert("err " + e, e)
2188            });
2189        }
2190
2191        tag.setTriggersOnSelectChange = function () {
2192            $('select').change(function () {
2193                if ($(this).val() && $(this).val() != -1 && $(this).valid()) {
2194                    $(this).parent().parent().removeClass("notvalid").addClass('isvalid');
2195                    $(this).blur();
2196                } else {
2197                    $(this).parent().parent().removeClass("isvalid").addClass('notvalid');
2198                }
2199            })
2200        }
2201
2202        tag.setTriggersOnInputChange = function () {
2203            $('input').blur(function () {
2204                if ($(this).val() && $(this).valid()) {
2205                    $(this).parent().removeClass("notvalid").addClass('isvalid');
2206                } else {
2207                    $(this).parent().removeClass("isvalid").addClass('notvalid');
2208                }
2209            })
2210        }
2211
2212        tag.setInitialValidState = function () {
2213            // Pour les selects
2214            $('select').each(function () {
2215                if ($(this).val() && $(this).val() != -1 && $(this).valid()) {
2216                    $(this).parent().parent().addClass('isvalid');
2217                }
2218            });
2219
2220            // Pour les inputs
2221            $('form[name="pfcoords"] input').each(function () {
2222                if ($(this).val() && $(this).valid()) {
2223                    $(this).parent().addClass('isvalid');
2224                }
2225            });
2226
2227        };
2228
2229        tag.isLocalhostEnvironment = function () {
2230            var host = window.location.hostname;
2231            return host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
2232        }
2233
2234        tag.prefillCoordinatesForLocalhost = function () {
2235            if (!tag.isLocalhostEnvironment()) {
2236                return;
2237            }
2238
2239            var form = $("form[name='pfcoords']");
2240            if (form.length !== 1) {
2241                return;
2242            }
2243
2244            function setValueIfEmpty(input, value) {
2245                if (!input.length) {
2246                    return;
2247                }
2248                if (input.val()) {
2249                    return;
2250                }
2251                input.val(value).trigger('input').trigger('change').trigger('blur');
2252            }
2253
2254            var nameInputs = form.find(".coord-name input[type='text']");
2255            setValueIfEmpty(nameInputs.eq(0), 'Emmanuel');
2256            setValueIfEmpty(nameInputs.eq(1), 'Balpe');
2257
2258            setValueIfEmpty(form.find(".form-email input[type='email']").first(), '[email protected]');
2259            setValueIfEmpty(form.find(".form-phone input[type='text']").first(), '0651494344');
2260
2261            var civiliteInputs = form.find("input[name='ci']");
2262            if (civiliteInputs.length && civiliteInputs.filter(':checked').length === 0) {
2263                var civiliteDefault = form.find("#civilite-0");
2264                if (civiliteDefault.length) {
2265                    civiliteDefault.prop('checked', true).trigger('change').trigger('blur');
2266                }
2267            }
2268
2269            var tradeSelect = form.find("select[name='v-trade']").first();
2270            if (tradeSelect.length && (!tradeSelect.val() || tradeSelect.val() === '-1')) {
2271                var firstTradeValue = tradeSelect.find("option").filter(function () {
2272                    var optionValue = $(this).val();
2273                    return optionValue && optionValue !== '-1' && /^\d+$/.test(optionValue);
2274                }).first().val();
2275                if (firstTradeValue) {
2276                    tradeSelect.val(firstTradeValue).trigger('change').trigger('blur');
2277                }
2278            }
2279
2280            setTimeout(function () {
2281                var modelSelect = form.find("select[name='v-mod']").first();
2282                if (!modelSelect.length || (modelSelect.val() && modelSelect.val() !== '-1')) {
2283                    return;
2284                }
2285                var firstModelValue = modelSelect.find("option").filter(function () {
2286                    var optionValue = $(this).val();
2287                    return optionValue && optionValue !== '-1' && /^\d+$/.test(optionValue);
2288                }).first().val();
2289                if (firstModelValue) {
2290                    modelSelect.val(firstModelValue).trigger('change').trigger('blur');
2291                }
2292            }, 0);
2293        }
2294
2295        tag.setupEmailTypoDetection = function () {
2296            var KNOWN_DOMAINS = [
2297                // Tier 1 — grands fournisseurs FR/internationaux (>10 000 réservations)
2298                'gmail.com', 'hotmail.fr', 'orange.fr', 'yahoo.fr', 'hotmail.com',
2299                'free.fr', 'laposte.net', 'sfr.fr', 'live.fr', 'wanadoo.fr',
2300                'outlook.fr', 'neuf.fr', 'yahoo.com', 'icloud.com',
2301                // Tier 2 — fournisseurs notables (1 000–10 000)
2302                'aol.com', 'bbox.fr', 'msn.com', 'outlook.com', 'gmx.fr',
2303                'numericable.fr', 'cegetel.net', 'aliceadsl.fr', 'club-internet.fr',
2304                'me.com', 'ymail.com',
2305                // Tier 3 — fournisseurs connus (200–1 000)
2306                'dbmail.com', 'netcourrier.com', 'aol.fr', 'live.com', 'protonmail.com',
2307                'libertysurf.fr', 'gmx.com', '9online.fr', 'club.fr', 'noos.fr',
2308                'orange.com', 'proton.me', 'lilo.org', 'infonie.fr', 'modulonet.fr',
2309                'numericable.com', 'mailo.com', 'mail.com', 'rocketmail.com', 'mail.ru',
2310                'mac.com', 'netc.fr', 'pm.me',
2311                // Tier 4 — fournisseurs moins courants mais réels (50–200)
2312                'tutanota.com', 'yahoo.it', 'yahoo.co.uk', 'googlemail.com', 'freesbee.fr',
2313                'online.fr', 'akeonet.com', 'dartybox.com', 'caramail.fr', 'net-c.com',
2314                'ik.me', 'yahoo.de', 'hotmail.co.uk', 'nordnet.fr', 'libero.it',
2315                'yahoo.ca', 'mail.fr', 'lavache.com', 'etik.com', 'caramail.com',
2316                'worldonline.fr', 'gmx.net', 'bluewin.ch', 'hotmail.es', 'duck.com',
2317                'gmx.de', 'live.be', 'tuta.io', 'riseup.net', 'posteo.net',
2318                'web.de', 'yandex.com', 'yandex.ru', 'hotmail.be', 'live.it',
2319                'sapo.pt', 'mozmail.com', 'mailfence.com', 'zoho.com',
2320                'simplelogin.com', 'skynet.be', 'live.ca', 'hotmail.co.uk'
2321            ];
2322
2323            function levenshtein(a, b) {
2324                var m = a.length, n = b.length, i, j;
2325                var dp = [];
2326                for (i = 0; i <= m; i++) { dp[i] = [i]; }
2327                for (j = 0; j <= n; j++) { dp[0][j] = j; }
2328                for (i = 1; i <= m; i++) {
2329                    for (j = 1; j <= n; j++) {
2330                        dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
2331                    }
2332                }
2333                return dp[m][n];
2334            }
2335
2336            var ALIASES = {
2337                'gmail.fr': 'gmail.com',
2338                'laposte.fr': 'laposte.net',
2339                'sfr.com': 'sfr.fr'
2340            };
2341
2342            function findSuggestion(domain) {
2343                if (ALIASES[domain]) { return ALIASES[domain]; }
2344                var best = null, bestDist = Infinity;
2345                for (var i = 0; i < KNOWN_DOMAINS.length; i++) {
2346                    var d = levenshtein(domain, KNOWN_DOMAINS[i]);
2347                    if (d < bestDist) { bestDist = d; best = KNOWN_DOMAINS[i]; }
2348                }
2349                return (bestDist >= 1 && bestDist <= 2) ? best : null;
2350            }
2351
2352            var emailInput = $(".form-email input[type='email']");
2353            var warningEl = $("#email-typo-warning");
2354            if (!emailInput.length || !warningEl.length) { return; }
2355
2356            emailInput.on('blur.emailtypo', function () {
2357                var val = $(this).val().trim().toLowerCase();
2358                var atIdx = val.lastIndexOf('@');
2359                warningEl.hide();
2360                if (atIdx < 1) { return; }
2361                var domain = val.slice(atIdx + 1);
2362                if (!domain || KNOWN_DOMAINS.indexOf(domain) !== -1) { return; }
2363                var suggestion = findSuggestion(domain);
2364                if (suggestion) {
2365                    warningEl.find('.email-suggestion-text').text(val.slice(0, atIdx) + '@' + suggestion);
2366                    warningEl.show();
2367                }
2368            });
2369
2370            warningEl.find('.email-typo-use-suggestion').on('click', function (e) {
2371                e.preventDefault();
2372                emailInput.val(warningEl.find('.email-suggestion-text').text()).trigger('blur');
2373                warningEl.hide();
2374            });
2375        };
2376
2377        tag.warningReservationNonAnnulableDisplayed = false;
2378        tag.setTriggersOnFormCoordinates = function () {
2379
2380            customCurrentBreadCrumb("<b>Vos informations</b>");
2381            tag.setTriggersOnInputChange();
2382            tag.setTriggersOnSelectChange();
2383            tag.setInitialValidState();
2384
2385            $("#payment-reassurance").remove();
2386
2387            // Monter le composant de code promo dans le formulaire de coordonnées
2388            tag.mountPromoCodeComponent();
2389
2390            function setOptionsForModeleSelect() {
2391                const mapVehicleModelElement = mapVehicleModel[$("select[name='v-trade']").val()];
2392                if (!mapVehicleModelElement) {
2393                    return;
2394                }
2395                var previousVal = $("select[name='v-mod']").val();
2396                $("select[name='v-mod']").empty();
2397                for (let index = 0; index < mapVehicleModelElement.length; index++) {
2398                    const model = mapVehicleModelElement[index];
2399                    if (index === 0) {
2400                        $("select[name='v-mod']").append('<option value="-1" selected="selected">Modèle du véhicule*</option>');
2401                    }
2402                    $("select[name='v-mod']").append('<option value="' + model.vehicleModelId + '">' + model.modelName + '</option>');
2403                }
2404                if (previousVal != null) {
2405                    $("select[name='v-mod']").val(previousVal);
2406                    $("select[name='v-mod']").blur();
2407                }
2408            }
2409
2410            setOptionsForModeleSelect();
2411
2412            $("select[name='v-trade']").change(function () {
2413                setOptionsForModeleSelect.call(this);
2414                var selectedTrade = $("select[name='v-trade']").val();
2415                if (!selectedTrade || selectedTrade === '-1' || !/^\d+$/.test(selectedTrade)) {
2416                    return;
2417                }
2418                // We still have to update the select in our backend otherwise the validation will fail..
2419                $.ajax({
2420                    url: tag.opts.context + tag.opts.url + '?action=loadSelectVehiculeModeles',
2421                    method: 'post',
2422                    data: {
2423                        trade: selectedTrade
2424                    },
2425                    cache: false
2426                }).done(function (json) {
2427                    // $("select[name='v-mod']").replaceWith(json.modeleSelectHtml);
2428                    // tag.setTriggersOnSelectChange();
2429                }).fail(function (e, m) {
2430                    myAlert("Erreur " + e.status, e.statusText)
2431                });
2432            });
2433
2434            tag.setupEmailTypoDetection();
2435            tag.prefillCoordinatesForLocalhost();
2436
2437        }
2438
2439        tag.mountPromoCodeComponent = function() {
2440            // Vérifier si le container existe
2441            if ($('#code-promo-container').length > 0) {
2442                // Créer la balise Riot
2443                $('#code-promo-container').html('<ctrl-tunnel-coupon-reduction></ctrl-tunnel-coupon-reduction>');
2444                // Monter le composant avec les données du purchase et un suffixe unique
2445                var mountedTag = riot.mount('#code-promo-container ctrl-tunnel-coupon-reduction', {
2446                    purchase: tag.purchase,
2447                    idSuffix: '-coords',
2448                    context: tag.opts.context,
2449                    url: tag.opts.url,
2450                    onUpdate: function(updatedPurchase) {
2451                        // Callback appelé quand le code promo est appliqué ou supprimé dans le formulaire coordonnées
2452                        tag.purchase = updatedPurchase;
2453
2454                        // Mettre à jour les composants de récapitulatif
2455                        if (tag.refs.recap) {
2456                            tag.refs.recap.purchase = updatedPurchase;
2457                            tag.refs.recap.update();
2458                            // Mettre à jour le composant coupon du récap
2459                            var recapCouponTag = tag.refs.recap.tags['ctrl-tunnel-coupon-reduction'];
2460                            if (recapCouponTag) {
2461                                recapCouponTag.purchase = updatedPurchase;
2462                                recapCouponTag.update();
2463                            }
2464                        }
2465                        if (tag.refs.recapModal) {
2466                            tag.refs.recapModal.purchase = updatedPurchase;
2467                            tag.refs.recapModal.update();
2468                            // Mettre à jour le composant coupon du récap modal
2469                            var recapModalCouponTag = tag.refs.recapModal.tags['ctrl-tunnel-coupon-reduction'];
2470                            if (recapModalCouponTag) {
2471                                recapModalCouponTag.purchase = updatedPurchase;
2472                                recapModalCouponTag.update();
2473                            }
2474                        }
2475
2476                        tag.update();
2477                        tag.refreshPurchase();
2478                    }
2479                });
2480
2481                // Sauvegarder la référence pour pouvoir mettre à jour plus tard
2482                if (mountedTag && mountedTag.length > 0) {
2483                    tag.promoCodeTag = mountedTag[0];
2484                }
2485            }
2486        }
2487
2488        var formCoordinatesValues = '';
2489
2490        tag.submitCoordinates = function (e) {
2491            e.preventUpdate = true
2492            if ($("form[name='pfcoords']").length === 1) {
2493                formCoordinatesValues = $("form[name='pfcoords']").serialize();
2494            }
2495
2496            window._trk && window._trk({
2497                event: 'ux_event',
2498                event_category: 'tunnel',
2499                event_action: 'submit_coordinates'
2500            });
2501
2502            showLoading(true);
2503            $.ajax({
2504                url: tag.opts.context + tag.opts.url + '?action=submitCoordinates',
2505                method: 'post',
2506                data: formCoordinatesValues,
2507                cache: false
2508            }).done(function (json) {
2509                showLoading(false);
2510                if (json.submit == 'needLogin') {
2511                    $("#popinLoginMsg").html(json.erreur);
2512                    tag.loadFormCoordinates();
2513                    tag.popinLogin();
2514                    window._trk && window._trk({
2515                        event: 'ux_event',
2516                        event_category: 'tunnel',
2517                        event_action: 'need_login'
2518                    });
2519                } else if (json.submit == 'formValidateException') {
2520                    //erreurs de saisie
2521                    tag.loadFormCoordinates();
2522                    window._trk && window._trk({
2523                        event: 'ux_event',
2524                        event_category: 'tunnel',
2525                        event_action: 'invalid_form'
2526                    });
2527                } else if (json.submit == 'InvalidVehicleSelectionException') {
2528                    //erreurs de saisie
2529                    tag.loadFormCoordinates();
2530                    window._trk && window._trk({
2531                        event: 'ux_event',
2532                        event_category: 'tunnel',
2533                        event_action: 'invalid_vehicle_selection'
2534                    });
2535                    myAlert(json.erreur, "Erreur");
2536
2537                } else if (json.submit == 'ko') {
2538                    //erreurs de saisie
2539                    myConfirm(json.erreur, "Erreur").then(function () {
2540                        window.location.href = tag.opts.context + json.url;
2541                    });
2542                    window._trk && window._trk({
2543                        event: 'ux_event',
2544                        event_category: 'tunnel',
2545                        event_action: 'form_error'
2546                    });
2547
2548                } else {
2549                    tag.purchase = json.purchase;
2550                    if (tag.refs.recap) {
2551                        tag.refs.recap.purchase = json.purchase;
2552                    }
2553                    if (tag.refs.recapModal) {
2554                        tag.refs.recapModal.purchase = json.purchase;
2555                    }
2556
2557                    // IMPORTANT : forcer le re-render du template AVANT d'injecter du contenu
2558                    tag.update();
2559
2560                    //tag.loadForm();
2561                    // On utilise le stepActif renvoyé par le serveur (peut être PAYMENT ou CONFIRMATION_WITHOUT_PAYMENT)
2562                    if (tag.purchase.stepActif === 'CONFIRMATION_WITHOUT_PAYMENT') {
2563                        tag.loadConfirmationWithoutPayment();
2564                    } else {
2565                        tag.loadFormPayment();
2566                    }
2567                    window._trk && window._trk({
2568                        event: 'ux_event',
2569                        event_category: 'tunnel',
2570                        event_action: 'to_payment'
2571                    });
2572                }
2573            }).fail(function (e, m) {
2574                showLoading(false);
2575                myAlert("Erreur " + e.status, e.statusText);
2576                window._trk && window._trk({
2577                    event: 'ux_event',
2578                    event_category: 'tunnel',
2579                    event_action: 'server_error'
2580                });
2581            });
2582        }
2583
2584        tag.loadFormOptions = function (e) {
2585            $.ajax({
2586                url: tag.opts.context + tag.opts.url + '?action=loadFormOptions',
2587                method: 'post',
2588                cache: false
2589            }).done(function (html) {
2590                tag.setNavigationActive('step-options');
2591                $(".main-sticky-header .navbar-full-width").addClass("options-tunnel");
2592                tag.purchase.stepActif = 'OPTIONS';
2593                tag.update();
2594                $(tag.refs.form_options).html(html);
2595                tag.setTriggersOnFormOptions();
2596                activeTooltips();
2597                $('html, body').animate({
2598                    scrollTop: $("a[name='top']").offset().top
2599                }, 500);
2600                tag.refreshPurchase();
2601                tag.loadFaqFragment('options');
2602            }).fail(function () {
2603                myAlert("err " + e, e)
2604            });
2605        }
2606
2607        tag.loadFaqFragment = function (faq) {
2608            $.ajax({
2609                url: tag.opts.context + tag.opts.url + '?action=loadFaqFragment&faq=' + faq,
2610                method: 'post',
2611                cache: false
2612            }).done(function (html) {
2613                $(".faq-tunnel").html(html);
2614            }).fail(function (e) {
2615                myAlert("Error loading FAQ Fragment: " + e)
2616            });
2617        }
2618
2619        tag.setNavigationActive = function (step) {
2620            $('.navigation-wrapper-main .stepper li').removeClass('active');
2621            $('.navigation-wrapper-main .stepper li').removeClass('past`');
2622            if (step === 'step-coordinates') {
2623                $('.stepper .step-options').addClass('past');
2624            }
2625            if (step === 'step-payment') {
2626                $('.stepper .step-options').addClass('past');
2627                $('.stepper .step-coordinates').addClass('past');
2628            }
2629            $('.stepper .' + step).addClass('active');
2630        }
2631
2632        tag.setTriggersOnFormOptions = function () {
2633
2634            customCurrentBreadCrumb("<b>Options</b>");
2635
2636            $("#payment-reassurance").remove();
2637
2638            // evenementiel change sur checkbox selection d'une option
2639            // qui met en valeur (fond couleur orange via css) l'option selectionnee
2640            $("input[name='pocv'],input[name='poma']").change(function () {
2641                var isSelected = $(this).is(':checked');
2642                if (isSelected) {
2643                    $(this).parents(".purchase-options").addClass("selected");
2644                } else {
2645                    $(this).parents(".purchase-options").removeClass("selected");
2646                }
2647            });
2648
2649            // CV gratuite par défaut pour les centres en péri
2649ode de lancement (745 = MLM, 738 = Cenon)
2650            if (tag.purchase.reservation.locationID === 745 || tag.purchase.reservation.locationID === 738) {
2651                var $pocv = $("input[name='pocv']");
2652                $pocv.prop("checked", true).parents(".purchase-options").addClass("selected");
2653                $pocv.closest(".purchase-options-content").find("label").text("Gratuite");
2654            }
2655
2656        }
2657
2658        tag.addOptionModificationRdv = function (e) {
2659            e.preventUpdate = true
2660            $("input[name='poma']").prop("checked", true);
2661        };
2662
2663        tag.popupOptionModificationReminderDisplayed = false
2664        tag.submitOptions = function (e) {
2665            e.preventUpdate = true
2666
2667            if (!tag.popupOptionModificationReminderDisplayed && $("input[name='poma']") != null && $("input[name='poma']").is(":visible") && !$("input[name='poma']").is(':checked')) {
2668                tag.popupOptionModificationReminderDisplayed = true;
2669                $('#popup-option-modification-reminder').modal('show');
2670                return;
2671            }
2672
2673            showLoading(true);
2674
2675            $.ajax({
2676                url: tag.opts.context + tag.opts.url + '?action=submitOptions',
2677                method: 'post',
2678                data: $("form[name='pfoptions']").serialize(),
2679                cache: false
2680
2681            }).done(function (json) {
2682                showLoading(false);
2683                if (json.submit == 'formValidateException') {
2684                    // erreurs de saisie
2685                    tag.loadFormCoordinates();
2686
2687                } else if (json.submit == 'ko') {
2688                    //erreurs de saisie
2689                    myConfirm(json.erreur, "Erreur").then(function () {
2690                        window.location.href = tag.opts.context + json.url;
2691                    });
2692                } else {
2693
2694                    tag.purchase = json.purchase;
2695                    if (tag.refs.recap) {
2696                        tag.refs.recap.purchase = json.purchase;
2697                    }
2698                    if (tag.refs.recapModal) {
2699                        tag.refs.recapModal.purchase = json.purchase;
2700                    }
2701
2702                    tag.loadFormCoordinates();
2703                }
2704
2705            }).fail(function (e, m) {
2706                showLoading(false);
2707                myAlert("Erreur " + e.status, e.statusText)
2708            });
2709        }
2710
2711        // Chargement du formulaire PAIEMENT
2712        tag.loadFormPayment = function (e) {
2713            $.ajax({
2714                url: tag.opts.context + tag.opts.url + '?action=loadFormPayment',
2715                method: 'post',
2716                cache: false
2717            }).done(function (html) {
2718                //tag.purchase.stepActif = 'PAYMENT'; // commente car ne tient pas compte du cas CONFIRMATION_WITHOUT_PAYMENT
2719                tag.setNavigationActive('step-payment');
2720
2721                $(".faq-tunnel").hide();
2722                tag.loadFaqFragment('payment');
2723                $(".help-center-footer").html('<a href="#" data-toggle="modal" data-target="#popup-faq">Besoin d\'aide pour votre réservation ?</a>')
2724                $(".cgv-footer").html('<a href="#" data-toggle="modal" data-target="#popup-cgv">Conditions générales de vente</a>')
2725                $(".confidentialite-footer").html('<a href="#" data-toggle="modal" data-target="#popup-confidentialite">Politique de confidentialité</a>')
2726                $(".main-sticky-header .navbar-full-width").removeClass("options-tunnel");
2727                $(".main-sticky-header .navbar-full-width").addClass("payment-tunnel");
2728                $(".need-help-order").addClass("payment-tunnel");
2729                $(".container-global.need-help-banner").addClass("payment-tunnel");
2730
2731                tag.update();
2732                $(tag.refs.form_payment).html(html);
2733                tag.setTriggersOnFormPayment();
2734                activeTooltips();
2735                $('html, body').animate({
2736                    scrollTop: $("a[name='top']").offset().top
2737                }, 500);
2738
2739                tag.refreshPurchase();
2740                tag.update();
2741
2742            }).fail(function () {
2743                myAlert("err " + e, e)
2744            });
2745        }
2746
2747        // Nouvelle fonction pour afficher la confirmation sans paiement (avoir ou code promo couvre tout)
2748        tag.loadConfirmationWithoutPayment = function (e) {
2749            console.log("=== loadConfirmationWithoutPayment START ===");
2750            console.log("tag.purchase.avoir:", tag.purchase.avoir);
2751            console.log("tag.purchase.reservation.promoCode:", tag.purchase.reservation.promoCode);
2752            console.log("tag.purchase.reservationRemiseCouponReduction:", tag.purchase.reservationRemiseCouponReduction);
2753            console.log("tag.purchase.reservationPrixFinal:", tag.purchase.reservationPrixFinal);
2754            console.log("tag.refs.form_payment:", tag.refs.form_payment);
2755
2756            tag.setNavigationActive('step-payment');
2757
2758            $(".faq-tunnel").hide();
2759            tag.loadFaqFragment('payment');
2760            $(".help-center-footer").html('<a href="#" data-toggle="modal" data-target="#popup-faq">Besoin d\'aide pour votre réservation ?</a>')
2761            $(".cgv-footer").html('<a href="#" data-toggle="modal" data-target="#popup-cgv">Conditions générales de vente</a>')
2762            $(".confidentialite-footer").html('<a href="#" data-toggle="modal" data-target="#popup-confidentialite">Politique de confidentialité</a>')
2763            $(".main-sticky-header .navbar-full-width").removeClass("options-tunnel");
2764            $(".main-sticky-header .navbar-full-width").addClass("payment-tunnel");
2765            $(".need-help-order").addClass("payment-tunnel");
2766            $(".container-global.need-help-banner").addClass("payment-tunnel");
2767
2768            tag.update();
2769
2770            // Déterminer le type de couverture : avoir ou code promo
2771            var hasAvoir = tag.purchase.avoir && parseFloat(tag.purchase.avoir.replace(',', '.')) > 0;
2772            var hasPromoCode = tag.purchase.reservation.promoCode && tag.purchase.reservationPrixFinal == 0;
2773
2774            var messageHtml = '';
2775            if (hasPromoCode && !hasAvoir) {
2776                // Cas code promo couvre 100%
2777                messageHtml = `
2778                        <p style="font-size: 16px; line-height: 1.6;">
2779                            Votre <strong>code promo</strong> vous permet de réserver gratuitement votre contrôle technique.
2780                        </p>
2781                        <p style="font-size: 14px; margin-top: 15px; color: #666;">
2782                            Aucun paiement par carte bancaire n'est nécessaire.
2783                            Cliquez sur le bouton ci-dessous pour confirmer votre réservation.
2784                        </p>`;
2785            } else if (hasAvoir && !hasPromoCode) {
2786                // Cas avoir couvre tout
2787                messageHtml = `
2788                        <p style="font-size: 16px; line-height: 1.6;">
2789                            Vous disposez d'un <strong>avoir de ${tag.purchase.avoir}&euro;</strong>,
2790                            ce qui vous permet de payer intégralement votre réservation.
2791                        </p>
2792                        <p style="font-size: 14px; margin-top: 15px; color: #666;">
2793                            Aucun paiement par carte bancaire n'est nécessaire.
2794                            Cliquez sur le bouton ci-dessous pour confirmer votre réservation.
2795                        </p>`;
2796            } else if (hasAvoir && hasPromoCode) {
2797                // Cas avoir + code promo couvrent tout
2798                messageHtml = `
2799                        <p style="font-size: 16px; line-height: 1.6;">
2800                            Votre <strong>code promo</strong> et votre <strong>avoir de ${tag.purchase.avoir}&euro;</strong>
2801                            vous permettent de réserver gratuitement votre contrôle technique.
2802                        </p>
2803                        <p style="font-size: 14px; margin-top: 15px; color: #666;">
2804                            Aucun paiement par carte bancaire n'est nécessaire.
2805                            Cliquez sur le bouton ci-dessous pour confirmer votre réservation.
2806                        </p>`;
2807            } else {
2808                // Cas par défaut (ne devrait pas arriver normalement)
2809                messageHtml = `
2810                        <p style="font-size: 16px; line-height: 1.6;">
2811                            Votre réservation est entièrement couverte, aucun paiement n'est nécessaire.
2812                        </p>
2813                        <p style="font-size: 14px; margin-top: 15px; color: #666;">
2814                            Cliquez sur le bouton ci-dessous pour confirmer votre réservation.
2815                        </p>`;
2816            }
2817
2818            // Créer le HTML de confirmation
2819            var confirmationHtml = `
2820                <div class="confirmation-without-payment">
2821                    <div class="alert alert-success" style="padding: 30px; margin: 20px 0; border-radius: 8px;">
2822                        <h3 style="margin-top: 0;"><i class="fas fa-check-circle"></i> Bonne nouvelle !</h3>
2823                        ${messageHtml}
2824                    </div>
2825
2826                    <div class="form-group" style="margin-top: 30px;">
2827                        <label class="checkbox-container" style="font-size: 14px;">
2828                            <input type="checkbox" id="cgv-checkbox-avoir" required>
2829                            <span class="checkmark"></span>
2830                            J'accepte les <a href="#" data-toggle="modal" data-target="#popup-cgv">conditions générales de vente</a>
2831                            et la <a href="#" data-toggle="modal" data-target="#popup-confidentialite">politique de confidentialité</a>
2832                        </label>
2833                    </div>
2834
2835                    <button type="button" class="btn btn-primary btn-click-to-action btn-validate-reservation" style="width: 100%; padding: 15px; font-size: 18px; margin-top: 20px;" disabled>
2836                        <i class="fas fa-check"></i> Réserver
2837                    </button>
2838                </div>
2839            `;
2840
2841            $(tag.refs.form_payment).html(confirmationHtml);
2842            console.log("HTML injected into form_payment");
2843            console.log("form_payment content:", $(tag.refs.form_payment).html().substring(0, 200));
2844
2845            // Activer le bouton quand les CGV sont acceptées
2846            $('#cgv-checkbox-avoir').on('change', function() {
2847                $('.btn-validate-reservation').prop('disabled', !$(this).is(':checked'));
2848            });
2849
2850            // Attacher le handler de clic au bouton Réserver
2851            $('.btn-validate-reservation').on('click', tag.submitReservationWithAvoir);
2852
2853            activeTooltips();
2854            $('html, body').animate({
2855                scrollTop: $("a[name='top']").offset().top
2856            }, 500);
2857
2858            tag.refreshPurchase();
2859            tag.update();
2860            console.log("=== loadConfirmationWithoutPayment END ===");
2861        }
2862
2863        // Fonction pour valider la réservation avec l'avoir (sans paiement CB)
2864        tag.submitReservationWithAvoir = function (e) {
2865            e.preventUpdate = true;
2866
2867            // Vérifier que les CGV sont acceptées
2868            if (!$('#cgv-checkbox-avoir').is(':checked')) {
2869                myAlert("Vous devez accepter les conditions générales de vente pour continuer.", "CGV requises");
2870                return;
2871            }
2872
2873            showLoading(true);
2874            $('.btn-validate-reservation').prop('disabled', true);
2875
2876            window._trk && window._trk({
2877                event: 'ux_event',
2878                event_category: 'tunnel',
2879                event_action: 'submit_reservation_with_avoir'
2880            });
2881
2882            $.ajax({
2883                url: tag.opts.context + tag.opts.url + '?action=submitPayment',
2884                method: 'post',
2885                cache: false
2886            }).done(function (json) {
2887                showLoading(false);
2888
2889                if (json.submit == 'ko') {
2890                    $('.btn-validate-reservation').prop('disabled', false);
2891                    myAlert(json.erreur, "Erreur").then(function () {
2892                        if (json.url) {
2893                            window.location.href = tag.opts.context + json.url;
2894                        }
2895                    });
2896                    window._trk && window._trk({
2897                        event: 'ux_event',
2898                        event_category: 'tunnel',
2899                        event_action: 'submit_avoir_error'
2900                    });
2901                } else if (json.submit == 'exceptionPaiementYetSubmited') {
2902                    // Paiement déjà soumis, on redirige vers la confirmation
2903                    window.location.href = tag.opts.context + json.url;
2904                } else {
2905                    // Succès - redirection vers la page de confirmation
2906                    tag.purchase = json.purchase;
2907                    window._trk && window._trk({
2908                        event: 'ux_event',
2909                        event_category: 'tunnel',
2910                        event_action: 'submit_avoir_success'
2911                    });
2912                    window.location.href = tag.opts.context + json.url;
2913                }
2914
2915            }).fail(function (e, m) {
2916                showLoading(false);
2917                $('.btn-validate-reservation').prop('disabled', false);
2918                myAlert("Erreur " + e.status, e.statusText);
2919                window._trk && window._trk({
2920                    event: 'ux_event',
2921                    event_category: 'tunnel',
2922                    event_action: 'submit_avoir_server_error'
2923                });
2924            });
2925        }
2926
2927        tag.setTriggersOnInputCardPayment = function () {
2928            const cardnumber = document.getElementById('cardnumber');
2929            //Mask the Credit Card Number Input
2930            var cardnumber_mask = new IMask(cardnumber, {
2931                mask: [
2932                    {
2933                        mask: '0000 0000 0000 0000',
2934                        cardtype: 'Unknown'
2935                    }
2936                ],
2937                dispatch: function (appended, dynamicMasked) {
2938                    var number = (dynamicMasked.value + appended).replace(/\D/g, '');
2939
2940                    for (var i = 0; i < dynamicMasked.compiledMasks.length; i++) {
2941                        let re = new RegExp(dynamicMasked.compiledMasks[i].regex);
2942                        if (number.match(re) != null) {
2943                            return dynamicMasked.compiledMasks[i];
2944                        }
2945                    }
2946                }
2947            });
2948
2949            const expirationdate = document.getElementById('expirationdate');
2950            var expirationdate_mask = new IMask(expirationdate, {
2951                mask: 'MM{/}YY',
2952                groups: {
2953                    YY: new IMask.MaskedPattern.Group.Range([0, 99]),
2954                    MM: new IMask.MaskedPattern.Group.Range([1, 12]),
2955                }
2956            });
2957
2958            const securitycode = document.getElementById('securitycode');
2959            var securitycode_mask = new IMask(securitycode, {
2960                mask: '0000',
2961            });
2962        }
2963
2964        function isGooglePayAvailable() {
2965            const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
2966            const isAndroid = /Android/.test(navigator.userAgent);
2967            const isSecure = window.isSecureContext;
2968
2969            // API PaymentRequest supportée ?
2970            const hasPaymentRequest = window.PaymentRequest !== undefined;
2971
2972            return isChrome && isSecure && hasPaymentRequest && isAndroid;
2973        }
2974
2975        tag.setTriggersOnFormPayment = function () {
2976
2977            // Si le widget Monext (VARIANT A/B) est présent dans la card CB,
2978            // on l'initialise mais on laisse l'accordion complet tourner normalement
2979            // pour que Paypal / Apple Pay / Google Pay restent accessibles.
2980            if ($('#PaylineWidget').length) {
2981                console.log("[Monext] PaylineWidget detected, initializing inside CB card");
2982                var token = $('#PaylineWidget').data('token');
2983                var env = $('#PaylineWidget').data('env') || 'HOMO';
2984                if (token) {
2985                    tag.initMonext(token, env);
2986                }
2987                // Pas de return : on continue pour mettre en place les triggers de l'accordion
2988            } else {
2989                tag.setTriggersOnInputCardPayment();
2990            }
2991
2992            customCurrentBreadCrumb("Vos informations &gt; <b>Paiement</b>");
2993
2994            // The Monext failure redirect renders the fallback CTA server-side (paymentErrorRetryable):
2995            // count it as an impression too, not only the widget-side promotion path.
2996            if ($('#mollie-cb-fallback-server-cta').length) {
2997                trackMollieCardFallbackShown();
2998            }
2999
3000            $("#radio-cb").prop("checked", false);
3001            $("#radio-paypal").prop("checked", false);
3002            $("#radio-paypal-4x").prop("checked", false);
3003            $("#radio-google").prop("checked", false);
3004            $("#radio-apple").prop("checked", false);
3005
3006            $(".payment-with-cb").hide();
3007            $(".payment-with-paypal").hide();
3008            $(".payment-with-paypal-4x").hide();
3009            $(".gpay-card-info-container-fill").hide();
3010            $(".payment-accept-cgv").hide();
3011            $(".payment-accept-cgv-paypal").hide();
3012
3013            $(".btn-validate-payment-montant").html(tag.purchase.reservationPrixFinal + "&euro;");
3014
3015            $("#payment-mode-cb").click(function () {
3016                $("#radio-cb").prop("checked", true);
3017                $("#radio-paypal").prop("checked", false);
3018                $("#radio-paypal-4x").prop("checked", false);
3019                $("#radio-google").prop("checked", false);
3020                $("#radio-apple").prop("checked", false);
3021
3022                $(".payment-with-cb").show();
3023                $(".payment-with-paypal").hide();
3024                $(".payment-with-paypal-4x").hide();
3025                $(".gpay-card-info-container-fill").hide();
3026                $(".payment-accept-cgv").show();
3027                $(".payment-accept-cgv-paypal").hide();
3028                $("#payment-reassurance").show();
3029
3030                window._trk && window._trk({
3031                    event: 'ux_event',
3032                    event_category: 'tunnel',
3033                    event_action: 'payment-mode-cb'
3034                });
3035            });
3036
3037            // VARIANT A/B : réinitialiser le widget Monext à l'ouverture de la card CB
3038            // (nécessaire si l'utilisateur a d'abord ouvert un autre mode de paiement)
3039            if ($('#PaylineWidget').length) {
3040                $('#paymentVisa').off('show.bs.collapse').on('show.bs.collapse', function () {
3041                    var token = $('#PaylineWidget').data('token');
3042                    var env = $('#PaylineWidget').data('env') || 'HOMO';
3043                    if (token) {
3044                        tag.initMonext(token, env);
3045                    }
3046                });
3047            }
3048            $("#payment-mode-paypal").click(function () {
3049                $("#radio-cb").prop("checked", false);
3050                $("#radio-paypal").prop("checked", true);
3051                $("#radio-paypal-4x").prop("checked", false);
3052                $("#radio-google").prop("checked", false);
3053                $("#radio-apple").prop("checked", false);
3054
3055                $(".payment-with-cb").hide();
3056                $(".payment-with-paypal").show();
3057                $(".payment-with-paypal-4x").hide();
3058                $(".gpay-card-info-container-fill").hide();
3059                $(".payment-accept-cgv").show();
3060                $(".payment-accept-cgv-paypal").show();
3061                $("#payment-reassurance").hide();
3062
3063                window._trk && window._trk({
3064                    event: 'ux_event',
3065                    event_category: 'tunnel',
3066                    event_action: 'payment-mode-paypal'
3067                });
3068            });
3069            $("#payment-mode-paypal-4x").click(function () {
3070                $("#radio-cb").prop("checked", false);
3071                $("#radio-paypal").prop("checked", false);
3072                $("#radio-paypal-4x").prop("checked", true);
3073                $("#radio-google").prop("checked", false);
3074                $("#radio-apple").prop("checked", false);
3075
3076                $(".payment-with-cb").hide();
3077                $(".payment-with-paypal").hide();
3078                $(".payment-with-paypal-4x").show();
3079                $(".gpay-card-info-container-fill").hide();
3080                $(".payment-accept-cgv").show();
3081                $(".payment-accept-cgv-paypal").show();
3082                $("#payment-reassurance").hide();
3083
3084                window._trk && window._trk({
3085                    event: 'ux_event',
3086                    event_category: 'tunnel',
3087                    event_action: 'payment-mode-paypal-4x'
3088                });
3089            });
3090            $("#payment-mode-google").click(function () {
3091                $("#radio-cb").prop("checked", false);
3092                $("#radio-paypal").prop("checked", false);
3093                $("#radio-paypal-4x").prop("checked", false);
3094                $("#radio-google").prop("checked", true);
3095                $("#radio-apple").prop("checked", false);
3096
3097                $(".payment-with-cb").hide();
3098                $(".payment-with-paypal").hide();
3099                $(".payment-with-paypal-4x").hide();
3100                $(".gpay-card-info-container-fill").show();
3101                $(".payment-accept-cgv").show();
3102                $(".payment-accept-cgv-paypal").hide();
3103                $("#payment-reassurance").hide();
3104
3105                window._trk && window._trk({
3106                    event: 'ux_event',
3107                    event_category: 'tunnel',
3108                    event_action: 'payment-mode-google'
3109                });
3110            });
3111            $("#payment-mode-apple").click(function () {
3112                $("#radio-cb").prop("checked", false);
3113                $("#radio-paypal").prop("checked", false);
3114                $("#radio-paypal-4x").prop("checked", false);
3115                $("#radio-google").prop("checked", false);
3116                $("#radio-apple").prop("checked", true);
3117
3118                $(".payment-with-cb").hide();
3119                $(".payment-with-paypal").hide();
3120                $(".payment-with-paypal-4x").hide();
3121                $(".gpay-card-info-container-fill").show();
3122                $(".payment-accept-cgv").show();
3123                $(".payment-accept-cgv-paypal").hide();
3124                $("#payment-reassurance").hide();
3125
3126                window._trk && window._trk({
3127                    event: 'ux_event',
3128                    event_category: 'tunnel',
3129                    event_action: 'payment-mode-apple'
3130                });
3131            });
3132
3133            if (window.ApplePaySession && ApplePaySession.canMakePayments()) {
3134                console.log("Apple Pay is available");
3135                $('#payment-mode-apple').parent().show();
3136            }
3137
3138            if (isGooglePayAvailable()) {
3139                console.log("Google Pay is available");
3140                $('#payment-mode-google').parent().show();
3141            }
3142
3143            // Ouvrir la section "Cartes de crédit" par défaut, quel que soit le nombre de modes disponibles
3144            $("#payment-mode-cb").click();
3145
3146            // Evenementiel sur le click du bouton de validation du paiement (uniquement en mode CB classique, pas Monext)
3147            const securityCodeInput = document.getElementById('securitycode');
3148            const submitButton = document.querySelector('[ref="btn_validate_payment"]');
3149            if (securityCodeInput && submitButton) {
3150                securityCodeInput.addEventListener('keydown', function (event) {
3151                    if (event.key === 'Enter') {
3152                        event.preventDefault(); // Prevent form submission or undesired default behavior
3153                        submitButton.click();   // Trigger the payment button click
3154                        window._trk && window._trk({
3155                            event: 'ux_event',
3156                            event_category: 'tunnel',
3157                            event_action: 'security_code_enterkey'
3158                        });
3159                    }
3160                });
3161            }
3162
3163            const headers = document.querySelectorAll(".accordion.payment-mode-wrapper .card-header");
3164            const accordionWrapper = document.getElementById("accordionPaymentMode");
3165            headers.forEach(header => {
3166                header.addEventListener("click", () => {
3167                    if (tag.isMobile) {
3168                        setTimeout(() => {
3169                            var $header = $(".main-sticky-header").first();
3170                            var bottomOnScreen = 45;
3171                            if ($header.length) {
3172                                const rect = $header[0].getBoundingClientRect();
3173                                bottomOnScreen = rect.bottom;
3174                            }
3175                            const offset = bottomOnScreen; // height of your sticky header, or desired margin
3176                            const top = header.getBoundingClientRect().top + window.scrollY - offset;
3177
3178                            window.scrollTo({
3179                                top: top,
3180                                behavior: 'smooth'
3181                            });
3182                        }, 300);
3183                    }
3184                });
3185            });
3186
3187        }
3188
3189        tag.submitPaymentPaypal = function () {
3190            $(tag.refs.btn_validate_payment).hide();
3191            showLoading(true);
3192
3193            window._trk && window._trk({
3194                event: 'ux_event',
3195                event_category: 'tunnel',
3196                event_action: 'submit-payment-paypal'
3197            });
3198
3199            $.ajax({
3200                url: tag.opts.context + tag.opts.url + '?action=submitPaymentPaypal',
3201                method: 'post',
3202                data: $("form[name='form-paiement']").serialize(),
3203                cache: false
3204            }).done(function (json) {
3205
3206                showLoading(false);
3207
3208                $(tag.refs.btn_validate_payment).show();
3209
3210                if (json.submit == 'ko') {
3211                    //erreurs de saisie
3212                    myConfirm(json.erreur, "Erreur").then(function () {
3213                        window.location.href = tag.opts.context + json.url;
3214                    });
3215                    window._trk && window._trk({
3216                        event: 'ux_event',
3217                        event_category: 'tunnel',
3218                        event_action: 'submit-payment-paypal-error'
3219                    });
3220                } else if (json.submit == 'formValidateException') {
3221                    //erreurs de saisie
3222                    myAlert("Validation du formulaire de paiement impossible, merci de v&eacute;rifier vos donn&eacute;es de paiement. (FVE)", "Paiement impossible");
3223                    tag.loadFormPayment();
3224                    window._trk && window._trk({
3225                        event: 'ux_event',
3226                        event_category: 'tunnel',
3227                        event_action: 'submit-payment-paypal-validation-error'
3228                    });
3229                } else if (json.submit == 'exceptionPaiementYetSubmited') {
3230                    //erreurs de saisie
3231                    myAlert("Validation du formulaire de paiement impossible, merci de v&eacute;rifier vos donn&eacute;es de paiement. (EPYS)", "Paiement impossible");
3232                    tag.loadFormPayment();
3233                    window._trk && window._trk({
3234                        event: 'ux_event',
3235                        event_category: 'tunnel',
3236                        event_action: 'submit-payment-paypal-check'
3237                    });
3238                } else {
3239                    tag.purchase = json.purchase;
3240                    window.location.href = json.urlRedirect;
3241                    window._trk && window._trk({
3242                        event: 'ux_event',
3243                        event_category: 'tunnel',
3244                        event_action: 'submit-payment-paypal-done'
3245                    });
3246                }
3247
3248            }).fail(function (e, m) {
3249                showLoading(false);
3250                $(tag.refs.btn_validate_payment).show();
3251                myAlert("Erreur " + e.status, e.statusText);
3252                window._trk && window._trk({
3253                    event: 'ux_event',
3254                    event_category: 'tunnel',
3255                    event_action: 'submit-payment-paypal-server-error'
3256                });
3257            });
3258        }
3259
3260        tag.submitPayment = function () {
3261
3262            $(tag.refs.btn_validate_payment).hide();
3263            showLoading(true);
3264
3265            window._trk && window._trk({
3266                event: 'ux_event',
3267                event_category: 'tunnel',
3268                event_action: 'submit_payment'
3269            });
3270
3271            $.ajax({
3272                url: tag.opts.context + tag.opts.url + '?action=submitPayment',
3273                method: 'post',
3274                data: $("form[name='form-paiement']").serialize(),
3275                cache: false
3276            }).done(function (json) {
3277                showLoading(false);
3278                $(tag.refs.btn_validate_payment).show();
3279                if (json.submit == 'formValidateException') {
3280                    //erreurs de saisie
3281                    myAlert("Validation du formulaire de paiement impossible, merci de v&eacute;rifier vos donn&eacute;es de paiement.", "Paiement impossible");
3282                    window._trk && window._trk({
3283                        event: 'ux_event',
3284                        event_category: 'tunnel',
3285                        event_action: 'submit_payment_validation_error'
3286                    });
3287                } else if (json.submit == 'ko') {
3288                    // erreurs de saisie
3289                    myAlert(json.erreur, "Erreur").then(function () {
3290                        window.location.href = tag.opts.context + json.url;
3291                    });
3292                    window._trk && window._trk({
3293                        event: 'ux_event',
3294                        event_category: 'tunnel',
3295                        event_action: 'submit_payment_error'
3296                    });
3297
3298                } else {
3299                    tag.purchase = json.purchase;
3300                    window._trk && window._trk({
3301                        event: 'ux_event',
3302                        event_category: 'tunnel',
3303                        event_action: 'submit_payment_success'
3304                    });
3305
3306                    if (json.token3DS != null && json.threeDSMethodNotificationURL != null) {
3307                        showLoading(true);
3308                        var iframe = $('<iframe id="idIframe3DSMethod" name="threeDSMethod" style="width: 0; height: 0; style="visibility: hidden;" src="javascript:false;" xmlns="http://www.w3.org/1999/xhtml">'
3309                            + '</iframe>');
3310                        $('body').append(iframe);
3311
3312                        $('#idIframe3DSMethod').on('load', function () {
3313                            var iframe = document.getElementById('idIframe3DSMethod');
3314                            var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
3315                            const form = $('<form id="webform0" name="" action="' + json.actionUrl + '" accept_charset="UTF-8" method="POST" target="threeDSMethod">'
3316                                + '<input type="hidden" name="_charset_" value="UTF-8"/>'
3317                                + '<input type="hidden" name="' + json.pareqFieldName + '" value="' + json.pareqFieldValue + '">'
3318                                + '</form>');
3319                            form.appendTo(iframeDoc.body);
3320                            form.submit();
3321                        });
3322
3323                        if (json.threeDSMethodNotificationURL != null) {
3324                            asyncWait(10).then(() => {
3325                                call3dsReturn(json.threeDSMethodNotificationURL);
3326                            });
3327                        }
3328                    } else if (json.token3DS != null && json.threeDSMethodNotificationURL == null) {
3329                        var form = $("<form name='downloadForm' action='" + json.actionUrl + "' method='POST'>"
3330                            + "<input type='hidden' name='" + json.termUrlName + "' value='" + json.termUrlValue + "'>"
3331                            + "<input type='hidden' name='" + json.pareqFieldName + "' value='" + json.pareqFieldValue + "'>"
3332                            + "<input type='hidden' name='" + json.mdFieldName + "' value='" + json.mdFieldValue + "'>"
3333                            + "</form>");
3334
3335                        form.appendTo($("body"));
3336
3337                        form.submit();
3338
3339                    } else {
3340                            window.location.href = tag.opts.context + json.url;
3341                    }
3342                }
3343
3344            }).fail(function (e, m) {
3345                showLoading(false);
3346                $(tag.refs.btn_validate_payment).show();
3347                myAlert("Erreur " + e.status, e.statusText);
3348                window._trk && window._trk({
3349                    event: 'ux_event',
3350                    event_category: 'tunnel',
3351                    event_action: 'submit_payment_server_error'
3352                });
3353            });
3354
3355        }
3356
3357        // GO BACK STEP
3358        tag.goBackStep = function (step) {
3359            // Cleanup Monext avant de quitter l'étape PAYMENT :
3360            // le DOM #PaylineWidget va être détruit par loadForm(), on doit tuer le timer
3361            // et reset le SDK pour éviter qu'il fire sur un élément mort.
3362            if (monextRefreshTimer) {
3363                clearTimeout(monextRefreshTimer);
3364                monextRefreshTimer = null;
3365            }
3366            monextRefreshFailures = 0;
3367            monextWidgetReady = false;
3368            if (window.Payline && window.Payline.Api && window.Payline.Api.reset) {
3369                var hasPaylineWidget = $('#PaylineWidget').length > 0;
3370                if (hasPaylineWidget || (tag.purchase && tag.purchase.stepActif === 'PAYMENT')) {
3371                    try {
3372                        window.Payline.Api.reset();
3373                    } catch (e) {
3374                        console.warn("[Monext] Payline.Api.reset() ignoré lors du retour arrière:", e);
3375                    }
3376                }
3377            }
3378            tag.purchase.stepActif = step;
3379            tag.loadForm();
3380        }
3381
3382        tag.goBackCoordinates = function () {
3383            tag.goBackStep('COORDINATES');
3384        }
3385
3386        tag.goBackOptions = function () {
3387            if (tag.purchase.optionContreVisite || tag.purchase.optionModifAppointment) {
3388                tag.goBackStep('OPTIONS');
3389            } else {
3390                tag.goBackStep('COORDINATES');
3391            }
3392
3393        }
3394
3395        tag.popinLogin = function () {
3396            $("#popinLogin").modal('show');
3397        }
3398
3399        // POPIN si besoin du PWD -> submit du PWD
3400        tag.submitPwd = function () {
3401            window._trk && window._trk({
3402                event: 'ux_event',
3403                event_category: 'tunnel',
3404                event_action: 'submit_password'
3405            });
3406            $.ajax({
3407                url: tag.opts.context + tag.opts.url + '?action=submitPwd',
3408                data: $("input[name='password']").serialize(),
3409                method: 'post',
3410                cache: false
3411
3412            }).done(function (json) {
3413
3414                showLoading(false);
3415
3416                if (json.submit == 'loginException') {
3417                    //mauvais login/pwd
3418                    $("#popinLoginMsg").html("Connexion impossible, v&eacute;rifiez votre mot de passe.").effect("highlight", {}, 3000);
3419                    window._trk && window._trk({
3420                        event: 'ux_event',
3421                        event_category: 'tunnel',
3422                        event_action: 'login_failed'
3423                    });
3424                } else if (json.submit == 'ko') {
3425                    //erreurs de saisie
3426                    myConfirm(json.erreur, "Erreur").then(function () {
3427                        window.location.href = tag.opts.context + json.url;
3428                    });
3429                    window._trk && window._trk({
3430                        event: 'ux_event',
3431                        event_category: 'tunnel',
3432                        event_action: 'login_error'
3433                    });
3434                } else {
3435                    // ferme la popin
3436                    $("#popinLogin").modal('hide');
3437                    // repost le formulaire
3438                    tag.submitCoordinates();
3439                    window._trk && window._trk({
3440                        event: 'ux_event',
3441                        event_category: 'tunnel',
3442                        event_action: 'login_success'
3443                    });
3444                }
3445
3446            }).fail(function () {
3447                myAlert("err " + e, e);
3448                window._trk && window._trk({
3449                    event: 'ux_event',
3450                    event_category: 'tunnel',
3451                    event_action: 'login_server_error'
3452                });
3453            });
3454        }
3455
3456        // A second tap while the payment is being created fired a second createMolliePayment request.
3457        var molliePaymentCreationInProgress = false;
3458        // Coming back from the Mollie page with the browser "back" button can restore this page from the
3459        // back/forward cache with the flag still set: release it, otherwise the buttons would stay dead.
3460        window.addEventListener('pageshow', function (event) {
3461            if (event.persisted) {
3462                molliePaymentCreationInProgress = false;
3463                showLoading(false);
3464            }
3465        });
3466
3467        // source: undefined = wallet buttons (Apple/Google Pay), 'cb_fallback' = "pay by card via Mollie"
3468        // link shown after a Monext failure. Distinct tracking event to measure fallback usage separately.
3469        tag.onMolliePaymentButtonClicked = function (source) {
3470            if (molliePaymentCreationInProgress) {
3471                return;
3472            }
3473            molliePaymentCreationInProgress = true;
3474            showLoading(true);
3475            window._trk && window._trk({
3476                event: 'ux_event',
3477                event_category: 'tunnel',
3478                event_action: source === 'cb_fallback' ? 'payment_mollie_cb_fallback_started' : 'payment_mollie_started'
3479            });
3480            console.log("Clicked on Mollie payment button (source=" + (source || 'wallet') + "): " + tag.opts.context + tag.opts.url + '?action=createMolliePayment');
3481            $.ajax({
3482                url: tag.opts.context + tag.opts.url + '?action=createMolliePayment',
3483                method: 'post',
3484                cache: false
3485            }).done(function (json) {
3486                console.log("json", json);
3487                if (json.submit == 'ko') {
3488                    molliePaymentCreationInProgress = false;
3489                    showLoading(false);
3490                    myAlert(json.erreur, "Erreur");
3491                    window._trk && window._trk({
3492                        event: 'ux_event',
3493                        event_category: 'tunnel',
3494                        event_action: 'payment_mollie_error'
3495                    });
3496                } else {
3497                    // Flag and loader stay on until the browser has left for the payment page.
3498                    window.location.href = json.redirect;
3499                    window._trk && window._trk({
3500                        event: 'ux_event',
3501                        event_category: 'tunnel',
3502                        event_action: 'payment_mollie_success'
3503                    });
3504                }
3505            }).fail(function (e, m) {
3506                molliePaymentCreationInProgress = false;
3507                showLoading(false);
3508                myAlert("Erreur " + e.status, e.statusText);
3509                window._trk && window._trk({
3510                    event: 'ux_event',
3511                    event_category: 'tunnel',
3512                    event_action: 'payment_mollie_server_error'
3513                });
3514            });
3515        }
3516
3517        // Single impression event per page load for the Mollie card fallback, whichever path
3518        // surfaced it first (server-rendered CTA after a Monext redirect failure, or widget-side
3519        // failure promoting the discreet link). Keeps exposure vs starts comparable in analytics.
3520        var mollieFallbackShownTracked = false;
3521        function trackMollieCardFallbackShown() {
3522            if (mollieFallbackShownTracked) {
3523                return;
3524            }
3525            mollieFallbackShownTracked = true;
3526            window._trk && window._trk({
3527                event: 'ux_event',
3528                event_category: 'tunnel',
3529                event_action: 'payment_mollie_cb_fallback_shown'
3530            });
3531        }
3532
3533        // Promote the always-present discreet "pay by card via Mollie" link (#mollie-cb-fallback,
3534        // rendered only when location.mollieActive) to a visible warning banner. Called when the
3535        // Monext widget reports a failure state client-side; the server-side failure path
3536        // (checkMonextPayment ERROR/SESSION_EXPIRED) renders its own CTA in the paymentError alert.
3537        var mollieFallbackHighlighted = false;
3538        tag.showMollieCardFallbackHighlight = function () {
3539            var $fallback = $('#mollie-cb-fallback');
3540            if (!$fallback.length || mollieFallbackHighlighted) {
3541                return;
3542            }
3543            mollieFallbackHighlighted = true;
3544            $fallback.addClass('alert alert-warning');
3545            $('#mollie-cb-fallback-intro').show();
3546            $('#mollie-cb-fallback-link').css('font-size', '');
3547            trackMollieCardFallbackShown();
3548        };
3549
3550        // État du widget Monext : timer de refresh, compteur d'échecs, flag d'init
3551        var monextRefreshTimer = null;
3552        var monextRefreshFailures = 0;
3553        var MAX_REFRESH_FAILURES = 3;
3554        var monextWidgetReady = false;
3555        // Flag positionné à true dès que onMonextFinalState est appelé.
3556        // Permet d'ignorer les callbacks AJAX de refresh en vol qui arriveraient
3557        // après la soumission du paiement (cause du code 1913 "Duplicated transaction").
3558        var monextPaymentCompleted = false;
3559
3560        // Utilisé pour recalculer le temps restant après un retour au premier plan (mobile)
3561        var monextRefreshScheduledAt = null;
3562        var monextRefreshTargetDelay = null;
3563        var monextRefreshEnv = null;
3564
3565        // Delay before the page sends the buyer to the return URL itself, once the widget has reached a final state.
3566        // A challenged (3DS) payment is brought back by the widget's own redirection, which unloads the page before
3567        // this timer fires. A frictionless payment gets no redirection at all (automatic return to the shop is not
3568        // enabled on the Monext side), so without this fallback the buyer stays on the loader forever.
3569        var MONEXT_RETURN_FALLBACK_DELAY_MS = 3000;
3570        var monextReturnTimer = null;
3571
3572        // checkMonextPayment reads the real status from Payline and handles every case (paid, already validated by the
3573        // webhook, failed, cancelled, pending), so the widget state only chooses between the return and cancel URLs.
3574        tag.scheduleMonextReturnFallback = function(state) {
3575            if (monextReturnTimer) {
3576                return;
3577            }
3578            var $widget = $('#PaylineWidget');
3579            var isCancel = String(state).indexOf('CANCEL') !== -1;
3580            var targetUrl = isCancel ? $widget.data('cancel-url') : $widget.data('return-url');
3581            if (!targetUrl) {
3582                console.warn("[Monext] Aucune URL de retour sur #PaylineWidget, pas de redirection de secours");
3583                return;
3584            }
3585            var token = $widget.attr('token');
3586            if (token) {
3587                targetUrl += '&token=' + encodeURIComponent(token);
3588            }
3589            monextReturnTimer = setTimeout(function() {
3590                console.log("[Monext] Pas de redirection du widget après l'état final " + state + ", redirection vers " + targetUrl);
3591                window.location.assign(targetUrl);
3592            }, MONEXT_RETURN_FALLBACK_DELAY_MS);
3593        };
3594
3595        tag.doMonextTokenRefresh = function() {
3596            console.log("[Monext] Renouvellement du token en cours...");
3597            var REFRESH_DELAY_MS = 13 * 60 * 1000;
3598            var RETRY_DELAY_MS = 30 * 1000;
3599            $.ajax({
3600                url: tag.opts.context + tag.opts.url + '?action=refreshMonextToken',
3601                method: 'POST',
3602                dataType: 'json',
3603                success: function(data) {
3604                    if (data && data.token) {
3605                        // Ne pas reset le widget si le paiement est déjà en cours de validation :
3606                        // un Payline.Api.reset() sur un paiement terminé provoquerait un code 1913
3607                        // (Duplicated transaction) en déclenchant une seconde requête sur le même ORDER_REFERENCE.
3608                        if (monextPaymentCompleted) {
3609                            console.log("[Monext] Paiement déjà en cours de validation — refresh token ignoré pour éviter doublon 1913");
3610                            return;
3611                        }
3612                        console.log("[Monext] Nouveau token reçu, rechargement du widget Payline...");
3613                        monextRefreshFailures = 0;
3614                        var $widget = $('#PaylineWidget');
3615                        $widget.attr('token', data.token);
3616                        $widget.attr('data-token', data.token);
3617                        if (window.Payline && window.Payline.Api) {
3618                            window.Payline.Api.reset();
3619                            console.log("[Monext] Payline.Api.reset() appelé avec le nouveau token");
3620                        }
3621                        tag.scheduleMonextTokenRefresh(monextRefreshEnv, REFRESH_DELAY_MS);
3622                    } else {
3623                        monextRefreshFailures++;
3624                        console.warn("[Monext] Échec du renouvellement de token (" + monextRefreshFailures + "/" + MAX_REFRESH_FAILURES + "):", data && data.error);
3625                        if (monextRefreshFailures >= MAX_REFRESH_FAILURES) {
3626                            console.warn("[Monext] Trop d'échecs consécutifs, rechargement complet du formulaire de paiement");
3627                            monextRefreshFailures = 0;
3628                            tag.loadFormPayment();
3629                        } else {
3630                            tag.scheduleMonextTokenRefresh(monextRefreshEnv, RETRY_DELAY_MS);
3631                        }
3632                    }
3633                },
3634                error: function(xhr, status, err) {
3635                    monextRefreshFailures++;
3636                    console.warn("[Monext] Erreur AJAX refreshMonextToken (" + monextRefreshFailures + "/" + MAX_REFRESH_FAILURES + "):", status, err);
3637                    if (monextRefreshFailures >= MAX_REFRESH_FAILURES) {
3638                        console.warn("[Monext] Trop d'échecs consécutifs, rechargement complet du formulaire de paiement");
3639                        monextRefreshFailures = 0;
3640                        tag.loadFormPayment();
3641                    } else {
3642                        tag.scheduleMonextTokenRefresh(monextRefreshEnv, RETRY_DELAY_MS);
3643                    }
3644                }
3645            });
3646        };
3647
3648        tag.scheduleMonextTokenRefresh = function(env, delayMs) {
3649            var REFRESH_DELAY_MS = 13 * 60 * 1000;
3650            var delay = delayMs || REFRESH_DELAY_MS;
3651
3652            monextRefreshEnv = env;
3653            monextRefreshScheduledAt = Date.now();
3654            monextRefreshTargetDelay = delay;
3655
3656            if (monextRefreshTimer) {
3657                clearTimeout(monextRefreshTimer);
3658            }
3659
3660            monextRefreshTimer = setTimeout(function() {
3661                tag.doMonextTokenRefresh();
3662            }, delay);
3663
3664            console.log("[Monext] Token refresh planifié dans " + Math.round(delay / 1000) + "s");
3665        };
3666
3667        // Sur mobile, les setTimeout sont suspendus quand la page passe en arrière-plan.
3668        // Au retour au premier plan, on recalcule le temps écoulé :
3669        // - si le délai est dépassé → refresh immédiat
3670        // - sinon → on reprend le timer avec le temps restant
3671        document.addEventListener('visibilitychange', function() {
3672            if (document.visibilityState === 'visible' && monextRefreshScheduledAt !== null && monextRefreshTimer !== null) {
3673                if (monextPaymentCompleted) {
3674                    console.log("[Monext] Paiement déjà en cours de validation — visibilitychange ignoré");
3675                    return;
3676                }
3677                var elapsed = Date.now() - monextRefreshScheduledAt;
3678                var remaining = monextRefreshTargetDelay - elapsed;
3679                console.log("[Monext] Retour au premier plan - elapsed=" + Math.round(elapsed / 1000) + "s, remaining=" + Math.round(remaining / 1000) + "s");
3680                if (remaining <= 0) {
3681                    // Le timer aurait dû s'être déclenché pendant la mise en background
3682                    console.log("[Monext] Délai dépassé pendant la mise en background, refresh immédiat");
3683                    clearTimeout(monextRefreshTimer);
3684                    monextRefreshTimer = null;
3685                    monextRefreshScheduledAt = null;
3686                    tag.doMonextTokenRefresh();
3687                } else {
3688                    // Reprendre avec le temps restant exact
3689                    console.log("[Monext] Reprise du timer avec " + Math.round(remaining / 1000) + "s restantes");
3690                    tag.scheduleMonextTokenRefresh(monextRefreshEnv, remaining);
3691                }
3692            }
3693        });
3694
3695        tag.initMonext = function(token, env) {
3696             if (monextWidgetReady) {
3697                 console.log("[Monext] initMonext ignoré, widget déjà initialisé");
3698                 return;
3699             }
3700             monextRefreshFailures = 0;
3701             monextPaymentCompleted = false; // reset pour une nouvelle session de paiement
3702             console.log("[Monext] initMonext called with token=" + token + " env=" + env);
3703
3704             var scriptUrl = (env === 'PROD')
3705                ? 'https://payment.cdn.payline.com/cdn/scripts/widget-min.js'
3706                : 'https://homologation-payment.cdn.payline.com/cdn/scripts/widget-min.js';
3707
3708             function waitForPaylineAndInit(token, attempts) {
3709                 attempts = attempts || 0;
3710                 if (attempts > 50) { // max 5 secondes
3711                     console.error("[Monext] Payline SDK not ready after 5s");
3712                     myAlert("Le service de paiement par carte n'a pas pu se charger. Veuillez recharger la page ou essayer un autre moyen de paiement.", "Erreur de chargement");
3713                     return;
3714                 }
3715                 if (window.Payline && window.Payline.Api) {
3716                     console.log("[Monext] Payline SDK ready, initializing widget");
3717                     var $widget = $('#PaylineWidget');
3718                     if ($widget.length) {
3719                         // Callbacks globaux requis par le widget Monext (data-event-* pointe sur le nom de la fonction dans window)
3720                         // Le widget appelle ces fonctions avec un objet {state: "..."} selon le cycle de vie du paiement
3721
3722                         window.onMonextWillInit = function() {
3723                             console.log("[Monext] willinit - widget about to initialize");
3724                         };
3725
3726                         window.onMonextWillShow = function() {
3727                             console.log("[Monext] willshow - widget about to show");
3728                         };
3729
3730                         window.onMonextDidShowState = function(obj) {
3731                             var shownState = obj && obj.state ? String(obj.state) : '';
3732                             console.log("[Monext] didshowstate - state=" + shownState);
3733                             // Widget-side failure without server roundtrip (e.g. card refused before 3DS
3734                             // redirect): promote the discreet Mollie card fallback link to a visible banner.
3735                             if (shownState.indexOf('FAILURE') !== -1 || shownState === 'TOKEN_EXPIRED') {
3736                                 tag.showMollieCardFallbackHighlight();
3737                             }
3738                         };
3739
3740                         // Appelé quand le paiement atteint un état terminal (succès, échec, annulation)
3741                         // avant la redirection vers returnURL/cancelURL
3742                         window.onMonextFinalState = function(obj) {
3743                             var state = obj && obj.state ? obj.state : obj;
3744                             console.log("[Monext] finalstatehasbeenreached - state=" + state + " obj=", obj);
3745                             // Verrouiller immédiatement pour bloquer tout callback AJAX de refresh en vol
3746                             // qui arriverait après cet événement et appellerait Payline.Api.reset()
3747                             // sur un paiement terminé → code 1913 "Duplicated transaction".
3748                             monextPaymentCompleted = true;
3749                             // Annuler le timer de refresh : le paiement est terminé, inutile de renouveler
3750                             if (monextRefreshTimer) {
3751                                 clearTimeout(monextRefreshTimer);
3752                                 monextRefreshTimer = null;
3753                             }
3754                             monextWidgetReady = false;
3755                             showLoading(true, "Validation du paiement en cours...");
3756                             tag.scheduleMonextReturnFallback(state);
3757                         };
3758
3759                         // Le widget Monext lit le token depuis l'attribut HTML "token" du #PaylineWidget
3760                         // window.Payline.Api.init() s'appelle sans paramètres
3761                         $widget.attr('token', token);
3762                         // Reset nécessaire si le SDK a déjà été initialisé (retour depuis coordinates/options) :
3763                         // le précédent #PaylineWidget DOM a été détruit par .html(), le SDK garde une ref morte.
3764                         if (window.Payline.Api.reset) {
3765                             window.Payline.Api.reset();
3766                             console.log("[Monext] Payline.Api.reset() called before re-init");
3767                         }
3768                         window.Payline.Api.init();
3769                         monextWidgetReady = true;
3770                         console.log("[Monext] Payline.Api.init() called, callbacks registered");
3771
3772                         // Démarrer le timer de rafraîchissement automatique (13 min avant expiration Payline)
3773                         tag.scheduleMonextTokenRefresh(env);
3774                     } else {
3775                         console.error("[Monext] #PaylineWidget container not found");
3776                     }
3777                 } else {
3778                     console.log("[Monext] Waiting for Payline SDK... attempt " + attempts);
3779                     setTimeout(function() { waitForPaylineAndInit(token, attempts + 1); }, 100);
3780                 }
3781             }
3782
3783             if (window.Payline) {
3784                 console.log("[Monext] Payline already in window, calling waitForPaylineAndInit");
3785                 waitForPaylineAndInit(token);
3786             } else {
3787                 console.log("[Monext] Loading Payline SDK from: " + scriptUrl);
3788                 var script = document.createElement('script');
3789                 script.src = scriptUrl;
3790                 script.onload = function() {
3791                     console.log("[Monext] Payline SDK script loaded");
3792                     waitForPaylineAndInit(token);
3793                 };
3794                 script.onerror = function() {
3795                     console.error("[Monext] Failed to load Payline SDK from: " + scriptUrl);
3796                     myAlert("Le service de paiement par carte est indisponible. Veuillez recharger la page ou essayer un autre moyen de paiement.", "Erreur de chargement");
3797                 };
3798                 document.head.appendChild(script);
3799             }
3800        }
3801
3802        tag.openModalWithPromoCode = function (e) {
3803            e.preventDefault();
3804            e.stopPropagation();
3805
3806            // Ouvrir la modal
3807            $('#tunnelRecapPopup').modal('show');
3808
3809            // Attendre que la modal soit complètement affichée
3810            $('#tunnelRecapPopup').on('shown.bs.modal', function () {
3811                // Activer le champ code promo dans le composant du récapitulatif
3812                if (tag.refs.recapModal) {
3813                    var couponTag = tag.refs.recapModal.tags['ctrl-tunnel-coupon-reduction'];
3814                    if (couponTag && !couponTag.affichageFieldCoupon) {
3815                        // Cocher la checkbox et afficher le champ
3816                        couponTag.affichageFieldCoupon = true;
3817                        couponTag.update();
3818
3819                        // Focus sur le champ de saisie après un court délai
3820                        setTimeout(function() {
3821                            if (couponTag.refs.code_coupon) {
3822                                $(couponTag.refs.code_coupon).focus();
3823                            }
3824                        }, 100);
3825                    }
3826                }
3827
3828                // Retirer le listener pour éviter qu'il se déclenche plusieurs fois
3829                $('#tunnelRecapPopup').off('shown.bs.modal');
3830            });
3831
3832            window._trk && window._trk({
3833                event: 'ux_event',
3834                event_category: 'tunnel',
3835                event_action: 'mobile_code_promo_link_clicked'
3836            });
3837        }
3838
3839        // Pas de lien "a.open-avis" rendu nulle part dans le tunnel (vérifié : ni dans ce tag, ni
3840        // dans tunnel.jsp) — l'ancien binding + loadTrustPilotInPopin (iframe vers avis-list.jsp)
3841        // n'avait donc plus aucun effet et a été retiré. avis-list.jsp n'est plus un document
3842        // complet mais un fragment sans <head> ni CSS (voir ctrl-technique-lib.js côté page de
3843        // résultats) : un iframe pointant dessus depuis ici afficherait une page nue dans un cadre
3844        // de 800×720. Si un lien avis doit un jour réapparaître dans le tunnel, réutiliser le
3845        // mécanisme ajax de ctrl-technique-lib.js#loadTrustPilotInPopin, pas l'iframe.
3846
3847});
3848
3849riot.tag2('ctrl-tunnel-breadcrumb', '<div class="tunnel-breadcrumb"> <ol> <li class="validated">{dictionary.STEP_RECHERCHE}</li> <virtual if="{this.purchase.stepActif==\'COORDINATES\'}"> <li class="actif">{dictionary.STEP_COORDINATES}</li> <li class="inactif" if="{this.purchase.withOptions}">{dictionary.STEP_OPTIONS}</li> <li class="inactif" if="{this.purchase.withPayment}">{dictionary.STEP_PAYMENT}</li> <li class="inactif" if="{!this.purchase.withPayment}">{dictionary.STEP_CONFIRMATION_WITHOUT_PAYMENT}</li> </virtual> <virtual if="{this.purchase.stepActif==\'OPTIONS\'}"> <li class="validated coordinates">{dictionary.STEP_COORDINATES} <span class="btn btn-sm btn-default" onclick="{this.goToStepCoordinates}"> <i class="fas fa-pencil-alt"></i> Modifier </span> </li> <li class="actif">{dictionary.STEP_OPTIONS}</li> <li class="inactif" if="{this.purchase.withPayment}">{dictionary.STEP_PAYMENT}</li> <li class="inactif" if="{!this.purchase.withPayment}">{dictionary.STEP_CONFIRMATION_WITHOUT_PAYMENT}</li> </virtual> <virtual if="{this.purchase.stepActif==\'PAYMENT\'}"> <li class="validated coordinates">{dictionary.STEP_COORDINATES} <span class="btn btn-sm btn-default" onclick="{this.goToStepCoordinates}"> <i class="fas fa-pencil-alt"></i> Modifier </span> </li> <li class="validated options" if="{this.purchase.withOptions}">{dictionary.STEP_OPTIONS} <span class="btn btn-sm btn-default" onclick="{this.goToStepOptions}"> <i class="fas fa-pencil-alt"></i> Modifier </span> </li> <li class="actif">{dictionary.STEP_PAYMENT}</li> </virtual> <virtual if="{this.purchase.stepActif==\'CONFIRMATION_WITHOUT_PAYMENT\'}"> <li class="validated coordinates">{dictionary.STEP_COORDINATES} <span class="btn btn-sm btn-default" onclick="{this.goToStepCoordinates}"> <i class="fas fa-pencil-alt"></i> Modifier </span> </li> <li class="validated options" if="{this.purchase.withOptions}">{dictionary.STEP_OPTIONS} <span class="btn btn-sm btn-default btn-modif" onclick="{this.goToStepOptions}"> <i class="fas fa-pencil-alt"></i> Modifier </span> </li> <li class="actif">{dictionary.STEP_CONFIRMATION_WITHOUT_PAYMENT}</li> </virtual> </ol> </div>', '', '', function(opts) {
3850	var tag = this;
3851	tag.mixin(dcMixin);
3852	tag.purchase = opts.purchase;
3853
3854	tag.goToStepCoordinates = function(e){
3855		tag.parent.goBackStep('COORDINATES');
3856	}
3857
3858	tag.goToStepOptions = function(e){
3859		tag.parent.goBackStep('OPTIONS');
3860	}
3861});
3862riot.tag2('ctrl-tunnel-coupon-reduction', '<div class="tunnel-coupon-reduction"> <virtual if="{!this.hasCouponReduction() && !this.isPartenaire}"> <p class="coupon-reduction-actif"> <input id="cp-reduc{this.idSuffix}" type="checkbox" onclick="{this.toggleFieldCoupon}" 3><label for="cp-reduc{this.idSuffix}">J\'ai un code de réduction</label> </p> <p if="{this.affichageFieldCoupon}" class="cp-reduc-saisie"> <input type="text" oninput="{this.valueCouponInputChange}" class="form-control" ref="code_coupon" placeholder="Code de r&eacute;duction"> <button type="button" class="btn btn-sm btn-default {this.isBoutonAppliquerActif?\'actif\':\'disabled\'}" disabled="{!this.isBoutonAppliquerActif}" onclick="{this.appliquerCoupon}">Appliquer </button> </p> </virtual> <p if="{this.hasCouponReduction()}" class="cp-reduc-done"> <span class="code-coupon-reduction">Code de r&eacute;duction : {this.purchase.reservation.promoCode}</span> <span class="material-icons" onclick="{this.supprimerCoupon}">highlight_off</span> </p> </div>', '', '', function(opts) {
3863        var tag = this;
3864        tag.affichageFieldCoupon = false;
3865        tag.mixin(dcMixin);
3866        tag.purchase = opts.purchase;
3867        tag.idSuffix = opts.idSuffix || '';
3868        tag.context = opts.context || (tag.parent && tag.parent.opts ? tag.parent.opts.context : '');
3869        tag.url = opts.url || (tag.parent && tag.parent.opts ? tag.parent.opts.url : '');
3870        tag.isPartenaire = tag.purchase.reservation.fromPartenaire;
3871        tag.isBoutonAppliquerActif = false;
3872
3873        tag.hasCouponReduction = function () {
3874
3875            return tag.purchase.reservation.promoCode != undefined;
3876        }
3877
3878        tag.toggleFieldCoupon = function () {
3879            tag.affichageFieldCoupon = !tag.affichageFieldCoupon;
3880        }
3881
3882        tag.valueCouponInputChange = function () {
3883
3884            var codeCoupon = $(tag.refs.code_coupon).val();
3885
3886            if (!!codeCoupon && codeCoupon.length >= 5) {
3887                tag.isBoutonAppliquerActif = true;
3888            } else {
3889                tag.isBoutonAppliquerActif = false;
3890            }
3891
3892        }
3893
3894        tag.appliquerCoupon = function () {
3895
3896            var codeCoupon = $(tag.refs.code_coupon).val();
3897
3898            showLoading(true);
3899            $.ajax({
3900                url: tag.context + tag.url + '?action=appliquerCouponReduction|' + codeCoupon,
3901                method: 'post',
3902                cache: false
3903
3904            }).done(function (json) {
3905                showLoading(false);
3906
3907                if (json.appliquerCouponReduction == 'formValidateException') {
3908
3909                    myAlert(json.erreur, "Erreur");
3910
3911                } else if (json.appliquerCouponReduction == 'ko') {
3912
3913                    myAlert(json.erreur, "Erreur");
3914
3915                } else if (json.appliquerCouponReduction == 'done') {
3916
3917                    var previousStep = window.riotTagCtrlTunnel2021 ? window.riotTagCtrlTunnel2021.purchase.stepActif : null;
3918                    tag.purchase = json.purchase;
3919                    if (tag.parent && tag.parent.purchase) {
3920                        tag.parent.purchase = json.purchase;
3921                    }
3922                    if (tag.parent && tag.parent.updatePurchase) {
3923                        tag.parent.updatePurchase();
3924                    }
3925                    if (opts.onUpdate) {
3926                        opts.onUpdate(json.purchase);
3927                    }
3928                    tag.handleStepChange(previousStep, json.purchase);
3929                    console.log("[ctrl-tunnel-coupon-reduction] updating parent tag", tag.parent);
3930                }
3931
3932            }).fail(function (e, m) {
3933                showLoading(false);
3934                myAlert("Erreur " + e.status, e.statusText)
3935            });
3936        }
3937
3938        tag.handleStepChange = function (previousStep, newPurchase) {
3939            var tunnelTag = window.riotTagCtrlTunnel2021;
3940            if (!tunnelTag || !previousStep) return;
3941            if (previousStep === newPurchase.stepActif) return;
3942            tunnelTag.purchase = newPurchase;
3943            tunnelTag.update();
3944            if (newPurchase.stepActif === 'CONFIRMATION_WITHOUT_PAYMENT') {
3945                tunnelTag.loadConfirmationWithoutPayment();
3946            } else if (newPurchase.stepActif === 'PAYMENT') {
3947                tunnelTag.loadFormPayment();
3948            }
3949        }
3950
3951        tag.supprimerCoupon = function () {
3952
3953            showLoading(true);
3954            $.ajax({
3955                url: tag.context + tag.url + '?action=supprimerCouponReduction',
3956                method: 'post',
3957                cache: false
3958
3959            }).done(function (json) {
3960
3961                showLoading(false);
3962
3963                if (json.suppressionCouponReduction == 'formValidateException') {
3964                    myAlert(json.erreur, "Erreur");
3965                } else if (json.suppressionCouponReduction == 'ko') {
3966
3967                    myAlert(json.erreur, "Erreur");
3968                } else if (json.suppressionCouponReduction == 'done') {
3969
3970                    var previousStep = window.riotTagCtrlTunnel2021 ? window.riotTagCtrlTunnel2021.purchase.stepActif : null;
3971                    tag.purchase = json.purchase;
3972                    if (tag.parent && tag.parent.purchase) {
3973                        tag.parent.purchase = json.purchase;
3974                    }
3975                    tag.affichageFieldCoupon = false;
3976                    if (tag.parent && tag.parent.update) {
3977                        tag.parent.update();
3978                    }
3979                    if (tag.parent && tag.parent.updatePurchase) {
3980                        tag.parent.updatePurchase();
3981                    }
3982                    if (opts.onUpdate) {
3983                        opts.onUpdate(json.purchase);
3984                    }
3985                    tag.handleStepChange(previousStep, json.purchase);
3986                    console.log("[ctrl-tunnel-coupon-reduction] updating parent tag", tag.parent);
3987                }
3988
3989            }).fail(function (e, m) {
3990                showLoading(false);
3991                myAlert("Erreur " + e.status, e.statusText)
3992            });
3993        }
3994
3995});
3996riot.tag2('ctrl-tunnel-recap', '<div class="cteasy-tunnel-recap"> <div class="recap cteasy-controle-technique-ct-row-header"> <virtual if="{this.purchase.stepActif == \'OPTIONS\' || this.purchase.stepActif == \'COORDINATES\'}"> <span class="titre">Détails de votre réservation</span> </virtual> <virtual if="{this.purchase.stepActif == \'PAYMENT\'}"> <span class="titre">Votre réservation</span> </virtual> </div> <div class="recap recap-synthese"> <div class="left-pane"> <p style="text-transform: capitalize; font-weight: bold; font-size: 18px;">{this.purchase.reservationI18nDate}</p> <virtual if="{this.purchase.reservation.firstName != null}"> <p style="text-transform: capitalize;">{this.purchase.reservation.salutationString} {this.purchase.reservation.firstName} {this.purchase.reservation.lastName}</p> </virtual> <p class="recap-type">{this.purchase.reservationI18nVehicleType} - {this.purchase.reservationI18nEnergyType}</p> </div> <virtual if="{this.purchase.stepActif==\'PAYMENT\'}"> <div class="modif"> <a class="btn btn-sm btn-default btn-modif" onclick="{this.goBackPreviousStep}"><span class="material-icons" data-icon="lock">edit</span></a> </div> </virtual> </div> <div class="recap recap-chrono"> <div class="recap-chrono-wrapper"> <ul class="funnel-timeline"> <li class="funnel-timeline-item timeline-selected"><span>Heure de pr&eacute;sentation</span> <span class="pull-right">{this.purchase.reservationHeurePresentation}</span></li> <li class="funnel-timeline-item"><span>Heure de rendez-vous</span> <span class="pull-right">{this.purchase.reservationHeureRendezVous}</span></li> <li class="funnel-timeline-item"><span>Heure de fin de contr&ocirc;le</span> <span class="pull-right">{this.purchase.reservationHeureFinDeControle}</span></li> </ul> </div> </div> <virtual if="{this.purchase.stepActif != \'OPTIONS\' && !this.purchase.reservation.modifyAppointment && !this.purchase.modificationDeadlinePassed}"> <virtual if="{!this.purchase.reservationInMoreThan14Days}"> <div class="recap recap-gestion"> <p class="title font-weight-bold"> <span>Rendez-vous non modifiable et non remboursable</span> </p> </div> </virtual> <virtual if="{this.purchase.reservationInMoreThan14Days}"> <div class="recap recap-gestion"> <p class="title"> <span><span class="font-weight-bold">Annulation gratuite</span><br>avant le <span class="font-weight-bold">{this.purchase.reservationDateAnnulation}</span></span> </p> <p style="margin-top: 3px;"><a href="#" data-toggle="modal" data-target="#popup-gestion-reservation" style="font-weight: normal; text-decoration: underline;">Consultez les conditions complètes</a></p> </div> </virtual> </virtual> <virtual if="{this.purchase.stepActif != \'OPTIONS\' && this.purchase.reservation.modifyAppointment}"> <div class="recap recap-gestion"> <p class="title"> <span>Annulation ou modification<br> avant le <span class="font-weight-bold">{this.purchase.reservationDateHeureAnnulation}</span> </span> </p> <p style="margin-top: 3px;"><a href="#" data-toggle="modal" data-target="#popup-gestion-reservation" style="font-weight: normal; text-decoration: underline;">Consultez les conditions complètes</a></p> </div> </virtual> <virtual if="{this.purchase.modificationDeadlinePassed && !this.purchase.reservationInMoreThan14Days}"> <div class="recap recap-gestion recap-gestion-non-remboursable"> <p class="title" style="color: black;"> <span class="font-weight-bold">Rendez-vous non annulable, non remboursable</span> </p> <p style="margin-top: 3px;"><a href="#" data-toggle="modal" data-target="#popup-gestion-reservation" style="font-weight: normal; text-decoration: underline;">Consultez les conditions complètes</a></p> <p style="margin-top: 3px;"><a href="{this.purchase.urlModificationRdv}" style="font-weight: normal; text-decoration: underline;">Choisir un autre rendez-vous</a></p> </div> </virtual> <div class="recap recap-centre"> <p class="recap-important">Adresse du centre technique</p> <p class="capitalize">{this.purchase.reservationCentreNameCteasy}</p> <p class="capitalize">{this.purchase.location.name}</p> <p class="addr-centre-ct">{this.purchase.reservationCentreFullAddress}</p> <virtual if="{this.purchase.reservationCentreNbNote > 0 && false}"> <p class="centre-note"><u>Noté : <span class="note">{this.purchase.reservationCentreAppreciation}</span></u> <b>{this.purchase.reservationCentreNote}/5</b></p> </virtual> <div class="list-infos-centre" style="display: none;"> Services du centre : <virtual if="{this.purchase.location.disableAccessAvailable != null && this.purchase.location.disableAccessAvailable == true}"> <div class="detail-infos-centre"> <span class="material-icons" data-icon="accessible">accessible</span> </div> </virtual> <virtual if="{this.purchase.location.coffeeInformation != null}"> <div class="detail-infos-centre"> <span class="material-icons" data-icon="local_cafe">local_cafe</span> </div> </virtual> <virtual if="{this.purchase.location.freeWifiAvailable != null && this.purchase.location.freeWifiAvailable == true}"> <div class="detail-infos-centre"> <span class="material-icons" data-icon="wifi">wifi</span> </div> </virtual> <virtual if="{this.purchase.location.franchise != null}"> <div class="detail-infos-centre"> <span class="material-icons" data-icon="tablet_mac">tablet_mac</span> </div> <div class="detail-infos-centre"> <span class="material-icons" data-icon="spa">spa</span> </div> </virtual> <virtual if="{this.purchase.location.airConditionnerAvailable != null && this.purchase.location.airConditionnerAvailable == true}"> <div class="detail-infos-centre"> <span class="material-icons" data-icon="ac_unit">ac_unit</span> </div> </virtual> </div> </div> <div class="recap recap-engagement" style="display: none;"> <p class="recap-important">Nos engagements</p> <p><i class="fa valid fa-check"></i>Centre agréé par le Ministère des Transports</p> <p><i class="fa valid fa-check"></i>Paiement sécurisé avec 3D Secure</p> <p><i class="fa valid fa-check"></i>Confirmation de réservation immédiate par e-mail</p> <p><i class="fa valid fa-check"></i>SMS de rappel 24H avant votre RDV</p> </div> <div class="recap recap-tarif"> <p class="recap-important">Détail du prix</p> <p><b>Tarif du centre</b><span class="pull-right" style="text-decoration: line-through; text-decoration-thickness: 1px; text-decoration-color: black;"><b>{this.purchase.reservation.basePriceLocationFormatted}&euro;</b></span></p> <p><b>Tarif CTeasy</b><span class="pull-right"><b>{this.purchase.reservationPrixAvecFraisDeServiceInclus}&euro;</b></span></p> <p if="{this.purchase.reservation.modifyAppointment}"> <span class="option-modify-appointment">Option modification RDV</span> <span class="pull-right">{this.purchase.montantOptionModifAppointmentPrix}&euro;</span> </p> <p if="{this.purchase.reservation.reservationInsurance}"> <span class="option-contre-visite">Option contre-visite</span> <virtual if="{this.purchase.reservation.cvInsuranceAmount == 0}"> <span class="pull-right" style="text-decoration: line-through; text-decoration-thickness: 1px; text-decoration-color: black;">{this.purchase.montantOptionContreVisitePrix}&euro;</span> </virtual> <virtual if="{this.purchase.reservation.cvInsuranceAmount > 0}"> <span class="pull-right">{this.purchase.montantOptionContreVisitePrix}&euro;</span> </virtual> </span> <p if="{this.purchase.avoir}"> <span>Votre avoir</span> <span class="pull-right font-weight-bold">-{this.purchase.avoir}&euro;</span> </p> <ctrl-tunnel-coupon-reduction purchase="{this.purchase}" idsuffix="-recap"></ctrl-tunnel-coupon-reduction> <p class="coupon-reduction" if="{this.purchase.reservation.promoCode}"> <span class="option-contre-visite">Code de r&eacute;duction</span> <span class="pull-right">-{this.purchase.reservationRemiseCouponReduction}&euro;</span> </p> </div> <div class="recap recap-tarif-final"> <div class="tarif-wrapper"> <virtual if="{this.purchase.stepActif == \'OPTIONS\'}"> <span>PRIX TOTAL</span> </virtual> <virtual if="{this.purchase.stepActif != \'OPTIONS\'}"> <span>PRIX FINAL</span> </virtual> <span class="pull-right">{this.purchase.reservationPrixFinal}&euro;</span> </div> </div> <p class="recap-taxes">Toutes taxes et redevances comprises</p> <div class="recap recap-economie cteasy-tunnel-recap"> <p>Félicitations, vous économisez <b>{this.purchase.reservationPrixEconomie}&euro;</b></p> </div> </div> <virtual if="{this.purchase.stepActif == \'OPTIONS\'}"> <div class="recap recap-moyens-paiement cteasy-tunnel-recap"> <p>Modalités de paiement admises :</p> <p>Visa, Mastercard, Maestro, Google Pay, Apple Pay, Paypal</p> </div> </virtual>', '', 'class="ctrl-tunnel-recap"', function(opts) {
3997        var tag = this;
3998        tag.mixin(dcMixin);
3999        tag.purchase = tag.opts.purchase;
4000        console.debug("tag.purchase:", tag.purchase);
4001
4002        tag.on('mount', function () {
4003            activePopovers();
4004        });
4005
4006        tag.goBackPreviousStep = function () {
4007            window.riotTagCtrlTunnel2021.goBackCoordinates()
4008            tag.purchase.stepActif = 'COORDINATES';
4009            $('#tunnelRecapPopup').modal('hide')
4010            tag.update();
4011        };
4012
4013        tag.updatePurchase = function () {
4014            console.log("updating tag ctrl-tunnel-recap");
4015            tag.parent.purchase = tag.purchase
4016            tag.parent.update();
4017            console.log("[ctrl-tunnel-recap] updating parent tag", tag.parent);
4018        }
4019
4020});
4021riot.tag2('ctrl-tunnelconfirmation', '<div class="row ctrl-tunnel-wrapper"> <div class="tunnel-wrapper-full-width-center tunnel-confirmation"> <div class="cteasy-controle-technique-ct-row-header"> <span class="material-icons">check_circle</span> <h1>Rendez-vous confirmé</h1> <div class="sous-titre">Votre paiement a bien été pris en compte</div> <div class="sous-titre">Nous vous avons envoyé une confirmation à votre adresse e-mail <i>{this.reservation.email}</i></div> </div> <div class="confirmation-reservation-ct"> <p>Merci {this.reservation.salutationString} {this.reservation.firstName} {this.reservation.lastName} ! Votre réservation CTeasy {this.purchase.location.nameCapitalized} est confirmée.</p> <ul> <li><span class="material-icons">check</span> Nous vous attendons le <b>{this.purchase.reservationI18nDate} à {this.purchase.reservationHeurePresentation}</b></li> <li><span class="material-icons">check</span> L\'adresse: <b>{this.purchase.reservationCentreName} {this.purchase.reservationCentreFullAddress}</b></li> </ul> </div> <table class="table table-bordered"> <thead> <tr> <th colspan="3" class="header"> <div> <span><strong>DÉTAILS DE VOTRE RÉSERVATION</strong></span> <span>Nº Réservation: <b>{this.reservation.reservationCode}</b></span> </div> </th> </tr> </thead> <tbody> <tr> <th scope="row">Heure de présentation <u>obligatoire</u></th> <td> <div> <span class="material-icons">calendar_month</span>{this.purchase.reservationI18nDate} </div> </td> <td> <div> <span class="material-icons">schedule</span> {this.purchase.reservationHeurePresentation} </div> </td> </tr> <tr> <th scope="row">Heure de RDV du CT</th> <td> <div> <span class="material-icons">calendar_month</span>{this.purchase.reservationI18nDate} </div> </td> <td> <div> <span class="material-icons">schedule</span> {this.purchase.reservationHeureRendezVous} </div> </td> </tr> <tr> <th scope="row">Heure de fin de contrôle</th> <td> <div> Je récupère ma voiture à : </div> </td> <td> <div> <span class="material-icons">schedule</span> {this.purchase.reservationHeureFinDeControle} </div> </td> </tr> <tr> <th scope="row">Adresse du centre</th> <td colspan="2"> <div class="address"> <p> <span class="material-icons">location_city</span> <span>{this.purchase.reservationCentreName}</span> </p> <p class="capitalize" style="padding-left:20px;">{this.purchase.location.nameCapitalized}</p> <p class="addr-centre-ct" style="padding-left:29px;">{this.purchase.reservationCentreFullAddress}</p> </div> </td> </tr> <tr> <th scope="row">Comment être à l\'heure à mon rdv ?</th> <td colspan="2"> <div> <span class="material-icons">map</span> <a href="{this.opts.linkGoogleMaps}" target="_blank" data-trk="booking_google_map">Google Maps</a> </div> </td> </tr> <tr> <th scope="row">Comment ne pas oublier mon rdv ?</th> <td colspan="2"> <div> <ul> <li> <span class="material-icons">calendar_month</span> <a href="{this.opts.linkCalendarICS}" download target="_blank" data-trk="add_booking_to_calendar">Outlook / Apple Calendar</a> </li> <li> <img riot-src="{this.opts.context}/pict/google_calendar_icon.png" alt="google calendar" width="25px"> <a href="{this.opts.linkGoogleCalendar}" target="_blank" data-trk="add_booking_to_google_calendar">Ajouter à Google Calendar</a></li> </ul> </div> </td> </tr> <tr> <th scope="row">Confirmation de réservation</th> <td colspan="2"> <a href="{this.opts.linkDownloadCoupon}" class="btn btn-primary btn-click-to-action" data-trk="download_booking_pdf"> Télécharger votre réservation </a> </td> </tr> <tr> <th scope="row">Mon véhicule</th> <td> <div> <span class="material-icons">local_gas_station</span> <span>{this.purchase.reservationI18nEnergyType}</span> </div> </td> <td> <div> <span class="material-icons">directions_car</span> <span>{this.purchase.reservation.makeName} {this.purchase.reservation.modelName}</span> </div> </td> </tr> </tbody> </table> <div class="earlier-slot-banner" if="{this.opts.earlierSlotEligible && !this.earlierSlotConsentDone}" style="background:linear-gradient(135deg,#0f766e,#0891b2);border-radius:12px;padding:28px 24px;margin:28px 0;color:#fff;
4021box-shadow:0 6px 20px rgba(8,145,178,0.30);text-align:center;"> <div style="display:flex;align-items:center;justify-content:center;gap:10px;font-size:23px;font-weight:bold;margin-bottom:10px;"> <span class="material-icons" style="font-size:32px;">access_time</span> <span>Un créneau plus tôt vous arrange ?</span> </div> <p style="margin:0 auto 20px;font-size:15px;line-height:1.5;max-width:560px;opacity:0.97;">Soyez prévenu par SMS et email dès qu\'une place se libère avant votre rendez-vous.</p> <button class="btn" onclick="{this.activateEarlierSlotConsent}" style="background:#fff;color:#0f766e;font-weight:bold;border:none;border-radius:8px;padding:14px 36px;font-size:17px;cursor:pointer;">Oui, prévenez-moi !</button> </div> <div class="earlier-slot-banner earlier-slot-banner-confirmed" if="{this.earlierSlotConsentDone}" style="background:#e8f5e9;border:2px solid #66bb6a;border-radius:12px;padding:22px;margin:28px 0;color:#2e7d32;text-align:center;font-weight:bold;font-size:16px;display:flex;align-items:center;justify-content:center;gap:10px;"> <span class="material-icons" style="font-size:30px;">check_circle</span> <span>C\'est noté ! On vous préviendra par SMS et email si un créneau se libère plus tôt.</span> </div> <table class="table table-bordered"> <thead> <tr> <th colspan="3" class="header"> <div> <span><strong>GÉRER VOTRE RÉSERVATION</strong></span> </div> </th> </tr> </thead> <tbody> <tr> <th scope="row">Accéder à mon compte</th> <td colspan="2"> <span>Vous pouvez vous connecter à votre compte CTEasy en cliquant sur le lien suivant. <br>S\'il s\'agit de votre première connexion ou si vous avez perdu votre mot de passe, vous pourrez demander une initialisation du mot de passe.</span> <a href="{this.opts.linkModification}" class="btn btn-primary btn-click-to-action" style="background-color: #ffb700" target="_blank" data-trk="modify_booking">Accéder à mon compte</a> </td> </tr> </tbody> </table> <table class="table table-bordered"> <thead> <tr> <th colspan="3" class="header"> <div> <span><strong>PAIEMENT ACCEPTÉ</strong></span> </div> </th> </tr> </thead> <tbody> <tr> <td>Tarif du centre</td> <td colspan="2"><span style="text-decoration: line-through; text-decoration-thickness: 1px; text-decoration-color: black;">{this.reservationPaymentBreakdown.basePriceLocation}&euro;</span></td> </tr> <tr> <td><b>Montant réservation</b></td> <td colspan="2">{this.reservationPaymentBreakdown.reservationPriceWithMargeAndFees}&euro;</td> </tr> <tr if="{this.reservationPaymentBreakdown.optionContreVisite.active}"> <td>Option contre-visite</td> <td colspan="2">{this.reservationPaymentBreakdown.optionContreVisite.price}&euro;</td> </tr> <tr if="{this.reservationPaymentBreakdown.optionModifyAppointment.active}"> <td>Option modification RDV</td> <td colspan="2">{this.reservationPaymentBreakdown.optionModifyAppointment.price}&euro;</td> </tr> <tr if="{this.reservationPaymentBreakdown.promoCode != null}"> <td>Code de réduction</td> <td colspan="2">{this.reservationPaymentBreakdown.promoCode}</td> </tr> <tr> <td><b>Montant final</b></td> <td colspan="2"><b>{this.reservationPaymentBreakdown.reservationPriceTotal}&euro;</b></td> </tr> <tr> <td></td> <td colspan="2">Félicitations, vous venez d\'économiser <b>{this.reservationPaymentBreakdown.reduction}&euro;</b></td> </tr> <tr> <th scope="row">Ma facture</th> <td colspan="2"> <a href="{this.opts.linkDownloadFacture}" class="btn btn-primary btn-click-to-action" data-trk="booking_download_invoice"> Télécharger la facture </a> </td> </tr> </tbody> </table> <table class="table table-bordered" if="{!this.isPartenaire}"> <thead> <tr> <th class="header partenaire"> <div class="toggle-container"> <span>Vous avez une démarche carte grise à effectuer ? Faites confiance à notre partenaire.</span> <div class="logo-toggle"> <img src="https://www.carte-grise-france.fr/images/logo.webp" alt="Carte Grise France" class="logo-cgf"> </div> </div> </th> </tr> </thead> <tbody> <tr> <td class="iframe-carte-grise-france"> <iframe src="https://www.carte-grise-france.fr/partenaire/ct-easy/immatriculation/" width="100%" frameborder="0" loading="lazy"></iframe> </td> </tr> </tbody> </table> <table class="table table-bordered"> <thead> <tr> <th class="header"> <span><strong>PRÉPARATION REQUISE</strong></span> </th> </thead> <tbody> <tr> <td> <ul> <li>Soyez au centre impérativement à l’heure de présentation indiquée</li> <li>Présentez-vous avec votre carte grise originale, aucune photocopie n’est acceptée</li> <li>Attention, le certificat d’immatriculation au format numérique n’est pas accepté pour passer un contrôle technique. <br> Conformément à la réglementation en vigueur, vous devez obligatoirement présenter le certificat d’immatriculation original (format papier) lors de votre rendez-vous. </li> <li>Présentez-vous avec votre email de confirmation de réservation imprimé ou sur présentation via le téléphone</li> </ul> </td> </tr> <tr if="{this.purchase.location.commentaire != null}
4021" style="border: dashed #FF6600;"> <td>{this.purchase.location.commentaire}</td> </tr> <tr if="{this.opts.extraAccessImagePath != \'null\'}"> <td> <div class="extra-access-images"> <span class="material-icons">help</span> <strong>Accès au centre de contrôle technique :</strong> </div> </td> </tr> <tr if="{this.opts.extraAccessImagePath != \'null\'}"> <td> <img riot-src="{this.opts.extraAccessImagePath}" alt="Accès au centre de contrôle technique" width="100%"> </td> </tr> </tbody> </table> <table class="table table-bordered"> <thead> <tr> <th class="header"> <span><strong>EN CAS D\'URGENCE</strong></span> </th> </thead> <tbody> <tr> <td> <div> <span class="material-icons">check</span> <strong>Pendant les horaires du service client du Lundi au Vendredi de 9h à 17h</strong> </div> <ul> <li>Contactez-nous via le Tchat en ligne</li> <li>Contactez-nous via le centre d\'aide sur le site Cteasy.com (Via le formulaire de contact)</li> </ul> </td> </tr> <tr> <td> <div> <span class="material-icons">check</span> <strong>En dehors des horaires du service client</strong> </div> <ul> <li>Contacter votre centre de contrôle technique <span class="capitalize font-weight-bold">{this.purchase.location.name}</span> au <b>{this.purchase.location.phoneNumberToContact}</b></li> </ul> </td> </tr> </tbody> </table> </div> </div>', 'ctrl-tunnelconfirmation,[data-is="ctrl-tunnelconfirmation"]{ }', '', function(opts) {
4022        var tag = this;
4023        tag.mixin(dcMixin);
4024        tag.purchase = opts.purchase;
4025        tag.reservation = tag.purchase.reservation;
4026        tag.isPartenaire = tag.purchase.isPartenaire;
4027        tag.reservationPaymentBreakdown = JSON.parse(opts.reservationPaymentBreakdown);
4028        tag.earlierSlotConsentDone = false;
4029
4030        tag.steps = {
4031            COORDINATES: 0,
4032            OPTION: 1,
4033            PAYMENT: 2,
4034            CONFIRMATION_WITHOUT_PAYMENT: 3,
4035            CONFIRMATION: 4,
4036        };
4037
4038        tag.activateEarlierSlotConsent = function (e) {
4039            $.ajax({
4040                url: tag.opts.context + tag.opts.urlconfirm + '?action=activateEarlierSlotConsent',
4041                method: 'post',
4042                cache: false
4043            }).done(function (json) {
4044                if (json && json.ok) {
4045                    tag.earlierSlotConsentDone = true;
4046                    tag.update();
4047                }
4048            }).fail(function () {
4049                // fail silently — do not break the confirmation page
4050            });
4051        }
4052
4053        tag.refreshPurchase = function (e) {
4054            window._trk && window._trk({
4055                event: 'ux_event',
4056                event_category: 'tunnel',
4057                event_action: 'confirmation_refresh'
4058            });
4059            $.ajax({
4060                url: tag.opts.context + tag.opts.urlconfirm + '?action=refreshPurchase',
4061                method: 'post',
4062                cache: false
4063            }).done(function (json) {
4064
4065                // Session expired: no purchase to refresh, keep the page rendered by the server.
4066                if (!json || !json.purchase) {
4067                    return;
4068                }
4069                tag.purchase = json.purchase;
4070                window.funnel = {
4071                    data: json,
4072                    steps: tag.steps,
4073                };
4074
4075                if (tag.purchase.callSEOConfirm == true) {
4076                    $(window).trigger('funnel:changed', json);
4077                } else {
4078                }
4079
4080                window._trk && window._trk({
4081                    event: 'ux_event',
4082                    event_category: 'tunnel',
4083                    event_action: 'confirmation_refresh_done'
4084                });
4085            }).fail(function (e, m) {
4086                showLoading(false);
4087                myAlert("Erreur " + e.status, e.statusText);
4088                window._trk && window._trk({
4089                    event: 'ux_event',
4090                    event_category: 'tunnel',
4091                    event_action: 'confirmation_refresh_error'
4092                });
4093            });
4094        }
4095        tag.refreshPurchase();
4096
4097});
4098riot.tag2('ctrl-vehicle-energy', '<div ref="main" class="dropdown has-feedback"> <div class="zone-sensitive" onclick="{this.searchInputFocus}" data-toggle="dropdown"> <input type="text" ref="txt" class="form-control {checked: selectedItem && selectedItem.code}" placeholder="{dictionary.TCC_SEARCH_PANEL_LABEL_ENERGY_TYPE}" autocomplete="off" readonly="readonly" onfocus="{this.searchInputFocus}
4098" riot-value="{this.getSelectedItemName()}"> <span class="material-icons">local_gas_station</span> <i class="fas fa-chevron-down"></i> </div> <div ref="dropdown_menu" class="dropdown-menu" aria-labelledby="searchInput"> <ul class="vehicle-energy-list"> <li each="{items}" class="{selected: this.selectedItem && code == this.selectedItem.code} {code}" onclick="{onselect(this)}"> <span class="energy-name">{name}</span> </li> </ul> </div> </div>', '', '', function(opts) {
4099      var tag = this;
4100      tag.mixin(dcMixin);
4101      tag.loading = true;
4102      tag.selectedItem = null;
4103      tag.items = tag.opts.items;
4104
4105      tag.searchInputFocus = function (e) {
4106          if(e && e.type == 'focus' && !$(tag.refs.dropdown_menu).hasClass('show')) {
4107            console.log('Energy Open', e);
4108            window._trk && window._trk({
4109              event: 'ux_event',
4110              event_category: 'search',
4111              event_action: 'Energy Type Open',
4112              event_label: tag.getSelectedItemName()
4113            });
4114          }
4115          tag.trigger('onDropdownOpen');
4116      };
4117
4118      tag.dropdownOpen = function () {
4119          $(tag.refs.dropdown_menu).addClass('show');
4120      };
4121
4122      tag.dropdownClose = function () {
4123      	$(tag.refs.dropdown_menu).removeClass('show');
4124      };
4125
4126      tag.getSelectedItemName = function () {
4127          if (tag.selectedItem != null)
4128              return tag.selectedItem.name;
4129          else
4130              return '';
4131      }
4132
4133      tag.onselect = function (item) {
4134          return function (e) {
4135              tag.select(item.code, true);
4136          };
4137      }
4138
4139      tag.select = function (value, withAutomaticSearch) {
4140          var oldSelection = tag.selectedItem;
4141          var result = $.grep(tag.items, function (item) {
4142              return item.code == value;
4143          });
4144
4145          if (result.length > 0) {
4146              tag.selectedItem = result[0];
4147          }
4148          else {
4149              tag.selectedItem = null;
4150          }
4151          tag.dropdownClose();
4152          tag.trigger('onchanged', tag.selectedItem, oldSelection, withAutomaticSearch);
4153          tag.update();
4154          if(withAutomaticSearch) {
4155            window._trk && window._trk({
4156              event: 'ux_event',
4157              event_category: 'search',
4158              event_action: 'Energy Type Select',
4159              event_label: tag.getSelectedItemName()
4160            });
4161          }
4162      };
4163
4164      tag.on('mount', function () {
4165          tag.select(tag.opts.type, false);
4166      });
4167
4168});
4169
4170riot.tag2('ctrl-vehicle-type', '<div ref="main" class="dropdown has-feedback"> <div class="zone-sensitive" onclick="{this.searchInputFocus}" data-toggle="dropdown"> <input type="text" ref="txt" class="form-control {checked: selectedItem && selectedItem.code}" placeholder="{dictionary.TCC_SEARCH_PANEL_LABEL_VEHICLE_TYPE}" autocomplete="off" readonly="readonly" onfocus="{this.searchInputFocus}" riot-value="{this.getSelectedItemName()}"> <span class="material-icons">directions_car_filled</span> <i class="fas fa-chevron-down"></i> </div> <div ref="dropdown_menu" class="dropdown-menu" aria-labelledby="searchInput"> <ul class="vehicle-type-list"> <li each="{items}" class="{selected: this.selectedItem && code == this.selectedItem.code}" onclick="{onselect(this)}"> <span class="vehicle-type-name">{name}</span> </li> <li class="disabled" style="opacity: 0.5; color: #999; cursor: not-allowed;"> <span class="vehicle-type-name" style="text-decoration: line-through;text-decoration-color: #000;text-decoration-thickness: 2px;">Moto (2 roues)</span> </li> </ul> <hr> <div class="padding-small"> <span class="help"> <ctrl-html content="{dictionary.TCC_SEARCH_PANEL_VEHICLE_TYPE_HELP}"></ctrl-html> </span> </div> </div> </div>', 'ctrl-vehicle-type,[data-is="ctrl-vehicle-type"]{ color:black; } ctrl-vehicle-type li > img,[data-is="ctrl-vehicle-type"] li > img{ filter: invert(100%); flex: 0 0 auto; width: 24px; }', '', function(opts) {
4171      var tag = this;
4172      tag.mixin(dcMixin);
4173      tag.selectedItem = null;
4174      tag.loading = true;
4175      tag.items = tag.opts.items;
4176
4177      tag.searchInputFocus = function (e) {
4178          if(e && e.type == 'focus' && !$(tag.refs.dropdown_menu).hasClass('show')) {
4179            window._trk && window._trk({
4180              event: 'ux_event',
4181              event_category: 'search',
4182              event_action: 'Vehicle Type Open',
4183              event_label: tag.getSelectedItemName()
4184            });
4185          }
4186          tag.trigger('onDropdownOpen');
4187      };
4188
4189      tag.dropdownOpen = function () {
4190          $(tag.refs.dropdown_menu).addClass('show');
4191      };
4192
4193      tag.dropdownClose = function () {
4194      	$(tag.refs.dropdown_menu).removeClass('show');
4195      };
4196
4197      tag.getSelectedItemName = function () {
4198          if (tag.selectedItem != null)
4199              return tag.selectedItem.name;
4200          else
4201              return '';
4202      }
4203
4204      tag.onselect = function (item) {
4205          return function (e) {
4206              tag.select(item.code, true);
4207          };
4208      }
4209
4210      tag.select = function (code, withAutomaticSearch) {
4211          var oldSelection = tag.selectedItem;
4212          var result = $.grep(tag.items, function (item) {
4213              return item.code == code;
4214          });
4215
4216          if (result.length > 0) {
4217              tag.selectedItem = result[0];
4218          }
4219          else {
4220              tag.selectedItem = null;
4221          }
4222
4223          tag.dropdownClose();
4224          tag.update();
4225          tag.trigger('onchanged', tag.selectedItem, oldSelection, withAutomaticSearch);
4226          if(withAutomaticSearch) {
4227            window._trk && window._trk({
4228              event: 'ux_event',
4229              event_category: 'search',
4230              event_action: 'Vehicle Type Select',
4231                event_label: tag.getSelectedItemName()
4232            });
4233          }
4234      };
4235
4236      tag.on('mount', function () {
4237          tag.select(tag.opts.type, false);
4238      });
4239
4240});
4241
4242
4243 var proxy = window.Proxy || function(dic){ return dic; };var dictionary = new proxy({
4244  "FUNNEL_SAVING": "Vous venez d'économiser {0} !",
4245  "FUNNEL_MY_VEHICLE_INFO": "Les informations ( à remplir ci dessous) sont nécessaires pour votre centre de contrôle technique.",
4246  "FUNNEL_OWNER_PHONE_NOTE": "Un SMS de rappel vous sera envoyé à ce numéro 24H avant votre rendez-vous.",
4247  "PHONE_CUSTOMER_SERVICE": "05 64 11 06 64",
4248  "FUNNEL_AUTH_CONTINUE_BUTTON": "Continuer",
4249  "FUNNEL_MY_SERVICES_OPTION_ACV_LONG_LOCKED": "En raison de l’application de la nouvelle réglementation du contrôle technique en vigueur depuis le 21 mai 2018, nous sommes dans l’obligation de suspendre l’option contre-visite. Nous travaillons actuellement avec nos centres partenaires pour vous proposer une nouvelle offre de contre-visite.",
4250 // "FUNNEL_CVG_ACCEPT": "En cliquant sur \"Confirmer l'achat\", je reconnais avoir pris connaissance des <a href=\"/cteasy/fr/a-propos/cgv/\" target=\"_blank\">Conditions Générales de Vente</a>, des <a href=\"/cteasy/fr/a-propos/conditions-generales-utilisation/\" target=\"_blank\">Conditions Générales d’Utilisation</a>,  de la <a href=\"/cteasy/fr/a-propos/politique-confidentialite-cookies/\" target=\"_blank\">Politique de confidentialité</a> du site cteasy.com et je les accepte.",
4251  "FUNNEL_CONFIRMATION_CODE": "Votre paiement a bien été confirmé. Voici votre code de réservation.",
4252  "FUNNEL_CONFIRMATION_INSTRUCTION": "<p>Merci de vous présenter impérativement le jour de votre contrôle avec <strong>votre coupon de réservation imprimé</strong>. <br/>N'oubliez pas de télécharger maintenant votre coupon en utilisant le lien ci-dessous.<br/>Vous pouvez aussi l'imprimer depuis la rubrique \"Mon compte\".</p>",
4253  "FUNNEL_DOWNLOAD_BUTTON": "Télécharger",
4254  "FUNNEL_SELECT_PRINCING": "Sélectionner un tarif",
4255  "FUNNEL_MY_ACCOUNT": "Mon compte",
4256  "FUNNEL_ASSISTANCE": "Numéro d'assistance réservation",
4257  "FUNNEL_ASSISTANCE_HORAIRES": "Du lundi au vendredi : 9h-17h",
4258  "FUNNEL_DERNIERE_ETAPE":"C'est déjà la dernière étape !",
4259  "FUNNEL_RESERVATION_SECURISEE":"Réservation sécurisée (cela prend moins de 2 minutes)",
4260  "FUNNEL_RESERVEZ_MSG":"Garantissez vous ce super tarif<br/>Confirmez votre réservation aujourd'hui car les prix peuvent augmenter !",
4261  "FUNNEL_RESERVEZ_MSG_PAIEMENT":"Ce tarif sera rapidement épuisé.<br/>Il est garanti 15 minutes. Réservez vite !",
4262  "FUNNEL_RESERVEZ_MSG_PAIEMENT_2":"Tranquilité d'esprit : <br />Une fois votre réservation payée, vous recevrez un mail instantané de confirmation de paiement à {0} et un sms de rappel 24H avant votre rendez-vous.",
4263  "STEP_RECHERCHE":"Recherche",
4264  "STEP_COORDINATES":"Coordonnées",
4265  "STEP_OPTIONS":"Options",
4266  "STEP_PAYMENT":"Paiement",
4267  "STEP_CONFIRMATION_WITHOUT_PAYMENT":"Confirmation sans paiement",
4268  "STEP_CONFIRMATION":"Confirmation",
4269  "TCC_SEARCH_PANEL_LABEL_LOCALITY": "Localisation",
4270  "TCC_SEARCH_PANEL_LOCALITY": "Où ?",
4271  "TCC_SEARCH_PANEL_LOCALITY_HELP1": "Entrez votre code postal ou le nom de votre commune.",
4272  "TCC_SEARCH_PANEL_LOCALITY_LIST_HELP1": "Merci de sélectionner votre commune dans la liste.",
4273  "TCC_SEARCH_PANEL_LOCALITY_LIST_TIP2": "<u>Astuce</u>\n    : La liste est longue! Essayez avec le code postal. C'est souvent le meilleur moyen pour trouver une commune.",
4274  "TCC_SEARCH_PANEL_LABEL_MONTH": "Date",
4275  "TCC_SEARCH_PANEL_MONTH": "Quand ?",
4276  "TCC_SEARCH_PANEL_MONTH_HELP1": "Astuce: Economisez plus d'argent avec nos tarifs les plus bas en réservant votre contrôle technique le plus tôt possible. Réservez jusqu'à 6 mois à l'avance, c'est la garantie de prix imbattables !",
4277  "TCC_SEARCH_PANEL_MONTH_TIP1": "<u>Astuce</u>\n    : Vous pouvez inscrire votre/vos véhicule(s) pour recevoir une notification lors de la mise en ligne de bonnes affaires à l'approche de votre/vos prochain(s) contrôle(s) technique(s).",
4278  "TCC_SEARCH_RESULT_NB_LOC_SING": "centre de contrôle technique pas cher trouvé",
4279  "TCC_SEARCH_RESULT_NB_LOC_PLUR": "centres de contrôle technique pas cher trouvés",
4280  "PLANNING2_UPDATE_OPEN": "✅",
4281  "PLANNING2_SOLD_OUT": "❌",
4282  "PLANNING2_CLOSED": "❌",
4283  "PLANNING2_MONTH_CALENDAR_LEGEND_BEST_OFFER": "Meilleure promo du mois",
4284  "PLANNING2_MONTH_CALENDAR_LEGEND_NOT_AVAILABLE": "Non disponible",
4285  "PLANNING2_WEEK_CALENDAR_LEGEND_BEST_OFFER": "Meilleure promo du jour",
4286  "PLANNING2_WEEK_CALENDAR_BACK_TO_MONTH": "Retour au calendrier du mois",
4287  "PLANNING2_GO_TO_FUNNEL": "Je réserve",
4288  "PLANNING2_MSG_NOSLOT": "Aucun créneau disponible ce jour-là",
4289  "PLANNING2_MSG_NOSLOT_PREVIOUS": "Jour précédent ayant des disponibilités : ",
4290  "PLANNING2_MSG_NOSLOT_NEXT": "Jour suivant ayant des disponibilités : ",
4291  "PLANNING2_ALERT_ERROR": "Une erreur s'est produite. Merci de réessayez d'ici quelques instants. Si le problème persiste, veuillez prendre contact avec le service client CTEasy.",
4292  "PLANNING2_BTN_RETRY": "Rééssayez",
4293  "PLANNING2_ALERT_EXPIRED": "Les données sont arrivés à expiration. Cliquez ci-dessous pour les recharger.",
4294  "PLANNING2_BTN_RELOAD": "Recharger",
4295  "PLANNING2_TITLE": "Choisissez le jour de votre rendez-vous",
4296  "PLANNING2_TITLE_WEEK": "Choisissez l'heure de votre rendez-vous",
4297  "PLANNING2_TITLE_ERROR": "Une erreur s'est produite",
4298  "PLANNING2_TITLE_LOADING": "Veuillez patienter quelques instants",
4299  "PLANNING2_TITLE_EXPIRED": "Données expirées",
4300  "PLANNING2_WAITING_FOR_FUNNEL": "Veuillez patienter, nous validons votre réservation...",
4301  "TCC_SEARCH_PANEL_LABEL_VEHICLE_TYPE": "Véhicule",
4302  "TCC_SEARCH_PANEL_VEHICLE_TYPE": "Type ?",
4303  "TCC_SEARCH_PANEL_LABEL_ENERGY_TYPE": "Carburant",
4304  "TCC_SEARCH_PANEL_ENERGY_TYPE": "Énergie ?",
4305  "SEARCH_ENGINE_SEARCH_BUTTON": "Voir les offres",
4306  "SEARCH_ENGINE_LOADING_TITLE": "Nous recherchons les meilleures offres parmi plus de 485 centres agréés...",
4307  "TCC_SEARCH_PANEL_VEHICLE_TYPE_HELP": "Pour tout véhicule à 4 roues motrices merci de choisir impérativement '4x4 (4 roues motrices)'.",
4308  "MODIFY_APPOINTMENT_REQUEST_TITLE": "Modification de rendez-vous",
4309  "MODIFY_APPOINTMENT_REQUEST_BUTTON": "Enregistrer",
4310  "MODIFY_APPOINTMENT_REQUEST_CONFIRM_BUTTON": "Confirmer",
4311  "MODIFY_APPOINTMENT_REQUEST_SAVED": "Votre demande de modification de rendez-vous a été enregistrée.",
4312  "MODIFY_APPOINTMENT_REQUEST_RES_OPTION_NOT_APPLICABLE": "La modification de rendez-vous n'est pas applicable à cette réservation.",
4313  "MODIFY_APPOINTMENT_REQUEST_RES_USED": "Votre centre de contrôle technique a marquée cette réservation comme utilisée.",
4314  "MODIFY_APPOINTMENT_REQUEST_RES_OPTION_USED": "L'option modification de rendez-vous a déjà été utilisée pour cette réservation.",
4315  "MODIFY_APPOINTMENT_REQUEST_RES_OPTION_NOT_SELECTED": "L'option modification de rendez-vous n'a pas été achetée avec cette réservation.",
4316  "MODIFY_APPOINTMENT_REQUEST_RES_REFUND": "Cette résrvation est en demande de remboursement.",
4317  "MODIFY_APPOINTMENT_REQUEST_RES_CANCELED": "Cette réservation a été annulée.",
4318  "MODIFY_APPOINTMENT_REQUEST_RES_NOT_PAYED": "Cette réservation n'a pas été payée.",
4319  "MODIFY_APPOINTMENT_REQUEST_RES_TOO_LATE": "Le délai de mise en oeuvre de l'option n'a pas été respecté. Vous aviez jusqu'à {duration} heures avant votre rendez-vous modifier votre rendez-vous.",
4320  "REFUND_REQUEST_TITLE": "Demande de remboursement",
4321  "REFUND_REQUEST_BUTTON": "Valider",
4322  "REFUND_REQUEST_BACK": "Retour",
4323  "REFUND_REQUEST_CONFIRM_BUTTON": "Confirmer la demande",
4324  "REFUND_REQUEST_SAVED": "<h2>Demande de remboursement enregistrée!</h2>\n        <p>Votre demande de remboursement a bien été enregistrée et va être traitée par notre service client.</p>",
4325  "REFUND_REQUEST_IMPOSSIBLE_NOT_PAYED": " <h2>Demande de remboursement impossible!</h2>\n<p>Cette réservation n'a pas été payée.</p>",
4326  "REFUND_REQUEST_IMPOSSIBLE_CANCELED": "<h2>Demande de remboursement impossible!</h2>\n <p>Cette réservation est marquée comme déjà annulée.</p>",
4327  "REFUND_REQUEST_IMPOSSIBLE_USED": "<h2>Demande de remboursement impossible!</h2>\n<p>Cette réservation est marquée comme utilisée (le contrôle technique a été effectué).</p>",
4328  "REFUND_REQUEST_IMPOSSIBLE_IN_PROCESS": "<h2>Demande de remboursement en cours!</h2>\n<p>Cette réservation est déjà en cours de traitement pour remboursement.</p>"
4329}, { get: function(obj, name) { if (obj[name] && !($.isDebug && $.isDebug())) return obj[name];
4329 return '[' + name + ']'; }});
4330 
4331
4332 
4333 var dcMixin = {
4334    init: function () {
4335        var tag = this;
4336        if(tag.dc != undefined) return;
4337        tag._ldc = null;
4338        tag._test=[];
4339        Object.defineProperty(tag, 'dc', {
4340            get: function () {
4341                if (tag._ldc) return tag._ldc;
4342                if (tag.parent && tag.parent.dc) return tag.parent.dc;
4343                return null;
4344            },
4345            set: function (value) {
4346                tag._ldc = value;
4347                tag._test.push(value);
4348            }                               
4349        });    
4350
4351        
4352
4353        
4354
4355        tag.isLoading = false;
4356        tag.exception = {};
4357        
4358        
4359
4360        tag.dcLookUp = function (s) {
4361            var parent = tag.parent;
4362            if (!s) {
4363                // On recherche un dc différent
4364                var dc = tag.dc;
4365                while (parent != null) {
4366                    if (parent.dc && parent.dc != dc) {
4367                        return parent.dc;
4368                    }
4369                    parent = parent.parent;
4370                }
4371                return null;
4372            }
4373            else if (Number.isInteger(s)) {
4374                while (parent != null && s > 0) {
4375                    s--;
4376                    parent = parent.parent;
4377                }
4378                if (parent != null) return parent.dc;
4379                return null;
4380            }
4381            else {
4382                s = s.toString().toUpperCase();
4383                while (parent != null) {
4384                    if (parent.root.tagName == s) {
4385                        return parent.dc;
4386                    }
4387                    parent = parent.parent;
4388                }
4389                return null;
4390            }
4391        };
4392        tag.tagLookUp = function (s) {
4393            var parent = tag.parent;
4394            if (Number.isInteger(s)) {
4395                while (parent != null && s > 0) {
4396                    s--;
4397                    parent = parent.parent;
4398                }
4399                return parent;
4400            }
4401            else {
4402                s = s.toString().toUpperCase();
4403                while (parent != null) {
4404                    if (parent.root.tagName == s) {
4405                        return parent;
4406                    }
4407                    parent = parent.parent;
4408                }
4409                return null;
4410            }
4411        };
4412        tag.bind = function (m) {
4413            var item = tag;
4414            while (item) {
4415                if (item[m] != undefined && item[m] != null) return item[m];
4416                item = item.parent;
4417            }
4418            return function () { alert('Binding: \'' + m + '\' not found!'); };
4419        };
4420        tag.on('mount', function () {
4421            if (tag.opts.dc) {
4422                tag._ldc = tag.opts.dc;
4423            }
4424        });
4425        tag.on('update', function () {
4426            if (tag.opts.dc) {
4427                tag._ldc = tag.opts.dc;
4428            }
4429        });
4430    }
4431}
4432
4433riot.tag2('riot-root', '<yield></yield>', '', '', function(opts) {
4434        var tag = this;
4435        tag.mixin(dcMixin);
4436
4437        tag.select = function (selector) {
4438            var l_SubTagElement = $(selector, tag.root);
4439            if(l_SubTagElement.length == 0) return null;
4440            return l_SubTagElement.get(0)._tag;
4441        };
4442        tag.on('mount', function () {
4443            $(tag.root).trigger('mount', [ tag ]);
4444        });
4445});
4446
4447
4448riot.tag2('row', '', '', '', function(opts) {
4449        var tag = this;
4450        tag.mixin(dcMixin);
4451        tag.root.innerHTML = tag.opts.content;
4452        tag.on('update', function () {
4453            tag.root.innerHTML = tag.opts.content;
4454        });
4455});
4456

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.