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:
2025-12-05 10:32:19 +01:00
parent f6c5e96534
commit e96ad7a244
12 changed files with 1348 additions and 320 deletions

View File

@@ -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

View File

@@ -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) {