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