Modal confirmation custom (assets/app.js) : - Remplace le confirm() natif du navigateur par une modal glassmorphism - Header glass-dark avec icône warning rouge + "Confirmation" - Message dynamique depuis data-confirm du formulaire - Boutons Annuler (glass) et Confirmer (rouge) - Fermeture via overlay, bouton Annuler ou Escape - Au clic Confirmer : supprime data-confirm et submit le formulaire Crontab Docker (docker/cron/crontab) : - 0 2 * * * app:clean:pending-delete (nettoyage clients pending_delete) - 0 5 * * * app:email-tracking:purge (purge tracking > 90j) - 0 6 * * * app:dns:check (vérification DNS) - 0 4 * * 0 app:meilisearch:setup (reindex complet dimanche 4h) - 0 7 * * * app:cloudflare:clean (nettoyage _acme-challenge) Ansible deploy.yml.disabled : - Ajout cron clean pending delete (daily 2h) - Ajout cron meilisearch full reindex (weekly dimanche 4h) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
211 lines
9.6 KiB
JavaScript
211 lines
9.6 KiB
JavaScript
import "./app.scss"
|
|
import { initEntrepriseSearch } from "./modules/entreprise-search.js"
|
|
|
|
// Membre / Super Admin : mutuellement exclusif
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const memberCheckbox = document.querySelector('input[value="siteconseil_member"]');
|
|
const adminCheckbox = document.querySelector('input[value="siteconseil_admin"]');
|
|
const otherGroupCheckboxes = () =>
|
|
[...document.querySelectorAll('input[name="groups[]"]')].filter(cb => cb !== memberCheckbox);
|
|
|
|
if (memberCheckbox && adminCheckbox) {
|
|
memberCheckbox.addEventListener('change', () => {
|
|
/* istanbul ignore next */ if (memberCheckbox.checked) otherGroupCheckboxes().forEach(cb => { cb.checked = false; });
|
|
});
|
|
|
|
adminCheckbox.addEventListener('change', () => {
|
|
if (!adminCheckbox.checked) return;
|
|
memberCheckbox.checked = false;
|
|
otherGroupCheckboxes().forEach(cb => { cb.checked = true; });
|
|
});
|
|
}
|
|
|
|
// Stats period selector
|
|
const periodSelect = document.getElementById('stats-period-select');
|
|
const customRange = document.getElementById('stats-custom-range');
|
|
if (periodSelect && customRange) {
|
|
periodSelect.addEventListener('change', () => {
|
|
customRange.classList.toggle('hidden', periodSelect.value !== 'custom');
|
|
});
|
|
}
|
|
|
|
// data-confirm — modal glassmorphism custom
|
|
const confirmModal = document.createElement('div');
|
|
confirmModal.id = 'confirm-modal';
|
|
confirmModal.className = 'hidden fixed inset-0 z-[100] flex items-center justify-center';
|
|
confirmModal.innerHTML = `
|
|
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="confirm-overlay"></div>
|
|
<div class="relative glass-heavy w-full max-w-md mx-4 overflow-hidden" style="border-radius: 16px;">
|
|
<div class="glass-dark text-white px-6 py-4 flex items-center gap-3" style="border-radius: 16px 16px 0 0;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<span class="text-sm font-bold uppercase tracking-widest">Confirmation</span>
|
|
</div>
|
|
<div class="p-6">
|
|
<p id="confirm-message" class="text-sm font-medium text-gray-700 leading-relaxed"></p>
|
|
<div class="flex justify-end gap-3 mt-6">
|
|
<button type="button" id="confirm-cancel" class="px-5 py-2 glass font-bold uppercase text-xs tracking-wider text-gray-700 hover:bg-gray-900 hover:text-white transition-all">Annuler</button>
|
|
<button type="button" id="confirm-ok" class="px-5 py-2 bg-red-600 text-white font-bold uppercase text-xs tracking-wider hover:bg-red-700 transition-all" style="border-radius: 6px;">Confirmer</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(confirmModal);
|
|
|
|
let pendingForm = null;
|
|
const confirmMessage = document.getElementById('confirm-message');
|
|
const confirmCancel = document.getElementById('confirm-cancel');
|
|
const confirmOk = document.getElementById('confirm-ok');
|
|
const confirmOverlay = document.getElementById('confirm-overlay');
|
|
|
|
const closeConfirm = () => { confirmModal.classList.add('hidden'); pendingForm = null; };
|
|
|
|
confirmCancel.addEventListener('click', closeConfirm);
|
|
confirmOverlay.addEventListener('click', closeConfirm);
|
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && pendingForm) closeConfirm(); });
|
|
|
|
confirmOk.addEventListener('click', () => {
|
|
if (pendingForm) {
|
|
confirmModal.classList.add('hidden');
|
|
pendingForm.removeAttribute('data-confirm');
|
|
pendingForm.requestSubmit();
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('form[data-confirm]').forEach(form => {
|
|
form.addEventListener('submit', (e) => {
|
|
if (form.dataset.confirm) {
|
|
e.preventDefault();
|
|
pendingForm = form;
|
|
confirmMessage.textContent = form.dataset.confirm;
|
|
confirmModal.classList.remove('hidden');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Sidebar dropdown toggle
|
|
document.querySelectorAll('.sidebar-dropdown-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const menu = btn.nextElementSibling;
|
|
const arrow = btn.querySelector('.sidebar-dropdown-arrow');
|
|
menu?.classList.toggle('hidden');
|
|
arrow?.classList.toggle('rotate-180');
|
|
});
|
|
});
|
|
|
|
// Mobile sidebar toggle
|
|
const sidebarToggle = document.getElementById('admin-sidebar-toggle');
|
|
const sidebar = document.getElementById('admin-sidebar');
|
|
const overlay = document.getElementById('admin-overlay');
|
|
|
|
if (sidebarToggle && sidebar && overlay) {
|
|
sidebarToggle.addEventListener('click', () => {
|
|
sidebar.classList.toggle('open');
|
|
});
|
|
|
|
overlay.addEventListener('click', () => {
|
|
sidebar.classList.remove('open');
|
|
});
|
|
}
|
|
|
|
// Mobile menu toggle (public)
|
|
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
const menuIconOpen = document.getElementById('menu-icon-open');
|
|
const menuIconClose = document.getElementById('menu-icon-close');
|
|
|
|
if (mobileMenuBtn && mobileMenu) {
|
|
mobileMenuBtn.addEventListener('click', () => {
|
|
mobileMenu.classList.toggle('hidden');
|
|
menuIconOpen?.classList.toggle('hidden');
|
|
menuIconClose?.classList.toggle('hidden');
|
|
});
|
|
}
|
|
|
|
// Cookie banner
|
|
const cookieBanner = document.getElementById('cookie-banner');
|
|
const cookieAccept = document.getElementById('cookie-accept');
|
|
const cookieRefuse = document.getElementById('cookie-refuse');
|
|
|
|
if (cookieBanner && !localStorage.getItem('cookie_consent')) {
|
|
cookieBanner.classList.remove('hidden');
|
|
}
|
|
|
|
cookieAccept?.addEventListener('click', () => {
|
|
localStorage.setItem('cookie_consent', 'accepted');
|
|
cookieBanner?.classList.add('hidden');
|
|
});
|
|
|
|
cookieRefuse?.addEventListener('click', () => {
|
|
localStorage.setItem('cookie_consent', 'refused');
|
|
cookieBanner?.classList.add('hidden');
|
|
});
|
|
|
|
// Search (customers & revendeurs)
|
|
const renderHit = (h, linkPrefix) =>
|
|
`<a href="${linkPrefix}${h.id}" class="block px-4 py-2 hover:bg-gray-50 border-b border-gray-100 text-xs">
|
|
<span class="font-bold">${h.fullName || h.raisonSociale || (h.firstName + ' ' + h.lastName)}</span>
|
|
${h.email ? `<span class="text-gray-400 ml-2">${h.email}</span>` : ''}
|
|
${h.codeRevendeur ? `<span class="ml-2 px-1 py-0.5 bg-gray-900 text-[#fabf04] text-[9px] font-bold">${h.codeRevendeur}</span>` : ''}
|
|
</a>`;
|
|
|
|
const performSearch = async (searchUrl, linkPrefix, results, q) => {
|
|
const resp = await fetch(`${searchUrl}?q=${encodeURIComponent(q)}`);
|
|
const hits = await resp.json();
|
|
results.innerHTML = hits.length === 0
|
|
? '<div class="px-4 py-3 text-xs text-gray-400">Aucun resultat.</div>'
|
|
: hits.map(h => renderHit(h, linkPrefix)).join('');
|
|
results.classList.remove('hidden');
|
|
};
|
|
|
|
const setupSearch = (inputId, resultsId, searchUrl, linkPrefix) => {
|
|
const input = document.getElementById(inputId);
|
|
const results = document.getElementById(resultsId);
|
|
if (!input || !results) return;
|
|
|
|
let debounce;
|
|
input.addEventListener('input', () => {
|
|
clearTimeout(debounce);
|
|
const q = input.value.trim();
|
|
if (q.length < 2) {
|
|
results.classList.add('hidden');
|
|
results.innerHTML = '';
|
|
return;
|
|
}
|
|
debounce = setTimeout(() => performSearch(searchUrl, linkPrefix, results, q), 300);
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
/* istanbul ignore next */ if (!results.contains(e.target) && e.target !== input) {
|
|
results.classList.add('hidden');
|
|
}
|
|
});
|
|
};
|
|
|
|
setupSearch('search-customers', 'search-results', '/admin/clients/search', '/admin/clients/');
|
|
setupSearch('search-revendeurs', 'search-results-revendeurs', '/admin/revendeurs/search', '/admin/revendeurs/');
|
|
|
|
// Tarif tabs
|
|
const tarifTabs = document.getElementById('tarif-tabs');
|
|
if (tarifTabs) {
|
|
const active = 'px-6 py-3 font-bold uppercase text-sm tracking-wider glass-dark text-white border border-white/20 border-b-0 cursor-pointer';
|
|
const inactive = 'px-6 py-3 font-bold uppercase text-sm tracking-wider glass text-gray-700 border border-gray-200 border-b-0 cursor-pointer hover:bg-white/80 transition-all';
|
|
|
|
tarifTabs.querySelectorAll('[data-tab]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
tarifTabs.querySelectorAll('[data-tab]').forEach(b => {
|
|
b.className = b.dataset.tab === tab ? active : inactive;
|
|
});
|
|
document.querySelectorAll('[id^="content-"]').forEach(el => {
|
|
/* istanbul ignore next */ if (el.closest('#tarif-tabs')) return;
|
|
el.classList.toggle('hidden', el.id !== 'content-' + tab);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Recherche entreprise (page creation client)
|
|
initEntrepriseSearch();
|
|
});
|