IMMOXIA — présentation
IMMOXIA
L’immobilier en mouvement.
ESPACE SÉCURISÉ

Bienvenue sur IMMOXIA

Tableau de bord

IMMOXIA • PIVOT SERVICES
🔎
Synchronisation serveur🔒 Administrateur
PIVOT SERVICES•IMMOBILIER•GESTION & SERVICESEnsemble, construisons votre patrimoine !
A
Administrateur Session active
PIVOT SERVICES

IMMOXIA — Centre de pilotage

Immobilier • Gestion • Services • Paiements

Ensemble, construisons votre patrimoine !
Bons de réservation
0
Suivi IMMOXIA
Commissions en attente
0 FCFA
À contrôler
Remises en validation
0
Double validation
Objectifs CTI
0%
À calculer

Accès rapides

Règle de sécurité tarifaire

Tarif officiel calculé automatiquement
Remise soumise au superviseur
Deuxième validation par le responsable opérationnel
Bon réduit interdit sans double validation
0
Utilisateurs
0
Clients / prospects
0
Transactions
0
Factures
0
Reçus
🏢
0
Biens immobiliers
👥
0
Clients & prospects
📋
0
Réservations
💰
0
Commissions
💼
0
Transactions
🎯
0
Agents / CTI

📥 Exports Excel

Exportez les données centrales IMMOXIA dans des fichiers compatibles Excel. Les exports sont générés à partir des données synchronisées du serveur.

🏢 Biens

Liste des biens, références, statut et agent responsable.

👥 Agents / CTI

Comptes, rôles et informations de suivi autorisées.

👤 Clients

Clients et prospects accessibles selon les droits.

📅 Réservations

Réservations et montants enregistrés.

💰 Commissions

Total, taux, payé et reste à payer par agent.

📊 Rapport complet

Exports complets des principales données centrales.

🔄 Données synchronisées

Chaque export interroge l’API centrale au moment de la génération.

Prêt.

📊 KPI — ORACLE

Le module KPI extrait les données d’IMMOXIA, calcule les objectifs et prépare le fichier Excel destiné à ORACLE.

Cycle actuel
Lun → Sam
6 jours ouvrés
Réalisation globale
0%
Objectif non atteint
Agents suivis
0
Export ORACLE
Samedi
Données visibles dans ORACLE le lundi suivant

📅 Extraction automatique du samedi

Le fichier Excel KPI destiné à ORACLE est généré automatiquement le samedi, une seule fois par samedi. La génération des reçus de caisse ne déclenche pas cet export.

📆 Comptabilisation mensuelle — chaque 25

Le 25 de chaque mois, IMMOXIA prépare un second fichier Excel séparé contenant les 8 feuilles : Synthèse, Performance CTI, Transactions, Encaissements, Factures, Clients & prospects, Biens et Audit.

Suivi individuel des CTI

CTIObjectifRéalisation% exactStatutActions

Règles KPI

Les pourcentages sont calculés exactement à partir de la réalisation et de l’objectif. Rouge = objectif non atteint, jaune = objectif à moyen, vert = objectif atteint. Le cycle est continu sur 6 jours ouvrés, lundi à samedi. Le KPI extrait le samedi et le fichier est destiné à ORACLE pour le cycle suivant.

⚙️ Compte et paramètres KPI

Compte KPI sécurisé
Identifiant, mot de passe, rôle et permissions dédiés
Actif

🎯 Objectifs des agents

CTIObjectif cycleRéalisation%StatutActions

📋 Nouveau bon de réservation

Vous pouvez saisir plusieurs biens/adresses.
Tarif officiel
0 FCFA
Remise demandée
Motif obligatoire si remise
Total à payer
0 FCFA
Statut : En préparation

Réservations enregistrées

RéférenceClientBiensServicePrixValidationActions

🧾 Facturation automatique

Les factures reçoivent automatiquement une référence unique au format FACT-2026001, puis FACT-2026002, FACT-2026003… La séquence est conservée sur l’appareil et repart à 001 à chaque nouvelle année.

Nouvelle facture

Prochaine référence

FACT-2026001
Numérotation automatique et chronologique
FormatFACT-AAAA + 3 chiffres
ExempleFACT-2026001 → FACT-2026002
Nouvelle annéeFACT-2027001

Historique des factures

N° factureDateClientObjetMontantActions

📅 Bon de visite

Tarif officiel : 2 500 FCFA
Basic — 1 bien
Validation requise avant génération.

Demandes de visite

RéférenceClientServicePrixValidationActions

💰 Commissions CTI

