import "./app.scss" import { initEntrepriseSearch } from "./modules/entreprise-search.js" // Membre / Super Admin : mutuellement exclusif document.addEventListener('DOMContentLoaded', () => { const memberCheckbox = document.querySelector('input[value="gp_member"]'); const adminCheckbox = document.querySelector('input[value="superadmin"]'); 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 = `
Confirmation

`; 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) => { const id = h.customerId || h.id; const name = h.fqdn || h.name || h.fullName || h.raisonSociale || (h.firstName + ' ' + h.lastName); const sub = h.customerName ? `${h.customerName}` : (h.email ? `${h.email}` : ''); return ` ${name} ${sub} ${h.codeRevendeur ? `${h.codeRevendeur}` : ''} `; }; 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 ? '
Aucun resultat.
' : 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/'); setupSearch('search-ndd', 'search-ndd-results', '/admin/services/ndd/search', '/admin/clients/'); setupSearch('search-websites', 'search-websites-results', '/admin/services/esyweb/search', '/admin/clients/'); // 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(); // Recherche globale (navbar admin) const globalInput = document.getElementById('global-search'); const globalResults = document.getElementById('global-search-results'); if (globalInput && globalResults) { const typeIcons = { client: '👤', ndd: '🌐', site: '💻', contact: '👥', revendeur: '🏢', }; const typeLabels = { client: 'Client', ndd: 'NDD', site: 'Site', contact: 'Contact', revendeur: 'Revendeur', }; let globalDebounce; globalInput.addEventListener('input', () => { clearTimeout(globalDebounce); const q = globalInput.value.trim(); if (q.length < 2) { globalResults.classList.add('hidden'); globalResults.innerHTML = ''; return; } globalDebounce = setTimeout(async () => { const resp = await fetch('/admin/global-search?q=' + encodeURIComponent(q)); const hits = await resp.json(); if (hits.length === 0) { globalResults.innerHTML = '
Aucun resultat.
'; } else { globalResults.innerHTML = hits.map(h => ` ${typeIcons[h.type] || ''}
${h.label} ${h.sub ? `${h.sub}` : ''}
${typeLabels[h.type] || h.type}
` ).join(''); } globalResults.classList.remove('hidden'); }, 250); }); document.addEventListener('click', (e) => { /* istanbul ignore next */ if (!globalResults.contains(e.target) && e.target !== globalInput) { globalResults.classList.add('hidden'); } }); globalInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') globalResults.classList.add('hidden'); }); } // ──────── Stripe Payment Modal ──────── initStripePayment(); // ──────── Tab search devis / avis ──────── initTabSearch('search-devis', 'search-devis-results'); initTabSearch('search-adverts', 'search-adverts-results'); initTabSearch('search-factures', 'search-factures-results'); // ──────── Devis lines repeater + drag & drop ──────── initDevisLines(); // ──────── Devis process : toggle formulaire de refus ──────── const refuseBtn = document.getElementById('refuse-toggle-btn'); const refuseForm = document.getElementById('refuse-form'); if (refuseBtn && refuseForm) { refuseBtn.addEventListener('click', () => refuseForm.classList.toggle('hidden')); } // ──────── Modal open/close generique (data-modal-open / data-modal-close) ──────── document.querySelectorAll('[data-modal-open]').forEach(btn => { btn.addEventListener('click', () => { const modal = document.getElementById(btn.dataset.modalOpen); if (modal) modal.classList.remove('hidden'); }); }); document.querySelectorAll('[data-modal-close]').forEach(btn => { btn.addEventListener('click', () => { const modal = document.getElementById(btn.dataset.modalClose); if (modal) modal.classList.add('hidden'); }); }); // ──────── Prestataire : recherche SIRET via API entreprise.data.gouv.fr ──────── const siretSearchBtn = document.getElementById('siret-search-btn'); const siretInput = document.getElementById('siret-search-input'); const siretResults = document.getElementById('siret-search-results'); if (siretSearchBtn && siretInput && siretResults) { siretSearchBtn.addEventListener('click', () => { const q = siretInput.value.trim(); if (q.length < 3) { siretResults.innerHTML = '

Saisissez au moins 3 caracteres.

'; siretResults.classList.remove('hidden'); return; } siretResults.innerHTML = '

Recherche...

'; siretResults.classList.remove('hidden'); fetch('/admin/prestataires/entreprise-search?q=' + encodeURIComponent(q)) .then(r => r.json()) .then(data => { const results = data.results || []; if (results.length === 0) { siretResults.innerHTML = '

Aucun resultat.

'; return; } siretResults.innerHTML = results.map(r => { const siege = r.siege || {}; const siret = siege.siret || ''; const nom = r.nom_complet || r.nom_raison_sociale || ''; const adresse = siege.adresse || ''; const cp = siege.code_postal || ''; const ville = siege.libelle_commune || ''; return ''; }).join(''); siretResults.querySelectorAll('.siret-result-item').forEach(item => { item.addEventListener('click', () => { const form = siretSearchBtn.closest('form'); if (!form) return; const set = (name, val) => { const el = form.querySelector('[name="' + name + '"]'); if (el) el.value = val; }; set('raisonSociale', item.dataset.nom); set('siret', item.dataset.siret); set('address', item.dataset.adresse); set('zipCode', item.dataset.cp); set('city', item.dataset.ville); siretResults.classList.add('hidden'); siretInput.value = ''; }); }); }) .catch(() => { siretResults.innerHTML = '

Erreur lors de la recherche.

'; }); }); siretInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); siretSearchBtn.click(); } }); document.addEventListener('click', (e) => { if (!siretResults.contains(e.target) && e.target !== siretInput && e.target !== siretSearchBtn) siretResults.classList.add('hidden'); }); } }); function initStripePayment() { const btnStripe = document.getElementById('btn-stripe'); const modal = document.getElementById('stripe-modal'); const overlay = document.getElementById('stripe-modal-overlay'); const closeBtn = document.getElementById('stripe-modal-close'); const payBtn = document.getElementById('stripe-pay-btn'); const errorsEl = document.getElementById('stripe-errors'); const paymentEl = document.getElementById('stripe-payment-element'); if (!btnStripe || !modal || !paymentEl) return; const pk = btnStripe.dataset.pk; const intentUrl = btnStripe.dataset.intentUrl; const successUrl = btnStripe.dataset.successUrl; const amount = btnStripe.dataset.amount; if (!pk) return; const stripe = Stripe(pk); let elements = null; let clientSecret = null; let isProcessing = false; const showModal = () => modal.classList.remove('hidden'); const hideModal = () => { if (!isProcessing) modal.classList.add('hidden'); }; closeBtn.addEventListener('click', hideModal); overlay.addEventListener('click', hideModal); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideModal(); }); let currentMethod = 'card'; function openStripeModal(method) { currentMethod = method; showModal(); payBtn.disabled = true; payBtn.textContent = 'Chargement...'; errorsEl.classList.add('hidden'); const bodyPayload = method === 'sepa' ? { method: 'sepa' } : {}; fetch(intentUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyPayload), }) .then(r => r.json()) .then(data => { if (data.error) { errorsEl.textContent = data.error; errorsEl.classList.remove('hidden'); payBtn.textContent = method === 'sepa' ? 'Prelever ' + amount + ' €' : method === 'paypal' ? 'Payer via PayPal ' + amount + ' €' : 'Payer ' + amount + ' €'; return; } clientSecret = data.clientSecret; const elementsOptions = { clientSecret: clientSecret, locale: 'fr', appearance: { theme: 'stripe', variables: { fontFamily: 'Arial, Helvetica, sans-serif', colorPrimary: method === 'sepa' ? '#2563eb' : '#4f46e5', borderRadius: '8px', }, }, }; elements = stripe.elements(elementsOptions); const paymentElement = elements.create('payment'); paymentEl.innerHTML = ''; paymentElement.mount('#stripe-payment-element'); paymentElement.on('ready', () => { payBtn.disabled = false; payBtn.textContent = method === 'sepa' ? 'Prelever ' + amount + ' €' : method === 'paypal' ? 'Payer via PayPal ' + amount + ' €' : 'Payer ' + amount + ' €'; }); paymentElement.on('change', (event) => { if (event.error) { errorsEl.textContent = event.error.message; errorsEl.classList.remove('hidden'); } else { errorsEl.classList.add('hidden'); } }); }) .catch(() => { errorsEl.textContent = 'Erreur de connexion au serveur de paiement.'; errorsEl.classList.remove('hidden'); payBtn.textContent = method === 'sepa' ? 'Prelever ' + amount + ' €' : method === 'paypal' ? 'Payer via PayPal ' + amount + ' €' : 'Payer ' + amount + ' €'; }); } btnStripe.addEventListener('click', () => openStripeModal('card')); // Bouton SEPA const btnSepa = document.getElementById('btn-sepa'); if (btnSepa) { btnSepa.addEventListener('click', () => openStripeModal('sepa')); } payBtn.addEventListener('click', async () => { if (!clientSecret || !elements || isProcessing) return; isProcessing = true; payBtn.disabled = true; payBtn.textContent = 'Traitement en cours...'; errorsEl.classList.add('hidden'); // confirmPayment gere automatiquement le 3D Secure (3DS) // Stripe ouvre la modal 3DS dans une iframe si necessaire const { error } = await stripe.confirmPayment({ elements: elements, confirmParams: { return_url: window.location.origin + successUrl + '?method=' + currentMethod, }, }); // Si on arrive ici, c'est qu'il y a eu une erreur // (en cas de succes, le navigateur est redirige vers return_url) if (error) { if (error.type === 'card_error' || error.type === 'validation_error') { errorsEl.textContent = error.message; } else { errorsEl.textContent = 'Une erreur inattendue est survenue. Veuillez reessayer.'; } errorsEl.classList.remove('hidden'); } isProcessing = false; payBtn.disabled = false; payBtn.textContent = currentMethod === 'sepa' ? 'Prelever ' + amount + ' €' : 'Payer ' + amount + ' €'; }); } function initTabSearch(inputId, resultsId) { const input = document.getElementById(inputId); const results = document.getElementById(resultsId); if (!input || !results) return; const url = input.dataset.url; let timeout = null; const stateLabels = { created: 'Cree', send: 'Envoye', accepted: 'Accepte', refused: 'Refuse', cancel: 'Annule' }; const stateColors = { created: 'bg-yellow-100 text-yellow-800', send: 'bg-blue-500/20 text-blue-700', accepted: 'bg-green-500/20 text-green-700', refused: 'bg-red-500/20 text-red-700', cancel: 'bg-gray-100 text-gray-600' }; input.addEventListener('input', () => { clearTimeout(timeout); const q = input.value.trim(); if (q.length < 2) { results.classList.add('hidden'); return; } timeout = setTimeout(async () => { const resp = await fetch(url + '?q=' + encodeURIComponent(q)); const hits = await resp.json(); if (hits.length === 0) { results.innerHTML = '
Aucun resultat.
'; } else { results.innerHTML = hits.map(h => `
${h.numOrder} ${h.customerName || ''}
${h.totalTtc || '0.00'} € ${stateLabels[h.state] || h.state}
` ).join(''); } results.classList.remove('hidden'); }, 250); }); document.addEventListener('click', (e) => { if (!results.contains(e.target) && e.target !== input) results.classList.add('hidden'); }); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') results.classList.add('hidden'); }); } function initDevisLines() { const container = document.getElementById('lines-container'); const addBtn = document.getElementById('add-line-btn'); const tplEl = document.getElementById('line-template'); const totalEl = document.getElementById('total-ht'); if (!container || !addBtn || !tplEl || !totalEl) return; const template = tplEl.innerHTML; let counter = 0; function renumber() { container.querySelectorAll('.line-row').forEach((row, idx) => { row.querySelector('.line-pos').textContent = '#' + (idx + 1); row.querySelector('.line-pos-input').value = idx; }); } function recalc() { let total = 0; container.querySelectorAll('.line-price').forEach(input => { const v = parseFloat(input.value); if (!isNaN(v)) total += v; }); totalEl.textContent = total.toFixed(2) + ' EUR'; } function addLine() { const html = template.replaceAll('__INDEX__', counter); const wrapper = document.createElement('div'); wrapper.innerHTML = html.trim(); const node = wrapper.firstChild; container.appendChild(node); counter++; renumber(); recalc(); } container.addEventListener('click', e => { if (e.target.classList.contains('remove-line-btn')) { e.target.closest('.line-row').remove(); renumber(); recalc(); } }); container.addEventListener('input', e => { if (e.target.classList.contains('line-price')) recalc(); }); addBtn.addEventListener('click', () => addLine()); // Validation : empeche l'envoi si un type est selectionne mais pas le service const form = document.getElementById('devis-form'); if (form) { form.addEventListener('submit', (e) => { const rows = container.querySelectorAll('.line-row'); for (const row of rows) { const typeSelect = row.querySelector('.line-type'); const serviceSelect = row.querySelector('.line-service-id'); if (!typeSelect || !serviceSelect) continue; const type = typeSelect.value; if (!type || type === 'hosting' || type === 'maintenance' || type === 'other' || type === 'ndd' || type === 'website') continue; // Type avec service obligatoire (esymail) mais pas selectionne — ndd et website autorisent le vide if (!serviceSelect.value && !serviceSelect.disabled && serviceSelect.options.length > 1) { e.preventDefault(); serviceSelect.focus(); serviceSelect.classList.add('border-red-500', 'ring-2', 'ring-red-300'); const pos = row.querySelector('.line-pos')?.textContent || ''; alert('Ligne ' + pos + ' : veuillez selectionner le service pour le type "' + typeSelect.options[typeSelect.selectedIndex].text + '".'); return; } } }); } // Chargement dynamique des services par type container.addEventListener('change', async (e) => { if (!e.target.classList.contains('line-type')) return; const select = e.target; const row = select.closest('.line-row'); const serviceSelect = row.querySelector('.line-service-id'); const type = select.value; serviceSelect.innerHTML = ''; serviceSelect.disabled = true; if (!type || type === 'hosting' || type === 'maintenance' || type === 'other') return; const url = select.dataset.servicesUrl.replace('__TYPE__', type); try { const resp = await fetch(url); const items = await resp.json(); if (items.length > 0) { items.forEach(item => { const opt = document.createElement('option'); opt.value = item.id; opt.textContent = item.label; serviceSelect.appendChild(opt); }); serviceSelect.disabled = false; } } catch (err) { /* silencieux */ } }); // Boutons prestations rapides : ajoute une ligne pre-remplie avec type auto document.querySelectorAll('.quick-price-btn').forEach(btn => { btn.addEventListener('click', () => { addLine(); const lastRow = container.querySelector('.line-row:last-child'); if (!lastRow) return; lastRow.querySelector('input[name$="[title]"]').value = btn.dataset.title || ''; lastRow.querySelector('textarea[name$="[description]"]').value = btn.dataset.description || ''; const priceInput = lastRow.querySelector('.line-price'); priceInput.value = btn.dataset.price || '0.00'; // Auto-set le type de service const lineType = btn.dataset.lineType || ''; if (lineType) { const typeSelect = lastRow.querySelector('.line-type'); if (typeSelect) { typeSelect.value = lineType; // Trigger change pour charger les services du client typeSelect.dispatchEvent(new Event('change', { bubbles: true })); } } recalc(); }); }); // Drag & drop reordering let draggedRow = null; container.addEventListener('dragstart', e => { const row = e.target.closest('.line-row'); if (!row) return; draggedRow = row; row.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; }); container.addEventListener('dragend', e => { const row = e.target.closest('.line-row'); if (row) row.classList.remove('dragging'); container.querySelectorAll('.line-row').forEach(r => r.classList.remove('drag-over')); draggedRow = null; renumber(); }); container.addEventListener('dragover', e => { e.preventDefault(); const target = e.target.closest('.line-row'); if (!target || target === draggedRow) return; container.querySelectorAll('.line-row').forEach(r => r.classList.remove('drag-over')); target.classList.add('drag-over'); }); container.addEventListener('drop', e => { e.preventDefault(); const target = e.target.closest('.line-row'); if (!target || !draggedRow || target === draggedRow) return; const rows = Array.from(container.querySelectorAll('.line-row')); const draggedIdx = rows.indexOf(draggedRow); const targetIdx = rows.indexOf(target); if (draggedIdx < targetIdx) { target.after(draggedRow); } else { target.before(draggedRow); } target.classList.remove('drag-over'); renumber(); }); // Prefill en mode edition const initial = container.dataset.initialLines; if (initial) { try { const arr = JSON.parse(initial); arr.sort((a, b) => (a.pos || 0) - (b.pos || 0)); arr.forEach(async (l) => { addLine(); const row = container.querySelector('.line-row:last-child'); if (!row) return; row.querySelector('input[name$="[title]"]').value = l.title || ''; row.querySelector('textarea[name$="[description]"]').value = l.description || ''; row.querySelector('.line-price').value = l.priceHt || '0.00'; // Pre-select type const typeSelect = row.querySelector('.line-type'); if (l.type && typeSelect) { typeSelect.value = l.type; // Charge les services si type avec serviceId if (l.serviceId && l.type !== 'hosting' && l.type !== 'maintenance' && l.type !== 'other') { const serviceSelect = row.querySelector('.line-service-id'); const url = typeSelect.dataset.servicesUrl.replace('__TYPE__', l.type); try { const resp = await fetch(url); const items = await resp.json(); serviceSelect.innerHTML = ''; items.forEach(item => { const opt = document.createElement('option'); opt.value = item.id; opt.textContent = item.label; if (item.id === l.serviceId) opt.selected = true; serviceSelect.appendChild(opt); }); serviceSelect.disabled = false; } catch (err) { /* ignore */ } } } }); recalc(); } catch (e) { /* ignore */ } } }