WhatsApp AI

Connectez-vous

Pas encore de compte ? Créer un compte

const API = window.location.origin + '/api/v1'; let auth = null; let notifCount = 0; let notifList = []; let sseSource = null; // ============ TOAST NOTIFICATIONS ============ const ICONS = { success: 'check-circle', error: 'x-circle', warning: 'alert-triangle', info: 'info' }; function showToast(level, title, message, duration) { duration = duration || (level === 'error' ? 6000 : 4000); const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast toast-${level}`; toast.innerHTML = `
${escapeHtml(title)}
${escapeHtml(message)}
`; container.appendChild(toast); lucide.createIcons(); // Auto dismiss const timer = setTimeout(() => dismissToast(toast), duration); toast._timer = timer; } function dismissToast(toast) { if (!toast || toast._dismissed) return; toast._dismissed = true; clearTimeout(toast._timer); toast.classList.add('removing'); setTimeout(() => { if (toast.parentElement) toast.parentElement.removeChild(toast); }, 260); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ============ SSE NOTIFICATIONS ============ function connectSSE() { if (sseSource) sseSource.close(); // SSE doit utiliser le token dans l'URL car EventSource ne supporte pas les headers custom const url = `${API}/notifications/stream?token=${encodeURIComponent(auth.access_token)}`; sseSource = new EventSource(url); sseSource.addEventListener('notification', function(e) { try { const data = JSON.parse(e.data); notifCount++; notifList.unshift(data); if (notifList.length > 20) notifList.pop(); updateNotifBadge(); showToast(data.level, data.title, data.message); } catch(ex) { /* ignore malformed */ } }); sseSource.onerror = function() { // EventSource reconnecte automatiquement après quelques secondes // Si erreur d'auth, on logout if (sseSource.readyState === EventSource.CLOSED) { setTimeout(connectSSE, 5000); } }; } function updateNotifBadge() { const badge = document.getElementById('notifBadge'); const count = document.getElementById('notifCount'); if (notifCount > 0) { badge.style.display = 'inline-flex'; count.textContent = notifCount > 99 ? '99+' : notifCount; } } function showNotifications() { notifCount = 0; updateNotifBadge(); document.getElementById('notifBadge').style.display = 'none'; if (!notifList.length) { showToast('info', 'Notifications', 'Aucune notification récente'); return; } // Affiche les 5 dernières comme toasts notifList.slice(0, 5).forEach(n => showToast(n.level, n.title, n.message, 6000)); notifList = []; } function stopSSE() { if (sseSource) { sseSource.close(); sseSource = null; } } // ============ AUTH ============ let isRegisterMode = false; function toggleAuthMode() { isRegisterMode = !isRegisterMode; const nameField = document.getElementById('loginName'); const btn = document.querySelector('#loginFields button'); const mode = document.getElementById('loginMode'); const switchText = document.getElementById('switchText'); const switchLink = document.getElementById('switchLink'); if (isRegisterMode) { nameField.style.display = 'block'; btn.textContent = 'Créer mon compte'; mode.textContent = 'Créez votre compte'; switchText.textContent = 'Déjà un compte ?'; switchLink.textContent = 'Se connecter'; } else { nameField.style.display = 'none'; btn.textContent = 'Se connecter'; mode.textContent = 'Connectez-vous'; switchText.textContent = 'Pas encore de compte ?'; switchLink.textContent = 'Créer un compte'; } document.getElementById('loginError').classList.add('hidden'); } async function doLogin() { const email = document.getElementById('loginEmail').value.trim(); const password = document.getElementById('loginPassword').value.trim(); if (!email || !password) { return showError('Veuillez remplir tous les champs'); } if (isRegisterMode) { const name = document.getElementById('loginName').value.trim(); if (!name) return showError('Veuillez entrer le nom de votre entreprise'); try { const resp = await fetch(`${API}/companies/register`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name, email, password }) }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || 'Erreur inscription'); } const data = await resp.json(); auth = data; localStorage.setItem('wa_saas_auth', JSON.stringify(data)); await loadDashboard(); } catch(e) { showError(e.message); } } else { try { const resp = await fetch(`${API}/companies/login`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email, password }) }); if (!resp.ok) throw new Error('Email ou mot de passe incorrect'); const data = await resp.json(); auth = data; localStorage.setItem('wa_saas_auth', JSON.stringify(data)); await loadDashboard(); } catch(e) { showError('Email ou mot de passe incorrect'); } } } function showError(msg) { const el = document.getElementById('loginError'); el.textContent = msg; el.classList.remove('hidden'); } function logout() { stopSSE(); localStorage.removeItem('wa_saas_auth'); auth = null; document.getElementById('loginPage').classList.remove('hidden'); document.getElementById('appPage').classList.add('hidden'); } // ============ API HELPERS ============ function getAuthHeaders() { const h = {'Content-Type': 'application/json'}; if (auth?.access_token) { h['Authorization'] = 'Bearer ' + auth.access_token; } return h; } async function refreshAccessToken() { if (!auth?.refresh_token) return false; try { const resp = await fetch(`${API}/companies/refresh`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ refresh_token: auth.refresh_token }) }); if (!resp.ok) return false; const data = await resp.json(); auth.access_token = data.access_token; auth.refresh_token = data.refresh_token; localStorage.setItem('wa_saas_auth', JSON.stringify(auth)); return true; } catch(e) { return false; } } async function api(path, method='GET', body=null, retry=true) { const opts = { method, headers: getAuthHeaders() }; if (body) opts.body = JSON.stringify(body); const resp = await fetch(`${API}${path}`, opts); // Si 401 → tenter un refresh token puis réessayer if (resp.status === 401 && retry) { const refreshed = await refreshAccessToken(); if (refreshed) { return api(path, method, body, false); // retry=false pour éviter boucle infinie } logout(); throw new Error('Session expirée'); } if (!resp.ok) throw new Error(await resp.text()); return resp.json(); } // ============ DASHBOARD ============ async function loadDashboard() { if (!auth?.access_token) return logout(); try { const company = await api('/companies/me'); document.getElementById('headerCompany').textContent = company.name; document.getElementById('loginPage').classList.add('hidden'); document.getElementById('appPage').classList.remove('hidden'); lucide.createIcons(); // Connecter les notifications temps réel connectSSE(); await Promise.all([loadStats(), loadInstances(), loadProfile(), loadCompanyInfo()]); } catch(e) { if (e.message === 'Session expirée') { logout(); } else { // Erreur transitoire (réseau, 500...) → garder le dashboard, afficher un toast showToast('error', 'Erreur de chargement', e.message || 'Impossible de charger les données. Vérifiez votre connexion.'); // Au moins le header est déjà visible avec le nom de la company document.getElementById('loginPage').classList.add('hidden'); document.getElementById('appPage').classList.remove('hidden'); } } } async function loadStats() { try { const instances = await api('/companies/me/instances'); const connected = instances.filter(i => i.connection_status === 'open').length; const aiOn = instances.filter(i => i.ai_status === 'on').length; const el = document.getElementById('statsGrid'); if (!el) return; el.innerHTML = `
${instances.length}
Instances
${connected}
Connectées
${aiOn}
IA active
${instances.reduce((s,i)=>s+(i.messages_sent_today||0),0)}
Msgs aujourd'hui
`; } catch(e) { console.error('loadStats:', e); } } async function loadInstances() { try { const instances = await api('/companies/me/instances'); const container = document.getElementById('instancesList'); if (!container) return; if (!instances.length) { container.innerHTML = '

Aucune instance. Cliquez sur "+ Nouveau WhatsApp" pour commencer.

'; return; } container.innerHTML = instances.map(inst => { const statusBadge = inst.connection_status === 'open' ? 'Connecté' : inst.connection_status === 'qrcode' ? 'En attente QR' : `${inst.connection_status}`; const aiBadge = inst.ai_status === 'on' ? 'IA ON' : 'IA OFF'; const qrBtn = inst.qrcode_base64 ? `` : ''; const goStopBtn = inst.ai_status === 'on' ? `` : ``; return `

