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
|
||||
|
||||
Reference in New Issue
Block a user