<script src="{{ asset("assets-commun/js/vue-#{app.environment}.min.js") }}"></script>
{% if app.session.get('ROLE_SUPPER_ADMIN') %}
{# Panneau de visualisation des erreurs Vue.js #}
{% include 'FrontCommun/error-logger-panel.html.twig' %}
{# Système de journalisation des erreurs Vue.js - À charger EN DERNIER #}
<script src="{{ asset('assets-commun/js/error-logger/error-logger.js') }}"></script>
<script src="{{ asset('assets-commun/js/error-logger/error-handler-init.js') }}"></script>
{% endif %}
{#<script type="module">
import * as Turbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>#}
<style>
/* Cœur rempli */
.heart-button .bi-heart-fill {
color: red;
}
/* Indicateur de chargement (spinner) */
.heart-button .loading-spinner {
display: none; /* Caché par défaut */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-top: 2px solid red;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>
<link rel="stylesheet" type="text/css" href="{{ asset('assets-commun/css/daterangepicker.css') }}"/>
<script type="text/javascript" src="{{ asset('assets-commun/js/daterangepicker.min.js') }}"></script>
<script type="text/javascript" src="{{ asset("assets-commun/js/jquery.number.js") }}"></script>
<script type="text/javascript" src="{{ asset("assets-commun/js/autoNumeric.min.js") }}"></script>
<link rel="stylesheet" href="{{ asset('assets-commun/css/jquery-confirm.min.css') }}">
<script src="{{ asset('assets-commun/js/jquery-confirm.min.js') }}"></script>
<script>
window.enabledHotelStayWorldwide = {{ app.request.server.get('ENABLED_HOTEL_STAY_WORLDWIDE') }};
if (typeof Vue != 'undefined')
Vue.options.delimiters = ['${', '}'];
{% if app.environment == 'prod' %}
Vue.config.devtools = false;
Vue.config.productionTip = false;
{% endif %}
/*Vue.config.errorHandler = (err, vm, info) => {
// err: error trace
// vm: component in which error occured
// info: Vue specific error information such as lifecycle hooks, events etc.
// TODO: Perform any custom logic or log to server
};*/
function flip(o) {
var newObj = {}
Object.keys(o).forEach((el, i) => {
newObj[o[el]] = el;
});
return newObj;
}
function autoNumeric(selector, options, version = 1) {
if ($(`${selector}:not(.formatted)`).length == 0)
return;
if (version == 1)
$(`${selector}:not(.formatted)`).number(true, options.decimalPlaces, options.decimalCharacter, options.digitGroupSeparator).addClass('formatted');
else if (version == 2)
document.querySelectorAll(`${selector}:not(.formatted)`).forEach(el => {
let _options = Object.assign({}, options);
if (el.dataset.currencySymbol !== undefined) {
_options.currencySymbol = el.dataset.currencySymbol
_options.currencySymbolPlacement = AutoNumeric.options.currencySymbolPlacement.suffix
}
if (el.dataset.unformatOnSubmit !== undefined) _options.unformatOnSubmit = (el.dataset.unformatOnSubmit == 'true' ? true : false)
if (el.dataset.digitGroupSeparator !== undefined) _options.digitGroupSeparator = el.dataset.digitGroupSeparator
if (el.dataset.decimalCharacter !== undefined) _options.decimalCharacter = el.dataset.decimalCharacter
if (el.dataset.decimalPlaces !== undefined) _options.decimalPlaces = el.dataset.decimalPlaces
if (el.dataset.minimumValue !== undefined) _options.minimumValue = el.dataset.minimumValue
if (el.dataset.maximumValue !== undefined) _options.maximumValue = el.dataset.maximumValue
new AutoNumeric(el, _options)
el.classList.add("formatted")
})
}
defaultOptionsMoney = {
digitGroupSeparator: '',
decimalCharacter: '.',
decimalPlaces:{{ deviseAgence().scale }},
unformatOnSubmit: true
}
defaultOptionsPercent = {
digitGroupSeparator: '',
decimalCharacter: '.',
decimalPlaces: 2
}
$(document).ready(function () {
autoNumeric('input.money', defaultOptionsMoney);
autoNumeric('input.percent', defaultOptionsPercent);
autoNumeric('input.money-v2', defaultOptionsMoney, 2);
autoNumeric('input.percent-v2', defaultOptionsPercent, 2);
$('input.money').attr('maxlength', 11);
$('input.percent').attr('maxlength', 6);
// Récupérer la liste de souhaits depuis la session Symfony
window.wishlist = {{ app.session.get('wishlist', []) | json_encode | raw }};
// Attacher l'événement à un parent existant
if (document.getElementById('vjs-list-hotels') != null) {
document.getElementById('vjs-list-hotels').addEventListener('click', function (event) {
// Vérifier si l'élément cliqué est un bouton "Like"
if (event.target.closest('.heart-button')) {
const heartButton = event.target.closest('.heart-button');
const hotelId = parseInt(heartButton.getAttribute('data-hotel-id'));
const heartIcon = heartButton.querySelector('.bi');
const spinner = heartButton.querySelector('.loading-spinner');
// Afficher le spinner et cacher l'icône de cœur
heartIcon.style.display = 'none';
spinner.style.display = 'block';
// Envoyer une requête AJAX pour ajouter/retirer de la liste de souhaits
fetch(`{{ path('wishlist_toggle',{hotelId:'_hotelId_'}) }}`.replace('_hotelId_', hotelId), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
})
.then(response => {
if (!response.ok) {
throw new Error('Erreur lors de la requête');
}
return response.json();
})
.then(data => {
if (data.success) {
// Mettre à jour l'icône du bouton
if (data.isInWishlist) {
heartIcon.classList.remove('bi-heart');
heartIcon.classList.add('bi-heart-fill');
// Ajouter l'hôtel à la liste de souhaits
wishlist.push(hotelId);
} else {
heartIcon.classList.remove('bi-heart-fill');
heartIcon.classList.add('bi-heart');
// Retirer l'hôtel de la liste de souhaits
wishlist.splice(wishlist.indexOf(hotelId), 1);
}
} else {
throw new Error(data.message || 'Erreur inconnue');
}
})
.catch(error => {
console.error('Erreur:', error);
// Afficher un message d'erreur à l'utilisateur
alert('Une erreur est survenue : ' + error.message);
})
.finally(() => {
// Cacher le spinner et réafficher l'icône de cœur
spinner.style.display = 'none';
heartIcon.style.display = 'block';
});
}
});
}
});
//================== Text Crope ==========================================
$.fn.extend({
toggleHtml: function (a, b) {
return this.html(this.html() == b ? a : b);
}
});
function checkEllipsisActive(div) {
var a = `<a href="javascript:void(0)" class="lire-la-suite" onclick="$(this).toggleHtml('Suite','Moins'); $(this).prev().toggleClass('text-crope')"><i>Suite</i></a>`
setTimeout(function () {
$(`${div} .text-crope`).each(function (i) {
if ($(this).innerHeight() < $(this)[0].scrollHeight && $(this).parent().find('.lire-la-suite').length == 0)
$(a).insertAfter($(this));
});
$(`${div} .text-crope`).each(function (i) {
if ($(this).innerWidth() < $(this)[0].scrollWidth && $(this).parent().find('.lire-la-suite').length == 0)
$(this).attr('title', $(this).html())
});
}, 500)
}
//checkEllipsisActive()
//================================================================================
{% if app.request.get('mdl') %}
$('#modal-login-register').modal()
$('#modal-login-register input.{{ app.request.get('mdl') }}').click()
{% endif %}
const slugify = str =>
str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
function viewAlert(msg, tag, autoClose, fntAction, titre, _options) {
var color = 'dark';
var icon = '';
var title = '';
var animation = 'zoom';
var closeAnimation = 'scale';
if (tag == 'success') {
color = 'green';
icon = 'icon-checkmark4';
title = 'Succès';
animation = 'scale';
closeAnimation = 'scale';
} else if (tag == 'danger') {
color = 'red';
icon = 'icon-blocked';
title = 'Alert!';
animation = 'rotateY';
closeAnimation = 'rotateY';
} else if (tag == 'warning') {
color = 'orange';
icon = 'icon-warning';
title = 'Attention';
animation = 'rotateYR';
closeAnimation = 'rotateYR';
} else if (tag == 'info') {
color = 'blue';
icon = 'icon-info3';
title = 'Info';
animation = 'rotateX';
closeAnimation = 'rotateX';
} else if (tag == 'primary') {
color = 'purple';
}
if (titre !== undefined && titre != null)
title = titre;
var options = {
icon: icon,
closeIcon: true,
type: color,
title: title,
content: msg,
animationBounce: 2.5,
closeAnimation: closeAnimation,
animation: animation,
buttons: {
btnOk: {
text: 'OK',
btnClass: 'btn-default',
keys: ['enter', 'echap'],
}
}
};
if (autoClose !== undefined && autoClose != null)
options.autoClose = 'btnOk|' + autoClose;
if (fntAction !== undefined && fntAction != null)
options.buttons.btnOk.action = fntAction;
if (_options !== undefined && _options != null)
$.each(_options, function (index, value) {
options[value.key] = value.val;
});
$.alert(options);
}
function demandConfirm(title, content, txtConfirm, fntAction) {
$.confirm({
title: title,
content: content,
icon: 'icon-question3',
closeIcon: true,
type: 'blue',
buttons: {
confirm: {
text: txtConfirm,
btnClass: 'btn-danger',
keys: ['enter'],
action: fntAction
},
cancel: {
text: 'Non',
btnClass: 'btn-default',
keys: ['echap'],
},
}
});
}
//================== Forcer le submit du formulaire une seul fois ==============================================
$(document).on('submit', 'form', function () {
form = $(this);
form.addClass('form-submitted');
setTimeout(function () {
form.removeClass('form-submitted');
}, 500);
})
$(window).on('beforeunload', function () {
$('.form-submitted').each(function () {
$(this).find('input').blur();
$(this).css('pointer-events', 'none');
btn = $(this).find('[type=submit]')
btn.prop('disabled', true)
btn.html('<i class="fa fa-spinner fa-spin"></i> <span class="text-loading">Veuillez patienter </span>')
})
});
//==============================================================================================================
function utf8_to_b64(str) {
return window.btoa(unescape(encodeURIComponent(str)));
}
function b64_to_utf8(str) {
return decodeURIComponent(escape(window.atob(str)));
}
Date.prototype.addDays = function (days) {
var date = new Date(this.valueOf());
date.setDate(date.getDate() + days);
return date;
}
String.prototype.removeSizeStyles = function () {
// Supprime tous les attributs class
let result = this.replace(/\s*class\s*=\s*["'][^"']*["']/gi, '');
// Supprime les propriétés de taille dans les attributs style
return result.replace(
/style\s*=\s*["'][^"']*?(width|height|min-width|min-height|max-width|max-height)\s*:\s*[^;"']+;?\s*[^"']*?["']/gi,
(match, p1) => {
// Supprime les propriétés de taille tout en préservant les autres styles
return match.replace(
/(width|height|min-width|min-height|max-width|max-height)\s*:\s*[^;"']+;?\s*/gi,
''
).replace(/\s*style\s*=\s*["']\s*["']/i, ''); // Supprime style vide
}
);
};
Number.prototype.formatMoney = function (decimals = 3, decimal_separator = ".", thousands_separator = " ", round = false, devise = true) {
{% set coursEchange = getCoursEchange() %}
let montant = {{ coursEchange.montant }}
if (!devise)
montant = 1
decimals ={{ coursEchange.scale }}
if (round)
decimals = 0
var n = this {{ coursEchange.operation }} montant,
decimals = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals,
decimal_separator = decimal_separator == undefined ? "." : decimal_separator,
thousands_separator = thousands_separator == undefined ? " " : thousands_separator,
s = n < 0 ? "-" : "",
i = String(parseInt(n = Math.abs(Number(n) || 0).toFixed(decimals))),
j = (j = i.length) > 3 ? j % 3 : 0;
return s + (j ? i.substr(0, j) + thousands_separator : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands_separator) + (decimals ? decimal_separator + Math.abs(n - i).toFixed(decimals).slice(2) : "");
};
$("form#newsletter").submit(function (event) {
event.preventDefault();
$('i#load-newsletter').toggleClass('hidden')
$.ajax({
type: "POST",
url: "{{ path('newsletter') }}",
data: $(this).serialize(),
}).done(function (data) {
$('i#load-newsletter').toggleClass('hidden')
$("form#newsletter").get(0).reset()
viewAlert('Merci beaucoup ! Nous vous enverrons chaque mois les meilleures offres ', 'success')
});
});
{% for type,flash in app.session.flashbag.all %}
{% for msg in flash %}
viewAlert(`{{ msg|raw }}`, '{{ type }}');
{% endfor %}
{% endfor %}
{% for flashError in app.flashes('verify_email_error') %}
viewAlert("{{ flashError }}", 'danger');
{% endfor %}
$('.logo').on({
mousedown: function () {
$(this).data('timer', setTimeout(function () {
window.location.href = '{{ path('redirect_dev_route') }}'
}, 3000));
},
mouseup: function () {
clearTimeout($(this).data('timer'));
}
});
function validatePassword() {
if (plain_password.value != confirm_password.value) {
confirm_password.setCustomValidity("Les mots de passe ne correspondent pas");
confirm_password.setAttribute('class', 'not_confirm_password form-control')
} else {
confirm_password.setCustomValidity('');
confirm_password.setAttribute('class', 'confirm_password form-control')
}
}
if ($('#plainPassword').length > 0 && $('#confirm_password').length > 0) {
var plain_password = document.getElementById("plainPassword")
, confirm_password = document.getElementById("confirm_password");
plain_password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
}
const scrollSmoothToBottom = (id) => {
const element = $(`div#${id}`);
element.animate({
scrollTop: element.prop("scrollHeight")
}, 1000);
}
const scrollToBottom = (id) => {
const element = document.getElementById(id);
element.scrollTop = element.scrollHeight;
}
/**
* Fonction externe pour afficher la pile d'appels de manière simplifiée
* @param {string} functionName - Nom de la fonction d'où l'appel provient
* @param {string} customMessage - Message personnalisé optionnel
*/
function logStackTrace(functionName = 'Unknown', customMessage = '') {
// Récupérer la pile d'appels
const stack = new Error().stack.split('\n');
// Filtrer et simplifier les appels (ignorer les appels Vue internes, webpack, etc.)
const relevantCalls = stack
.slice(2) // Ignorer Error et logStackTrace
.filter(line => {
const ignored = ['vue-dev', 'webpack', 'installHook', 'evaluate', 'overrideMethod',
'computedGetter', 'renderList', 'eval', 'Vue._render', 'vm9708'];
return !ignored.some(term => line.includes(term));
})
.slice(0, 8) // Limiter à 8 appels pertinents
.map(line => {
// Extraire et nettoyer les informations
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
if (match) {
const [, func, file, line, col] = match;
const fileName = file.split('/').pop();
return { func: func.trim(), file: fileName, line, col };
}
return null;
})
.filter(Boolean);
// Afficher le schéma simplifié
console.group(`%c🔍 Call Stack - ${functionName}`, 'color: #ff6b35; font-weight: bold; font-size: 13px;');
if (customMessage) {
console.log(`%c💬 ${customMessage}`, 'color: #004e89; font-style: italic; font-size: 11px;');
}
console.log('%c' + getStackDiagram(relevantCalls), 'font-family: monospace; color: #333; font-size: 11px; white-space: pre;');
// Afficher la pile complète dans un groupe collapsé
console.groupCollapsed('%c📋 Full Stack Trace', 'color: #666; font-size: 11px;');
console.trace(`Full trace from ${functionName}`);
console.groupEnd();
console.groupEnd();
}
/**
* Génère un schéma visuel de la pile d'appels
*/
function getStackDiagram(calls) {
let diagram = '\n';
calls.forEach((call, index) => {
const isLast = index === calls.length - 1;
const prefix = isLast ? '└─' : '├─';
const connector = isLast ? ' ' : '│ ';
diagram += `${prefix} ${call.func} (${call.file}:${call.line})\n`;
if (index < calls.length - 1) {
diagram += `${connector}\n`;
}
});
return diagram;
}
</script>