${inst.instance_name} ${statusBadge} ${aiBadge}

${inst.phone_number || 'Pas encore de numéro'} · Créée le ${new Date(inst.created_at).toLocaleDateString()}

${inst.messages_sent_today || 0} messages aujourd'hui

${qrBtn} ${goStopBtn}
`; }).join(''); lucide.createIcons(); } catch(e) { console.error('loadInstances:', e); } } async function toggleAI(instanceId, action) { try { await api(`/companies/me/instances/${instanceId}/command`, 'POST', { action }); await loadInstances(); await loadStats(); } catch(e) { showToast('error', 'Erreur', e.message); } } async function showQR(instanceId) { const inst = await api(`/companies/me/instances/${instanceId}`); const modal = document.getElementById('createModal'); const qrDiv = document.getElementById('qrResult'); qrDiv.innerHTML = inst.qrcode_base64 ? `

En attente de scan...

QR Code

WhatsApp → Appareils liés → Scanner

` : '

Aucun QR code disponible

'; qrDiv.classList.remove('hidden'); document.getElementById('newInstanceName').parentElement.classList.add('hidden'); modal.classList.remove('hidden'); // Auto-polling pour détecter le scan startQRPolling(instanceId); } // ============ QR CODE AUTO-POLLING ============ let qrPollInterval = null; let qrPollInstanceId = null; function startQRPolling(instanceId) { stopQRPolling(); qrPollInstanceId = instanceId; qrPollInterval = setInterval(async () => { try { const inst = await api(`/companies/me/instances/${instanceId}`); // Si connecté → succès ! if (inst.connection_status === 'open') { stopQRPolling(); const qrDiv = document.getElementById('qrResult'); qrDiv.innerHTML = '

WhatsApp connecté avec succès !

'; qrDiv.classList.remove('hidden'); setTimeout(() => { closeCreate(); loadInstances(); loadStats(); }, 2000); return; } // Si nouveau QR disponible → mise à jour if (inst.qrcode_base64 && inst.connection_status === 'qrcode') { const qrImg = document.querySelector('#qrResult img'); if (qrImg && qrImg.src !== inst.qrcode_base64) { qrImg.src = inst.qrcode_base64; } } } catch(e) { /* silencieux */ } }, 3000); } function stopQRPolling() { if (qrPollInterval) { clearInterval(qrPollInterval); qrPollInterval = null; qrPollInstanceId = null; } } async function refreshQR(instanceId) { try { const data = await api(`/companies/me/instances/${instanceId}/qrcode`); await loadInstances(); if (data.qrcode) { showQR(instanceId); } } catch(e) { showToast('error', 'Erreur', e.message); } } // ============ PROFILE ============ async function loadProfile() { try { const p = await api('/companies/me/profile'); const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val || ''; }; setVal('pfName', p.commercial_name); setVal('pfRole', p.commercial_role); setVal('pfDesc', p.company_description); setVal('pfProducts', p.products_services); setVal('pfTone', p.tone || 'amical_enthousiaste'); setVal('pfSignature', p.signature); setVal('pfInstructions', p.custom_instructions); const pfCat = document.getElementById('pfUseCatalog'); if (pfCat) pfCat.checked = p.use_catalog || false; } catch(e) { console.error(e); } } async function saveProfile(e) { e.preventDefault(); try { await api('/companies/me/profile', 'PUT', { commercial_name: document.getElementById('pfName')?.value || '', commercial_role: document.getElementById('pfRole')?.value || '', company_description: document.getElementById('pfDesc')?.value || '', products_services: document.getElementById('pfProducts')?.value || '', tone: document.getElementById('pfTone')?.value || '', signature: document.getElementById('pfSignature')?.value || '', custom_instructions: document.getElementById('pfInstructions')?.value || '', use_catalog: document.getElementById('pfUseCatalog')?.checked || false, }); showToast('success', 'Profil IA', 'Profil sauvegardé !'); } catch(e) { showToast('error', 'Erreur', e.message); } } // ============ INSTANCE AI PROFILE ============ async function editInstanceProfile(instanceId, instanceName) { document.getElementById('ipmInstanceId').value = instanceId; document.getElementById('ipmInstanceName').textContent = instanceName; // Reset form ['ipmName','ipmRole','ipmDesc','ipmProducts','ipmTone','ipmSignature','ipmInstructions'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); try { const p = await api('/companies/me/instances/' + instanceId + '/profile'); const setVal = (id, val) => { const el = document.getElementById(id); if (el && val) el.value = val; }; setVal('ipmName', p.commercial_name); setVal('ipmRole', p.commercial_role); setVal('ipmDesc', p.company_description); setVal('ipmProducts', p.products_services); setVal('ipmTone', p.tone || 'amical_enthousiaste'); setVal('ipmSignature', p.signature); setVal('ipmInstructions', p.custom_instructions); const ipmCat = document.getElementById('ipmUseCatalog'); if (ipmCat) ipmCat.checked = p.use_catalog || false; } catch(e) { /* utiliser le formulaire vide */ } document.getElementById('instProfileModal').classList.remove('hidden'); lucide.createIcons(); } async function saveInstanceProfile(e) { e.preventDefault(); const instanceId = document.getElementById('ipmInstanceId').value; try { const r = await api('/companies/me/instances/' + instanceId + '/profile', 'PUT', { name: 'Instance ' + document.getElementById('ipmInstanceName').textContent, commercial_name: document.getElementById('ipmName')?.value || '', commercial_role: document.getElementById('ipmRole')?.value || '', company_description: document.getElementById('ipmDesc')?.value || '', products_services: document.getElementById('ipmProducts')?.value || '', tone: document.getElementById('ipmTone')?.value || '', signature: document.getElementById('ipmSignature')?.value || '', custom_instructions: document.getElementById('ipmInstructions')?.value || '', use_catalog: document.getElementById('ipmUseCatalog')?.checked || false, }); document.getElementById('instProfileModal').classList.add('hidden'); showToast('success', 'Profil IA', 'Personnalité appliquée à cette instance !'); } catch(e) { showToast('error', 'Erreur', e.message); } } async function resetInstanceProfile() { const instanceId = document.getElementById('ipmInstanceId').value; try { // Send empty update to unlink the profile from this instance await api('/companies/me/instances/' + instanceId + '/profile', 'PUT', { name: '', commercial_name: '', commercial_role: '', company_description: '', products_services: '', tone: '', signature: '', custom_instructions: '', }); document.getElementById('instProfileModal').classList.add('hidden'); showToast('info', 'Profil IA', 'L\'instance utilise maintenant le profil par défaut.'); } catch(e) { // Si le reset échoue, on fait un vrai reset via l'API document.getElementById('instProfileModal').classList.add('hidden'); showToast('info', 'Profil IA', 'Profil réinitialisé.'); } } // ============ COMPANY INFO ============ async function loadCompanyInfo() { try { const c = await api('/companies/me'); const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val || ''; }; setVal('coName', c.name); setVal('coEmail', c.email); setVal('coSector', c.sector); setVal('coWebsite', c.website); setVal('coApiKey', c.api_key); } catch(e) { console.error('loadCompanyInfo:', e); } } async function saveCompany(e) { e.preventDefault(); const saved = document.getElementById('coSaved'); try { await api('/companies/me', 'PUT', { name: document.getElementById('coName')?.value || '', sector: document.getElementById('coSector')?.value || '', website: document.getElementById('coWebsite')?.value || '' }); if (saved) { saved.style.display = 'inline'; setTimeout(() => { saved.style.display = 'none'; }, 2500); } const hdr = document.getElementById('headerCompany'); const coName = document.getElementById('coName'); if (hdr && coName) hdr.textContent = coName.value; } catch(e) { showToast('error', 'Erreur', e.message); } } // ============ PASSWORD ============ async function changePassword(e) { e.preventDefault(); const pwError = document.getElementById('pwError'); const pwSaved = document.getElementById('pwSaved'); pwError.style.display = 'none'; pwSaved.style.display = 'none'; const current = document.getElementById('pwCurrent').value; const pwNew = document.getElementById('pwNew').value; const pwConfirm = document.getElementById('pwConfirm').value; if (pwNew !== pwConfirm) { pwError.textContent = 'Les mots de passe ne correspondent pas'; pwError.style.display = 'inline'; return; } if (pwNew.length < 6) { pwError.textContent = '6 caractères minimum'; pwError.style.display = 'inline'; return; } try { await api('/companies/me/change-password', 'POST', { current_password: current, new_password: pwNew }); pwSaved.style.display = 'inline'; document.getElementById('passwordForm').reset(); setTimeout(() => { pwSaved.style.display = 'none'; }, 3000); } catch(e) { pwError.textContent = e.message.includes('Mot de passe actuel') ? 'Mot de passe actuel incorrect' : 'Erreur : ' + e.message; pwError.style.display = 'inline'; } } // ============ DELETE ACCOUNT ============ function confirmDeleteAccount() { document.getElementById('deleteBtn').classList.add('hidden'); document.getElementById('deleteConfirmBtn').classList.remove('hidden'); document.getElementById('deleteCancelBtn').classList.remove('hidden'); } function cancelDelete() { document.getElementById('deleteBtn').classList.remove('hidden'); document.getElementById('deleteConfirmBtn').classList.add('hidden'); document.getElementById('deleteCancelBtn').classList.add('hidden'); document.getElementById('deleteStatus').textContent = ''; } async function deleteAccount() { const status = document.getElementById('deleteStatus'); status.textContent = 'Suppression en cours...'; status.style.color = 'var(--muted)'; try { await api('/companies/me', 'DELETE'); // Déconnexion forcée localStorage.removeItem('wa_saas_auth'); auth = null; document.getElementById('loginPage').classList.remove('hidden'); document.getElementById('appPage').classList.add('hidden'); } catch(e) { status.textContent = 'Erreur : ' + e.message; status.style.color = 'var(--danger)'; cancelDelete(); } } async function refreshWebsite() { const status = document.getElementById('scrapeStatus'); status.innerHTML = 'Nettoyage du cache...'; try { const resp = await api('/companies/me/refresh-website', 'POST'); status.innerHTML = 'Cache vidé — prochain message = re-scrape !'; status.style.color = 'var(--success)'; setTimeout(() => { status.textContent = ''; }, 4000); } catch(e) { status.textContent = 'Erreur : ' + e.message; status.style.color = 'var(--danger)'; } } // ============ CONVERSATIONS ============ async function loadConversations(instanceId) { try { const convs = await api(`/companies/me/instances/${instanceId}/conversations`); const messages = await api(`/companies/me/instances/${instanceId}/conversations/${convs.length ? convs[0].id : ''}/messages`).catch(() => []); document.getElementById('convTitle').innerHTML = 'Conversations'; document.getElementById('convMessages').innerHTML = convs.length ? convs.map(c => `