CTIPrestationMontant encaisséTauxCommissionStatut

BON DE COMMISSION

IMMOBILIER
N° BC-IMMO-2026-0001
Signatures après impression
1. INFORMATIONS GÉNÉRALES
DépartementOpérations
ServiceImmobilier / CTI
ResponsableResponsable Immobilier
Agent demandeurCTI
2. INFORMATIONS DU CTI
MatriculeÀ renseigner
Nom et prénomÀ renseigner
TéléphoneÀ renseigner
EmailÀ renseigner
3. DÉTAIL DE LA PRESTATION
Type d'opérationVisite / Réservation / Contrat
Référence dossierDOS-IMMO-2026-0001
Montant encaissé0 FCFA
Taux de commission25 %
Commission à payer0 FCFA
4. STATUT DE LA COMMISSION
En attente → Vérifiée → Validée → Payable → Payée
5. SIGNATURES — APRÈS IMPRESSION
Le CTI
Nom / signature
Le Superviseur Immobilier
Nom / signature
La Caisse
Signature + cachet
Le Responsable des Opérations
Nom / signature
Ensemble, construisons votre patrimoine !

Options du menu

BON DE SORTIE

IMMOBILIER
N° BS-IMMO-2026-0001
Document prêt pour validation
1. INFORMATIONS DE LA SORTIE
DemandeurÀ renseigner
ServiceImmobilier
MotifÀ renseigner
Montant0 FCFA
2. VALIDATION
En attente → Vérifiée → Validée
3. SIGNATURES — APRÈS IMPRESSION
Demandeur
Signature
Superviseur
Signature
Caisse
Signature + cachet
Responsable
Signature
PIVOT SERVICES — Immobilier

Options du menu

💳 Tarifs officiels & remises contrôlées

L'agent choisit une prestation ; IMMOXIA calcule le tarif officiel. Une remise doit suivre Agent → Superviseur → Responsable opérationnel. Sans double validation, le bon reste au tarif officiel.

ServiceFormuleTarif officielInclus / descriptionRemise

🕘 Historique / traçabilité

Aucune opération enregistrée.

🏢 Biens

RéférenceAdresseTypePrixStatutPropriétaireActions

👥 Nouveau client / prospect

Recherchez l’adresse sur Google Maps ou utilisez la position de l’appareil. L’adresse et les coordonnées seront enregistrées dans la fiche client.

📌 Pipeline CRM

ClientStatutCTIAdresseRelanceActions

🎯 Prospection CTI

DateProspectActionRésultatCompte renduActions

📍 Terrain

Activités enregistrées

DateAgentLieuClientActivitéRésultatActions

📘 Protocole

Journal du protocole

DateRéférenceÉtapeStatutParObservationActions

💼 Mes transactions

Chaque agent ne voit et ne modifie que ses propres transactions.

Transactions de mon compte

DateClientBienTypeMontantStatutActions

⚙️ Centre opérationnel

Supervision des requêtes, validations et opérations extra-ville.

🧩 Modules avancés

Modules complémentaires IMMOXIA.

📄 Documents & génération

RéférenceTypeDateCréé parStatut

📊 Statistiques

Les statistiques sont alimentées par les données IMMOXIA.

🧾 Reçus de caisse

Chaque reçu reçoit automatiquement une référence unique au format REC-2026001, puis REC-2026002, REC-2026003, etc.

Derniers reçus

RéférenceDateBénéficiaireMotifMontantAction

REÇU DE CAISSE

PIVOT SERVICES — IMMOBILIER
N° REC-2026001
INFORMATIONS DU REÇU
Date—
Bénéficiaire—
Téléphone—
Mode de paiement—
Motif / prestation—
Montant0 FCFA
CERTIFICATION
Reçu établi par la Caisse de PIVOT SERVICES. Référence unique : REC-2026001.
SIGNATURE
Le bénéficiaire
Signature
La Caisse
Signature + cachet
Ensemble, construisons votre patrimoine !

💬 Messagerie & réunions

Écrivez directement à un agent, créez des groupes et organisez des réunions vidéo avec Jitsi Meet.

✉️ Nouveau message

👥 Créer un groupe

Maintenez plusieurs choix pour sélectionner plusieurs agents.

🗨️ Conversations

📹 Créer une réunion Jitsi

📅 Mes réunions

DateTitreOrganisateurParticipantsAction

👤 Utilisateurs & permissions

Le prénom, le nom, le département et le service sont enregistrés. L’identifiant est généré automatiquement selon le service/département.

