feat(v2.0.4): Corrections diverses et tri des tableaux devis
- Correction affichage email contact dans SAP (models/msap.php) - Ajout fonctionnalité tri des tableaux devis (jsap.js, jdevis.js) - Améliorations diverses vues devis et SAP - Mise à jour contrôleurs et modèles export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -37,14 +37,107 @@ 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 = 30 // le seuil de marge du RR sur ce devis, par défaut à 30 %
|
||||
let seuilMargeDV = 20 // le seuil de marge du DV sur ce devis, par défaut à 20 %
|
||||
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('#')
|
||||
|
||||
@@ -3790,6 +3883,148 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
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
|
||||
|
||||
@@ -11,6 +11,100 @@ let oldIdLnEnCours;
|
||||
let oldIdLnArchives;
|
||||
let nbCommentChat = 0;
|
||||
let selectedXmlDevis = new Set();
|
||||
let searchSapTimeout = null;
|
||||
|
||||
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('#');
|
||||
@@ -786,6 +880,201 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Fonctions de recherche SAP
|
||||
let elSearchSAP = document.getElementById('searchSAP');
|
||||
let elBtnResetSearchSAP = document.getElementById('btnResetSearchSAP');
|
||||
|
||||
function restoreSearchSAP() {
|
||||
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
|
||||
const savedTerm = sessionStorage.getItem(storageKey);
|
||||
if (savedTerm && savedTerm.length >= 3) {
|
||||
elSearchSAP.value = savedTerm;
|
||||
elBtnResetSearchSAP.style.display = 'inline-block';
|
||||
performSearchSAP(savedTerm);
|
||||
} else {
|
||||
elSearchSAP.value = '';
|
||||
elBtnResetSearchSAP.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function performSearchSAP(term) {
|
||||
if (term.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = panel === 'archives' ? 'archives' : 'encours';
|
||||
|
||||
fetch('/jxdevis/search_devis_sap', {
|
||||
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);
|
||||
filterDevisTablesSAP(devisIds, context);
|
||||
updateBadgesSAP(data.nb_devis);
|
||||
} else {
|
||||
console.error('Erreur recherche:', data.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur AJAX:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function filterDevisTablesSAP(devisIds, context) {
|
||||
if (context === 'encours') {
|
||||
const statuts = document.querySelectorAll('[id^="tblBodyDos"]');
|
||||
statuts.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr.ligEnCours');
|
||||
rows.forEach((row) => {
|
||||
const cells = row.querySelectorAll('.celEnCours');
|
||||
if (cells.length > 0) {
|
||||
const rowId = parseInt(cells[0].getAttribute('data-rid'));
|
||||
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_') || row.id.startsWith('trArchTous_'))) {
|
||||
const rowId = parseInt(row.id.replace('trArch_', '').replace('trArchTous_', ''));
|
||||
if (devisIds.includes(rowId)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateBadgesSAP(nbDevis) {
|
||||
Object.keys(nbDevis).forEach((statutId) => {
|
||||
const liElements = document.querySelectorAll('[id^="liStat"]');
|
||||
liElements.forEach((li) => {
|
||||
li.setAttribute('data-after-text', nbDevis[statutId] || '0');
|
||||
li.setAttribute('data-after-type', 'orange badge top left');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetSearchSAP() {
|
||||
elSearchSAP.value = '';
|
||||
elBtnResetSearchSAP.style.display = 'none';
|
||||
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
|
||||
sessionStorage.removeItem(storageKey);
|
||||
|
||||
const context = panel === 'archives' ? '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 = '';
|
||||
});
|
||||
});
|
||||
const tbodyTous = document.getElementById('tblBodyDosArchTous');
|
||||
if (tbodyTous) {
|
||||
const rowsTous = tbodyTous.querySelectorAll('tr');
|
||||
rowsTous.forEach((row) => {
|
||||
row.style.display = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elSearchSAP.addEventListener('input', function () {
|
||||
const term = this.value.trim();
|
||||
|
||||
if (searchSapTimeout) {
|
||||
clearTimeout(searchSapTimeout);
|
||||
}
|
||||
|
||||
if (term.length >= 3) {
|
||||
elBtnResetSearchSAP.style.display = 'inline-block';
|
||||
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
|
||||
sessionStorage.setItem(storageKey, term);
|
||||
searchSapTimeout = setTimeout(() => {
|
||||
performSearchSAP(term);
|
||||
}, 300);
|
||||
} else if (term.length === 0) {
|
||||
resetSearchSAP();
|
||||
} else {
|
||||
elBtnResetSearchSAP.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
elBtnResetSearchSAP.addEventListener('click', function () {
|
||||
resetSearchSAP();
|
||||
});
|
||||
|
||||
// Hook sur changement d'onglet pour restaurer la recherche appropriée
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
if ($(this).attr('href') == '#tabEnCours') {
|
||||
panel = 'enCours';
|
||||
restoreSearchSAP();
|
||||
} else if ($(this).attr('href') == '#tabArchives') {
|
||||
panel = 'archives';
|
||||
restoreSearchSAP();
|
||||
}
|
||||
});
|
||||
|
||||
restoreSearchSAP();
|
||||
|
||||
initTableSort();
|
||||
|
||||
// Gestion des états actifs des onglets départements (multiples <ul>)
|
||||
// Utiliser l'événement Bootstrap 'shown.bs.tab' au lieu de 'click'
|
||||
const allDeptTabs = document.querySelectorAll('.dept-tab a');
|
||||
allDeptTabs.forEach((tab) => {
|
||||
$(tab).on('shown.bs.tab', function(e) {
|
||||
// Retirer 'active' de tous les onglets départements sauf celui-ci
|
||||
document.querySelectorAll('.dept-tab').forEach((li) => {
|
||||
if (li !== this.parentElement) {
|
||||
li.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// Retirer 'active' de l'onglet "Tous"
|
||||
const tousLi = document.querySelector('a[href="#dosArchTous"]').parentElement;
|
||||
tousLi.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Gestion du clic sur "Tous"
|
||||
const tousTab = document.querySelector('a[href="#dosArchTous"]');
|
||||
if (tousTab) {
|
||||
$(tousTab).on('shown.bs.tab', function(e) {
|
||||
// Retirer 'active' de tous les onglets départements
|
||||
document.querySelectorAll('.dept-tab').forEach((li) => {
|
||||
li.classList.remove('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add new functions
|
||||
function updateExportButton() {
|
||||
if (selectedXmlDevis.size > 0) {
|
||||
|
||||
Reference in New Issue
Block a user