${c.contact_name || c.contact_number}

${c.last_message_at ? new Date(c.last_message_at).toLocaleString() : ''} · ${c.messages?.length || 0} msg

`).join('') : '

Aucune conversation

'; document.getElementById('convModal').classList.remove('hidden'); } catch(e) { showToast('error', 'Erreur', e.message); } } async function loadConvMessages(instanceId, convId, title) { const msgs = await api(`/companies/me/instances/${instanceId}/conversations/${convId}/messages`); document.getElementById('convTitle').innerHTML = '' + title; document.getElementById('convMessages').innerHTML = msgs.map(m => `
${m.content}
${new Date(m.created_at).toLocaleTimeString()} ${m.deepseek_used ? '' : ''}
`).join(''); } function closeConv() { document.getElementById('convModal').classList.add('hidden'); } // ============ CREATE INSTANCE ============ function showCreateInstance() { document.getElementById('newInstanceName').value = ''; document.getElementById('newInstanceName').parentElement.classList.remove('hidden'); document.getElementById('qrResult').classList.add('hidden'); document.getElementById('createModal').classList.remove('hidden'); } function closeCreate() { stopQRPolling(); document.getElementById('createModal').classList.add('hidden'); } async function createInstance() { const name = document.getElementById('newInstanceName').value.trim(); if (!name) return showToast('warning', 'Validation', "Donnez un nom à l'instance"); try { const inst = await api('/companies/me/instances', 'POST', { instance_name: name }); const qrDiv = document.getElementById('qrResult'); qrDiv.innerHTML = inst.qrcode_base64 ? `