L’identifiant est proposé automatiquement à partir du prénom et du nom. Vous pouvez le modifier.
PrénomNomIdentifiantDépartementServiceE-mailRôleÉtatAction

🔐 Sécurité & connexion

Gestion des rôles côté interface
Journal d'audit local
API centrale disponible dans le dossier server/
Sessions serveur à activer en production
Ne pas utiliser le mode local comme base de données de production

🌐 API IMMOXIA

Le projet contient maintenant une API Node.js sans dépendance externe, avec stockage JSON côté serveur, authentification par session, rôles, CRM, prospection et journal d'audit.

🚀 IMMOXIA — Production complète

Centre de gestion intégré : contrats, paiements, caisse, commissions, notifications et rapports.

Contrats
0
Paiements
0 FCFA
Notifications
0
Rapports
0

📄 Contrat immobilier

💳 Paiement / encaissement

🔔 Notifications & tâches

DateMessagePrioritéDestinataireÉtat

📊 Rapports de direction

📋 Historique contrats / paiements

TypeRéférenceClientMontantStatut
'; const blob=new Blob([doc],{type:'application/vnd.ms-excel'}); const url=URL.createObjectURL(blob), a=document.createElement('a'); a.href=url; a.download='IMMOXIA_KPI_ORACLE_'+now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-'+String(now.getDate()).padStart(2,'0')+'.xls'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url),1000); immoxiaStorage.setItem('immoxia_last_kpi_export',now.toISOString().slice(0,10)); log(auto?'Export KPI automatique du samedi — ORACLE':'Export KPI manuel — ORACLE'); } $('exportKpi').onclick=()=>exportKpiExcel(false); function exportExcelRows(filename, rows, sheet='IMMOXIA'){ const data=Array.isArray(rows)?rows:[]; const keys=[...new Set(data.flatMap(x=>x&&typeof x==='object'?Object.keys(x):[]))]; const esc=v=>String(v??'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); const head=''+keys.map(k=>''+esc(k)+'').join('')+''; const body=data.map(x=>''+keys.map(k=>''+esc(x?.[k])+'').join('')+'').join(''); const xml=`

${esc(sheet)}

${head}${body}
`; const blob=new Blob([xml],{type:'application/vnd.ms-excel;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a'); a.href=url;a.download=filename.endsWith('.xls')?filename:filename+'.xls';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000); } async function fetchExportResource(resource){const v=await apiFetch(resource);return Array.isArray(v)?v:(Array.isArray(v?.data)?v.data:[])} async function exportResource(resource,label){try{$('exportStatus').textContent='Préparation de l’export '+label+'…';const rows=await fetchExportResource(resource);exportExcelRows('IMMOXIA_'+resource.toUpperCase()+'_'+new Date().toISOString().slice(0,10)+'.xls',rows,label);$('exportStatus').textContent=rows.length+' enregistrement(s) exporté(s) : '+label+'.';log('Export Excel — '+label)}catch(e){$('exportStatus').textContent='Échec de l’export : '+e.message;alert(e.message)}} $('exportBiens')?.addEventListener('click',()=>exportResource('biens','Biens immobiliers')); $('exportAgents')?.addEventListener('click',()=>exportResource('users','Agents / CTI')); $('exportClients')?.addEventListener('click',()=>exportResource('clients','Clients & prospects')); $('exportReservations')?.addEventListener('click',()=>exportResource('reservations','Réservations')); $('exportCommissions')?.addEventListener('click',()=>exportResource('commissions','Commissions')); $('exportAllExcel')?.addEventListener('click',async()=>{try{$('exportStatus').textContent='Préparation du rapport complet…';for(const [r,l] of [['biens','Biens'],['users','Agents / CTI'],['clients','Clients & prospects'],['reservations','Réservations'],['commissions','Commissions']]){const rows=await fetchExportResource(r);exportExcelRows('IMMOXIA_'+r.toUpperCase()+'_'+new Date().toISOString().slice(0,10)+'.xls',rows,l)}$('exportStatus').textContent='Rapport complet exporté en fichiers Excel séparés.'}catch(e){$('exportStatus').textContent='Échec : '+e.message;alert(e.message)}}); function saveTransaction(){const s=txSession();if(!s){alert('Connectez-vous avant d’enregistrer une transaction.');return}const client=$('txClient').value.trim(),amount=Number($('txAmount').value)||0;if(!client)return alert('Le client est obligatoire.');if(amount<=0)return alert('Le montant doit être supérieur à 0 FCFA.');const arr=txList();if(txEditId){const idx=arr.findIndex(x=>x.id===txEditId);if(idx<0)return alert('Transaction introuvable.');if(!txIsAdmin()&&arr[idx].ownerId!==txOwnerId())return alert('Accès refusé : cette transaction appartient à un autre compte.');arr[idx]={...arr[idx],client,bien:$('txBien').value.trim(),type:$('txType').value,amount,status:$('txStatus').value,date:$('txDate').value||new Date().toISOString().slice(0,10),updatedAt:new Date().toISOString()};txSet(arr);log('Modification transaction '+arr[idx].id);alert('Transaction modifiée.')}else{const item={id:'TX-'+Date.now().toString(36).toUpperCase(),ownerId:txOwnerId(),ownerLogin:s.login,ownerName:s.name,client,bien:$('txBien').value.trim(),type:$('txType').value,amount,status:$('txStatus').value,date:$('txDate').value||new Date().toISOString().slice(0,10),createdAt:new Date().toISOString()};arr.unshift(item);txSet(arr);log('Transaction créée '+item.id+' par '+s.login);alert('Transaction enregistrée dans votre compte.')}txReset();renderTransactions()} window.editTransaction=function(id){const x=txList().find(v=>v.id===id);if(!x)return;if(!txIsAdmin()&&x.ownerId!==txOwnerId())return alert('Accès refusé : cette transaction appartient à un autre compte.');$('txClient').value=x.client||'';$('txBien').value=x.bien||'';$('txType').value=x.type||'Vente';$('txAmount').value=x.amount||0;$('txStatus').value=x.status||'En cours';$('txDate').value=x.date||'';txEditId=id;$('txSave').textContent='💾 Enregistrer les modifications';$('txCancel').style.display='inline-flex';} window.deleteTransaction=function(id){const arr=txList(),idx=arr.findIndex(v=>v.id===id);if(idx<0)return;const x=arr[idx];if(!txIsAdmin()&&x.ownerId!==txOwnerId())return alert('Accès refusé : cette transaction appartient à un autre compte.');if(!confirm('Supprimer cette transaction ?'))return;arr.splice(idx,1);txSet(arr);log('Suppression transaction '+id);renderTransactions();alert('Transaction supprimée.')} $('txSave')?.addEventListener('click',saveTransaction);$('txCancel')?.addEventListener('click',txReset);$('txDate')&&( $('txDate').value=new Date().toISOString().slice(0,10)); renderTransactions(); const oldUpdateSessionUI=updateSessionUI;updateSessionUI=function(){oldUpdateSessionUI();renderTransactions();}; $('addAgentBtn')?.addEventListener('click',()=>addAgent());$('agentSaveBtn')?.addEventListener('click',saveAgentModal);$('agentCloseBtn')?.addEventListener('click',closeAgentModal);$('agentModal')?.addEventListener('click',e=>{if(e.target.id==='agentModal')closeAgentModal()});document.addEventListener('click',e=>{const b=e.target.closest('.user-edit');if(b)editUser(Number(b.dataset.index));const d=e.target.closest('.user-delete');if(d)deleteUser(Number(d.dataset.index));}); '); w.document.close(); }; window.printReceipt=function(){window.print();}; window.shareReceipt=function(mode){const r=window.currentReceipt; if(!r){alert('Générez ou sélectionnez un reçu.');return;} const text=`Reçu ${r.ref}\nBénéficiaire : ${r.client}\nMotif : ${r.reason||'—'}\nMontant : ${money(r.amount)}`; if(mode==='whatsapp'){location.href='https://wa.me/?text='+encodeURIComponent(text);} else if(navigator.share){navigator.share({title:r.ref,text}).catch(()=>{});} else navigator.clipboard?.writeText(text).then(()=>alert('Détails copiés.'));}; document.addEventListener('DOMContentLoaded',()=>{refreshNext();renderRows(); const a=get(); if(a[0])render(a[0]);}); })(); '); w.document.close(); w.focus(); } window.printDoc=function(kind){const doc=typeof getDoc==='function'?getDoc(kind):null;if(!doc){alert('Document introuvable.');return}printHtml('IMMOXIA — '+(typeof docTitle==='function'?docTitle(kind):'Document'),doc.outerHTML)}; window.printInvoice=function(i){const arr=typeof invoiceList==='function'?invoiceList():[],inv=arr[i]||(typeof currentInvoiceFromForm==='function'?currentInvoiceFromForm():null);if(!inv){alert('Aucune facture à imprimer.');return}const html=typeof invoiceDocHtml==='function'?invoiceDocHtml(inv):'';if(!html){alert('Facture introuvable.');return}printHtml('Facture '+inv.ref,'
'+html+'
')}; window.printReceipt=function(){const d=document.getElementById('receiptDoc');if(!d){alert('Reçu introuvable.');return}printHtml('Reçu de caisse',d.outerHTML)}; window.deleteDoc=function(kind){ if(kind==='reservation'){if(!confirm('Supprimer le brouillon de réservation ?'))return;['resClient','resPhone','resBien','discountReason'].forEach(id=>{const e=document.getElementById(id);if(e)e.value=''});['discount','extra'].forEach(id=>{const e=document.getElementById(id);if(e)e.value=0});const preview=document.getElementById('reservationPreviewDoc');if(preview)preview.remove();if(typeof updatePrice==='function')updatePrice();if(typeof log==='function')log('Suppression du brouillon de réservation');alert('Brouillon de réservation supprimé.');return} const title=typeof docTitle==='function'?docTitle(kind):'document';if(!confirm('Supprimer définitivement ce brouillon de '+title+' ?'))return;immoxiaStorage.setItem('immoxia_doc_deleted_'+kind,'1');immoxiaStorage.removeItem('immoxia_doc_'+kind);const doc=typeof getDoc==='function'?getDoc(kind):null;if(doc){doc.innerHTML='

🗑 Brouillon supprimé

Ce document n’est plus actif.

';const b=document.getElementById('restoreDeletedDoc');if(b)b.onclick=function(){immoxiaStorage.removeItem('immoxia_doc_deleted_'+kind);location.reload()}}if(typeof log==='function')log('Suppression du bon '+kind);alert('Document supprimé.') }; window.deleteInvoice=function(i){const arr=typeof invoiceList==='function'?invoiceList():[],inv=arr[i];if(!inv)return;if(!confirm('Supprimer '+inv.ref+' ? Le numéro restera consommé pour éviter toute réutilisation.'))return;arr.splice(i,1);if(typeof setInvoiceList==='function')setInvoiceList(arr);if(typeof log==='function')log('Suppression facture '+inv.ref+' — numéro conservé');if(typeof renderInvoices==='function')renderInvoices();const shell=document.getElementById('invoiceDocShell');if(shell)shell.style.display='none';if(typeof refreshInvoiceRef==='function')refreshInvoiceRef()}; })(); ';const blob=new Blob([html],{type:'text/html;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=title+'.html';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000);toast('Fichier enregistré : '+title+'.html')}; // Biens window.openBienForm=function(){$('bienForm').style.display='block';$('bienRef').focus()};window.closeBienForm=function(){$('bienForm').style.display='none'};window.focusBienSearch=function(){$('bienSearch').focus()}; window.saveBien=function(){const ref=$('bienRef').value.trim(),address=$('bienAddress').value.trim();if(!ref||!address)return toast('Référence et adresse obligatoires.');const a=db('immoxia_biens',[]);if(a.some(x=>x.ref.toLowerCase()===ref.toLowerCase()))return toast('Cette référence existe déjà.');a.unshift({id:uid(),ref,address,type:$('bienType').value,price:Number($('bienPrice').value||0),owner:$('bienOwner').value.trim(),status:$('bienStatus').value,date:new Date().toLocaleString('fr-FR')});put('immoxia_biens',a); apiSyncResource('biens',[a[0]]); renderBiens();closeBienForm();['bienRef','bienAddress','bienPrice','bienOwner'].forEach(id=>$(id).value='');toast('Bien enregistré.')}; window.renderBiens=function(){const b=$('bienRows');if(!b)return;const q=($('bienSearch')?.value||'').toLowerCase();const a=db('immoxia_biens',[]).filter(x=>[x.ref,x.address,x.owner,x.type,x.status].join(' ').toLowerCase().includes(q));b.innerHTML=a.length?a.map((x,i)=>`${escapeHtml(x.ref)}${escapeHtml(x.address)}${escapeHtml(x.type)}${money(x.price)}${escapeHtml(x.status)}${escapeHtml(x.owner||'—')}`).join(''):'Aucun bien enregistré.'}; window.deleteBien=function(id){const a=db('immoxia_biens',[]),i=a.findIndex(x=>x.id===id);if(i<0)return;if(!confirm('Supprimer ce bien ?'))return;a.splice(i,1);put('immoxia_biens',a);renderBiens()}; // Terrain / Protocole window.saveTerrain=function(){const x={id:uid(),date:$('terrainDate').value||new Date().toISOString().slice(0,10),agent:$('terrainAgent').value.trim(),lieu:$('terrainLieu').value.trim(),client:$('terrainClient').value.trim(),activity:$('terrainActivity').value,result:$('terrainResult').value,notes:$('terrainNotes').value.trim(),createdAt:new Date().toISOString()};if(!x.agent||!x.lieu)return toast('Agent et lieu obligatoires.');const a=db('immoxia_terrain',[]);a.unshift(x);put('immoxia_terrain',a); apiSyncResource('terrain',[a[0]]); renderTerrain();$('terrainNotes').value='';toast('Activité terrain enregistrée.')}; window.renderTerrain=function(){const b=$('terrainRows');if(!b)return;const a=db('immoxia_terrain',[]);b.innerHTML=a.map((x,i)=>`${escapeHtml(x.date)}${escapeHtml(x.agent)}${escapeHtml(x.lieu)}${escapeHtml(x.client||'—')}${escapeHtml(x.activity)}${escapeHtml(x.result)}`).join('')||'Aucune activité.'};window.deleteTerrain=function(i){const a=db('immoxia_terrain',[]);if(!a[i]||!confirm('Supprimer cette activité ?'))return;const gone=a[i];a.splice(i,1);put('immoxia_terrain',a);if(API_BASE&&immoxiaStorage.getItem('immoxia_api_token')&&gone?.id)apiDeleteResource('terrain',gone.id);renderTerrain()}; window.saveProtocol=function(){const x={id:uid(),date:new Date().toLocaleString('fr-FR'),ref:$('protocolRef').value.trim(),step:$('protocolStep').value,status:$('protocolStatus').value,by:sess()?.name||role(),note:$('protocolNote').value.trim()};if(!x.ref)return toast('La référence opération est obligatoire.');const a=db('immoxia_protocol',[]);a.unshift(x);put('immoxia_protocol',a); apiSyncResource('protocol',[a[0]]); renderProtocol();toast('Validation protocolaire enregistrée.')};window.renderProtocol=function(){const b=$('protocolRows');if(!b)return;const a=db('immoxia_protocol',[]);b.innerHTML=a.map((x,i)=>`${escapeHtml(x.date)}${escapeHtml(x.ref)}${escapeHtml(x.step)}${escapeHtml(x.status)}${escapeHtml(x.by)}${escapeHtml(x.note||'')}`).join('')||'Aucune validation.'};window.deleteProtocol=function(i){const a=db('immoxia_protocol',[]);if(!a[i]||!confirm('Supprimer cette ligne du protocole ?'))return;const gone=a[i];a.splice(i,1);put('immoxia_protocol',a);if(API_BASE&&immoxiaStorage.getItem('immoxia_api_token')&&gone?.id)apiDeleteResource('protocol',gone.id);renderProtocol()}; // Prospection : sauvegarde indépendante et suppression window.saveProspection=function(){const client=$('prospectClient').value.trim();if(!client)return toast('Le prospect est obligatoire.');const a=db('immoxia_prospection',[]);a.unshift({id:uid(),date:new Date().toLocaleString('fr-FR'),client,action:$('prospectAction').value,result:$('prospectResult').value,notes:$('prospectNotes').value.trim(),owner:sess()?.name||role()});put('immoxia_prospection',a); apiSyncResource('prospection',[a[0]]); renderProspection();$('prospectClient').value='';$('prospectNotes').value='';toast('Action de prospection enregistrée.')}; window.deleteProspection=async function(i){const a=db('immoxia_prospection',[]),x=a[i];if(!x||!confirm('Supprimer cette action de prospection ?'))return;if(API_BASE&&immoxiaStorage.getItem('immoxia_api_token')&&x.id)await apiDeleteResource('prospection',x.id);a.splice(i,1);put('immoxia_prospection',a);renderProspection();toast('Action de prospection supprimée.')}; window.renderProspection=function(){const b=$('prospectRows');if(!b)return;const a=db('immoxia_prospection',[]);b.innerHTML=a.map((x,i)=>`${escapeHtml(x.date)}${escapeHtml(x.client)}${escapeHtml(x.action)}${escapeHtml(x.result)}${escapeHtml(x.notes||'')}`).join('')||'Aucune action de prospection.'}; const ps=$('prospectSave');if(ps)ps.onclick=saveProspection; // Utilisateurs : e-mail pro + suppression fiable window.renderUsers=function(){const b=$('userRows');if(!b)return;b.innerHTML=users.map((u,i)=>`${escapeHtml(u.firstName||String(u.name||'').split(' ')[0]||'—')}${escapeHtml(u.lastName||String(u.name||'').split(' ').slice(1).join(' ')||'—')}${escapeHtml(u.login||'—')}${escapeHtml(u.department||'—')}${escapeHtml(u.service||'—')}${escapeHtml(u.email||'—')}${escapeHtml(u.role)}${u.active===0?'Suspendu':'Actif'}${i?`
`:'Compte principal'}`).join('')||'Aucun compte enregistré.'}; window.saveUser=async function(){return window.saveUserFinal?window.saveUserFinal():toast('Fonction de création indisponible.');}; window.deleteUser=async function(i){if(i===0)return toast('Le compte principal Administrateur ne peut pas être supprimé.');const a=users,u=a[i];if(!u)return;if(!confirm('Supprimer définitivement le compte '+u.name+' ?'))return;if(API_BASE&&immoxiaStorage.getItem('immoxia_api_token')&&u.id){const remote=await apiDeleteResource('users',u.id);if(!remote?.ok)return toast('Suppression serveur refusée ou impossible.');}a.splice(i,1);setDB(DB.users,a);renderUsers();log('Suppression compte utilisateur '+u.login);toast('Compte supprimé définitivement.');}; window.editUser=async function(i){const u=users[i];if(!u)return;const firstName=prompt('Prénom',u.firstName||'');if(firstName===null)return;const lastName=prompt('Nom',u.lastName||'');if(lastName===null)return;const department=prompt('Département',u.department||'');if(department===null)return;const service=prompt('Service',u.service||'');if(service===null)return;const email=prompt('E-mail professionnel',u.email||'');if(email===null)return;const roleN=prompt('Rôle',u.role);if(roleN===null)return;const password=prompt('Nouveau mot de passe (laisser vide pour conserver)','');const patch={firstName:firstName.trim(),lastName:lastName.trim(),name:(firstName.trim()+' '+lastName.trim()).trim(),department:department.trim(),service:service.trim(),email:email.trim(),role:roleN.trim()||u.role};if(password)patch.password=password;try{const remote=await apiCall('/users?id='+encodeURIComponent(u.id),{method:'PUT',body:JSON.stringify(patch)});if(!remote)return toast('Modification du compte impossible sur le serveur.');users=await apiCall('/users');setDB(DB.users,users);renderUsers();toast('Compte modifié avec succès.')}catch(e){toast(e.message||'Modification impossible.')}}; window.apiCreateResource=async function(resource,item){ const t=apiToken(); if(!t)return {ok:false,error:'Connexion serveur indisponible.'}; const headers={'Content-Type':'application/json',Authorization:'Bearer '+t}; try{const r=await fetch(API+'/'+resource,{method:'POST',headers,body:JSON.stringify(item)});const data=await r.json().catch(()=>({}));return {ok:r.ok,status:r.status,data,error:data.error||('HTTP '+r.status),duplicate:!!data.duplicate};}catch(e){return {ok:false,error:'Serveur indisponible.'};} }; window.apiSyncResource=async function(resource,items){ const t=apiToken(); if(!t||!Array.isArray(items)) return null; let last=null; for(const item of items){ if(!item||!item.id) continue; last=await apiCall(API+'/'+resource+'?id='+encodeURIComponent(item.id),{method:'PUT',body:JSON.stringify(item)}); if(!last) last=await apiCall(API+'/'+resource,{method:'POST',body:JSON.stringify(item)}); } return last; }; window.apiDeleteResource=async function(resource,id){ if(!id||!apiToken()) return null; return await apiCall(API+'/'+resource+'?id='+encodeURIComponent(id),{method:'DELETE'}); }; window.apiLoadResource=async function(resource,localKey){ const remote=await apiCall(API+'/'+resource,{method:'GET'}); if(!remote||!Array.isArray(remote)) return false; try{immoxiaStorage.setItem(localKey,JSON.stringify(remote))}catch(e){localStorage.setItem(localKey,JSON.stringify(remote))} return true; }; /* Connexion : API centrale d'abord, mode local ensuite pour garder l'application utilisable hors serveur. */ window.IMMOXIA_LOGIN=async function(login,password){ login=String(login||'').trim(); password=String(password||''); if(!login||!password) return null; let remote=null; try{ remote=await apiCall('/auth/login',{method:'POST',body:JSON.stringify({login,password})}); }catch(e){ remote=null; } if(remote&&remote.token&&remote.user){ apiSet(tokenKey,remote.token); const s={...remote.user,at:new Date().toISOString()}; apiSet('immoxia_session',JSON.stringify(s)); return s; } /* Compte de secours demandé pour le premier démarrage Cloudflare avant initialisation D1. */ if(login.toUpperCase()==='IF-001' && password==='PIVOTGROUP'){ const s={id:'u-if001-local',login:'IF-001',name:'Administrateur IMMOXIA',role:'Administrateur',email:'',localBootstrap:true,at:new Date().toISOString()}; apiSet(tokenKey,'local-bootstrap-token'); apiSet('immoxia_session',JSON.stringify(s)); return s; } return null; }; /* Remplace le submit de démarrage existant, y compris après rechargement. */ window.IMMOXIA_REPAIR_LOGIN=function(){ const id=document.getElementById('startupLoginId'),pw=document.getElementById('startupLoginPass'),btn=document.getElementById('startupLoginBtn'); if(!id||!pw||!btn)return; const submit=async e=>{ e&&e.preventDefault(); const err=document.getElementById('startupLoginError'); if(err){err.textContent='Connexion en cours…';err.style.display='block'} btn.disabled=true; const s=await window.IMMOXIA_LOGIN(id.value,pw.value); btn.disabled=false; if(s){ if(err){err.textContent='';err.style.display='none'} if(typeof unlock==='function')unlock(); else {document.body.classList.remove('immoxia-locked');const m=document.getElementById('startupLogin');if(m)m.style.display='none'} if(typeof updateSessionUI==='function')updateSessionUI(); if(typeof apiBootstrap==='function')apiBootstrap(); }else{ if(err){err.textContent='Identifiant ou mot de passe incorrect.';err.style.display='block'} pw.value='';pw.focus(); } return false; }; btn.onclick=submit; id.onkeydown=e=>{if(e.key==='Enter')submit(e)}; pw.onkeydown=e=>{if(e.key==='Enter')submit(e)}; }; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',IMMOXIA_REPAIR_LOGIN,{once:false}); else IMMOXIA_REPAIR_LOGIN(); /* Suppress duplicate local handlers for the principal save buttons and bind the working final handlers. */ function bind(id,fn){ const b=document.getElementById(id); if(!b)return; b.onclick=null; b.addEventListener('click',e=>{e.preventDefault();e.stopImmediatePropagation();Promise.resolve(fn()).catch(err=>{console.error(err);if(typeof msg==='function')msg('Erreur : '+err.message)})},true); } function finalSave(name){ return window[name]||window[name.replace('Final','')]; } setTimeout(()=>{ bind('saveRes',()=>window.saveReservationFinal?window.saveReservationFinal():window.saveReservation()); bind('crmSave',()=>window.saveCRMFinal?window.saveCRMFinal():window.saveCRM()); bind('prospectSave',()=>window.saveProspectionFinal?window.saveProspectionFinal():window.saveProspection()); bind('userSave',()=>window.saveUserFinal?window.saveUserFinal():window.saveUser()); },50); })(); `); w.document.close();w.focus();setTimeout(()=>{w.print()},350); }; /* Remplace le bouton d'impression caisse pour éviter window.print() sur toute la plateforme. */ document.addEventListener('click',function(e){ const b=e.target.closest('#caisse button'); if(!b)return; if(/Imprimer/i.test(b.textContent||'')){ e.preventDefault();e.stopImmediatePropagation();window.printReceiptThermal(); } },true); /* Suppressions locales restantes : transactions, si le bouton appelle deleteTransaction. */ const oldDeleteTx=window.deleteTransaction; window.deleteTransaction=async function(id){ const arr=read('immoxia_transactions_v1',[]), idx=arr.findIndex(x=>x.id===id);if(idx<0)return; const x=arr[idx], sess=read('immoxia_session',null); if(sess && sess.role!=='Administrateur' && x.ownerId && x.ownerId!==sess.id && x.ownerId!==sess.login)return alert('Accès refusé : cette transaction appartient à un autre compte.'); if(!confirm('Supprimer définitivement cette transaction ?'))return; if(!(await remoteDelete('transactions',x.id)))return alert('Suppression serveur impossible. La transaction a été conservée.'); arr.splice(idx,1);write('immoxia_transactions_v1',arr); if(typeof renderTransactions==='function')renderTransactions(); if(typeof log==='function')log('Suppression transaction '+id); }; /* Impression générique : ouvrir un document propre plutôt que d'imprimer toute l'interface. */ window.previewAndPrintDocument=function(kind){ if(typeof getDoc!=='function')return alert('Document introuvable.'); const doc=getDoc(kind);if(!doc)return alert('Document introuvable.'); const w=window.open('','_blank','width=1000,height=800');if(!w)return alert("Autorisez les fenêtres contextuelles pour l'impression."); w.document.open();w.document.write(`IMMOXIA — ${esc(kind)}
${doc.outerHTML} `); w.document.close();w.focus(); }; })();