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(/'/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>€</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éneau que vous avez choisi ne semble plus disponible dans le centre de contrôle technique. Merci de choisir un créneau différent. Si le problè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>€ </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} {this.prevDayWithSlot.DayNum} {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} {this.nextDayWithSlot.DayNum} {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ô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>€</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)}€</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é</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 :</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>€</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>€</p> <p class="comment" id="le-plus-proche-comment">à 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ÉCIÉ</span></p> <p><span class="price" id="le-plus-apprecie-price">X</span>€</p> <p class="comment" id="le-plus-apprecie-comment">Noté 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("à "+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>ÉTAPE 1</strong> <i class="fas fa-caret-right"></i> Vé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 é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}€</span></p> </virtual> <virtual if="{this.purchase.stepActif != \'PAYMENT\' && this.purchase.stepActif != \'CONFIRMATION_WITHOUT_PAYMENT\'}"> <p>PRIX TOTAL: <span>{this.purchase.reservationPrixFinal}€</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é ?</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">×</span> </button> </div> <div class="modal-body"> <p>Vous êtes sur le point d\'économiser <span class="font-weight-bold amount">{this.purchase.reservationPrixEconomie}€</span> </p> <p> Ãtes-vous certain de vouloir vous passer de l\'option <b>modification & annulation</b> de rendez-vous à <b>{this.purchase.montantOptionModifAppointment}€</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}€</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 €) </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 + "€"); 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}€</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}€</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 > <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 + "€"); 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érifier vos donné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érifier vos donné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érifier vos donné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é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é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é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é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ô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}€</b></span></p> <p><b>Tarif CTeasy</b><span class="pull-right"><b>{this.purchase.reservationPrixAvecFraisDeServiceInclus}€</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}€</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}€</span> </virtual> <virtual if="{this.purchase.reservation.cvInsuranceAmount > 0}"> <span class="pull-right">{this.purchase.montantOptionContreVisitePrix}€</span> </virtual> </span> <p if="{this.purchase.avoir}"> <span>Votre avoir</span> <span class="pull-right font-weight-bold">-{this.purchase.avoir}€</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éduction</span> <span class="pull-right">-{this.purchase.reservationRemiseCouponReduction}€</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}€</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}€</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}€</span></td> </tr> <tr> <td><b>Montant réservation</b></td> <td colspan="2">{this.reservationPaymentBreakdown.reservationPriceWithMargeAndFees}€</td> </tr> <tr if="{this.reservationPaymentBreakdown.optionContreVisite.active}"> <td>Option contre-visite</td> <td colspan="2">{this.reservationPaymentBreakdown.optionContreVisite.price}€</td> </tr> <tr if="{this.reservationPaymentBreakdown.optionModifyAppointment.active}"> <td>Option modification RDV</td> <td colspan="2">{this.reservationPaymentBreakdown.optionModifyAppointment.price}€</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}€</b></td> </tr> <tr> <td></td> <td colspan="2">Félicitations, vous venez d\'économiser <b>{this.reservationPaymentBreakdown.reduction}€</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.