//! jdevis.js let chkPageLoad = true // indique que la page vient d'être chargée pour la première fois let idDevis = 0 let fkUser = 0 let fkRole = 0 let devIp = '0' let fkUserDevis = 0 let fkStatutDevis = 0 let chkValidat = 0 let oldColorLn let oldIdLn let chkChange = 0 let idMarche = '' let idNewMarche = '' // dans le cas on l'utilisateur change de marché sur un devis déjà créé let chkClientsSecteur let chkShowDevisArchives = false // indique si on affiche les devis archivés ou non let chkCreateClient = false // On charge les produits du marché du devis en cours dans un tableau pour ne pas avoir à les recharger à chaque fois let dataProduitsMarche = [] let dataProduitsMercurial = [] // Produits du marché hybride pour l'onglet Mercurial let chkMarcheHybride = false // Indique si le marché du devis est hybride //! Pour ne charger les clients du secteur ou de toute la France qu'en cas de changement de la valeur du chkbox let oldChkClientsSecteur = 2 let clients = [] let chkPrixNets = false // le marché du devis en cours est-il en prix nets ou pas let remiseMarcheDeBase = 0 // la remise de base du marché let chkSaisieRemise = false let chkRemisesMarche = true // indique si toutes les lignes du devis appliquent la remise du marché ou pas let aRemisesMarches = [] let devisTotalHT = 0 let devisTotalRemHT = 0 let devisTotalMarge = 0 let chkRegleSeuilsMarge = false // indique si le marché sélectionné prend en compte les seuils de marge fixés dans les familles de produits let seuilMargeRR = 40 // le seuil de marge du RR sur ce devis, par défaut à 40 % (MAJ 05/11/2025) let seuilMargeDV = 30 // le seuil de marge du DV sur ce devis, par défaut à 30 % (MAJ 05/11/2025) let intervalRefresh let nbCommentChat = 0 let draggedElement = null // l'élément qui est en train d'être déplacé (la ligne du produit du devis lors d'un drag and drop) const tableSortStates = new Map() function initTableSort() { const allTables = document.querySelectorAll('[id^="tblDos"], [id^="tblDosArch"]') allTables.forEach(table => { const tableId = table.id const headers = table.querySelectorAll('th[data-sortable="true"]') const tbody = table.querySelector('tbody') if (!tbody) return if (!tableSortStates.has(tableId)) { tableSortStates.set(tableId, { originalOrder: null, currentSort: { column: null, direction: null } }) } headers.forEach(header => { header.addEventListener('click', function() { const columnIndex = parseInt(this.getAttribute('data-column-index')) const sortType = this.getAttribute('data-sort-type') sortTable(tableId, columnIndex, sortType, this) }) }) }) } function sortTable(tableId, columnIndex, sortType, headerElement) { const table = document.getElementById(tableId) const tbody = table.querySelector('tbody') const rows = Array.from(tbody.querySelectorAll('tr')) const state = tableSortStates.get(tableId) if (!state.originalOrder) { state.originalOrder = rows.slice() } const allHeaders = table.querySelectorAll('th[data-sortable="true"]') allHeaders.forEach(h => h.style.fontWeight = 'normal') let sortedRows if (state.currentSort.column === columnIndex && state.currentSort.direction === 'desc') { sortedRows = state.originalOrder.slice() state.currentSort = { column: null, direction: null } } else { const direction = (state.currentSort.column === columnIndex && state.currentSort.direction === 'asc') ? 'desc' : 'asc' sortedRows = rows.slice().sort((a, b) => { const aCell = a.cells[columnIndex] const bCell = b.cells[columnIndex] if (!aCell || !bCell) return 0 let aValue = aCell.textContent.trim() let bValue = bCell.textContent.trim() if (sortType === 'number') { aValue = aValue.replace(/[^\d,.-]/g, '').replace(',', '.') bValue = bValue.replace(/[^\d,.-]/g, '').replace(',', '.') aValue = parseFloat(aValue) || 0 bValue = parseFloat(bValue) || 0 return direction === 'asc' ? aValue - bValue : bValue - aValue } else if (sortType === 'date') { aValue = parseDateFromText(aValue) bValue = parseDateFromText(bValue) if (!aValue && !bValue) return 0 if (!aValue) return direction === 'asc' ? 1 : -1 if (!bValue) return direction === 'asc' ? -1 : 1 return direction === 'asc' ? aValue - bValue : bValue - aValue } else { const comparison = aValue.localeCompare(bValue, 'fr') return direction === 'asc' ? comparison : -comparison } }) state.currentSort = { column: columnIndex, direction } headerElement.style.fontWeight = 'bold' } tbody.innerHTML = '' sortedRows.forEach(row => tbody.appendChild(row)) } function parseDateFromText(dateText) { const match = dateText.match(/(\d{2})\/(\d{2})[\/\s](\d{4})/) if (!match) return null const [, day, month, year] = match return new Date(year, month - 1, day) } window.addEventListener('DOMContentLoaded', (event) => { console.log('#') // Initialisation des éléments utilisés let elCelDevis = document.getElementsByClassName('celDevis') let elCelArchives = document.getElementsByClassName('celArchives') let elBtnDupDevis = document.getElementsByClassName('btnDupDevis') let elBtnSupprDevis = document.getElementsByClassName('btnSupprDevis') let elBtnExpExcelDevis = document.getElementsByClassName('btnExpExcelDevis') let elBtnValDevis = document.getElementsByClassName('btnValDevis') let elBtnPdfDevis = document.getElementsByClassName('btnPdfDevis') let elBtnReactiverDevis = document.getElementsByClassName('btnReactiverDevis') console.log('Nombre de boutons btnReactiverDevis trouvés:', elBtnReactiverDevis.length) let elBtnValidationRR = document.getElementById('btnValidationRR') let elBtnRefusRR = document.getElementById('btnRefusRR') let elBtnCloseRR = document.getElementById('btnCloseRR') let elBtnClosePDF = document.getElementById('btnClosePDF') let elBtnDevisArchives = document.getElementById('btnDevisArchives') let elBtnCreateDevis = document.getElementById('btnCreateDevis') let elBtnCreateClient = document.getElementById('btnCreateClient') let elBtnCancelCreateClient = document.getElementById('btnCancelCreateClient') let elBtnSaveCreateClient = document.getElementById('btnSaveCreateClient') let elBtnSpeciaux = document.getElementById('btnSpeciaux') let elBtnCancelSpeciaux = document.getElementById('btnCancelSpeciaux') let elBtnSaveSpeciaux = document.getElementById('btnSaveSpeciaux') let elBtnSaveEnTete = document.getElementById('btnSaveEnTete') let elBtnSaveSelProduits = document.getElementById('btnSaveSelProduits') let elBtnSaveDevis = document.getElementById('btnSaveDevis') let elBtnSaveDevisAndSend = document.getElementById('btnSaveDevisAndSend') let elChkClientsSecteur = document.getElementById('inp_chk_clients_secteur') let elInputSearchProducts = document.querySelectorAll("input[id^='inpSearchProduct_']") let elInputQtes = document.querySelectorAll("input[name^='inpQte_']") let elInputRemises = document.querySelectorAll("input[name^='inpRemise_']") let elChkVariantes = document.querySelectorAll("input[type='checkbox'][name^='chkVariante_']") let elInputDateDemande = document.getElementById('inp_date_demande') let elInputDateRemise = document.getElementById('inp_date_remise') let elListOngletsProduits = document.getElementById('listOngletsProduits') let elOngletsProduits = document.querySelectorAll('[id^="onglet_"]') let elProdSelect = document.querySelectorAll('input[type="checkbox"][name^="chkBoxProd_"]') let elInpCommentGesteComm = document.getElementById('inpCommentGesteComm') let elChatBtnSend = document.getElementById('chatBtnSend') let elBtnCancelCommentProd = document.getElementById('btnCancelCommentProd') let elBtnSaveCommentProd = document.getElementById('btnSaveCommentProd') //! Au chargement de la page on affiche le menu vertical de choix du devis et on cache les 3 onglets const elDivDevis = document.getElementById('divDevis') const elDossStatuts = document.getElementById('vb-dossiers-statuts') const elDossArchives = document.getElementById('vb-dossiers-archives') // Par défaut on n'affiche pas le chat document.getElementById('chat-container').style.display = 'none' const elVerticalBar = document.getElementById('verticalBar') const elBtnSideBarDevis = document.getElementById('btnSideBarDevis') // par défaut on affiche les dossiers par statuts de devis elDossStatuts.classList.remove('hidden') elDossArchives.classList.add('hidden') elVerticalBar.style.width = '1100px' elDivDevis.style.display = 'none' //! On récupère les données contextuelles propres à l'utilisateur fetch('/jxpost/get_context', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { const ret = response.json() ret.then(function (data) { const user = data.user fkUser = user.rowid fkRole = user.fk_role devIp = data.devip const session = data.session }) }) let clickDevisArchives = function () { // click sur le bouton de la sidebar pour afficher les devis archivés ou revenir sur les devis en cours elDossStatuts.classList.toggle('hidden') idDevis = 0 const archivesHidden = elDossArchives.classList.toggle('hidden') if (archivesHidden) { this.innerHTML = 'Mes devis archivés' chkShowDevisArchives = false document.getElementById('chat-message-input').classList.remove('hidden') // Il faut afficher tous les boutons d'enregistrement elBtnSaveEnTete.classList.remove('hidden') elBtnSaveSelProduits.classList.remove('hidden') elBtnSaveDevis.classList.remove('hidden') elBtnSaveDevisAndSend.classList.remove('hidden') elBtnSaveSpeciaux.classList.remove('hidden') } else { this.innerHTML = 'Mes devis en cours' chkShowDevisArchives = true document.getElementById('chat-message-input').classList.add('hidden') // Il faut cacher tous les boutons d'enregistrement elBtnSaveEnTete.classList.add('hidden') elBtnSaveSelProduits.classList.add('hidden') elBtnSaveDevis.classList.add('hidden') elBtnSaveDevisAndSend.classList.add('hidden') elBtnSaveSpeciaux.classList.add('hidden') } document.getElementById('chat-container').style.display = 'none' elDivDevis.style.display = 'none' return false } let clickLigDevis = function () { //! L'utilisateur vient de cliquer sur un devis dans la liste de gauche //! On ne fait rien si l'utilisateur clique sur le même devis if (this.getAttribute('data-rid') != idDevis) { if (chkChange == 1) { if (confirm('Attention, vous avez des modifications non enregistrées sur ce devis. Voulez-vous continuer ?')) { chkChange = 0 } else { return false } } idDevis = this.getAttribute('data-rid') showLoading() // on met à jour l'input caché contenant l'id du devis sélectionné document.getElementById('inpIdDevis').value = idDevis let dataFamilles //! on charge les familles de groupes de produits pour mettre à jour le tableau de chaque onglet fetch('/jxdevis/load_familles', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification('Erreur', "Le chargement des familles de produits n'a pas abouti", 'error') } else { const ret = response.json() ret.then(function (data) { dataFamilles = data }) } }) // effectue la requête ajax fetch pour charger les produits du marché fetch('/jxdevis/load_devis', { method: 'POST', body: JSON.stringify({ cid: idDevis }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => { if (!response.ok) { showNotification('Erreur', "Le chargement des infos de l'en-tête de ce devis n'a pas abouti", 'error') } else { const ret = response.json() ret .then(function (data) { fkUserDevis = data[0].fk_user fkStatutDevis = data[0].fk_statut_devis chkValidat = data[0].chk_validat idMarche = data[0].fk_marche idNewMarche = data[0].fk_marche // par défaut le nouveau marché est le même que le marché en cours sur ce devis chkClientsSecteur = data[0].chk_clients_secteur showDevisEnTete(data) updateBtnSpeciaux(data[0].chk_speciaux) showDevisTotaux(data) }) .then(function () { //! Une fois le marché trouvé, on charge les infos du marché préchargé dans l'en-tête du devis fetch('/jxdevis/load_devis_marche_infos', { method: 'POST', body: JSON.stringify({ cid: idMarche }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification('Erreur', "Le chargement des infos du marché n'a pas abouti", 'error') } else { const ret = response.json() //! Boucle sur le résultat de la requête ajax ret.then(function (data) { showDevisMarcheInfos(data) chkRegleSeuilsMarge = data[0].chk_regle_seuils_marge //! On charge ensuite les produits du marché de ce devis fetch('/jxdevis/load_devis_marche_produits', { method: 'POST', body: JSON.stringify({ cid: idMarche }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification('Erreur', "Le chargement des produits du marché n'a pas abouti", 'error') } else { const ret = response.json() ret.then(function (data) { dataProduitsMarche = data showDevisMarcheProduits(dataFamilles, data) // on charge les produits enregistrés pour ce devis dans 2 tableaux distincts tblProduitsSelect (2ème onglet) et tblDevisPro (3ème onglet) fetch('/jxdevis/load_devis_produits', { method: 'POST', body: JSON.stringify({ cid: idDevis }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification( 'Erreur', "Le chargement des produits de ce devis n'a pas abouti", 'error' ) } else { const ret = response.json() //! Boucle sur le résultat de la requête ajax ret.then(function (data) { showDevisProduits(data) }) } }) }) } }) }) } }) }) } }) .catch((error) => { showNotification('Erreur', "Le chargement des infos de l'en-tête de ce devis n'a pas abouti", 'error') }) hideLoading() chkPageLoad = false elDivDevis.style.display = 'block' refreshChat() //! On met enfin en évidence la ligne cliquée Array.from(elCelDevis).forEach(function (ligDevis) { if (ligDevis.getAttribute('data-rid') == oldIdLn) { ligDevis.style.backgroundColor = oldColorLn } else if (ligDevis.getAttribute('data-rid') == idDevis) { oldColorLn = ligDevis.style.backgroundColor ligDevis.style.backgroundColor = '#9bbce7' } }) oldIdLn = idDevis chkChange = 0 } } let clickLigArchives = function () { //! L'utilisateur vient de cliquer sur un devis archivé dans la liste de gauche //! On ne fait rien si l'utilisateur clique sur le même devis if (this.getAttribute('data-rid') != idDevis) { if (chkChange == 1) { if ( confirm( 'Attention, vous avez des modifications non enregistrées sur le devis en cours. Voulez-vous continuer ?' ) ) { chkChange = 0 } else { return false } } idDevis = this.getAttribute('data-rid') refreshChat() //! On met enfin en évidence la ligne cliquée Array.from(elCelArchives).forEach(function (ligArchive) { if (ligArchive.getAttribute('data-rid') == oldIdLn) { ligArchive.style.backgroundColor = oldColorLn } else if (ligArchive.getAttribute('data-rid') == idDevis) { oldColorLn = ligArchive.style.backgroundColor ligArchive.style.backgroundColor = '#9bbce7' } }) oldIdLn = idDevis chkChange = 0 } } function showDevisEnTete(ret) { // Affiche les données de l'en-tête du devis const data = ret[0] document.getElementById('inp_rowid').value = data.rowid document.getElementById('inp_num_opportunite').value = data.num_opportunite document.getElementById('inp_date_demande').value = data.date_demande document.getElementById('inp_date_remise').value = data.date_remise document.getElementById('inp_fk_user').value = data.fk_user document.getElementById('inp_fk_marche').value = data.fk_marche // On surveille un changement dans le champ fk_marche, ce qui peut provoquer la suppression des produits du devis s'il enregistre ce changement document.getElementById('inp_fk_marche').addEventListener('change', function () { idNewMarche = this.value console.log('idNewMarche :' + idNewMarche) }) if (data.chk_clients_secteur == '1') { document.getElementById('inp_chk_clients_secteur').checked = true } else { document.getElementById('inp_chk_clients_secteur').checked = false } if (data.chk_clients_secteur != oldChkClientsSecteur) { // la valeur du chk_clients_secteur est différente de l'actuelle, on charge les clients du commercial sur son secteur ou sur toute la France changeClientsSecteur() oldChkClientsSecteur == 2 ? (chkChange = 0) : (chkChange = 1) oldChkClientsSecteur = data.chk_clients_secteur } document.getElementById('inp_fk_client').value = data.fk_client console.log('fk_type_new :' + data.type_new_client) if (data.fk_client == 0) { document.getElementById('inp_lib_client').value = data.lib_new_client document.getElementById('inp_adresse1').value = data.adresse1_new_client document.getElementById('inp_adresse2').value = data.adresse2_new_client document.getElementById('inp_adresse3').value = data.adresse3_new_client document.getElementById('inp_cp').value = data.cp_new_client document.getElementById('inp_ville').value = data.ville_new_client document.getElementById('inp_contact_nom').value = data.contact_new_nom document.getElementById('inp_contact_prenom').value = data.contact_new_prenom document.getElementById('inp_contact_fonction').value = data.contact_new_fonction document.getElementById('inp_email').value = data.new_email document.getElementById('inp_telephone').value = data.new_telephone document.getElementById('inp_mobile').value = data.new_mobile document.getElementById('selTypeEtab').value = data.type_new_client elBtnCreateClient.innerHTML = 'Modifier ce nouveau client' if (elBtnCreateClient.classList.contains('btn-primary')) { elBtnCreateClient.classList.remove('btn-primary') elBtnCreateClient.classList.add('btn-info') } } else { document.getElementById('inp_lib_client').value = data.libelle document.getElementById('inp_adresse1').value = data.adresse1 document.getElementById('inp_adresse2').value = data.adresse2 document.getElementById('inp_adresse3').value = data.adresse3 document.getElementById('inp_cp').value = data.cp document.getElementById('inp_ville').value = data.ville document.getElementById('selTypeEtab').value = data.type_client elBtnCreateClient.innerHTML = 'Créer un nouveau client' if (elBtnCreateClient.classList.contains('btn-info')) { elBtnCreateClient.classList.remove('btn-info') elBtnCreateClient.classList.add('btn-primary') } // Charger les contacts du client et sélectionner le contact du devis loadContactsClient(data.code).then(() => { if (data.fk_contact && data.fk_contact > 0) { document.getElementById('sel_contact').value = data.fk_contact // Afficher les infos du contact const contact = contactsClient.find(c => c.rowid == data.fk_contact) if (contact) { displayContactInfos(contact) } } }) } if (data.chk_devis_photos == '1') { document.getElementById('inp_chk_devis_photos').checked = true } else { document.getElementById('inp_chk_devis_photos').checked = false } // Gestion et affichage des commentaires document.getElementById('inp_commentaire').value = data.commentaire document.getElementById('inpCommentDevis').value = data.comment_devis elInpCommentGesteComm.value = data.comment_geste_comm // On supprime systématiquement la ligne de validation du devis let rowCommentValidatDevis = document.getElementById('rowCommentValidatDevis') if (rowCommentValidatDevis !== null) { rowCommentValidatDevis.remove() } if (fkUserDevis != fkUser && fkRole < 3) { // Le user actuel n'est pas le créateur du devis, et son rôle est le DIR-CO ou un DV const tblBodyComment = document.getElementById('tblCommentDevis').getElementsByTagName('tbody')[0] // Insertion d'une nouvelle ligne et création de ses colonnes : on prend ici le fk_produit let newRowComment = tblBodyComment.insertRow(0) newRowComment.id = 'rowCommentValidatDevis' let celLabel = newRowComment.insertCell(0) celLabel.innerHTML = '' let celComment = newRowComment.insertCell(1) celComment.classList.add('w-60') celComment.innerHTML = '' let celBtnValid = newRowComment.insertCell(2) celBtnValid.classList.add('w-40') celBtnValid.innerHTML = '
' celBtnValid.innerHTML += '
' document.getElementById('btnValidatDevis').addEventListener('click', clickValidatDevis) document.getElementById('btnRefusDevis').addEventListener('click', clickRefusDevis) } } function updateBtnSpeciaux(chkSpeciaux) { // Met à jour en fonction le bouton btnSpeciaux console.log('chkSpeciaux = ' + chkSpeciaux) const btnSpeciaux = document.getElementById('btnSpeciaux') if (chkSpeciaux == '1') { btnSpeciaux.innerHTML = 'Modifier les produits spéciaux ' btnSpeciaux.classList.remove('btn-warning') btnSpeciaux.classList.add('btn-info') } else { btnSpeciaux.innerHTML = 'Ajouter des produits spéciaux ' btnSpeciaux.classList.remove('btn-info') btnSpeciaux.classList.add('btn-warning') } } function showDevisTotaux(ret) { // Affiche les totaux du devis const data = ret[0] document.getElementById('inpTotalHT').value = formatAmount(data.montant_total_ht) document.getElementById('inpTotalRemHT').value = formatAmount(data.montant_total_ht_remise) document.getElementById('inpTotalMarge').value = data.marge_totale // on renseigne les valeurs globales de ces 3 données devisTotalHT = data.montant_total_ht devisTotalRemHT = data.montant_total_ht_remise devisTotalMarge = data.marge_totale // on met à jour le bouton de sauvegarde du devis updateBtnSaveDevisAndSend() } function updateBtnSaveDevisAndSend() { // Si la marge Totale est inférieure au seuil de latitude, on change le bouton en orange ou rouge let btn = document.getElementById('btnSaveDevisAndSend') let typRole = 'DV' if (fkRole == 3 || fkRole > 19) { typRole = 'RR' } console.log('updateBtnSaveDevisAndSend : chkRemisesMarche = ' + chkRemisesMarche + ' & typRole = ' + typRole) // if ((chkPrixNets || chkRemisesMarche) && typRole == "RR") { if (chkPrixNets || chkRemisesMarche) { // Modif du 10/04/2024 : dans tous les rôles si le devis est en prix nets ou en remises marchés on passe directement le devis à l'ADV/SAP // Si le marché est en prix nets, ou si les lignes produits sont en remises marchés, // on ne peut pas modifier les remises donc on envoie le devis directement à l'ADV/SAP if (elInpCommentGesteComm.value != '') { btn.classList.add('btn-warning') btn.classList.remove('btn-success') btn.classList.remove('btn-danger') btn.innerHTML = 'Demander Accord DV/DCG' btn.dataset.statut = '3' } else { btn.classList.add('btn-primary') btn.classList.remove('btn-danger') btn.classList.remove('btn-warning') btn.innerHTML = 'Demander Traitement SAP' btn.dataset.statut = '4' } } else { const margeTotale = parseFloat(document.getElementById('inpTotalMarge').value) const latitudeRR = parseFloat(document.getElementById('inp_latitudeRR').value) const latitudeDV = parseFloat(document.getElementById('inp_latitudeDV').value) console.log('btnSaveDevisAndSend : Marge totale =' + margeTotale + ' vs DV ' + latitudeDV + ' & RR ' + latitudeRR) if ((typRole == 'RR' && margeTotale < latitudeRR) || (fkRole > 2 && elInpCommentGesteComm.value != '')) { // si on est un RR commercial et que la marge totale est inférieure à la latitude RR 30%, on demande l'accord DV // ou si on est un DV ou RR commercial et qu'on a saisi un geste commercial btn.classList.add('btn-warning') btn.classList.remove('btn-success') btn.classList.remove('btn-danger') btn.innerHTML = 'Demander Accord DV/DCG' btn.dataset.statut = '3' } else { if ( (fkRole == 2 && margeTotale < latitudeDV) || (fkRole == 2 && fkUserDevis == fkUser && elInpCommentGesteComm.value != '') ) { // si on est un DV et que la marge totale est inférieure à la latitude DV 20%, on demande l'accord DIR-CO // ou si on est un DV et que le devis est le sien et qu'on a saisi un geste commercial btn.classList.add('btn-danger') btn.classList.remove('btn-success') btn.classList.remove('btn-warning') btn.innerHTML = 'Demander Accord DIR-CO' btn.dataset.statut = '2' } else { // sinon on envoie le devis directement à l'ADV/SAP btn.classList.add('btn-primary') btn.classList.remove('btn-danger') btn.classList.remove('btn-warning') btn.innerHTML = 'Demander Traitement SAP' btn.dataset.statut = '4' } } } } function showDevisProduits(ret) { //! On affiche les produits du devis dans les 2 tableaux // tblProduitsSelect (2ème onglet : le tableau des produits sélectionnés) // tblDevisPro (3ème onglet : le tableau de saisie des qté) // On vide le tableau tblProduitsSelect let tblBodySelect = document.getElementById('tblProduitsSelect').getElementsByTagName('tbody')[0] tblBodySelect.innerHTML = '' // On vide le tableau tblDevisPro let tblBodyPro = document.getElementById('tblDevisPro').getElementsByTagName('tbody')[0] tblBodyPro.innerHTML = '' // Si le marché est un marché de prix nets, on met les champs de saisie des remises en readonly let readonlyRemise = chkPrixNets ? 'readonly="readonly"' : '' // Ajout du 20/02/2025 : si marché hybride, les produits de ce marché sont en prix nets let readonlyRemiseProduit = '' // Ajout du 26 juin 2024 : si l'utilisateur est le DC ou DV ou DGC, ils peuvent modifier les remises dans tous les cas if (fkRole < 3 || fkRole == 5) { readonlyRemise = '' } // Fin de l'ajout du 26 juin 2024 if (ret.length > 0) { // au moins un produit trouvé pour ce devis let nbProduits = ret.length // on récupère le premier fk_produit, pour simuler un changement sur ce produit pour recalculer les totaux en fin de boucle const fkProduit1 = ret[0]['fk_produit'] for (let key in ret) { if (ret.hasOwnProperty(key)) { // Récupération des valeurs de la ligne let val = ret[key] // On initialise le readonlyremise par produit pour gérer les cas de marché hybride où leurs produits sont en Prix Nets readonlyRemiseProduit = readonlyRemise // Insertion d'une nouvelle ligne et création de ses colonnes : on prend ici le fk_produit let newRowSelect = tblBodySelect.insertRow(-1) let celChkBox = newRowSelect.insertCell(0) celChkBox.className = 'text-center' celChkBox.innerHTML = '' let celCode = newRowSelect.insertCell(1) celCode.innerHTML = val['code'] let celLibelle = newRowSelect.insertCell(2) celLibelle.innerHTML = val['libelle'] // Sur le tableau tblBodyPro // Insertion d'une nouvelle ligne et création de ses colonnes : on prend ici le rowid de devis_produits let newRowPro = tblBodyPro.insertRow(-1) newRowPro.id = 'trPro_' + val['fk_produit'] newRowPro.dataset.ordre = val['ordre'] newRowPro.dataset.rid = val['fk_produit'] newRowPro.dataset.code = val['code'] newRowPro.dataset.achat = val['prix_achat_net'] newRowPro.dataset.achatdiscount = val['prix_achat_net'] newRowPro.dataset.vente = val['prix_vente'] newRowPro.dataset.discount1 = val['prc_discount_1'] newRowPro.dataset.quantite1 = val['quantite_1'] newRowPro.dataset.discount2 = val['prc_discount_2'] newRowPro.dataset.quantite2 = val['quantite_2'] newRowPro.dataset.discount3 = val['prc_discount_3'] newRowPro.dataset.quantite3 = val['quantite_3'] newRowPro.dataset.discount4 = val['prc_discount_4'] newRowPro.dataset.quantite4 = val['quantite_4'] newRowPro.dataset.discount5 = val['prc_discount_5'] newRowPro.dataset.quantite5 = val['quantite_5'] newRowPro.dataset.discount6 = val['prc_discount_6'] newRowPro.dataset.quantite6 = val['quantite_6'] newRowPro.setAttribute('draggable', 'true') newRowPro.addEventListener('dragstart', handleDragStart) newRowPro.addEventListener('dragover', handleDragOver) newRowPro.addEventListener('drop', handleDrop) let celCodePro = newRowPro.insertCell(-1) const svgColor = val['commentaire'] == '' ? 'lightgray' : 'red' const svgComment = '' let inputOrdreHidden = '' let inputCommentHidden = '' celCodePro.innerHTML = val['code'] + ' ' + svgComment + inputOrdreHidden + inputCommentHidden document.getElementById('commentProd_' + val['fk_produit']).addEventListener('click', showCommentProd) let celLibellePro = newRowPro.insertCell(1) celLibellePro.innerHTML = val['libelle'] let celPrixVentePro = newRowPro.insertCell(2) celPrixVentePro.className = 'text-right' celPrixVentePro.innerHTML = formatAmount(val['prix_vente']) + ' €' let celQtePro = newRowPro.insertCell(3) celQtePro.innerHTML = '' document.getElementById('inpQte_' + val['fk_produit']).addEventListener('change', calculDevis) let celRemisePro = newRowPro.insertCell(4) // Nouveau code 21/09 // S'il y a une remise de base sur le marché, on vérifie chaque remise produit pour l'aligner à cette remise de base si elle est supérieure let remiseProduit = val['remise'] console.log('Remise de base : ' + remiseMarcheDeBase + ' vs remise sur le produit : ' + val['remise']) if (remiseMarcheDeBase > 0) { if (val['remise'] < remiseMarcheDeBase) { console.log('La remise du produit est inférieure à la remise de base, on la force à la remise de base') remiseProduit = remiseMarcheDeBase } } // Fin du nouveau code du 21/09 // AJOUT DU 20/02/25 : on regarde si ce produit a un chk_prix_net et s'il est à 1 (marché hybride) if (val['chk_prix_net']) { console.log('on a un chk_prix_net : ' + val['chk_prix_net']) if (val['chk_prix_net'] == '1') { console.log('Le produit ' + val['code'] + ' est sur un marché hybride donc chk_prix_net=1') readonlyRemiseProduit = 'readonly="readonly"' } } celRemisePro.innerHTML = '
%
' if (readonlyRemiseProduit == '') { document.getElementById('inpRemise_' + val['fk_produit']).addEventListener('change', calculDevis) } // nouvelle colonne PU vente avec remise let celPUVenteRemPro = newRowPro.insertCell(5) celPUVenteRemPro.innerHTML = '
' // Fin nouvelle colonne let celHTPro = newRowPro.insertCell(6) celHTPro.innerHTML = '
' let celVariante = newRowPro.insertCell(7) celVariante.className = 'text-center' celVariante.innerHTML = '' document.getElementById('chkVariante_' + val['fk_produit']).addEventListener('change', calculDevis) let celMargePro = newRowPro.insertCell(8) celMargePro.innerHTML = '
%
' // on calcule et enregistre le prix d'achat discount du produit si on a un prc_discount let chkDiscount = false if (val['prc_discount_6'] > 0 && val['quantite_6'] > 0) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_6'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_6'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_6 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_5'] > 0 && val['quantite_5'] > 0 && !chkDiscount) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_5'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_5'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_5 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_4'] > 0 && val['quantite_4'] > 0 && !chkDiscount) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_4'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_4'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_4 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_3'] > 0 && val['quantite_3'] > 0 && !chkDiscount) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_3'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_3'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_3 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_2'] > 0 && val['quantite_2'] > 0 && !chkDiscount) { if (parseInt(val['qte']) >= parseInt(val['quantite_2'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_2'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_2 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_1'] > 0 && val['quantite_1'] > 0 && !chkDiscount) { if (parseInt(val['qte']) >= parseInt(val['quantite_1'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_1'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_1 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (chkRegleSeuilsMarge == 1) { // Le marché demande la prise en compte des seuils de marge RR et DV paramétrés dans la table produits_familles if (val['marge_rr'] > seuilMargeRR) seuilMargeRR = val['marge_rr'] if (val['marge_dv'] > seuilMargeDV) seuilMargeDV = val['marge_dv'] } } } // On met à jour les seuils de marge RR et DV en fonction du marché et des produits document.getElementById('inp_latitudeRR').value = seuilMargeRR document.getElementById('inp_latitudeDV').value = seuilMargeDV // On simule le changement de quantité sur la première ligne pour recalculer les totaux const inpQte = document.getElementById('inpQte_' + fkProduit1) const event = new Event('change') inpQte.dispatchEvent(event) chkChange = 0 } } function showDevisMarcheInfos(ret) { // On affiche les infos du marché if (ret.length == 1) { let line = ret[0] chkPrixNets = line.chk_prix_nets == 1 ? true : false chkMarcheHybride = line.chk_marche_hybride == 1 ? true : false // Si le marché est hybride, créer le panel et charger les produits if (chkMarcheHybride) { // Créer le panel Mercurial s'il n'existe pas if (!document.getElementById('tabMercurial')) { const tabContent = document.querySelector('#divProduitsDisponibles .tab-content') const divPanel = document.createElement('div') divPanel.setAttribute('role', 'tabpanel') divPanel.className = 'tab-pane' divPanel.id = 'tabMercurial' divPanel.innerHTML = `
Sélection Code Libellé Famille
` tabContent.appendChild(divPanel) } // Charger les produits Mercurial fetch('/jxdevis/load_produits_mercurial', { method: 'POST', body: JSON.stringify({ fk_marche: idMarche }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (response.ok) { const ret = response.json() ret.then(function (data) { dataProduitsMercurial = data showProduitsMercurial(data) // Créer l'onglet Mercurial en dernière position après affichage des produits if (!document.getElementById('liOngletMercurial')) { const ulOnglets = document.getElementById('listOngletsProduits') const liOnglet = document.createElement('li') liOnglet.setAttribute('role', 'presentation') liOnglet.id = 'liOngletMercurial' liOnglet.innerHTML = 'Mercurial
 
' ulOnglets.appendChild(liOnglet) } }) } }) } else { // Supprimer l'onglet et le panel Mercurial s'ils existent const liOnglet = document.getElementById('liOngletMercurial') if (liOnglet) { liOnglet.remove() } const divPanel = document.getElementById('tabMercurial') if (divPanel) { divPanel.remove() } } document.getElementById('inp_latitudeRR').value = seuilMargeRR document.getElementById('inp_latitudeDV').value = seuilMargeDV document.getElementById('titleMarche').innerHTML = 'Informations du marché ' + line.libelle + '' // le titre du panel des infos marché dans l'onglet 3. Devis $('#tdTxRemiseTrim').text(line.taux_remise_trimestrielle + ' %') $('#tdTxRemiseSeme').text(line.taux_remise_semestrielle + ' %') $('#tdTxRemiseAnnu').text(line.taux_remise_annuelle + ' %') document.getElementById('tdDebutFin').innerHTML = convertMySQLDateToFrenchDate(line.date_debut) + ' - ' + convertMySQLDateToFrenchDate(line.date_fin) // vérifie la date de validité du prix du marché est inférieure à la date du jour const today = new Date() if (line.date_validite_prix < today) { document.getElementById('tdDateValiditePrix').style.color = 'red' } else { // vérifie que cette date est encore valable dans 2 mois const today2mois = new Date() today2mois.setMonth(today2mois.getMonth() + 2) if (line.date_validite_prix < today2mois) { document.getElementById('tdDateValiditePrix').style.color = 'orange' } else { document.getElementById('tdDateValiditePrix').style.color = 'green' } } document.getElementById('tdDateValiditePrix').innerHTML = convertMySQLDateToFrenchDate(line.date_validite_prix) $('#tdGarantie').text(line.garantie) $('#tdRemisesCo').text(line.remises_commerciales) // On affiche les remises de marché if ( line.remise_palier_1 == 0 && line.remise_taux_1 == 0 && line.remise_palier_2 == 0 && line.remise_palier_3 == 0 && !chkPrixNets ) { document.getElementById('trRemisesMarche').style.display = 'none' } else { let remisesMarche = '' if (line.remise_palier_1 > 0) { remisesMarche += line.remise_taux_1 + '% à partir de ' + line.remise_palier_1 + 'k€' } else { remisesMarche += line.remise_taux_1 + '% de base' remiseMarcheDeBase = line.remise_taux_1 } if (line.remise_palier_2 > 0) { remisesMarche += ', ' + line.remise_taux_2 + '% à partir de ' + line.remise_palier_2 + 'k€' } if (line.remise_palier_3 > 0) { remisesMarche += ', ' + line.remise_taux_3 + '% à partir de ' + line.remise_palier_3 + 'k€' } if (line.remise_palier_4 > 0) { remisesMarche += ', ' + line.remise_taux_4 + '% à partir de ' + line.remise_palier_4 + 'k€' } if (chkPrixNets) { remisesMarche += ' (PRIX NETS)' } document.getElementById('tdRemisesMarche').innerHTML = remisesMarche document.getElementById('trRemisesMarche').style.display = 'block' // on ajoute le readonly à tous les champs de saisie des remises de marché si le marché est en prix nets et que c'est un RR if (chkPrixNets && (fkRole == 3 || fkRole == 4 || fkRole > 5)) { // on boucle sur tous les inputs inpRemise_* console.log('Prix Nets et RR : on boucle sur tous les inputs inpRemise_* pour les mettre en readonly') let inputs = document.getElementsByTagName('input') for (let i = 0; i < inputs.length; i++) { if (inputs[i].id.substr(0, 10) == 'inpRemise_') { inputs[i].readOnly = true } } } } // on sauvegarde les remises de marché dans le array aRemisesMarches aRemisesMarches = [ line.remise_palier_1, line.remise_taux_1, line.remise_palier_2, line.remise_taux_2, line.remise_palier_3, line.remise_taux_3, line.remise_palier_4, line.remise_taux_4, ] // on met à jour le bouton de sauvegarde du devis updateBtnSaveDevisAndSend() } else { $('#tdTxRemiseTrim').text('-') $('#tdTxRemiseSeme').text('-') $('#tdTxRemiseAnnu').text('-') $('#tdDebutFin').text('-') $('#tdDateValiditePrix').text('-') $('#tdGarantie').text('-') $('#tdRemisesCo').text('-') } } function showDevisMarcheProduits(dFamilles, dProduits) { // on met à jour les onglets de familles de groupes de produits showLoading() let famillesChargees = [] dFamilles.forEach(function (lineFamille) { let libIdFamille = lineFamille.libelle.replace(/ /g, '_') let idFamille = lineFamille.rowid let dataProduitsFamille = [] let tblBodyProduits = document.getElementById('tblProduits_' + libIdFamille).getElementsByTagName('tbody')[0] tblBodyProduits.innerHTML = '' // on charge les produits de cette famille dProduits.forEach(function (lineProduit) { if (lineProduit.fk_famille == idFamille) { // on renseigne cet idFamille dans le tableau des familles chargées si ce n'est pas déjà fait (pour éviter les doublons) if (famillesChargees.indexOf(libIdFamille) == -1) { famillesChargees.push(libIdFamille) } // on enregistre les données de ce produit dans le tableau dataProduitsFamille dataProduitsFamille.push(lineProduit) } }) // Une fois que tous les produits de cette famille sont chargés, on les affiche showProduitsFamille(dataProduitsFamille, libIdFamille) // puis on affecte les données de ce tableau dataProduitsFamille à l'autocomplete de l'input de recherche de produits de cette famille autocompleteProduitsFamille( document.getElementById('inpSearchProduct_' + libIdFamille), dataProduitsFamille, libIdFamille, idFamille ) }) // Enfin, on affiche que les onglets des familles de produits chargées // 1. On trie les onglets par ordre croissant de leur id_ordre let sortedOnglets = Array.from(elOngletsProduits).sort(function (a, b) { const aIndex = parseInt(a.id.split('_')[1]) const bIndex = parseInt(b.id.split('_')[1]) if (aIndex < bIndex) { return -1 } else if (aIndex > bIndex) { return 1 } else { return 0 } }) // 2. On boucle sur ces onglets triés en ordre décroissant et on les affiche dans le bon ordre sortedOnglets.forEach(function (onglet) { elListOngletsProduits.appendChild(onglet.parentNode) onglet.classList.remove('hidden') }) // 3. On cache maintenant les onglets des familles de produits non chargées, et on les pousse à la fin de la liste let nbOnglets = elOngletsProduits.length Array.from(elOngletsProduits).forEach(function (onglet) { const libIdFamille = onglet.getAttribute('data-famille') if (famillesChargees.indexOf(libIdFamille) == -1) { onglet.classList.add('hidden') nbOnglets-- // et on pousse cet onglet à la fin de la liste elListOngletsProduits.appendChild(onglet.parentNode) } }) console.log('nbOnglets : ' + nbOnglets) Array.from(elOngletsProduits).forEach(function (onglet) { const libIdFamille = onglet.getAttribute('data-famille') if (famillesChargees.indexOf(libIdFamille) > -1) { // on récupère le titre de l'onglet, on calcule sa longueur, let titreOnglet = onglet.innerText.trim() const espace = titreOnglet.indexOf(' ') if (espace > 0) { titreOnglet = titreOnglet.substring(0, espace) + '
' + titreOnglet.substring(espace + 1) onglet.innerHTML = titreOnglet } else { const longueur = titreOnglet.length if (longueur < 10) { onglet.innerHTML = titreOnglet + '

' } } } }) // 4. Enfin, on force le nav-justified à se réorganiser elListOngletsProduits.classList.remove('nav-justified') elListOngletsProduits.classList.add('nav-justified') hideLoading() } function showProduitsFamille(dProduits, libIdFamille) { // Affiche tous les produits d'une famille dans le 2ème onglet Produits // dProduits ne contient que les produits de cette famille let tblBody = document.getElementById('tblProduits_' + libIdFamille).getElementsByTagName('tbody')[0] tblBody.innerHTML = '' // on charge les produits de cette famille dProduits.forEach(function (lineProduit) { // Insertion d'une nouvelle ligne et création de ses colonnes showLineProduitFamille(tblBody, lineProduit, libIdFamille) }) } function showLineProduitFamille(tblBody, lineProduit, libIdFamille) { let newRow = tblBody.insertRow(0) newRow.className = 'ligProduit_' + libIdFamille newRow.id = 'ligProduit_' + libIdFamille + '_' + lineProduit.rowid newRow.setAttribute('data-rid', lineProduit.rowid) let celChkBox = newRow.insertCell(0) celChkBox.className = 'chkBox_' + libIdFamille + ' text-center' celChkBox.setAttribute('data-rid', lineProduit.rowid) celChkBox.innerHTML = '' let celCode = newRow.insertCell(1) celCode.innerHTML = lineProduit.code let celLibelle = newRow.insertCell(2) celLibelle.innerHTML = lineProduit.libelle let celFamille = newRow.insertCell(3) celFamille.innerHTML = lineProduit.lib_famille } function showProduitsMercurial(dProduits) { // Affiche tous les produits Mercurial dans l'onglet dédié let tblBody = document.getElementById('tblProduits_Mercurial').getElementsByTagName('tbody')[0] tblBody.innerHTML = '' if (dProduits && dProduits.length > 0) { dProduits.forEach(function (lineProduit) { let newRow = tblBody.insertRow(-1) newRow.className = 'ligProduit_Mercurial' newRow.id = 'ligProduit_Mercurial_' + lineProduit.rowid newRow.setAttribute('data-rid', lineProduit.rowid) let celChkBox = newRow.insertCell(0) celChkBox.className = 'chkBox_Mercurial text-center' celChkBox.setAttribute('data-rid', lineProduit.rowid) celChkBox.innerHTML = '' let celCode = newRow.insertCell(1) celCode.innerHTML = lineProduit.code let celLibelle = newRow.insertCell(2) celLibelle.innerHTML = lineProduit.libelle let celFamille = newRow.insertCell(3) celFamille.innerHTML = lineProduit.lib_famille || '-' }) // Brancher l'autocomplete sur le champ de recherche autocompleteProduitsFamille( document.getElementById('inpSearchProduct_Mercurial'), dProduits, 'Mercurial', 0 ) } } $('a[data-toggle="tab"]').on('show.bs.tab', function (e) { if (idDevis == 0) { if ($(this).attr('href') == '#tabproduits' || $(this).attr('href') == '#tabdevis') { showNotification( 'Erreur', "Vous devez d'abord sélectionner un devis dans la liste de vos devis à gauche", 'warning' ) return false } } else { if (chkChange == 1) { //! il y a un changement en cours... if ( $(this).attr('href') == '#tabentete' || $(this).attr('href') == '#tabproduits' || $(this).attr('href') == '#tabdevis' ) { if ( confirm( "Attention, vous avez fait des modifications non enregistrées sur cette page du devis. Vous allez perdre d'éventuelles modifications importantes. Voulez-vous continuer ?" ) ) { chkChange = 0 } else { return false } } } } }) function changeClientsSecteur() { // en cas de changement de secteur, on recharge les clients du commercial sur ce secteur ou sur toute la France chkClientsSecteur = document.getElementById('inp_chk_clients_secteur').checked ? 1 : 0 console.log('changement de secteur clients : ' + chkClientsSecteur) if (fkUser > 0) { fetch('/jxdevis/load_clients_devis', { method: 'POST', body: JSON.stringify({ user: fkUser, secteur: chkClientsSecteur.toString(), }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification('Erreur', "Le chargement des clients n'a pas abouti", 'error') } else { const retClients = response.json() retClients.then(function (dataClients) { clients = dataClients // on charge les clients du commercial sur son secteur ou sur toute la France dans l'autocomplete autocompleteClient(document.getElementById('inp_lib_client'), dataClients) }) } }) } } function clickDupDevis() { idDevis = this.getAttribute('data-rid') if (confirm('Confirmez-vous la duplication de ce devis n° ' + idDevis + ' ?')) { showLoading() const formData = new FormData() formData.append('rid', idDevis) fetch('/jxdevis/duplic_devis', { method: 'POST', body: formData, }) .then((response) => { if (!response.ok) { throw new Error('Erreur réseau') } return response.text() }) .then((data) => { showNotification('Duplication', 'Duplication du devis effectuée avec succès', 'success') hideLoading() setTimeout(function () { location.reload() }, 2000) }) .catch((error) => { showNotification('Erreur', 'Erreur lors de la duplication de ce devis : ' + error.message, 'error') hideLoading() }) } } function clickReactiverDevis(e) { console.log('clickReactiverDevis appelé') idDevis = this.getAttribute('data-rid') console.log('ID du devis à réactiver:', idDevis) if ( confirm( 'Voulez-vous réactiver ce devis archivé n° ' + idDevis + ' ?\n\nLe devis passera du statut "Archivé" au statut "En cours".' ) ) { showLoading() // Utilisation de fetch API (vanilla JS) au lieu de jQuery const formData = new FormData() formData.append('rid', idDevis) fetch('/jxdevis/reactiver_devis', { method: 'POST', body: formData, }) .then((response) => response.json()) .then((data) => { showNotification('Réactivation', 'Le devis a été réactivé avec succès', 'success') hideLoading() setTimeout(function () { location.reload() }, 2000) }) .catch((error) => { showNotification('Erreur', 'Erreur lors de la réactivation de ce devis : ' + error, 'error') hideLoading() }) } } function clickExpExcelDevis(e) { const idDevis = this.dataset.rid if (confirm("Confirmez l'exportation de ce devis #" + idDevis + ' en Excel ?')) { const url = '/expxls/export_sap_devis/' + idDevis window.open(url) return false } } let clickSupprDevis = function (e) { const idDevis = this.dataset.rid if (confirm('Confirmez la suppression définitive de ce devis #' + idDevis + ' ?')) { showLoading() fetch('/jxdevis/delete_devis', { method: 'POST', body: JSON.stringify({ cid: idDevis }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification('Erreur', "La suppression du devis n'a pas abouti", 'error') } else { const retDevis = response.json() retDevis.then(function (dataDevis) { // On supprime la ligne du tableau qui contenait ce bouton let trDevis = document.getElementById('tr_' + idDevis) trDevis.parentNode.removeChild(trDevis) showNotification('Suppression', 'Suppression du devis effectuée avec succès', 'success') setTimeout(function () { location.reload() }, 2000) // 2000 millisecondes = 2 secondes }) } }) hideLoading() } return false } let clickCreateClient = function () { if (this.innerHTML == 'Créer un nouveau client') { if (confirm('Voulez-vous créer un nouveau client pour ce devis ?')) { document.getElementById('frmCreateClient').reset() showModal(document.getElementById('modalCreateClient')) document.getElementById('inp_create_libelle').focus() } } else { document.getElementById('inp_create_libelle').value = document.getElementById('inp_lib_client').value document.getElementById('inp_create_type_client').value = document.getElementById('selTypeEtab').value document.getElementById('inp_create_adresse1').value = document.getElementById('inp_adresse1').value document.getElementById('inp_create_adresse2').value = document.getElementById('inp_adresse2').value document.getElementById('inp_create_adresse3').value = document.getElementById('inp_adresse3').value document.getElementById('inp_create_cp').value = document.getElementById('inp_cp').value document.getElementById('inp_create_ville').value = document.getElementById('inp_ville').value showModal(document.getElementById('modalCreateClient')) document.getElementById('inp_create_libelle').focus() } } let clickCancelCreateClient = function () { hideModal(document.getElementById('modalCreateClient')) } let clickSaveCreateClient = function () { showLoading() const clientData = { libelle: document.getElementById('inp_create_libelle').value, type_client: document.getElementById('inp_create_type_client').value, adresse1: document.getElementById('inp_create_adresse1').value, adresse2: document.getElementById('inp_create_adresse2').value, adresse3: document.getElementById('inp_create_adresse3').value, cp: document.getElementById('inp_create_cp').value, ville: document.getElementById('inp_create_ville').value } fetch('/jxdevis/save_new_client', { method: 'POST', body: JSON.stringify(clientData), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => response.json()) .then((data) => { hideLoading() if (data.success) { const newClientId = data.rowid const newClientCode = data.code document.getElementById('inp_fk_client').value = newClientId document.getElementById('inp_lib_client').value = clientData.libelle document.getElementById('selTypeEtab').value = clientData.type_client document.getElementById('inp_type_client').value = clientData.type_client document.getElementById('inp_adresse1').value = clientData.adresse1 document.getElementById('inp_adresse2').value = clientData.adresse2 document.getElementById('inp_adresse3').value = clientData.adresse3 document.getElementById('inp_cp').value = clientData.cp document.getElementById('inp_ville').value = clientData.ville hideModal(document.getElementById('modalCreateClient')) document.getElementById('btnCreateClient').innerHTML = 'Modifier le client' document.getElementById('btnCreateClient').classList.remove('btn-primary') document.getElementById('btnCreateClient').classList.add('btn-info') loadContactsClient(newClientCode).then(() => { showNotification('Succès', 'Client créé avec succès. Vous pouvez maintenant gérer ses contacts.', 'success') }) chkCreateClient = false } else { showNotification('Erreur', data.message || 'Erreur lors de la création du client', 'error') } }) .catch((error) => { hideLoading() console.error('Erreur création client:', error) showNotification('Erreur', 'Impossible de créer le client', 'error') }) } let clickSpeciaux = function () { showLoading() // On cherche dans la table devis_speciaux s'il y a une ligne pour ce devis fetch('/jxdevis/load_devis_speciaux', { method: 'POST', body: JSON.stringify({ cid: idDevis }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { showNotification('Erreur', "Le chargement des produits spéciaux n'a pas abouti", 'error') } else { const retSpeciaux = response.json() retSpeciaux.then(function (dataSpeciaux) { // on vide les 5 lignes de produits spéciaux pour éviter de reprendre des données d'un autre devis for (i = 1; i <= 5; i++) { document.getElementById('inp_specialFkProduit_' + i).value = '' document.getElementById('inp_specialCode_' + i).value = '' document.getElementById('inp_specialLibe_' + i).value = '' document.getElementById('inp_specialQte_' + i).value = '' document.getElementById('inp_specialCout_' + i).value = '' document.getElementById('inp_chk_specialEchantillon_' + i).checked = false document.getElementById('inp_specialDate_' + i).value = '' document.getElementById('inp_specialConcurrent_' + i).value = '' document.getElementById('inp_specialDescription_' + i).value = '' } if (dataSpeciaux.length > 0) { // on a trouvé une ligne dans la table devis_speciaux const data = dataSpeciaux[0] // on charge les données dans le formulaire document.getElementById('inp_idDevis_speciaux').value = data.fk_devis if (data.chk_livr_multi == '1') { document.getElementById('inp_chk_livr_multi').checked = true } else { document.getElementById('inp_chk_livr_multi').checked = false } document.getElementById('inp_nb_livr').value = data.nb_livr document.getElementById('inp_date_livr_1').value = data.date_livr_1 for (i = 1; i <= 5; i++) { document.getElementById('inp_specialFkProduit_' + i).value = data[`fk_produit_${i}`] document.getElementById('inp_specialCode_' + i).value = data[`code_produit_${i}`] document.getElementById('inp_specialLibe_' + i).value = data[`lib_produit_${i}`] document.getElementById('inp_specialQte_' + i).value = data[`qte_${i}`] document.getElementById('inp_specialCout_' + i).value = data[`surcout_${i}`] if (data[`chk_echantillon_${i}`] == '1') { document.getElementById('inp_chk_specialEchantillon_' + i).checked = true } else { document.getElementById('inp_chk_specialEchantillon_' + i).checked = false } if (data[`date_echantillon_${i}`] != '0000-00-00') { document.getElementById('inp_specialDate_' + i).value = data[`date_echantillon_${i}`] } document.getElementById('inp_specialConcurrent_' + i).value = data[`lib_concurrent_${i}`] document.getElementById('inp_specialDescription_' + i).value = data[`description_${i}`] } document.getElementById('inp_specialEmail').value = data.email if (data.chk_email == 1) { document.getElementById('inp_specialEmail').style.backgroundColor = 'lightgreen' } else { document.getElementById('inp_specialEmail').style.backgroundColor = 'white' } } else { // on n'a pas trouvé de ligne dans la table devis_speciaux // on vide les champs du formulaire document.getElementById('inp_idDevis_speciaux').value = idDevis document.getElementById('frmSpeciaux').reset() } autocompleteProdSpecial(document.getElementById('inp_specialCode_1'), '1', dataProduitsMarche) autocompleteProdSpecial(document.getElementById('inp_specialCode_2'), '2', dataProduitsMarche) autocompleteProdSpecial(document.getElementById('inp_specialCode_3'), '3', dataProduitsMarche) autocompleteProdSpecial(document.getElementById('inp_specialCode_4'), '4', dataProduitsMarche) autocompleteProdSpecial(document.getElementById('inp_specialCode_5'), '5', dataProduitsMarche) }) } }) hideLoading() showModal(document.getElementById('modalSpeciaux')) } let clickCancelSpeciaux = function () { hideModal(document.getElementById('modalSpeciaux')) } let clickSaveSpeciaux = function () { let frmData = new FormData(document.getElementById('frmSpeciaux')) let objData = {} frmData.forEach(function (value, key) { objData[key] = value }) console.log(objData) showLoading() fetch('/jxdevis/save_devis_speciaux', { method: 'POST', body: JSON.stringify(objData), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }).then((response) => { if (!response.ok) { hideLoading() showNotification('Erreur', "L'enregistrement des produits spéciaux de ce devis n'a pas abouti", 'error') } else { const ret = response.json() ret.then(function (data) { hideLoading() showNotification('Succès', 'Enregistrement des produits spéciaux de ce devis effectué', 'success') }) } }) hideLoading() hideModal(document.getElementById('modalSpeciaux')) chkChange = 0 return false } let clickSaveEnTete = function () { if (document.getElementById('inp_lib_client').value == '') { showNotification('Erreur', 'Enregistrement impossible : vous devez sélectionner ou créer un client', 'error') document.getElementById('inp_lib_client').focus() return false } if (document.getElementById('inp_num_opportunite').value == '') { showNotification('Erreur', "Enregistrement impossible : vous devez saisir un N° d'opportunité", 'error') document.getElementById('inp_num_opportunite').focus() return false } if (document.getElementById('sel_contact').value == '0') { showNotification( 'Erreur', 'Enregistrement impossible : vous devez sélectionner un contact', 'error' ) document.getElementById('sel_contact').focus() return false } if (document.getElementById('inp_fk_marche').value == '0') { showNotification('Erreur', 'Enregistrement impossible : vous devez sélectionner un marché', 'error') document.getElementById('inp_fk_marche').focus() return false } const dateDemande = document.getElementById('inp_date_demande').value const dateRemise = document.getElementById('inp_date_remise').value // Vérification de la validité des dates saisies if (isNaN(Date.parse(dateDemande))) { showNotification( 'Erreur', "Enregistrement impossible : la date de la demande n'est pas saisie ou est incorrecte", 'error' ) dateDemande.focus() return false } if (isNaN(Date.parse(dateRemise))) { showNotification( 'Erreur', "Enregistrement impossible : la date de la remise n'est pas saisie ou est incorrecte", 'error' ) dateRemise.focus() return false } if (dateDemande !== '' && dateRemise !== '') { const dateDemandeObj = new Date(Date.parse(dateDemande)) const dateRemiseObj = new Date(Date.parse(dateRemise)) if (dateRemiseObj <= dateDemandeObj) { showNotification( 'Erreur', 'Enregistrement impossible : la date de remise au client doit être supérieure à la date de la demande', 'error' ) dateRemise.focus() return false // empêcher l'enregistrement du formulaire } } // Vérification du non changement du marché if (idDevis > 0) { if (idMarche != document.getElementById('inp_fk_marche').value) { if ( !confirm( 'Vous avez changé le marché de ce devis. Confirmez-vous ce changement ? Cela va supprimer tous les produits enregistrés de ce devis.' ) ) { return false } } } showLoading() //! on récupère tous les input de la form dans dataform const form = document.getElementById('frmDevisEntete') const formData = new FormData(form) fetch('/jxdevis/save_devis_entete', { method: 'POST', body: formData, }) .then((response) => { if (!response.ok) { throw new Error('Erreur réseau') } return response.json() }) .then((data) => { hideLoading() showNotification('Enregistrement', 'Enregistrement effectué avec succès', 'success') setTimeout(function () { location.reload() }, 2000) }) .catch((error) => { showNotification('Erreur', 'Erreur lors de la sauvegarde du devis : ' + error.message, 'error') hideLoading() }) chkChange = 0 return false } let clickCreateDevis = function () { if (confirm('Voulez-vous créer un nouveau devis ?')) { //! On vide tous les champs de la form frmDevisEntete document.getElementById('frmDevisEntete').reset() document.getElementById('inp_rowid').value = 0 document.getElementById('inp_fk_user').value = fkUser document.getElementById('inp_fk_marche').value = 0 document.getElementById('inp_fk_client').value = '' document.getElementById('inp_chk_devis_photos').checked = false document.getElementById('inp_num_opportunite').focus() //! On vérifie le bon chargement des clients if (oldChkClientsSecteur == 2) { // les clients n'ont pas encore été chargés document.getElementById('inp_chk_clients_secteur').checked = true oldChkClientsSecteur = 1 changeClientsSecteur() } chkChange = 1 elDivDevis.style.display = 'block' chkPageLoad = false idDevis = 0 } } const inpSearchProduit = document.getElementById('inpSearchProduit') if (inpSearchProduit) { inpSearchProduit.addEventListener('keypress', function (e) { if (e.which == 13) { const searchValue = document.getElementById('inpSearchProduit').value if (searchValue.length > 2) { const formData = new FormData() formData.append('term', searchValue) fetch('/jxdevis/load_devis_produits_search', { method: 'POST', body: formData, }) .then((response) => { if (!response.ok) { throw new Error('Erreur réseau') } return response.json() }) .then((data) => { const tblProduits = document.getElementById('tblProduits') const rowCount = tblProduits.rows.length for (let i = rowCount - 1; i > 0; i--) { tblProduits.deleteRow(i) } data.forEach(function (line) { const row = tblProduits.insertRow() row.innerHTML = '' + line.code + '' + line.libelle + '' }) }) .catch((error) => { showNotification('Erreur', 'Erreur lors de la recherche de produits : ' + error.message, 'error') }) return false } else { showNotification('Saisie', 'Vous devez saisir au moins 3 caractères', 'warning') return false } } }) } $(document).on('change', 'input[name^="chkBoxProd_"]', function () { // on ajoute un produit disponible dans la liste des produits sélectionnés const rid = this.dataset.rid const code = this.dataset.code const libelle = this.dataset.libelle //! 1. on ajoute ce produit dans la liste des produits sélectionnés $('#tblProduitsSelect').append( '' + code + '' + libelle + '' ) //! 2. on cache la ligne de ce produit de la liste des produits disponibles à gauche const trProd = this.parentNode.parentNode trProd.style.display = 'none' chkChange = 1 return false }) $(document).on('change', 'input[name^="chkProdSelect_"]', function () { // on supprime un produit sélectionné pour le remettre dans la liste des produits disponibles let rid = this.dataset.rid let code = this.dataset.code let libelle = this.dataset.libelle //! 1. on remet ce produit dans la liste des produits du catalogue const chkBoxProd = document.querySelector('#chkBoxProd_' + rid) if (chkBoxProd) { // Si l'élément existe, on peut procéder chkBoxProd.checked = false const trProd = chkBoxProd.parentNode.parentNode trProd.style.display = 'table-row' } else { console.warn(`L'élément #chkBoxProd_${rid} n'existe pas dans le DOM`) } //! 2. on supprime ce produit de la liste des produits sélectionnés const trProdSelect = this.parentNode.parentNode trProdSelect.parentNode.removeChild(trProdSelect) chkChange = 1 return false }) let clickSaveSelProduits = function () { //! Sauve la liste des produits sélectionnés d'un devis const tblBodySelect = document.getElementById('tblProduitsSelect').getElementsByTagName('tbody')[0] const nbProduits = tblBodySelect.rows.length showLoading() let aProduits = new Array() let lstProduits = '' // if (nbProduits > 0) { //! On parcourt la liste des produits sélectionnés for (let i = 0, row; (row = tblBodySelect.rows[i]); i++) { const rid = row.cells[0].firstElementChild.dataset.rid aProduits.push(rid) //! On crée une chaîne liste des produits sélectionnés avec le "s" comme séparateur lstProduits += ';' + rid } let dataProduits = {} dataProduits['cid'] = idDevis dataProduits['produits'] = lstProduits fetch('/jxdevis/save_devis_produits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(dataProduits), }).then((response) => { if (!response.ok) { hideLoading() showNotification('Erreur', "L'enregistrement des produits de ce devis n'a pas abouti", 'error') } else { const ret = response.json() ret.then(function (data) { showDevisPro(data) hideLoading() showNotification('Succès', 'Enregistrement des ' + nbProduits + ' produits de ce devis effectué', 'success') chkChange = 0 }) } }) return false } function showDevisPro(data) { //! Rafraîchit la liste des produits d'un devis dans le 3ème onglet Devis const tblBodyPro = document.getElementById('tblDevisPro').getElementsByTagName('tbody')[0] tblBodyPro.innerHTML = '' if (data.length > 0) { // au moins un produit trouvé pour ce devis const nbProduits = data.length // Si le marché est un marché de prix nets, on met les champs de saisie des remises en readonly let readonlyRemise = chkPrixNets ? 'readonly="readonly"' : '' // Ajout du 20/02/2025 : si marché hybride, les produits de ce marché sont en prix nets let readonlyRemiseProduit = '' // Ajout du 26 juin 2024 : si l'utilisateur est le DC ou DV ou DGC, ils peuvent modifier les remises dans tous les cas if (fkRole < 3 || fkRole == 5) { readonlyRemise = '' } // Fin de l'ajout du 26 juin 2024 // on récupère le premier fk_produit, pour simuler un changement sur ce produit pour recalculer les totaux en fin de boucle const fkProduit1 = data[0]['fk_produit'] for (let key in data) { if (data.hasOwnProperty(key)) { // Récupération des valeurs de la ligne let val = data[key] // 20/02/2025 : On initialise le readonlyremise par produit pour gérer les cas de marché hybride où leurs produits sont en Prix Nets readonlyRemiseProduit = readonlyRemise // on insère la ligne pour la saisie du commentaire au-dessus de la ligne du produit let newRowCom = tblBodyPro.insertRow(-1) newRowCom.className = 'hidden' newRowCom.id = 'trCom_' + val['fk_produit'] newRowCom.dataset.rid = val['fk_produit'] let celCom = newRowCom.insertCell(0) celCom.colSpan = 8 celCom.innerHTML = '
' // Insertion d'une nouvelle ligne et création de ses colonnes let newRowPro = tblBodyPro.insertRow(-1) newRowPro.id = 'trPro_' + val['fk_produit'] newRowPro.dataset.rid = val['fk_produit'] newRowPro.dataset.ordre = val['ordre'] newRowPro.dataset.code = val['code'] newRowPro.dataset.achat = val['prix_achat_net'] newRowPro.dataset.achatdiscount = val['prix_achat_net'] newRowPro.dataset.vente = val['prix_vente'] newRowPro.dataset.discount1 = val['prc_discount_1'] newRowPro.dataset.quantite1 = val['quantite_1'] newRowPro.dataset.discount2 = val['prc_discount_2'] newRowPro.dataset.quantite2 = val['quantite_2'] newRowPro.dataset.discount3 = val['prc_discount_3'] newRowPro.dataset.quantite3 = val['quantite_3'] newRowPro.dataset.discount4 = val['prc_discount_4'] newRowPro.dataset.quantite4 = val['quantite_4'] newRowPro.dataset.discount5 = val['prc_discount_5'] newRowPro.dataset.quantite5 = val['quantite_5'] newRowPro.dataset.discount6 = val['prc_discount_6'] newRowPro.dataset.quantite6 = val['quantite_6'] let celCodePro = newRowPro.insertCell(0) const svgColor = val['commentaire'] == '' ? 'lightgray' : 'red' const svgComment = '' let inputOrdreHidden = '' celCodePro.innerHTML = val['code'] + ' ' + svgComment + inputOrdreHidden document.getElementById('commentProd_' + val['fk_produit']).addEventListener('click', showCommentProd) let celLibellePro = newRowPro.insertCell(1) celLibellePro.innerHTML = val['libelle'] let celPrixVentePro = newRowPro.insertCell(2) celPrixVentePro.className = 'text-right' celPrixVentePro.innerHTML = formatAmount(val['prix_vente']) + ' €' let celQtePro = newRowPro.insertCell(3) celQtePro.innerHTML = '' document.getElementById('inpQte_' + val['fk_produit']).addEventListener('change', calculDevis) let celRemisePro = newRowPro.insertCell(4) // Nouveau code 21/09 // S'il y a une remise de base sur le marché, on vérifie chaque remise produit pour l'aligner à cette remise de base si elle est supérieure let remiseProduit = val['remise'] console.log('Remise de base : ' + remiseMarcheDeBase + ' vs remise sur le produit : ' + val['remise']) if (remiseMarcheDeBase > 0) { if (val['remise'] < remiseMarcheDeBase) { console.log('La remise du produit est inférieure à la remise de base, on la force à la remise de base') remiseProduit = remiseMarcheDeBase } } // Fin du nouveau code du 21/09 // 20/02/2025 if (val['chk_prix_net']) { if (val['chk_prix_net'] == 1) { console.log('Le produit ' + val['code'] + ' est sur un marché hybride donc chk_prix_net=1') readonlyRemiseProduit = 'readonly=readonly' } } celRemisePro.innerHTML = '
%
' if (readonlyRemiseProduit == '') { document.getElementById('inpRemise_' + val['fk_produit']).addEventListener('change', calculDevis) } // nouvelle colonne PU vente avec remise let celPUVenteRemPro = newRowPro.insertCell(5) celPUVenteRemPro.innerHTML = '
' // Fin nouvelle colonne let celHTPro = newRowPro.insertCell(6) celHTPro.innerHTML = '
' let celVariante = newRowPro.insertCell(7) celVariante.className = 'text-center' celVariante.innerHTML = '' document.getElementById('chkVariante_' + val['fk_produit']).addEventListener('change', calculDevis) let celMargePro = newRowPro.insertCell(8) celMargePro.innerHTML = '
%
' // on calcule et enregistre le prix d'achat discount du produit si on a un prc_discount let chkDiscount = false if (val['prc_discount_6'] > 0 && val['quantite_6'] > 0) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_6'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_6'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_6 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_5'] > 0 && val['quantite_5'] > 0 && !chkDiscount) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_5'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_5'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_5 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_4'] > 0 && val['quantite_4'] > 0 && !chkDiscount) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_4'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_4'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_4 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_3'] > 0 && val['quantite_3'] > 0 && !chkDiscount) { // il y a un prc_discount sur ce produit if (parseInt(val['qte']) >= parseInt(val['quantite_3'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_3'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_3 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_2'] > 0 && val['quantite_2'] > 0 && !chkDiscount) { if (parseInt(val['qte']) >= parseInt(val['quantite_2'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_2'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_2 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (val['prc_discount_1'] > 0 && val['quantite_1'] > 0 && !chkDiscount) { if (parseInt(val['qte']) >= parseInt(val['quantite_1'])) { // on applique le prc_discount const prixAchat = (val['prix_achat_net'] * 1 * (val['prc_discount_1'] * 1)) / 100 // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne newRowPro.dataset.achatdiscount = prixAchat chkDiscount = true console.log('showDevisPro prc_discount_1 appliqué pour la ref ' + val['code'] + ' : ' + prixAchat) } } if (chkRegleSeuilsMarge == 1) { // Le marché demande la prise en compte des seuils de marge RR et DV paramétrés dans la table produits_familles if (val['marge_rr'] != seuilMargeRR) seuilMargeRR = val['marge_rr'] if (val['marge_dv'] != seuilMargeDV) seuilMargeDV = val['marge_dv'] console.log( "C'est un marché qui prend en compte les seuils de marge RR et DV : " + seuilMargeRR + ' / ' + seuilMargeDV ) } } } // On met à jour les seuils de marge RR et DV en fonction du marché et des produits document.getElementById('inp_latitudeRR').value = seuilMargeRR document.getElementById('inp_latitudeDV').value = seuilMargeDV // On simule le changement de quantité sur la première ligne pour recalculer les totaux const inpQte = document.getElementById('inpQte_' + fkProduit1) const event = new Event('change') inpQte.dispatchEvent(event) } } let showCommentProd = function () { console.log('click sur le SVG commentProd de la ligne ' + this.dataset.rid) document.getElementById('inp_commentProdId').value = this.dataset.rid document.getElementById('modCommentProdTitre').innerHTML = 'Commentaire sur le produit ' + this.dataset.code const inpComment = document.getElementById('inp_commentProd') inpComment.value = document.getElementById('inpCom_' + this.dataset.rid).value showModal(document.getElementById('modalCommentProd')) inpComment.focus() return false } function controlRemisesProduits(totalHT) { // Contrôle des remises du marché en fonction du total HT du devis // on arrondit le total HT à l'entier supérieur const totHT = Math.ceil(totalHT) // la remise calculée sur ce devis et à appliquer let txRemiseAppliquee = 0.0 // aRemisesMarches = [line.remise_palier_1, line.remise_taux_1, line.remise_palier_2, line.remise_taux_2, line.remise_palier_3, line.remise_taux_3, line.remise_palier_4, line.remise_taux_4]; const totPalier1 = aRemisesMarches[0] * 1000 const txPalier1 = aRemisesMarches[1] const totPalier2 = aRemisesMarches[2] * 1000 const txPalier2 = aRemisesMarches[3] const totPalier3 = aRemisesMarches[4] * 1000 const txPalier3 = aRemisesMarches[5] const totPalier4 = aRemisesMarches[6] * 1000 const txPalier4 = aRemisesMarches[7] console.log('controlRemisesProduits totHT : ' + totHT + ' totPalier1 : ' + totPalier1 + ' txPalier1 : ' + txPalier1) if (txPalier1 == 0 && txPalier2 == 0 && txPalier3 == 0 && txPalier4 == 0) { // pas de remise sur ce marché return 0 } else { if (totPalier1 >= 0 && txPalier1 > 0) { // il y a une remise sur ce marché à partir de ce total HT, si ce total est atteint on applique la remise console.log( 'controlRemisesProduits totHT : ' + totHT + ' totPalier1 : ' + totPalier1 + ' txPalier1 : ' + txPalier1 ) if (totHT >= totPalier1) txRemiseAppliquee = txPalier1 } if (totPalier2 > 0 && txPalier2 > 0) { // il y a une remise sur ce marché à partir de ce total HT, si ce total est atteint on applique la remise if (totHT >= totPalier2) txRemiseAppliquee = txPalier2 } if (totPalier3 > 0 && txPalier3 > 0) { // il y a une remise sur ce marché à partir de ce total HT, si ce total est atteint on applique la remise if (totHT >= totPalier3) txRemiseAppliquee = txPalier3 } if (totPalier4 > 0 && txPalier4 > 0) { // il y a une remise sur ce marché à partir de ce total HT, si ce total est atteint on applique la remise if (totHT >= totPalier4) txRemiseAppliquee = txPalier4 } console.log('controlRemisesProduits totalHT : ' + totHT + ' txRemiseAppliquee : ' + txRemiseAppliquee) return txRemiseAppliquee } } function getPrixAchatAvecDiscount(idProduit, qte) { let trPro = document.getElementById('trPro_' + idProduit) const prixAchat = parseFloat(trPro.dataset.achat) const qtt = parseInt(qte, 10) // console.log("==== Début de getPrixAchatAvecDiscount pour la ref " + trPro.dataset.code + " : prix achat " + prixAchat + " et qte " + qtt); let prixAchatDiscount = prixAchat let discount = [] discount[1] = Array(parseFloat(trPro.dataset.discount1), parseInt(trPro.dataset.quantite1, 10)) discount[2] = Array(parseFloat(trPro.dataset.discount2), parseInt(trPro.dataset.quantite2, 10)) discount[3] = Array(parseFloat(trPro.dataset.discount3), parseInt(trPro.dataset.quantite3, 10)) discount[4] = Array(parseFloat(trPro.dataset.discount4), parseInt(trPro.dataset.quantite4, 10)) discount[5] = Array(parseFloat(trPro.dataset.discount5), parseInt(trPro.dataset.quantite5, 10)) discount[6] = Array(parseFloat(trPro.dataset.discount6), parseInt(trPro.dataset.quantite6, 10)) for (let inc = 6; inc > 0; inc--) { const dscnt = discount[inc][0] const qntt = discount[inc][1] if (dscnt > 0 && qntt > 0) { if (qtt >= qntt) { // on applique le prc_discount prixAchatDiscount = prixAchat - (prixAchat * dscnt) / 100 console.log( '=== idProduit : ' + idProduit + ' (prc_discount_' + inc + ' applique de ' + dscnt + '%, qte : ' + qtt + ' pour une quantité mini de ' + qntt + ') = ' + prixAchatDiscount ) break } } } // On met à jour le nouveau prix achat de ce produit dans le dataset achatdiscount de sa ligne trPro.dataset.achatdiscount = prixAchatDiscount.toFixed(2) // console.log("==== Fin de getPrixAchatAvecDiscount pour la ref " + trPro.dataset.code); return prixAchatDiscount } let calculDevis = function () { console.log('calculDevis...') const idProduit = this.dataset.rid // On récupère toutes les infos de ce produit au niveau de sa ligne trPro_XX stockées en dataset let trPro = document.getElementById('trPro_' + idProduit) const code = trPro.dataset.code let prixAchat = trPro.dataset.achat const prixVente = trPro.dataset.vente // console.log("idProduit: " + idProduit + ", code : " + code + ", prixAchat: " + prixAchat + ", prixVente: " + prixVente); let qte = 0 let remise = 0 let variante = 0 let typeInput = '' // qte, remise, variante if (this.name.indexOf('inpQte') > -1) { // c'est la quantité qui a changé qte = this.value remise = document.getElementById('inpRemise_' + idProduit).value variante = document.getElementById('chkVariante_' + idProduit).checked typeInput = 'qte' } else if (this.name.indexOf('inpRemise') > -1) { // c'est la remise qui a changé qte = document.getElementById('inpQte_' + idProduit).value remise = this.value variante = document.getElementById('chkVariante_' + idProduit).checked typeInput = 'remise' chkSaisieRemise = true } else if (this.name.indexOf('chkVariante') > -1) { // c'est la variante qui a changé qte = document.getElementById('inpQte_' + idProduit).value remise = document.getElementById('inpRemise_' + idProduit).value variante = this.checked typeInput = 'variante' } let totalHt = 0 let totalDevisHt = 0 let totalDevisHtRemise = 0 let txMarge = 0 let coutTotalAchat = 0 let margeTotale = 0 // on calcule le total HT de cette ligne let remiseProduit = 0 if (variante) { remiseProduit = 0 totalHt = 0 } else { remiseProduit = (remise * 1 * (prixVente * 1)) / 100 totalHt = (prixVente * 1 - remiseProduit * 1) * (qte * 1) } let inpHT = document.getElementById('inpHT_' + idProduit) inpHT.value = parseFloat(totalHt).toFixed(2) // Modif du 25/04 : on calcule la marge même si c'est une variante / option //if (variante) { txMarge = 0 //} else { if (prixAchat !== '0.00' && prixVente !== '0.00' && qte > 0) { let prixVenteApresRemise = prixVente if (remise > 0) { prixVenteApresRemise = prixVente - (prixVente * 1 * (remise * 1)) / 100 } console.log( 'Marge sur code : ' + code + ' - prixAchat : ' + prixAchat + ' - prixVente : ' + prixVente + ' - prixVenteApresRemise : ' + prixVenteApresRemise ) txMarge = ((prixVenteApresRemise * 1 - prixAchat * 1) / prixVenteApresRemise) * 100 } else { txMarge = 0 console.log( 'ERREUR idProduit : ' + idProduit + ', code : ' + code + ' - prixAchat : ' + prixAchat + ' - prixVente : ' + prixVente ) if (qte > 0) showNotification( 'Info', "Le prix d'achat et/ou le prix de vente n'est pas renseigné pour ce produit, la marge ne peut pas être calculée.", 'info' ) } //} let inpMG = document.getElementById('inpMG_' + idProduit) inpMG.value = parseFloat(txMarge).toFixed(2) console.log('Boucle 1 : calcul Total HT sans remise') //! on boucle sur tous les éléments dont le name commence par inpQte_ pour calculer le total HT sans remise for (let i = 0, elInp; (elInp = document.querySelectorAll("[name ^= 'inpQte_' ]")[i]); i++) { const idProd = elInp.dataset.rid const ligne = document.getElementById('trPro_' + idProd) const code = ligne.dataset.code const vente = ligne.dataset.vente * 1 const qte = elInp.value * 1 // Mise à jour du 09/11 : on calcule le prix d'achat du produit avec éventuel discount suivant sa qté const achat = getPrixAchatAvecDiscount(idProd, elInp.value) // Fin de la mise à jour du 09/11 const varOption = document.getElementById('chkVariante_' + idProd).checked if (!varOption) { // calcul avec juste la quantité et le prix de vente const vente = elInp.dataset.vente * 1 totalDevisHt += qte * vente } // Mise à jour du 03/11/2023 : nouvelle colonne du Prix de Vente Unitaire (avec remise) // On met à jour le PUVenteRem sur la ligne const remProd = document.getElementById('inpRemise_' + idProd).value const remise = remProd * 1 let puVenteApresRemise = vente if (remise > 0) { puVenteApresRemise = vente - (vente * remise) / 100 } document.getElementById('inpPUVenteRem_' + idProd).value = puVenteApresRemise.toFixed(2) console.log('--- 1 Produit : ' + code + ' - PUVenteApresRemise : ' + puVenteApresRemise) // Fin de la mise à jour du 03/11/2023 // Modif du 25/04 : on calcule la marge même si c'est une variante / option //if (variante) { txMarge = 0 //} else { if (achat > 0 && vente > 0 && qte > 0) { let venteApresRemise = vente if (remise > 0) { venteApresRemise = vente - (vente * remise) / 100 } console.log( '--- 1 Marge sur : ' + code + ' - achat : ' + achat + ' - vente : ' + vente + ' - venteApresRemise : ' + venteApresRemise ) // Ajout du 0&/12/2023 pour éviter le -infinity if (venteApresRemise > 0) txMarge = ((venteApresRemise - achat) / venteApresRemise) * 100 } else { txMarge = 0 console.log('--- 1 ERREUR : ' + code + ' - achat : ' + achat + ' - vente : ' + vente) if (qte > 0) showNotification( 'Info', "Le prix d'achat et/ou le prix de vente n'est pas renseigné pour ce produit " + code + ', la marge ne peut pas être calculée.', 'info' ) } //} let inpMG = document.getElementById('inpMG_' + idProd) inpMG.value = parseFloat(txMarge).toFixed(2) } // on met à jour le total HT du devis avant remise let inpTotalHT = document.getElementById('inpTotalHT') inpTotalHT.value = totalDevisHt.toFixed(2) // le total HT du devis a été recalculé, on contrôle les remises sur les produits du devis let txRemiseAppliquee = 0 txRemiseAppliquee = controlRemisesProduits(totalDevisHt) // On réinitialise le flag chkRemisesMarche if (txRemiseAppliquee > 0) { // on a une remise de base sur ce devis, par défaut toutes les lignes produits respectent cette remise chkRemisesMarche = true } else { chkRemisesMarche = false } console.log('calculDevis txRemiseAppliquee : ' + txRemiseAppliquee + ' & chkRemisesMarche : ' + chkRemisesMarche) console.log('Boucle 2 : calcul du total HT du devis apres remise') //! on boucle sur tous les éléments dont le name commence par inpRemise_ pour calculer le total HT avec remise for (let i = 0, elInp; (elInp = document.querySelectorAll("[name ^= 'inpRemise_' ]")[i]); i++) { // calcul avec la quantité, le prix de vente et la remise const idProd = elInp.dataset.rid const ligne = document.getElementById('trPro_' + idProd) const vente = ligne.dataset.vente * 1 const achat = ligne.dataset.achatdiscount * 1 const rem = elInp.value * 1 let remise = 0 if (txRemiseAppliquee > 0) { if (rem == txRemiseAppliquee) { // cette ligne produit a un taux de remise identique à la remise de base du devis remise = txRemiseAppliquee * 1 } else { // cette ligne produit a un taux de remise différent de la remise de base du devis, on remet la remise de base du devis // elInp.value = txRemiseAppliquee; // remise = txRemiseAppliquee * 1; // cette remise est différente de la remise de base du devis, le devis ne respecte pas la remise de base, // donc on peut envoyer le devis à validation si la marge n'est pas bonne chkRemisesMarche = false remise = elInp.value * 1 } // elInp.readOnly = true; } else { remise = elInp.value * 1 // elInp.readOnly = false; } const varOption = document.getElementById('chkVariante_' + idProd).checked if (!varOption) { const inpQte = document.getElementById('inpQte_' + idProd) const qte = inpQte.value const remiseProduit = (remise * vente) / 100 console.log('--- 2 remiseProduit : ' + remise + ' * ' + vente + ' / 100 = ' + remiseProduit) totalDevisHtRemise += (vente - remiseProduit) * (qte * 1) coutTotalAchat += achat * 1 * (qte * 1) console.log( '--- 2 ligne code ' + ligne.dataset.code + ' = idProd : ' + idProd + ', vente : ' + vente + ', achat : ' + achat + ', qté : ' + qte + ', remise : ' + remise + ', remiseProduit : ' + remiseProduit ) } } let inpTotalRemHT = document.getElementById('inpTotalRemHT') inpTotalRemHT.value = formatAmount(totalDevisHtRemise) // on met à jour la marge totale let totalRFA = 0 // on prend le total HT après remise // on recherche une RFA sur ce marché // console.log("RFA TRIM : " + document.getElementById("tdTxRemiseTrim").textContent); const RFAtrimestrielle = parseFloatFromPercentageString(document.getElementById('tdTxRemiseTrim').textContent) if (RFAtrimestrielle > 0) { totalRFA = totalDevisHtRemise * (RFAtrimestrielle / 100) } else { // console.log("RFA SEME : " + document.getElementById("tdTxRemiseSeme").textContent); const RFAsemestrielle = parseFloatFromPercentageString(document.getElementById('tdTxRemiseSeme').textContent) if (RFAsemestrielle > 0) { totalRFA = totalDevisHtRemise * (RFAsemestrielle / 100) } else { // console.log("RFA ANNU : " + document.getElementById("tdTxRemiseAnnu").textContent); const RFAannuelle = parseFloatFromPercentageString(document.getElementById('tdTxRemiseAnnu').textContent) if (RFAannuelle > 0) { // console.log("On prend en compte la RFAannuelle : " + RFAannuelle + " & totalDevisHtRemise : " + totalDevisHtRemise); totalRFA = totalDevisHtRemise * (RFAannuelle / 100) } } } console.log('CoutTotalAchat affiché : ' + coutTotalAchat + ', totalRFA : ' + totalRFA) // on ajoute le coût total de la RFA au total Achat coutTotalAchat += totalRFA // et on calcule la marge totale if (totalDevisHtRemise > 0) { margeTotale = ((totalDevisHtRemise - coutTotalAchat) / totalDevisHtRemise) * 100 } else { margeTotale = 0 } let inpTotalMG = document.getElementById('inpTotalMarge') inpTotalMG.value = margeTotale.toFixed(2) console.log( 'margeTotale : ' + margeTotale + ', soit (totalDevisHtRemise : ' + totalDevisHtRemise + ' - coutTotalAchat : ' + coutTotalAchat + ') / totalDevisHtRemise = ' + (totalDevisHtRemise - coutTotalAchat) / totalDevisHtRemise ) if (devIp == '1') { let inpCoutTotalAchat = document.getElementById('inpCoutTotalAchat') inpCoutTotalAchat.value = coutTotalAchat.toFixed(2) } // on renseigne les valeurs globales de ces 3 données mises à jour devisTotalHT = totalDevisHt devisTotalRemHT = totalDevisHtRemise devisTotalMarge = margeTotale // Si la marge Totale est inférieure au seuil de latitude, on change le bouton en orange ou rouge const latitudeRR = document.getElementById('inp_latitudeRR').value const latitudeDV = document.getElementById('inp_latitudeDV').value let btn = document.getElementById('btnSaveDevisAndSend') // enfin, on met à jour le bouton de sauvegarde du devis updateBtnSaveDevisAndSend() chkChange = 1 } let clickSaveDevis = function () { showLoading() let frmData = new FormData(document.getElementById('frmDevis')) let objData = {} frmData.forEach(function (value, key) { objData[key] = value }) fetch('/jxdevis/save_devis', { method: 'POST', body: JSON.stringify(objData), headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }).then(function (response) { if (!response.ok) { hideLoading() showNotification('Erreur', "L'enregistrement du devis n'a pas abouti", 'error') } else { const ret = response.json() ret.then(function (data) { hideLoading() // On met à jour la ligne du tableau des devis let elLigDevis = document.getElementById('tr_' + data.rid) elLigDevis.cells[7].innerHTML = data.totalremht + ' €' elLigDevis.cells[8].innerHTML = data.totalmarge + ' %' showNotification('Devis enregistré', 'Le devis a bien été enregistré', 'success') }) } }) chkChange = 0 return false } let clickSaveDevisAndSend = function () { const btnLibelle = this.innerHTML // si le btnLibelle contient "SAP", on envoie le devis à SAP let confLibelle = '' if (btnLibelle.indexOf('SAP') > -1) { confLibelle = 'traitement SAP ?' //! On controle que ce devis ne soit pas en cours de validation et qu'il n'ait pas été validé ! if (fkRole < 3) { // Uniquement pour le DIR-CO et les DV if (fkStatutDevis == 2 || fkStatutDevis == 3) { if (chkValidat == 0) { showNotification( 'Erreur', "Ce devis est en cours de validation et n'a pas encore été validé. Vous devez d'abord saisir un commentaire de validation et cliquer sur le bouton 'Valider ce devis'", 'error' ) return false } } } } else if (btnLibelle.indexOf('DIR-CO') > -1) { confLibelle = 'accord DIR-CO ?' } else if (btnLibelle.indexOf('DV/DCG') > -1) { confLibelle = 'accord DV/DCG ?' } if (confirm('Voulez-vous enregistrer et envoyer ce devis pour ' + confLibelle)) { clickSaveDevis() //! on modifie le statut du devis pour le passer à "2 : en cours de validation" ou en "3: validé" let data = {} data['cid'] = idDevis data['statut'] = this.getAttribute('data-statut') data['commentaire'] = 'Devis enregistré et transmis' fetch('/jxdevis/statut_devis', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }) showNotification('Devis enregistré', 'Le devis a bien été enregistré et transmis', 'success') setTimeout(function () { location.reload() }, 2000) // 2000 millisecondes = 2 secondes } return false } let clickValDevis = function () { // Le RR visualise le PDF SAP pour le valider ou non idDevis = this.getAttribute('data-rid') fetch('/jximport/get_files', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, body: JSON.stringify({ cid: idDevis, sup: 'devis_pdf_sap', }), }).then(function (response) { if (response.ok) { const ret = response.json() ret.then(function (data) { if (data.length > 0) { for (let key in data) { if (data.hasOwnProperty(key)) { // Récupération des valeurs de la ligne let val = data[key] // On ajoute le lien vers le fichier const leFichier = val['dir0'] + val['fichier'] const elLien = document.getElementById('embPdfSAP') elLien.setAttribute('src', leFichier) // On affiche le formulaire de validation frmValidationRR document.getElementById('frmValidationRR').style.display = 'block' document.getElementById('btnClosePDF').style.display = 'none' showModal(document.getElementById('modalPDFSAP')) break } } } else { showNotification('Erreur', "Aucun fichier PDF SAP n'a été trouvé", 'error') } }) } }) return false } let clickPdfDevis = function () { idDevis = this.getAttribute('data-rid') fetch('/jximport/get_files', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, body: JSON.stringify({ cid: idDevis, sup: 'devis_pdf_sap', }), }).then(function (response) { if (response.ok) { const ret = response.json() ret.then(function (data) { if (data.length > 0) { for (let key in data) { if (data.hasOwnProperty(key)) { // Récupération des valeurs de la ligne let val = data[key] // On ajoute le lien vers le fichier const leFichier = val['dir0'] + val['fichier'] const elLien = document.getElementById('embPdfSAP') elLien.setAttribute('src', leFichier) // On cache le formulaire de validation frmValidationRR document.getElementById('frmValidationRR').style.display = 'none' document.getElementById('btnClosePDF').style.display = 'block' showModal(document.getElementById('modalPDFSAP')) break } } } else { showNotification('Erreur', "Aucun fichier PDF SAP n'a été trouvé", 'error') } }) } }) return false } let clickValidationRR = function () { const inpCommentPDFSAP = document.getElementById('inpCommentPDFSAP') // if (inpCommentPDFSAP.value == "") { // showNotification("Validation impossible", "Vous devez saisir un commentaire", "error"); // return false; // } if (confirm('Confirmez-vous la validation de ce document et du devis ?')) { let data = {} data['cid'] = idDevis data['statut'] = 7 // 7 = devis validé par le RR, il est à envoyer au client par SAP data['commentaire'] = inpCommentPDFSAP.value ? inpCommentPDFSAP.value : 'Devis validé par le RR' fetch('/jxdevis/statut_devis', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }) showNotification('Devis validé', 'Le devis a bien été validé', 'success') hideModal(document.getElementById('modalPDFSAP')) setTimeout(function () { location.reload() }, 2000) // 2000 millisecondes = 2 secondes } } let clickRefusRR = function () { const inpCommentPDFSAP = document.getElementById('inpCommentPDFSAP') if (inpCommentPDFSAP.value == '') { showNotification('Refus impossible', 'Vous devez saisir un commentaire expliquant votre refus.', 'error') return false } if (confirm('Confirmez-vous le refus de ce document ?')) { let data = {} data['cid'] = idDevis data['statut'] = 4 // 4 = ddocument refusé par le RR, il revient à 4 à traiter par SAP data['commentaire'] = inpCommentPDFSAP.value fetch('/jxdevis/statut_devis', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }) showNotification('Document refusé', 'Refus enregistré, le devis revient à ADV', 'success') hideModal(document.getElementById('modalPDFSAP')) setTimeout(function () { location.reload() }, 2000) // 2000 millisecondes = 2 secondes } } let clickCloseRR = function () { if (confirm('Voulez-vous fermer ce document PDF sans y répondre ?')) { hideModal(document.getElementById('modalPDFSAP')) } return false } let clickClosePDF = function () { hideModal(document.getElementById('modalPDFSAP')) return false } let clickValidatDevis = function () { // Un DV ou le DIR-CO valide le devis const inpCommentValidatDevis = document.getElementById('inpCommentValidatDevis') // if (inpCommentValidatDevis.value == "") { // showNotification("Validation impossible", "Vous devez saisir un commentaire", "error"); // return false; // } if (fkRole == 2) { const libBtnSave = document.getElementById('btnSaveDevisAndSend').innerHTML if (libBtnSave.indexOf('DIR-CO') > -1) { // Le DV veut valider un devis qui demande l'accord du DIR-CO showNotification('Validation impossible', "Vous devez demander l'accord du DIR-CO", 'error') return false } } if (confirm("Confirmez-vous la validation de ce devis ? Le devis sera transmis à l'ADV pour traitement SAP.")) { let data = {} data['cid'] = idDevis data['chk_validat'] = 1 data['commentaire'] = inpCommentValidatDevis.value ? inpCommentValidatDevis.value : '-' fetch('/jxdevis/validat_devis', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }) showNotification('Devis validé', 'Le devis a bien été validé.', 'success') setTimeout(function () { location.reload() }, 2000) // 2000 millisecondes = 2 secondes } } let clickRefusDevis = function () { // Un DV ou le DIR-CO refuse le devis const inpCommentValidatDevis = document.getElementById('inpCommentValidatDevis') if (inpCommentValidatDevis.value == '') { showNotification('Refus impossible', 'Vous devez saisir un commentaire expliquant votre refus', 'error') return false } if (confirm('Confirmez-vous le refus de ce devis ?')) { let data = {} data['cid'] = idDevis data['chk_validat'] = 0 data['commentaire'] = inpCommentValidatDevis.value fetch('/jxdevis/validat_devis', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }) showNotification('Devis refusé', 'Le devis a bien été refusé.', 'success') setTimeout(function () { location.reload() }, 2000) // 2000 millisecondes = 2 secondes } } function autocompleteClient(input, list) { //! Autocomplete pour la recherche de client //Add an event listener to compare the input value with list items input.addEventListener('input', function () { //Close the existing list if it is open closeList() //If the input is empty, exit the function if (!this.value) return false //Create a suggestions
and add it to the element containing the input field suggestions = document.createElement('div') suggestions.setAttribute('id', 'suggestionsClients') suggestions.setAttribute('class', 'autocomplete-items') this.parentNode.appendChild(suggestions) //Iterate through all entries in the list and find matches (15 max) let nbSuggestionsFound = 0 for (let i = 0; i < list.length; i++) { if (list[i]['rech'].toUpperCase().includes(this.value.toUpperCase())) { //If a match is found, create a suggestion
and add it to the suggestions
suggestion = document.createElement('div') suggestion.innerHTML = list[i]['rech'] suggestion.addEventListener('click', function () { // on a cliqué sur une suggestion, on met à jour les champs du formulaire avec les infos du client input.value = list[i]['libelle'] // this.innerHTML; document.getElementById('inp_fk_client').value = list[i]['rowid'] document.getElementById('inp_adresse1').value = list[i]['adresse1'] document.getElementById('inp_adresse2').value = list[i]['adresse2'] document.getElementById('inp_adresse3').value = list[i]['adresse3'] document.getElementById('inp_cp').value = list[i]['cp'] document.getElementById('inp_ville').value = list[i]['ville'] document.getElementById('selTypeEtab').value = list[i]['type_client'] // Charger les contacts du client sélectionné loadContactsClient(list[i]['code']) // on ferme la liste des suggestions closeList() }) suggestion.style.cursor = 'pointer' suggestion.style.backgroundColor = 'lightyellow' suggestions.appendChild(suggestion) nbSuggestionsFound++ if (nbSuggestionsFound > 15) break } } }) function closeList() { let suggestions = document.getElementById('suggestionsClients') if (suggestions) suggestions.parentNode.removeChild(suggestions) } } function autocompleteProdSpecial(input, num, list) { //! Autocomplete pour la recherche de produit spécial //Add an event listener to compare the input value with list items input.addEventListener('input', function () { //Close the existing list if it is open closeList() //If the input is empty, exit the function if (!this.value) return false //Create a suggestions
and add it to the element containing the input field suggestions = document.createElement('div') suggestions.setAttribute('id', 'suggestionsProdSpecial') suggestions.setAttribute('class', 'autocomplete-items') this.parentNode.appendChild(suggestions) //Iterate through all entries in the list and find matches (15 max) let nbSuggestionsFound = 0 for (let i = 0; i < list.length; i++) { if (list[i]['code'].toUpperCase().includes(this.value.toUpperCase())) { //If a match is found, create a suggestion
and add it to the suggestions
suggestion = document.createElement('div') suggestion.innerHTML = list[i]['rech'] suggestion.addEventListener('click', function () { // on a cliqué sur une suggestion, on met à jour les champs du formulaire avec les infos du client input.value = list[i]['code'] // this.innerHTML; document.getElementById('inp_specialFkProduit_' + num).value = list[i]['rowid'] document.getElementById('inp_specialLibe_' + num).value = list[i]['libelle'] // on ferme la liste des suggestions closeList() }) suggestion.style.cursor = 'pointer' suggestion.style.backgroundColor = 'lightyellow' suggestions.appendChild(suggestion) nbSuggestionsFound++ if (nbSuggestionsFound > 15) break } } }) function closeList() { let suggestions = document.getElementById('suggestionsProdSpecial') if (suggestions) suggestions.parentNode.removeChild(suggestions) } } let searchProducts = function (el) { //! L'utilisateur vient de taper au clavier dans un champ de recherche de produit if (el.keyCode === 13) { showLoading() const searchTerm = this.value const libIdFamille = this.id.substring(this.id.indexOf('_') + 1) const idFamille = this.getAttribute('data-idFamille') fetchSearchProducts(searchTerm, libIdFamille, idFamille) hideLoading() return false } } let autocompleteProduitsFamille = function (input, list, libIdFamille, idFamille) { //! Autocomplete pour la recherche de produits d'une famille input.addEventListener('input', function () { // si l'input est vide, on sort if (!this.value) { showProduitsFamille(list, libIdFamille) return false } // ou si sa longueur est inférieure à 2 caractères if (this.value.length < 2) return false let tblBody = document.getElementById('tblProduits_' + libIdFamille).getElementsByTagName('tbody')[0] tblBody.innerHTML = '' let nbSuggestionsFound = 0 for (let key in list) { if (list.hasOwnProperty(key)) { // Récupération des valeurs de la ligne let val = list[key] if (val['rech'].toUpperCase().includes(this.value.toUpperCase())) { // On affiche la ligne showLineProduitFamille(tblBody, val, libIdFamille) } } } }) return false } // Use a MutationObserver to monitor the DOM for changes let observerInputSearchProducts = new MutationObserver(function (mutationsList) { for (var mutation of mutationsList) { if (mutation.type === 'childList') { // If new nodes have been added to the DOM, attach event listeners to any new input elements var addedNodes = mutation.addedNodes for (var node of addedNodes) { if (node instanceof HTMLElement) { var newInputs = node.querySelectorAll("input[id^='inpSearchProduct_']") newInputs.forEach(function (newInput) { newInput.addEventListener('keyup', function () { console.log('keyup sur input de recherche de produit') if (event.keyCode === 13) { let searchTerm = newInput.value let libIdFamille = newInput.id.split('_')[1] let idFamille = newInput.getAttribute('data-idFamille') fetchSearchProducts(searchTerm, libIdFamille, idFamille) } }) }) } } } } }) function fetchSearchProducts(searchTerm, libIdFamille, idFamille) { // On cherche les produits correspondant au terme de recherche dans dataProduitsMarche qui contient la liste des produits du marché par famille // On nettoie la liste des produits de la famille let tblBodyProduits = document.getElementById('tblProduits_' + libIdFamille).getElementsByTagName('tbody')[0] tblBodyProduits.innerHTML = '' // on boucle sur la liste des produits du marché en cours sur ce devis, et on charge les produits de cette famille qui correspondent au terme de recherche dataProduitsMarche.forEach(function (lineProduit) { if ( lineProduit.fk_famille == idFamille && (lineProduit.libelle.toUpperCase().includes(searchTerm.toUpperCase()) || lineProduit.code.toUpperCase().includes(searchTerm.toUpperCase())) ) { // TODO : il faut aussi vérifier que le produit n'est pas déjà dans la liste des produits sélectionnés // on vérifie que le produit n'est pas déjà dans la liste des produits sélectionnés let isProductAlreadySelected = false const tblBodyProductsSelect = document.getElementById('tblProduitsSelect').getElementsByTagName('tbody')[0] const rowsProductsSelect = tblBodyProductsSelect.getElementsByTagName('tr') for (let i = 0; i < rowsProductsSelect.length; i++) { let rowProductSelect = rowsProductsSelect[i] if (rowProductSelect.getAttribute('data-rid') == lineProduit.rowid) { isProductAlreadySelected = true break } } if (!isProductAlreadySelected) { // Insertion d'une nouvelle ligne et création de ses colonnes let newRow = tblBodyProduits.insertRow(0) newRow.className = 'ligProduit_' + libIdFamille newRow.id = 'ligProduit_' + libIdFamille + '_' + lineProduit.rowid newRow.setAttribute('data-rid', lineProduit.rowid) let celChkBox = newRow.insertCell(0) celChkBox.className = 'chkBox_' + libIdFamille + ' text-center' celChkBox.setAttribute('data-rid', lineProduit.rowid) celChkBox.innerHTML = '' let celCode = newRow.insertCell(1) celCode.innerHTML = lineProduit.code let celLibelle = newRow.insertCell(2) celLibelle.innerHTML = lineProduit.libelle let celFamille = newRow.insertCell(3) celFamille.innerHTML = lineProduit.lib_famille } } }) } // Start observing the DOM for changes observerInputSearchProducts.observe(document.body, { childList: true, subtree: true, }) elInpCommentGesteComm.addEventListener('input', function () { updateBtnSaveDevisAndSend() }) function refreshChat() { if (idDevis > 0) { //! On récupère les données de devis_histo fetch('/jxchat/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, body: JSON.stringify({ cid: idDevis, }), }).then((response) => { const ret = response.json() ret.then(function (data) { // on vérifie si le nombre de commentaires a changé if (nbCommentChat != data.length) { // Si c'est le cas on rafraîchit tous les commentaires // On supprime tous les commentaires const chatContainer = document.getElementById('chat-bubbles') const chatBubbles = chatContainer.getElementsByClassName('chat-bubble') while (chatBubbles.length > 0) { chatBubbles[0].parentNode.removeChild(chatBubbles[0]) } // On ajoute tous les commentaires for (let i = 0; i < data.length; i++) { const chatBubble = document.createElement('div') chatBubble.classList.add('chat-bubble') if (data[i].fk_user == fkUser) { chatBubble.classList.add('right-chat-bubble') } else { chatBubble.classList.add('left-chat-bubble') } const initiales = data[i].prenom.substring(0, 1) + data[i].libelle.substring(0, 1) const userInfo = document.createElement('div') userInfo.classList.add('user-info') const userInitials = document.createElement('div') userInitials.classList.add('user-initials') userInitials.innerHTML = initiales userInfo.appendChild(userInitials) const usernameDate = document.createElement('div') usernameDate.classList.add('username-date') const username = document.createElement('span') username.classList.add('username') username.innerHTML = data[i].prenom + ' ' + data[i].libelle usernameDate.appendChild(username) const date = document.createElement('span') date.classList.add('date') date.innerHTML = data[i].date_histo usernameDate.appendChild(date) userInfo.appendChild(usernameDate) chatBubble.appendChild(userInfo) const message = document.createElement('div') message.classList.add('message') message.innerHTML = data[i].commentaire chatBubble.appendChild(message) chatContainer.appendChild(chatBubble) } nbCommentChat = data.length // et on vide le champ de saisie s'il n'a pas le focus if (!document.getElementById('chatInputMessage').hasFocus) { document.getElementById('chatInputMessage').value = '' } } const chatCont = document.getElementById('chat-container') chatCont.style.display = 'block' }) }) } } let chatSendMessage = function () { // On récupère le message à envoyer const message = document.getElementById('chatInputMessage').value // On récupère l'id du devis // On envoie le message fetch('/jxchat/save_message', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, body: JSON.stringify({ cid: idDevis, message: message, fkuser: fkUser, }), }).then((response) => { const ret = response.json() ret.then(function (data) { refreshChat() }) }) } const elSelectTypeClient = document.getElementById('selTypeEtab') elSelectTypeClient.addEventListener('change', function () { document.getElementById('inp_type_client').value = this.value }) function handleDragStart(e) { draggedElement = this // on enregistre l'élément draggé this.style.opacity = '0.4' e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/html', this.outerHTML) console.log('DragStart : ' + this.dataset.code) } function handleDragOver(e) { if (e.preventDefault) { e.preventDefault() } e.dataTransfer.dropEffect = 'move' console.log('DragOver au-dessus de : ' + this.dataset.code) return false } function handleDrop(e) { if (e.stopPropagation) { e.stopPropagation() } console.log('Drop sur : ' + this.dataset.code) if (this !== e.target && this === e.target.parentNode) { console.log('Drop dedans') const dropHTML = e.dataTransfer.getData('text/html') this.insertAdjacentHTML('beforebegin', dropHTML) const dropElem = this.previousSibling // Supprimer l'élément original draggedElement.remove() // Update draggedElement to point to the new element draggedElement = dropElem // Reset opacity and input states draggedElement.style.opacity = '1.0' const inputs = draggedElement.querySelectorAll('input') inputs.forEach((input) => (input.disabled = false)) // Ajouter un événement click à l'élément svg const svgElement = dropElem.querySelector('#commentProd_' + dropElem.dataset.rid) svgElement.addEventListener('click', showCommentProd) addDnDHandlers(dropElem) updateOrder() } return false } function addDnDHandlers(elem) { elem.addEventListener('dragstart', handleDragStart) elem.addEventListener('dragover', handleDragOver) elem.addEventListener('drop', handleDrop) } function updateOrder() { // Met à jour l'ordre des lignes dans le tableau des produits du devis var rows = document.querySelectorAll('#tblDevisPro tr') rows.forEach((row, index) => { const sonCode = row.dataset.code if (sonCode) { row.dataset.ordre = index + 1 const fkProduit = row.dataset.rid console.log('index : ' + index + ' code : ' + sonCode) document.getElementById('inpOrdre_' + fkProduit).value = index } }) showNotification( 'Ordre', "Ordre des lignes produits mis à jour. Pensez à enregistrer le devis pour que l'ordre soit bien pris en compte.", 'success' ) } let clickCancelCommentProd = function () { hideModal(document.getElementById('modalCommentProd')) } let clickSaveCommentProd = function () { // Sauvegarde du commentaire produit const fkProd = document.getElementById('inp_commentProdId').value const commentaire = document.getElementById('inp_commentProd').value document.getElementById('inpCom_' + fkProd).value = commentaire const svgElement = document.getElementById('commentProd_' + fkProd) const svgNewColor = commentaire == '' ? 'lightgray' : 'red' svgElement.querySelector('use').style.fill = svgNewColor showNotification( 'Commentaire', 'Votre commentaire a bien été enregistré dans le tableau. Enregistrez votre devis pour sauvegarder définitivement le commentaire.', 'success' ) hideModal(document.getElementById('modalCommentProd')) } //! ========== GESTION DES CONTACTS ========== let contactsClient = [] // Liste des contacts du client en cours let currentFkClient = 0 // ID du client en cours // Charger les contacts d'un client function loadContactsClient(fkClient) { if (fkClient == 0) { document.getElementById('sel_contact').innerHTML = '' document.getElementById('btnGererContacts').disabled = true document.getElementById('divContactInfos').style.display = 'none' contactsClient = [] currentFkClient = 0 return Promise.resolve() } currentFkClient = fkClient return fetch('/jxcontacts/load_contacts', { method: 'POST', body: JSON.stringify({ fk_client: fkClient }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => response.json()) .then((data) => { contactsClient = data const selContact = document.getElementById('sel_contact') selContact.innerHTML = '' data.forEach((contact) => { const option = document.createElement('option') option.value = contact.rowid option.textContent = contact.nom + ' ' + contact.prenom + (contact.principal == 1 ? ' ⭐' : '') selContact.appendChild(option) }) document.getElementById('btnGererContacts').disabled = false // Si un seul contact, le sélectionner automatiquement if (data.length === 1) { selContact.value = data[0].rowid displayContactInfos(data[0]) } }) .catch((error) => { console.error('Erreur chargement contacts:', error) showNotification('Erreur', 'Impossible de charger les contacts', 'error') }) } // Afficher les infos d'un contact en lecture seule function displayContactInfos(contact) { document.getElementById('info_contact_nom_prenom').textContent = (contact.prenom || '') + ' ' + (contact.nom || '') document.getElementById('info_contact_fonction').textContent = contact.fonction || '-' document.getElementById('info_contact_email').textContent = contact.email || '-' document.getElementById('info_contact_telephone').textContent = contact.telephone || '-' document.getElementById('info_contact_mobile').textContent = contact.mobile || '-' document.getElementById('divContactInfos').style.display = 'block' } // Événement changement de contact document.getElementById('sel_contact').addEventListener('change', function () { const contactId = this.value if (contactId == 0) { document.getElementById('divContactInfos').style.display = 'none' return } const contact = contactsClient.find((c) => c.rowid == contactId) if (contact) { displayContactInfos(contact) } }) // Ouvrir la modale de gestion des contacts document.getElementById('btnGererContacts').addEventListener('click', function () { document.getElementById('inp_fk_client_contacts').value = currentFkClient loadContactsTable() showModal(document.getElementById('modalGererContacts')) }) // Charger la table des contacts function loadContactsTable() { const tbody = document.querySelector('#tblContacts tbody') tbody.innerHTML = 'Chargement...' fetch('/jxcontacts/load_contacts', { method: 'POST', body: JSON.stringify({ fk_client: currentFkClient }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => response.json()) .then((data) => { tbody.innerHTML = '' if (data.length === 0) { tbody.innerHTML = 'Aucun contact trouvé' return } data.forEach((contact) => { const tr = document.createElement('tr') tr.innerHTML = ` ${contact.nom || ''} ${contact.prenom || ''} ${contact.fonction || ''} ${contact.telephone || ''} ${contact.email || ''} ${contact.principal == 1 ? 'Oui' : ''} ` tbody.appendChild(tr) }) // Attacher les événements document.querySelectorAll('.btnEditContact').forEach((btn) => { btn.addEventListener('click', function () { editContact(this.getAttribute('data-id')) }) }) document.querySelectorAll('.btnDeleteContact').forEach((btn) => { btn.addEventListener('click', function () { deleteContact(this.getAttribute('data-id')) }) }) document.querySelectorAll('.btnSetPrincipal').forEach((btn) => { btn.addEventListener('click', function () { setPrincipalContact(this.getAttribute('data-id')) }) }) }) .catch((error) => { console.error('Erreur chargement table contacts:', error) tbody.innerHTML = 'Erreur de chargement' }) } // Nouveau contact document.getElementById('btnNouveauContact').addEventListener('click', function () { document.getElementById('modEditContactTitreText').textContent = 'Nouveau contact' document.getElementById('frmEditContact').reset() document.getElementById('inp_contact_rowid').value = '0' document.getElementById('inp_contact_fk_client').value = currentFkClient showModal(document.getElementById('modalEditContact')) }) // Éditer un contact function editContact(contactId) { fetch('/jxcontacts/load_contact', { method: 'POST', body: JSON.stringify({ rowid: contactId }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => response.json()) .then((contact) => { document.getElementById('modEditContactTitreText').textContent = 'Modifier le contact' document.getElementById('inp_contact_rowid').value = contact.rowid document.getElementById('inp_contact_fk_client').value = contact.fk_client document.getElementById('inp_contact_nom_edit').value = contact.nom || '' document.getElementById('inp_contact_prenom_edit').value = contact.prenom || '' document.getElementById('inp_contact_fonction_edit').value = contact.fonction || '' document.getElementById('inp_contact_email_edit').value = contact.email || '' document.getElementById('inp_contact_telephone_edit').value = contact.telephone || '' document.getElementById('inp_contact_mobile_edit').value = contact.mobile || '' document.getElementById('inp_contact_principal_edit').checked = contact.principal == 1 showModal(document.getElementById('modalEditContact')) }) .catch((error) => { console.error('Erreur chargement contact:', error) showNotification('Erreur', 'Impossible de charger le contact', 'error') }) } // Sauvegarder un contact document.getElementById('btnSaveEditContact').addEventListener('click', function () { const form = document.getElementById('frmEditContact') const formData = new FormData(form) fetch('/jxcontacts/save_contact', { method: 'POST', body: formData, }) .then((response) => response.json()) .then((data) => { if (data.ret === 'ok') { showNotification('Succès', data.msg, 'success') hideModal(document.getElementById('modalEditContact')) loadContactsTable() loadContactsClient(currentFkClient) // Recharger le sélecteur } else { showNotification('Erreur', data.msg, 'error') } }) .catch((error) => { console.error('Erreur sauvegarde contact:', error) showNotification('Erreur', 'Impossible de sauvegarder le contact', 'error') }) }) // Supprimer un contact function deleteContact(contactId) { if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) return fetch('/jxcontacts/delete_contact', { method: 'POST', body: JSON.stringify({ rowid: contactId }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => response.json()) .then((data) => { if (data.ret === 'ok') { showNotification('Succès', data.msg, 'success') loadContactsTable() loadContactsClient(currentFkClient) } else { showNotification('Erreur', data.msg, 'error') } }) .catch((error) => { console.error('Erreur suppression contact:', error) showNotification('Erreur', 'Impossible de supprimer le contact', 'error') }) } // Définir contact principal function setPrincipalContact(contactId) { fetch('/jxcontacts/set_principal', { method: 'POST', body: JSON.stringify({ rowid: contactId, fk_client: currentFkClient }), headers: { 'Content-Type': 'application/json;charset=utf-8', Accept: 'application/json;charset=utf-8', }, }) .then((response) => response.json()) .then((data) => { if (data.ret === 'ok') { showNotification('Succès', data.msg, 'success') loadContactsTable() loadContactsClient(currentFkClient) } else { showNotification('Erreur', data.msg, 'error') } }) .catch((error) => { console.error('Erreur définition contact principal:', error) showNotification('Erreur', 'Impossible de définir le contact principal', 'error') }) } // Fermer les modales document.getElementById('btnFermerContacts').addEventListener('click', function () { hideModal(document.getElementById('modalGererContacts')) }) document.getElementById('btnCancelEditContact').addEventListener('click', function () { hideModal(document.getElementById('modalEditContact')) }) //! ========== FIN GESTION DES CONTACTS ========== //! Configuration des événements //! Sur chaque cellule du tableau des devis ayant la classe celDevis, on affecte un événement click qui appelle la fonction clickLigDevis() Array.from(elCelDevis).forEach(function (lnDevis) { lnDevis.addEventListener('click', clickLigDevis) }) Array.from(elCelArchives).forEach(function (lnArchives) { lnArchives.addEventListener('click', clickLigArchives) }) //! Sur chaque bouton de modification du tableau des marchés ayant la classe btnModMarche, on affecte un événement click qui appelle la fonction clickModMarche() Array.from(elBtnDupDevis).forEach(function (dupDevis) { dupDevis.addEventListener('click', clickDupDevis) }) Array.from(elBtnExpExcelDevis).forEach(function (expExcelDevis) { expExcelDevis.addEventListener('click', clickExpExcelDevis) }) Array.from(elBtnValDevis).forEach(function (valDevis) { valDevis.addEventListener('click', clickValDevis) }) Array.from(elBtnPdfDevis).forEach(function (pdfDevis) { pdfDevis.addEventListener('click', clickPdfDevis) }) Array.from(elBtnReactiverDevis).forEach(function (reactiverDevis) { console.log('Attachement événement click sur bouton réactiver:', reactiverDevis) reactiverDevis.addEventListener('click', clickReactiverDevis) }) Array.from(elBtnSupprDevis).forEach(function (supprDevis) { supprDevis.addEventListener('click', clickSupprDevis) }) elBtnDevisArchives.addEventListener('click', clickDevisArchives) elBtnCreateDevis.addEventListener('click', clickCreateDevis) elBtnValidationRR.addEventListener('click', clickValidationRR) elBtnRefusRR.addEventListener('click', clickRefusRR) elBtnCloseRR.addEventListener('click', clickCloseRR) elBtnClosePDF.addEventListener('click', clickClosePDF) elBtnCreateClient.addEventListener('click', clickCreateClient) elBtnCancelCreateClient.addEventListener('click', clickCancelCreateClient) elBtnSaveCreateClient.addEventListener('click', clickSaveCreateClient) elBtnSaveEnTete.addEventListener('click', clickSaveEnTete) elBtnSpeciaux.addEventListener('click', clickSpeciaux) elBtnCancelSpeciaux.addEventListener('click', clickCancelSpeciaux) elBtnSaveSpeciaux.addEventListener('click', clickSaveSpeciaux) elBtnSaveSelProduits.addEventListener('click', clickSaveSelProduits) elBtnSaveDevis.addEventListener('click', clickSaveDevis) elBtnSaveDevisAndSend.addEventListener('click', clickSaveDevisAndSend) elChkClientsSecteur.addEventListener('change', changeClientsSecteur) elChatBtnSend.addEventListener('click', chatSendMessage) elBtnCancelCommentProd.addEventListener('click', clickCancelCommentProd) elBtnSaveCommentProd.addEventListener('click', clickSaveCommentProd) Array.from(elInputSearchProducts).forEach(function (inpSearch) { inpSearch.addEventListener('keyup', searchProducts) }) Array.from(elInputQtes).forEach(function (inpQte) { inpQte.addEventListener('change', calculDevis) }) Array.from(elInputRemises).forEach(function (inpRemise) { inpRemise.addEventListener('change', calculDevis) }) Array.from(elChkVariantes).forEach(function (chkVariante) { chkVariante.addEventListener('change', calculDevis) }) let elSearchDevis = document.getElementById('searchDevis') let elBtnResetSearch = document.getElementById('btnResetSearch') let searchTimeout = null function restoreSearch() { const savedTerm = sessionStorage.getItem('devisSearchTerm') if (savedTerm && savedTerm.length >= 3) { elSearchDevis.value = savedTerm elBtnResetSearch.style.display = 'inline-block' performSearch(savedTerm) } } function performSearch(term) { if (term.length < 3) { return } const context = chkShowDevisArchives ? 'archives' : 'encours' fetch('/jxdevis/search_devis', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ term: term, context: context, }), }) .then((response) => response.json()) .then((data) => { if (data.success) { const devisIds = data.devis.map((d) => d.rowid) filterDevisTables(devisIds, context) updateBadges(data.nb_devis) } else { console.error('Erreur recherche:', data.message) } }) .catch((error) => { console.error('Erreur AJAX:', error) }) } function filterDevisTables(devisIds, context) { if (context === 'encours') { const statuts = document.querySelectorAll('[id^="tblBodyDos"]') statuts.forEach((tbody) => { const rows = tbody.querySelectorAll('tr') rows.forEach((row) => { if (row.id && row.id.startsWith('tr_')) { const rowId = parseInt(row.id.replace('tr_', '')) if (devisIds.includes(rowId)) { row.style.display = '' } else { row.style.display = 'none' } } }) }) } else { const archives = document.querySelectorAll('[id^="tblBodyDosArch"]') archives.forEach((tbody) => { const rows = tbody.querySelectorAll('tr') rows.forEach((row) => { if (row.id && row.id.startsWith('trArch_')) { const rowId = parseInt(row.id.replace('trArch_', '')) if (devisIds.includes(rowId)) { row.style.display = '' } else { row.style.display = 'none' } } }) }) } } function updateBadges(nbDevis) { Object.keys(nbDevis).forEach((statutId) => { const badge = document.querySelector('[id^="liStat"]') if (badge) { badge.setAttribute('data-after-text', nbDevis[statutId]) badge.setAttribute('data-after-type', 'orange badge top left') } }) } function resetSearch() { elSearchDevis.value = '' elBtnResetSearch.style.display = 'none' sessionStorage.removeItem('devisSearchTerm') const context = chkShowDevisArchives ? 'archives' : 'encours' if (context === 'encours') { const statuts = document.querySelectorAll('[id^="tblBodyDos"]') statuts.forEach((tbody) => { const rows = tbody.querySelectorAll('tr') rows.forEach((row) => { row.style.display = '' }) }) } else { const archives = document.querySelectorAll('[id^="tblBodyDosArch"]') archives.forEach((tbody) => { const rows = tbody.querySelectorAll('tr') rows.forEach((row) => { row.style.display = '' }) }) } } elSearchDevis.addEventListener('input', function () { const term = this.value.trim() if (searchTimeout) { clearTimeout(searchTimeout) } if (term.length >= 3) { elBtnResetSearch.style.display = 'inline-block' sessionStorage.setItem('devisSearchTerm', term) searchTimeout = setTimeout(() => { performSearch(term) }, 300) } else if (term.length === 0) { resetSearch() } else { elBtnResetSearch.style.display = 'none' } }) elBtnResetSearch.addEventListener('click', function () { resetSearch() }) restoreSearch() initTableSort() elBtnSideBarDevis.addEventListener('click', function () { if (elVerticalBar.style.width == '10px') { elVerticalBar.style.width = '1100px' // Largeur de la barre lorsqu'elle est ouverte // et son contenu est affiché document.getElementById('verticalBarContent').style.display = 'block' intervalRefresh = setInterval(refreshChat, 6000) // Refresh every 6 seconds (1000 ms = 1 second) } else { elVerticalBar.style.width = '10px' // Largeur de la barre lorsqu'elle est fermée // et son contenu est caché document.getElementById('verticalBarContent').style.display = 'none' setTimeout(function () { clearInterval(intervalRefresh) }, 1000) } }) })