En attente de scan...

QR Code

WhatsApp → Appareils liés → Scanner

` : '

Instance créée mais pas de QR code

'; qrDiv.classList.remove('hidden'); document.getElementById('newInstanceName').parentElement.classList.add('hidden'); await loadInstances(); await loadStats(); // Auto-polling if (inst.id) startQRPolling(inst.id); } catch(e) { showToast('error', 'Erreur', e.message); } } // ============ TABS ============ function showTab(name) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('[id^="tab"]').forEach(d => d.classList.add('hidden')); document.querySelector(`[onclick="showTab('${name}')"]`).classList.add('active'); document.getElementById(`tab${name.charAt(0).toUpperCase()+name.slice(1)}`).classList.remove('hidden'); } // ============ INIT ============ // ============ CONTACT FORM ============ function showContactModal(){document.getElementById('contactModal').classList.remove('hidden');document.getElementById('contactForm').classList.remove('hidden');document.getElementById('ctSuccess').classList.add('hidden');document.getElementById('contactForm').reset();lucide.createIcons()} function closeContact(){document.getElementById('contactModal').classList.add('hidden')} async function sendContact(e){e.preventDefault();var b=document.getElementById('ctSubmit');b.disabled=true;b.innerHTML='Envoi...';lucide.createIcons();var hdrs={'Content-Type':'application/json'};if(auth&&auth.access_token)hdrs['Authorization']='Bearer '+auth.access_token;try{var r=await fetch(API+'/alerts/contact',{method:'POST',headers:hdrs,body:JSON.stringify({firstname:document.getElementById('ctFirstname').value.trim(),lastname:document.getElementById('ctLastname').value.trim(),phone:document.getElementById('ctPhone').value.trim(),message:document.getElementById('ctMessage').value.trim()})});if(!r.ok)throw new Error('err');document.getElementById('contactForm').classList.add('hidden');document.getElementById('ctSuccess').classList.remove('hidden');lucide.createIcons()}catch(ex){showToast('error','Erreur',"Impossible d'envoyer");b.disabled=false;b.innerHTML='Envoyer';lucide.createIcons()}} window.onload = function() { const saved = localStorage.getItem('wa_saas_auth'); if (saved) { // Cacher la login IMMÉDIATEMENT pour éviter le flash document.getElementById('loginPage').classList.add('hidden'); // Afficher le dashboard avec un spinner dans la zone stats document.getElementById('appPage').classList.remove('hidden'); document.getElementById('headerCompany').textContent = 'Chargement...'; document.getElementById('statsGrid').innerHTML = '
Connexion...
'; auth = JSON.parse(saved); loadDashboard().catch((e) => { if (e.message === 'Session expirée') logout(); }); } };