« ;
out+= »
« ;
el.innerHTML=out;
}
function showAddAdminModal(){
var ex=document.getElementById(« aam »);if(ex)ex.remove();
var m=document.createElement(« div »);m.id= »aam »;
m.style.cssText= »position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:99999;display:flex;align-items:center;justify-content:center »;
var is= »width:100%;padding:11px;border-radius:12px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#fff;font-size:14px;font-family:Inter,sans-serif;box-sizing:border-box;margin-top:6px »;
var ls= »font-size:11px;font-weight:700;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.05em »;
m.innerHTML= »« ;
out+= »« ;
out+= »
« ;
if(!admins.length){
out+= »Gestion
« ;
out+= »Administrateurs
Aucun administrateur
« ;
}else{
admins.forEach(function(a){
out+= »« ;
out+= »« ;
if(a.id!==S.session.id){
out+= »« ;
}else{
out+= »Vous« ;
}
out+= »
« ;
});
}
out+= »« +(a.name|| »â »)+ »
« ;
out+= »âïž Â«Â +(a.email|| »â »)+ »
« ;
if(a.phone)out+= »đ « +a.phone+ »
« ;
out+= » »
+ »
« ;
document.body.appendChild(m);
}
function submitNewAdmin(){
var name=(document.getElementById(« an-name »)?.value|| » »).trim();
var email=(document.getElementById(« an-email »)?.value|| » »).trim();
var phone=(document.getElementById(« an-phone »)?.value|| » »).trim();
var pw=(document.getElementById(« an-pw »)?.value|| » »).trim();
if(!name||!email||!pw){toast(« Nom, email et mot de passe requis », »err »);return;}
if(S.users.find(function(u){return u.email===email;})){toast(« Email déjà utilisé », »err »);return;}
S.users.push({id: »adm »+Date.now(),name:name,email:email,phone:phone,password:pw,role: »admin »,active:true,createdAt:new Date().toISOString()});
S.logs.unshift({id: »log »+Date.now(),action: »Admin créé : « +name,user:S.session.name,ts:new Date().toISOString()});
saveToStorage();
document.getElementById(« aam »).remove();
toast(« Compte admin créé pour « +name+ » â »);
renderSidebar();renderPage();
}
function deleteAdmin(userId){
var a=S.users.find(function(u){return u.id===userId;});
if(!a)return;
if(!confirm(« Supprimer le compte admin de « +a.name+ » ? »))return;
S.users=S.users.filter(function(u){return u.id!==userId;});
S.logs.unshift({id: »log »+Date.now(),action: »Admin supprimé : « +a.name,user:S.session.name,ts:new Date().toISOString()});
saveToStorage();toast(a.name+ » supprimĂ© â »);
renderSidebar();renderPage();
}
function normTrade(t){
if(!t) return  »;
return t.toLowerCase()
.replace(/Ă©/g,’e’).replace(/Ăš/g,’e’).replace(/ĂȘ/g,’e’).replace(/Ă«/g,’e’)
.replace(/Ă /g,’a’).replace(/Ăą/g,’a’).replace(/Ă€/g,’a’)
.replace(/Ăč/g,’u’).replace(/Ă»/g,’u’).replace(/ĂŒ/g,’u’)
.replace(/Ăź/g,’i’).replace(/ĂŻ/g,’i’)
.replace(/ĂŽ/g,’o’).replace(/ö/g,’o’)
.replace(/ç/g,’c’)
.trim();
}
function viderCorbeille(){
var count=S.leads.filter(function(l){return !!l.deleted;}).length;
if(!count){toast(« Corbeille déjà vide »);return;}
if(!confirm(« Supprimer définitivement « +count+ » lead(s) ? Irréversible. »))return;
S.leads=S.leads.filter(function(l){return !l.deleted;});
S.logs.unshift({id: »log »+Date.now(),action: »Corbeille vidée (« +count+ » supprimé(s)) »,user:S.session.name,ts:new Date().toISOString()});
saveToStorage();toast(« Corbeille vidĂ©e â »);renderSidebar();renderPage();
}
// confirmReattribuer avec sync garantie
function confirmReattribuer(leadId){
var sel=document.getElementById(« reassign-select »);
if(!sel)return;
var uid=sel.value;
var u=S.users.find(function(x){return x.id===uid;});
var lead=S.leads.find(function(l){return l.id===leadId;});
if(!lead||!u)return;
var oldName=lead.assignedName|| »â »;
lead.assignedTo=u.id;
lead.assignedName=u.name;
lead.crmStatus= »nouveau »;
S.logs.unshift({id: »log »+Date.now(),action: »RĂ©attribuĂ© : « +lead.name+ » (« +oldName+ » â « +u.name+ ») »,user:S.session.name,ts:new Date().toISOString()});
// Sauvegarder immédiatement sur Supabase
try{localStorage.setItem(« nh_data »,JSON.stringify({users:S.users,leads:S.leads,logs:S.logs,tickets:S.tickets}));}catch(e){}
var SB= »https://rivjpxutbehnbrprabzb.supabase.co »;
var SK= »eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJpdmpweHV0YmVobmJycHJhYnpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU1NzcxNzcsImV4cCI6MjA5MTE1MzE3N30.wTzjmAjb9utnrgqhBboBmkD8TucUCuDhqm0l_ZW6r6A »;
fetch(SB+ »/rest/v1/nh_data?id=eq.main »,{
method: »PATCH »,
headers:{« Content-Type »: »application/json », »apikey »:SK, »Authorization »: »Bearer « +SK, »Prefer »: »return=minimal »},
body:JSON.stringify({leads:S.leads,users:S.users,logs:S.logs,tickets:S.tickets,updated_at:new Date().toISOString()})
});
var modal=document.getElementById(« reassign-modal »);if(modal)modal.remove();
toast(« RĂ©attribuĂ© à « +u.name+ » â »);
renderSidebar();renderPage();
}
var TRADES=[« Electricien », »Installateur PV », »Installateur IRVE », »Plombier », »Chauffagiste », »Menuisier », »Peintre », »Macon », »Carreleur », »Couvreur », »Plaquiste », »Climatisation/PAC », »Autre »];
var ZONES=[« 01″, »02″, »03″, »04″, »05″, »06″, »07″, »08″, »09″, »10″, »11″, »12″, »13″, »14″, »15″, »16″, »17″, »18″, »19″, »20″, »21″, »22″, »23″, »24″, »25″, »26″, »27″, »28″, »29″, »30″, »31″, »32″, »33″, »34″, »35″, »36″, »37″, »38″, »39″, »40″, »41″, »42″, »43″, »44″, »45″, »46″, »47″, »48″, »49″, »50″, »51″, »52″, »53″, »54″, »55″, »56″, »57″, »58″, »59″, »60″, »61″, »62″, »63″, »64″, »65″, »66″, »67″, »68″, »69″, »70″, »71″, »72″, »73″, »74″, »75″, »76″, »77″, »78″, »79″, »80″, »81″, »82″, »83″, »84″, »85″, »86″, »87″, »88″, »89″, »90″, »91″, »92″, »93″, »94″, »95″, »971″, »972″, »973″, »974″, »976″];
// âââ DATA âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
const USERS_INIT = [
{id: »u1″,role: »admin »,name: »Jamal »,email: »f.nouvelhorizon@gmail.com »,password: »Kolai.jamal2025″,credits:null,active:true},
{id: »u2″,role:(document.getElementById(« nc-role »)&&document.getElementById(« nc-role »).value)|| »client »,name: »Marc Dupont »,email: »marc@elec75.fr »,password: »client123″,credits:5,trade: »Ălectricien »,zone: »75″,active:true,phone: »06 11 22 33 44″},
{id: »u3″,role:(document.getElementById(« nc-role »)&&document.getElementById(« nc-role »).value)|| »client »,name: »Sophie Renard »,email: »sophie@solaire13.fr »,password: »client123″,credits:12,trade: »Installateur PV »,zone: »13″,active:true,phone: »06 55 66 77 88″},
{id: »u4″,role: »client »,name: »Karim Bensaid »,email: »karim@irve69.fr »,password: »client123″,credits:3,trade: »Installateur IRVE »,zone: »69″,active:true,phone: »06 99 88 77 66″},
];
const now = Date.now();
const h = ms => new Date(now – ms).toISOString();
const LEADS_INIT = [
{id: »l1″,name: »M. Faure »,phone: »06 12 34 56 78″,email: »faure@mail.fr »,zone: »75″,trade: »Ălectricien »,project: »RĂ©novation tableau Ă©lectrique »,budget: »2 500 âŹÂ »,createdAt:h(10*3600000),assignedTo:null,status: »pending »,crmStatus: »nouveau »,notes:[]},
{id: »l2″,name: »Mme Leblanc »,phone: »07 98 76 54 32″,email: »leblanc@mail.fr »,zone: »13″,trade: »Installateur PV »,project: »Installation 6 kWc + batterie MyLight »,budget: »12 000 âŹÂ »,createdAt:h(36*3600000),assignedTo: »u3″,status: »assigned »,crmStatus: »devis »,notes:[{text: »Devis envoyĂ© par email »,at:h(5*3600000)}]},
{id: »l3″,name: »M. Girard »,phone: »06 55 44 33 22″,email: »girard@mail.fr »,zone: »69″,trade: »Installateur IRVE »,project: »Borne recharge 22kW garage »,budget: »1 800 âŹÂ »,createdAt:h(20*3600000),assignedTo:null,status: »pending »,crmStatus: »nouveau »,notes:[]},
{id: »l4″,name: »M. Martin »,phone: »06 11 22 33 44″,email: »martin@mail.fr »,zone: »75″,trade: »Ălectricien »,project: »Mise aux normes NF C 15-100″,budget: »4 200 âŹÂ »,createdAt:h(52*3600000),assignedTo: »u2″,status: »assigned »,crmStatus: »rappel »,notes:[{text: »Premier appel â pas rĂ©pondu. »,at:h(48*3600000)}]},
{id: »l5″,name: »Mme Petit »,phone: »07 66 77 88 99″,email: »petit@mail.fr »,zone: »13″,trade: »Installateur PV »,project: »Autoconsommation 9 kWc »,budget: »15 000 âŹÂ »,createdAt:h(2*3600000),assignedTo:null,status: »pending »,crmStatus: »nouveau »,notes:[]},
{id: »l6″,name: »M. Rousseau »,phone: »06 33 44 55 66″,email: »rousseau@mail.fr »,zone: »75″,trade: »Ălectricien »,project: »VMC double flux maison »,budget: »3 800 âŹÂ »,createdAt:h(80*3600000),assignedTo: »u2″,status: »assigned »,crmStatus: »contacte »,notes:[{text: »RDV confirmĂ© mardi 14h. »,at:h(70*3600000)}]},
];
// ââ SUPABASE CLOUD STORAGE ââââââââââââââââââââââââââââââââââââââââââââââââââ
const _SB_URL = ‘https://rivjpxutbehnbrprabzb.supabase.co’;
const _SB_KEY = ‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJpdmpweHV0YmVobmJycHJhYnpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU1NzcxNzcsImV4cCI6MjA5MTE1MzE3N30.wTzjmAjb9utnrgqhBboBmkD8TucUCuDhqm0l_ZW6r6A’;
const _SB_HEADERS = {‘apikey’:_SB_KEY,’Authorization’:’Bearer ‘+_SB_KEY,’Content-Type’:’application/json’};
async function loadFromCloud() {
try {
const r = await fetch(_SB_URL+’/rest/v1/nh_data?id=eq.main’, {headers:_SB_HEADERS});
const d = await r.json();
if(d && d[0]) {
const row = d[0];
return {
session:null, page: »leads »,
users: row.users || JSON.parse(JSON.stringify(USERS_INIT)),
leads: row.leads || JSON.parse(JSON.stringify(LEADS_INIT)),
logs: row.logs || [],
tickets: row.tickets || [],
filterStatus: »all », filterZone: »all », filterTrade: »all »
};
window._cloudLoaded=true;
renderSidebar();
if(S.page===’tickets’)renderPage();
}
} catch(e) { console.log(‘Supabase load error:’, e); }
// Fallback localStorage
try {
const s = localStorage.getItem(‘nh_data’);
if(s) { const d=JSON.parse(s); return {session:null,page: »leads »,users:d.users||JSON.parse(JSON.stringify(USERS_INIT)),leads:(function(){
var remote = d.leads || JSON.parse(JSON.stringify(LEADS_INIT));
var local = S && S.leads ? S.leads : [];
return remote.map(function(rl){
var ll = local.find(function(l){return l.id===rl.id;});
if(ll && ll.crmStatus && ll.crmStatus !== rl.crmStatus) {
rl.crmStatus = ll.crmStatus;
}
return rl;
});
})(),logs:d.logs||[],tickets:d.tickets||[],filterStatus: »all »,filterZone: »all »,filterTrade: »all »}; }
} catch(e) {}
return {session:null,page: »leads »,users:JSON.parse(JSON.stringify(USERS_INIT)),leads:JSON.parse(JSON.stringify(LEADS_INIT)),logs:[],tickets:[],filterStatus: »all »,filterZone: »all »,filterTrade: »all »};
}
let _saveTimer = null;
async function saveToCloud() {
if(!S||!S.session)return;
try {
const body = JSON.stringify({
id:’main’,
users: S.users,
leads: S.leads,
logs: S.logs,
tickets: S.tickets,
updated_at: new Date().toISOString()
});
await fetch(_SB_URL+’/rest/v1/nh_data?id=eq.main’, {
method:’PATCH’,
headers:{…_SB_HEADERS,’Prefer’:’return=minimal’},
body
});
console.log(‘Cloud saved â’);
} catch(e) { console.log(‘Supabase save error:’, e); }
}
// Init depuis localStorage en attendant le cloud
function saveToStorage(){
try{localStorage.setItem(« nh_data »,JSON.stringify({users:S.users,leads:S.leads,logs:S.logs,tickets:S.tickets}));}catch(e){}
var SB= »https://rivjpxutbehnbrprabzb.supabase.co »;
var SK= »eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJpdmpweHV0YmVobmJycHJhYnpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU1NzcxNzcsImV4cCI6MjA5MTE1MzE3N30.wTzjmAjb9utnrgqhBboBmkD8TucUCuDhqm0l_ZW6r6A »;
fetch(SB+ »/rest/v1/nh_data?id=eq.main »,{
method: »PATCH »,
headers:{« Content-Type »: »application/json », »apikey »:SK, »Authorization »: »Bearer « +SK, »Prefer »: »return=minimal »},
body:JSON.stringify({leads:S.leads,users:S.users,logs:S.logs,tickets:S.tickets,updated_at:new Date().toISOString()})
}).catch(function(){});
}
function loadFromStorage() {
try {
const s = localStorage.getItem(‘nh_data’);
if(s) { const d=JSON.parse(s); return {session:null,page: »leads »,users:d.users||JSON.parse(JSON.stringify(USERS_INIT)),leads:d.leads||JSON.parse(JSON.stringify(LEADS_INIT)),logs:d.logs||[],tickets:d.tickets||[],filterStatus: »all »,filterZone: »all »,filterTrade: »all »}; }
} catch(e) {}
return {session:null,page: »leads »,users:JSON.parse(JSON.stringify(USERS_INIT)),leads:JSON.parse(JSON.stringify(LEADS_INIT)),logs:[],tickets:[],filterStatus: »all »,filterZone: »all »,filterTrade: »all »};
}
let S = loadFromStorage();
// Cloud load au dĂ©marrage dĂ©sactivĂ© â chargement au login uniquement
// âââ UTILS ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
let toastTimer;
function toast(txt,type= »ok »){
const t=document.getElementById(« toast »);
const s={ok:{bg: »rgba(52,211,153,.1) »,b: »rgba(52,211,153,.25) »,c: »#34D399″},err:{bg: »rgba(255,59,48,.1) »,b: »rgba(255,59,48,.25) »,c: »#FF3B30″},warn:{bg: »rgba(245,164,31,.1) »,b: »rgba(245,164,31,.25) »,c: »#F5A41F »}};
const x=s[type]||s.ok;
Object.assign(t.style,{background:x.bg,border: »1px solid « +x.b,color:x.c,display: »block »,boxShadow: »0 12px 40px rgba(0,0,0,.6) »});
t.textContent=(type=== »ok »? »â « :type=== »warn »? »â « : »â « )+txt;
clearTimeout(toastTimer); toastTimer=setTimeout(()=>{t.style.display= »none »},3500);
}
function fmt(iso){return new Date(iso).toLocaleDateString(« fr-FR »,{day: »2-digit »,month: »short »,hour: »2-digit »,minute: »2-digit »});}
function fmtLong(iso){return new Date(iso).toLocaleDateString(« fr-FR »,{day: »2-digit »,month: »long »,year: »numeric »});}
function freshBadge(createdAt){
const hrs=(Date.now()-new Date(createdAt))/3600000;
if(hrs<=24) return '24h‘;
if(hrs<=48) return '48h‘;
if(hrs<=72) return '72h‘;
return ‘+72h‘;
}
const CRM={
nouveau:{label: »Nouveau »,color: »var(–blue) »},
contacte:{label: »Contacté »,color: »var(–purple) »},
devis:{label: »Devis envoyé »,color: »var(–gold) »},
rappel:{label: »à rappeler »,color: »var(–or) »},
gagne:{label: »GagnĂ© â »,color: »var(–green) »},
perdu:{label: »Perdu »,color: »var(–red) »},
};
function crmBadge(s){
const x=CRM[s]||{label:s,color: »var(–t3) »};
return `${x.label}`;
}
function ico(n,s=16){
const d={
leads:« ,
users:« ,
map:« ,
log:« ,
crm:« ,
stats:« ,
logout:« ,
plus:« ,
close:« ,
trash:« ,
edit:« ,
phone:« ,
mail:« ,
note:« ,
check:« ,
auto:« ,
chat:« ,
alert:« ,
ticket:« ,
inbox:« ,
};
return `${d[n]|| »}`;
}
function closeModal(id){const m=document.getElementById(id);if(m)m.remove();}
// âââ AUTH âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
document.addEventListener(« keydown »,e=>{
if(e.key=== »Enter »&&document.getElementById(« login-screen »).style.display!== »none ») doLogin();
});
function showForgotPassword(){
const bg=document.createElement(« div »);bg.className= »modal-bg »;bg.id= »forgot-modal »;
bg.innerHTML=’Nouvel administrateur
 »
+ »Créer un compte admin
 » + » »
+ »
 »
+ »Nom complet *
 »
+ »Email *
 »
+ »Téléphone
 »
+ »Mot de passe *
 »
+ » »
+ » »
+ » »
+ »
đ
Mot de passe oublié
Entrez votre email. L\’administrateur vous recontactera sous 24h.
Votre email
Mot de passe actuel
‘;
const cancelBtn=forced? » »:’‘;
const warning=forced?’â ïž Changez votre mot de passe provisoire avant de continuer.
‘: » »; bg.innerHTML=’‘+(forced? »PremiĂšre connexion »: »SĂ©curité »)+’
Changer mon mot de passe
‘+warning+’Nouveau mot de passe
Confirmer
‘+cancelBtn+’
 »
+ »
« ;
document.body.appendChild(m);
}
function confirmReattribuer(leadId){
var sel=document.getElementById(« reassign-select »);
if(!sel)return;
var uid=sel.value;
var u=S.users.find(function(x){return x.id===uid;});
var lead=S.leads.find(function(l){return l.id===leadId;});
if(!lead||!u)return;
var oldName=lead.assignedName|| »â »;
lead.assignedTo=u.id;lead.assignedName=u.name;lead.crmStatus= »nouveau »;
S.logs.unshift({id: »log »+Date.now(),action: »RĂ©attribuĂ© : « +lead.name+ » (« +oldName+ » â « +u.name+ ») »,user:S.session.name,ts:new Date().toISOString()});
saveToStorage();
document.getElementById(« reassign-modal »).remove();
toast(« RĂ©attribuĂ© à « +u.name+ » â »);
renderSidebar();renderPage();
}
async function doLogin(){
// Charger données fraßches depuis Supabase avant de valider
try {
const loaded = await loadFromCloud();
S.users = loaded.users;
S.leads = loaded.leads;
S.logs = loaded.logs;
S.tickets = loaded.tickets;
} catch(e) { console.log(‘Cloud load at login failed, using cache’); }
const email=document.getElementById(« l-email »).value.trim();
const pw=document.getElementById(« l-pw »).value;
const u=S.users.find(x=>x.email===email&&x.password===pw&&x.active!==false);
const err=document.getElementById(« l-err »);
if(!u){err.style.display= »block »;err.textContent=S.users.find(x=>x.email===email)? »Compte suspendu â contactez l’administrateur »: »Email ou mot de passe incorrect »;return;}
err.style.display= »none »;
S.session=u;
document.getElementById(« login-screen »).style.display= »none »;
document.getElementById(« app »).style.display= »flex »;
startNhPoll();
renderSidebar();renderPage();
setTimeout(checkMustChangePw,300);
}
// ââ Auto-refresh Supabase toutes les 10 secondes ââââââââââââââââââââââââââââ
var _nhPollInterval = null;
function startNhPoll(){
if(_nhPollInterval) clearInterval(_nhPollInterval);
_nhPollInterval = setInterval(function(){
if(!S || !S.session) return;
var SB= »https://rivjpxutbehnbrprabzb.supabase.co »;
var SK= »eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJpdmpweHV0YmVobmJycHJhYnpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU1NzcxNzcsImV4cCI6MjA5MTE1MzE3N30.wTzjmAjb9utnrgqhBboBmkD8TucUCuDhqm0l_ZW6r6A »;
fetch(SB+ »/rest/v1/nh_data?id=eq.main »,{
headers:{« apikey »:SK, »Authorization »: »Bearer « +SK}
}).then(function(r){return r.json();}).then(function(data){
if(!data||!data[0]) return;
var remote = data[0];
var leadsChanged = JSON.stringify(remote.leads) !== JSON.stringify(S.leads);
var usersChanged = JSON.stringify(remote.users) !== JSON.stringify(S.users);
if(leadsChanged || usersChanged){
S.leads = remote.leads || S.leads;
S.users = remote.users || S.users;
S.logs = remote.logs || S.logs;
S.tickets = remote.tickets || S.tickets;
// Mettre à jour les crédits de la session
var me = S.users.find(function(u){return u.id===S.session.id;});
if(me) S.session.credits = me.credits;
renderSidebar();
renderPage();
}
if(S.page===’tickets’){var _tc=document.getElementById(‘tickets-container’);if(!_tc||!_tc.innerHTML.trim())renderPage();}
}).catch(function(){});
}, 10000);
}
function doLogout(){
S.session=null;
document.getElementById(« app »).style.display= »none »;
document.getElementById(« login-screen »).style.display= »flex »;
document.getElementById(« l-email »).value= » »;
document.getElementById(« l-pw »).value= » »;
}
// âââ SIDEBAR âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
function renderSidebar(){
const s=S.session;
document.getElementById(« sb-role »).textContent=s.role=== »admin »? »Administrateur »: »Artisan »;
document.getElementById(« sb-name »).textContent=s.name;
const pending=S.leads.filter(l=>l.status=== »pending »).length;
const myLeads=S.leads.filter(l=>l.assignedTo===s.id).length;
const rappel=S.leads.filter(l=>l.assignedTo===s.id&&l.crmStatus=== »rappel »).length;
const openTickets=S.tickets.filter(t=>t.status=== »open »).length;
const adminNav=[
{id: »leads »,label: »Leads entrants »,icon: »leads »,badge:pending||0},
{id: »clients »,label: »Artisans & crédits »,icon: »users »},
{id: »admins »,label: »Administrateurs »,icon: »admin »},
{id: »stats »,label: »Statistiques »,icon: »stats »},
{id: »logs »,label: »Journal »,icon: »log »},
];
const clientNav=[
{id: »leads »,label: »Leads disponibles »,icon: »map »},
{id: »crm »,label: »Mon CRM »,icon: »crm »,badge:rappel||0},
{id: »my-leads »,label: »Mes contacts »,icon: »leads »,badge:myLeads||0},
];
const nav=s.role=== »admin »?adminNav:clientNav;
document.getElementById(« sb-nav »).innerHTML=nav.map(item=>`
`).join(«  »);
const cw=document.getElementById(« sb-credits »);
const sb=document.getElementById(« support-sidebar-btn »);
if(s.role=== »client »){
cw.style.display= »block »;
const me=S.users.find(u=>u.id===s.id)||s;
const n=me.credits||0;
document.getElementById(« sb-credit-n »).textContent=n;
document.getElementById(« sb-credit-n »).style.color=n>0? »var(–or) »: »var(–red) »;
document.getElementById(« sb-credit-warn »).style.display=n===0? »block »: »none »;
sb.style.display= »flex »;
} else {
cw.style.display= »none »;
sb.style.display= »none »;
}
}
function setPage(p){S.page=p;renderSidebar();renderPage();}
// âââ ROUTER âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
function renderPage(){
const m=document.getElementById(« main »);
if(S.page===’admins’&&S.session&&S.session.role===’admin’){renderAdmins();return;}
if(S.page=== »leads ») if(S.session&&S.session.role===’admin’){renderLeads(m);}else{renderMyLeads();}
else if(S.page=== »clients ») renderClients(m);
else if(S.page=== »logs ») renderLogs(m);
else if(S.page=== »stats ») renderStats(m);
else if(S.page=== »crm ») renderCRM(m);
else if(S.page=== »my-leads ») renderMyLeads(m);
else if(S.page=== »support ») renderSupport(m);
else if(S.page=== »tickets »||S.page=== »support »){
if(S.session&&S.session.role!== »admin »){renderSupport(m);}
else{m.innerHTML=’RĂ©attribuer
 »
+ »« +(lead.name|| »â »)+ »
 » + »Choisir un artisan
 »
+ » »+opts+ » »
+ » »
+ » »
+ » »
+ »
📧
Tickets support
Les tickets sont gérés par email.
Leads entrants
${S.leads.filter(l=>!l.deleted).length} contacts ${pending>0?’‘+pending+’ en attente‘: »}
Toutes zones${zoneOpts}
Tous métiers${tradeOpts}
`: »}
${filtered.map(l=>leadCard(l,S)).join(«  »)||’
${trashed.length>0?`
Aucun lead
‘}
đïž Corbeille (${trashed.length})
${tl.name|| »â »}
SupprimĂ© par ${tl.deletedBy|| »â »}
${ico(« check »,13)} AttribuĂ© Ă ${aUser?aUser.name: »â »}
`;
} else {
const opts=S.users.filter(u=>u.role=== »client »&&u.active!==false).map(u=>`<option value="${u.id}" ${u.credits${u.name} â ${u.credits} crĂ©dit${u.credits!==1? »s »: » »}${normTrade(u.trade)===normTrade(lead.trade)&&String(u.zone).substring(0,2)===String(lead.zone).substring(0,2)? » â »: » »}`).join(«  »);
action=`
${lead.assignedTo ? `` :  »}
${elig.length>0?``: » »}
${elig.length===0?`â Aucun artisan Ă©ligible
`: » »} `; } } // Boutons admin : corbeille + dĂ©sattribuer var trashBtn2 = S.session && S.session.role === « admin » ? ‘‘ : «  »; var unassignBtn2 = (S.session && S.session.role === « admin » && lead.assignedTo) ? ‘‘ : «  »; var reassignBtn = (lead.assignedTo && s.role === « admin ») ? ‘‘ : «  »; const reclamBtn=isMine?``: » »; return `${freshBadge(lead.createdAt)}${isA?crmBadge(lead.crmStatus): » »}
${reveal?lead.name:`Contact masqué`}
${lead.trade}
Zone ${lead.zone}
${lead.project}
${reveal?`${ico(« phone »,12)} ${lead.phone}
${ico(« mail »,12)} ${lead.email}
${lead.comment ? `${ico(« mail »,12)} ${lead.email}
Budget : ${lead.budget}
${lead.comment}
` : «  »}${lead.source && lead.source !== « Borne de recharge France » ? `${lead.source}
` : «  »}`:`Coordonnées visibles aprÚs attribution
`}
${fmt(lead.createdAt)}
${action}${reclamBtn}${reassignBtn}${trashBtn2}${unassignBtn2}
${n}
${x.label}
${l.name}
${l.budget}
${freshBadge(l.createdAt)}
${l.notes&&l.notes.length?`${ico(« note »,10)} ${l.notes.length} note${l.notes.length>1? »s »: » »}
`: » »}
Vide
`;
return `
${x.label}
${colLeads.length}
${cards}
Suivi commercial
Mon CRM
${myLeads.length} contact${myLeads.length!==1? »s »: » »} · glisser-déposer pour changer le statut
${statRow}
${kanban}
Glissez une carte pour changer son statut · Cliquez pour ouvrir la fiche
${n.text}
${fmt(n.at)}
Aucune note
`;
const isPerdu=lead.crmStatus=== »perdu »;
const bg=document.createElement(« div »);
bg.className= »modal-bg »; bg.id= »lead-detail-modal »;
bg.innerHTML=`
Fiche contact
${lead.name}
${lead.trade}
Zone ${lead.zone}
${freshBadge(lead.createdAt)}
${ico(« phone »,14)}${lead.phone}
${ico(« mail »,14)}${lead.email}
${lead.project}
Budget : ${lead.budget}
Statut CRM
${statusOpts}
đĄ Vous pouvez aussi glisser la carte directement dans le Kanban
Notes & suivi
${lead.notes?lead.notes.length:0} note${lead.notes&&lead.notes.length!==1? »s »: » »}
${notesHtml}
${isPerdu?``: » »}
Support
Aucun ticket en cours
‘; return; } el.innerHTML=tickets.map(function(t){ var replies=t.replies||[]; var sc=t.status===’closed’?’#4ade80′:’#f5a41f’; var sl=t.status===’closed’?’Resolu’:’Ouvert’; var ini=(t.userName||’A’); var hist=’‘;
hist+=’
‘;
var zone= »;
if(t.status!==’closed’){
if(isAdmin||t.userId===S.session.id){
zone+=’‘;
hist+=’
‘;
replies.forEach(function(r){
var adm=r.role===’admin’;
var dir=adm?’flex-direction:row-reverse’: »;
var ta=adm?’text-align:right’: »;
var bg=adm?’rgba(26,36,114,.45)’:’rgba(255,255,255,.07)’;
var br=adm?’10px 0 10px 10px’:’0 10px 10px 10px’;
var bd=adm?’rgba(100,130,255,.3)’:’rgba(255,255,255,.1)’;
var abg=adm?’#1a2472′:’#374151′;
hist+=’‘+ini[0].toUpperCase()+’
‘;
hist+=’‘+ini+’
‘;
hist+=’‘+t.message+’
‘;
hist+=’
‘;
});
hist+=’‘+((r.author||’?’)[0].toUpperCase())+’
‘;
hist+=’‘+(r.author||’Support’)+’
‘;
hist+=’‘+r.text+’
‘;
zone+= »;
zone+=’‘;
zone+=’
‘;
}
if(isAdmin){
zone+=’‘;
}
} else {
zone=’Ticket resolu
‘;
}
return ‘‘
+’
‘;
}).join( »);
}
function closeTicket(ticketId){
var t=(S.tickets||[]).find(function(x){return x.id===ticketId;});
if(!t)return;
t.status=’closed’;t.closedAt=new Date().toISOString();
S.logs.unshift({id:’log_’+Date.now(),action:’Ticket resolu : ‘+t.subject,user:S.session.name,ts:new Date().toISOString()});
saveToCloud(S);toast(‘Ticket marque comme resolu’);renderSidebar();renderPage();
}
function replyTicket(ticketId){
var t=(S.tickets||[]).find(function(x){return x.id===ticketId;});
if(!t)return;
var inp=document.getElementById(‘rp_’+ticketId);
if(!inp)return;
var txt=(inp.value|| »).trim();
if(!txt)return;
if(!t.replies)t.replies=[];
t.replies.push({id:’r_’+Date.now(),text:txt,author:S.session.name,role:S.session.role,userId:S.session.id,at:new Date().toISOString()});
S.logs.unshift({id:’log_’+Date.now(),action:’Message support : ‘+t.subject,user:S.session.name,ts:new Date().toISOString()});
saveToCloud(S);
var subj=S.session.role===’admin’?'[NH Support] Reponse admin – ‘+t.subject:'[NH Support] Message artisan – ‘+t.subject;
fetch(‘https://formspree.io/f/xeepbayl’,{method:’POST’,headers:{‘Content-Type’:’application/json’},body:JSON.stringify({_subject:subj,message:txt,artisan:S.session.name,ticket:t.subject})}).catch(function(){});
toast(‘Message envoye’);renderSidebar();if(S.page===’tickets’)renderTickets(document.getElementById(‘main’));else renderPage();
}
function renderMyLeads(){
var myLeads = S.leads.filter(function(l){ return l.assignedTo === S.session.id && l.crmStatus !== « gagne » && l.crmStatus !== « perdu »; });
var archived = S.leads.filter(function(l){ return l.assignedTo === S.session.id && (l.crmStatus === « gagne » || l.crmStatus === « perdu »); });
var avail = []; // Leads disponibles masques cote artisan â visibles apres attribution uniquement
var credits = S.session.credits || 0;
var out = «  »;
// Leads disponibles
if(avail.length > 0){
out += « ‘
+’‘+t.subject+’‘
+’‘+sl+’‘
+’
‘
+’‘+replies.length+’ message’+(replies.length>1?’s’: »)+’
‘
+hist+zone+’Disponibles ( » + avail.length + « )
« ;
avail.forEach(function(lead){
var tradeSpan = «  » + (lead.trade|| »â ») + « « ;
var zoneSpan = « ZONE  » + (lead.zone|| »â ») + « « ;
var claimBtn = credits > 0
? «  »
: « Credits insuffisants â contactez le support
« ;
out += « « ;
out += «Â
« ;
});
}
// Leads attribues â coordonnees visibles + bouton CRM
if(myLeads.length > 0){
out += « « ;
out += « « ;
out += «  » + (lead.age|| »â ») + « h
« ;
out += « Nom confidentiel
« ;
out += «  » + tradeSpan + zoneSpan + «Â
 » + (lead.project|| » ») + «Â
« ;
out += « đ Coordonnees visibles apres attribution
« ;
out += claimBtn + « Mes contacts ( » + myLeads.length + « )
« ;
myLeads.forEach(function(lead){
var sc = lead.crmStatus === « gagne » ? « #22c55e » : lead.crmStatus === « perdu » ? « #ef4444 » : lead.crmStatus === « contacte » ? « #3b82f6 » : « var(–or) »;
var tradeSpan = «  » + (lead.trade|| »â ») + « « ;
var zoneSpan = « ZONE  » + (lead.zone|| »â ») + « « ;
var budgetLine = lead.budget ? « Budget :  » + lead.budget + «Â
 » : «  »;
var statusSel = «  »;
statusSel += « Nouveau »;
statusSel += « Contacte »;
statusSel += « Gagne »;
statusSel += « Perdu »;
statusSel += «  »;
var crmBtn = « « ;
out += « « ;
out += «Â
« ;
});
}
if(!avail.length && !myLeads.length){
out = « « ;
out += «Â
« ;
out += «  » + (lead.name|| »Contact ») + «Â
« ;
out += «  » + (lead.crmStatus|| »nouveau »).toUpperCase() + «  » + tradeSpan + zoneSpan + «Â
« ;
out += «  » + (lead.project|| » ») + «Â
« ;
out += « « ;
out += «Â
« ;
out += « đ  » + (lead.phone|| »â ») + «Â
« ;
out += « âïž Â Â» + (lead.email|| »â ») + «Â
« ;
out += budgetLine + «  » + crmBtn + statusSel + «Â
« ;
out += « Aucun contact disponible pour le moment.
« ;
}
document.getElementById(« main »).innerHTML = «  »
+ «  »
+ out + «Â
« ;
}
function renderClients(el){
const clients=S.users.filter(function(u){return u.role!== »admin »;}).filter(u=>u.role=== »client »);
const cards=clients.map(c=>{
const cl=S.leads.filter(l=>l.assignedTo===c.id).length;
const gagne=S.leads.filter(l=>l.assignedTo===c.id&&l.crmStatus=== »gagne »).length;
const low=c.credits<=2; const inactive=c.active===false;
return `Leads disponibles
 »
+ «  » + myLeads.length +  » contact » + (myLeads.length !== 1 ? « s » : «  ») +  » attribue » + (myLeads.length !== 1 ? « s » : «  ») + «Â
${c.name}
${inactive?`Suspendu`: » »}
${c.email}
${c.trade}
Zone ${c.zone}
${c.credits}
crédit${c.credits!==1? »s »: » »}
${cl}
reçus
0? »var(–green) »: »var(–red) »} »>${inactive? »OFF »:c.credits>0? »Actif »: »Ăpuisé »}
statut
${gagne}
gagnés
Crédits
${[10,30,60].map(n=>``).join(«  »)}
${[5,10].map(n=>``).join(«  »)}
Gestion abonnements
Artisans & Crédits
${clients.length} artisans · 1 crédit = 1 contact exclusif
${cards}
Vue d’ensemble
Statistiques
${[[« Total leads »,total, »var(–t) »],[« En attente »,pending, »var(–gold) »],[« AttribuĂ©s »,assigned, »var(–or) »],[« GagnĂ©s »,gagne, »var(–green) »],[« Perdus »,perdu, »var(–red) »],[« CrĂ©dits actifs »,totalCredits, »var(–blue) »],[« Artisans »,clients.length, »var(–purple) »],[« Taux attribution »,total>0?Math.round(assigned/total*100)+ »% »: »0% », »var(–or) »],[« Tickets ouverts »,S.tickets.filter(t=>t.status=== »open »).length, »var(–blue) »]].map(([l,v,c])=>``).join(«  »)}
${v}
${l}
Répartition par métier
${trades.map(({t,n})=>{const pct=Math.round(n/total*100)||0;return`${t}${n} (${pct}%)
Performance artisans
${clients.map(c=>{const tot=S.leads.filter(l=>l.assignedTo===c.id).length;const g=S.leads.filter(l=>l.assignedTo===c.id&&l.crmStatus=== »gagne »).length;const tx=tot>0?Math.round(g/tot*100):0;return`${c.name}
${c.trade} · Zone ${c.zone}
20? »var(–green) »:tx>10? »var(–gold) »: »var(–t) »} »>${tx}%
${g}/${tot}
${l.action}
${fmt(l.at)}
Aucune activité
`;
el.innerHTML=`Traçabilité
Journal d’activitĂ©
${S.logs.length} événement${S.logs.length!==1? »s »: » »}
Nouveau client
Ajouter un artisan
Nom *
Téléphone
Email (login) *
Mot de passe *
Métier${TRADES.map(t=>`${t}`).join(«  »)}
Zone (CP) *
Crédits initiaux
RĂŽleArtisan / ClientAdministrateur
Modifier ${u.name}
Nom
Nouveau mdp
Métier${TRADES.map(t=>`${t}`).join(«  »)}
Zone
Nouveau contact
Ajouter un lead
Nom *
Téléphone *
Email
Zone / CP *
Métier${TRADES.map(t=>`${t}`).join(«  »)}
Budget estimé
Description du projet
đ§
Ouvrir un ticket support
Notre équipe vous répondra par email dans les plus brefs délais.
‘; card.appendChild(iconWrap); var lblS=document.createElement(‘label’); lblS.textContent=’Sujet *’; lblS.style.cssText=’display:block;color:rgba(255,255,255,0.7);font-size:13px;font-weight:600;margin-bottom:8px;margin-top:0;’; var sel=document.createElement(‘select’); sel.id=’tkSubject’; sel.style.cssText=’width:100%;background:rgba(30,30,50,0.95);border:1px solid rgba(255,255,255,0.12);color:#fff;padding:11px 14px;border-radius:10px;font-size:14px;outline:none;cursor:pointer;margin-bottom:16px;’; [‘– Choisir un sujet –‘,’ProblĂšme de connexion’,’Question sur mes leads’,’ProblĂšme de facturation’,’Demande de modification de compte’,’Lead incorrect ou mauvaise qualitĂ©’,’Autre’].forEach(function(t,i){ var o=document.createElement(‘option’); o.textContent=t;o.value=i===0? »:t; sel.appendChild(o); }); card.appendChild(lblS);card.appendChild(sel); var lblM=document.createElement(‘label’); lblM.textContent=’Votre message *’; lblM.style.cssText=’display:block;color:rgba(255,255,255,0.7);font-size:13px;font-weight:600;margin-bottom:8px;’; var ta=document.createElement(‘textarea’); ta.id=’tkMsg’;ta.rows=6;ta.placeholder=’DĂ©crivez votre problĂšme en dĂ©tail…’; ta.style.cssText=’width:100%;background:rgba(30,30,50,0.95);border:1px solid rgba(255,255,255,0.12);color:#fff;padding:12px 14px;border-radius:10px;font-size:14px;resize:vertical;outline:none;font-family:inherit;box-sizing:border-box;margin-bottom:16px;’; card.appendChild(lblM);card.appendChild(ta); var info=document.createElement(‘div’); info.style.cssText=’background:rgba(249,115,22,0.08);border:1px solid rgba(249,115,22,0.2);border-radius:10px;padding:12px 16px;margin-bottom:20px;display:flex;gap:10px;align-items:flex-start;’; info.innerHTML=’đ§Notre Ă©quipe vous rĂ©pondra Ă ‘+(art.email|| »)+’
‘; card.appendChild(info); var btn=document.createElement(‘button’); btn.id=’tkBtn’; btn.textContent=’Envoyer le ticket’; btn.style.cssText=’width:100%;background:linear-gradient(135deg,#f97316,#ea580c);color:#fff;border:none;padding:14px;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;’; btn.onclick=function(ev){submitTicket(ev);}; card.appendChild(btn); wrap.appendChild(card); m.innerHTML= »; m.appendChild(wrap); } async function submitTicket(ev){ if(ev){ev.preventDefault();ev.stopPropagation();} var btn=document.getElementById(‘tkBtn’); var subj=document.getElementById(‘tkSubject’).value; var msg=document.getElementById(‘tkMsg’).value; if(!subj||!msg){toast(‘Remplissez tous les champs’,’err’);return;} btn.disabled=true;btn.textContent=’Envoi en cours…’; var art=S.session||{}; try{ var r=await fetch(‘https://formspree.io/f/mkokwgal’,{ method:’POST’, headers:{‘Content-Type’:’application/json’,’Accept’:’application/json’}, body:JSON.stringify({email:art.email|| »,_replyto:art.email|| »,name:art.name||art.email|| »,trade:art.trade|| »,zone:art.zone|| »,subject:'[NH Support] ‘+subj,message:msg,date:new Date().toLocaleString(‘fr-FR’)}) }); if(r.ok){ toast(‘Ticket envoyĂ© ! Nous vous rĂ©pondons par email.’); setPage(‘leads’); }else{throw new Error();} }catch(e){ toast(‘Erreur envoi. Contactez support@nouvelhorizon.online’,’err’); btn.disabled=false;btn.textContent=’Envoyer le ticket’; } }