/** * Système d'édition inline avec verrouillage */ class InlineEditing { constructor() { this.locks = new Map(); this.lockCheckInterval = null; this.init(); } init() { this.setupEventListeners(); this.startLockCheck(); } setupEventListeners() { // Détecter les clics sur les cellules éditables document.addEventListener('click', (e) => { if (e.target.classList.contains('editable-cell')) { this.startEditing(e.target); } }); // Détecter les clics en dehors pour sauvegarder document.addEventListener('click', (e) => { if (!e.target.closest('.editable-cell, .inline-edit-input')) { this.saveAllPending(); } }); // Détecter les touches pour sauvegarder document.addEventListener('keydown', (e) => { if (e.key === 'Enter') { this.saveCurrentEditing(); } else if (e.key === 'Escape') { this.cancelCurrentEditing(); } }); } startEditing(cell) { if (cell.classList.contains('editing')) { return; } const entityType = cell.dataset.entityType; const entityId = cell.dataset.entityId; const field = cell.dataset.field; const currentValue = cell.textContent.trim(); // Vérifier si déjà en cours d'édition if (this.isEditing(entityType, entityId, field)) { return; } // Acquérir le verrou this.acquireLock(entityType, entityId).then((success) => { if (success) { this.createEditInput(cell, entityType, entityId, field, currentValue); } else { this.showLockMessage(cell); } }); } createEditInput(cell, entityType, entityId, field, currentValue) { cell.classList.add('editing'); const input = document.createElement('input'); input.type = 'text'; input.value = currentValue; input.className = 'inline-edit-input form-control'; input.dataset.entityType = entityType; input.dataset.entityId = entityId; input.dataset.field = field; // Remplacer le contenu cell.innerHTML = ''; cell.appendChild(input); input.focus(); input.select(); // Sauvegarder automatiquement après 2 secondes d'inactivité let saveTimeout; input.addEventListener('input', () => { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { this.saveField(entityType, entityId, field, input.value); }, 2000); }); } async acquireLock(entityType, entityId) { try { const response = await fetch(`/api/${entityType.toLowerCase()}/${entityId}/lock`, { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const data = await response.json(); if (response.ok) { this.locks.set(`${entityType}_${entityId}`, { entityType, entityId, acquiredAt: new Date(), expiresAt: new Date(data.expiresAt) }); return true; } else { console.error('Erreur lors de l\'acquisition du verrou:', data.error); return false; } } catch (error) { console.error('Erreur réseau:', error); return false; } } async releaseLock(entityType, entityId) { try { await fetch(`/api/${entityType.toLowerCase()}/${entityId}/unlock`, { method: 'POST', headers: { 'Content-Type': 'application/json', } }); this.locks.delete(`${entityType}_${entityId}`); } catch (error) { console.error('Erreur lors de la libération du verrou:', error); } } async extendLock(entityType, entityId) { try { const response = await fetch(`/api/${entityType.toLowerCase()}/${entityId}/extend-lock`, { method: 'POST', headers: { 'Content-Type': 'application/json', } }); if (response.ok) { const data = await response.json(); const lock = this.locks.get(`${entityType}_${entityId}`); if (lock) { lock.expiresAt = new Date(data.expiresAt); } } } catch (error) { console.error('Erreur lors de la prolongation du verrou:', error); } } async saveField(entityType, entityId, field, value) { try { const response = await fetch(`/api/${entityType.toLowerCase()}/${entityId}/update-field`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ field: field, value: value }) }); const data = await response.json(); if (response.ok) { // Mettre à jour l'affichage this.updateCellDisplay(entityType, entityId, field, value); this.showSuccessMessage(`Champ ${field} mis à jour`); } else { this.showErrorMessage(data.error || 'Erreur lors de la sauvegarde'); } } catch (error) { console.error('Erreur lors de la sauvegarde:', error); this.showErrorMessage('Erreur réseau lors de la sauvegarde'); } } updateCellDisplay(entityType, entityId, field, value) { const cell = document.querySelector(`[data-entity-type="${entityType}"][data-entity-id="${entityId}"][data-field="${field}"]`); if (cell) { cell.textContent = value; cell.classList.remove('editing'); } } isEditing(entityType, entityId, field) { const cell = document.querySelector(`[data-entity-type="${entityType}"][data-entity-id="${entityId}"][data-field="${field}"]`); return cell && cell.classList.contains('editing'); } saveCurrentEditing() { const editingInput = document.querySelector('.inline-edit-input'); if (editingInput) { const entityType = editingInput.dataset.entityType; const entityId = editingInput.dataset.entityId; const field = editingInput.dataset.field; const value = editingInput.value; this.saveField(entityType, entityId, field, value); } } cancelCurrentEditing() { const editingInput = document.querySelector('.inline-edit-input'); if (editingInput) { const cell = editingInput.parentElement; const originalValue = cell.dataset.originalValue || ''; cell.innerHTML = originalValue; cell.classList.remove('editing'); } } saveAllPending() { const editingInputs = document.querySelectorAll('.inline-edit-input'); editingInputs.forEach(input => { const entityType = input.dataset.entityType; const entityId = input.dataset.entityId; const field = input.dataset.field; const value = input.value; this.saveField(entityType, entityId, field, value); }); } startLockCheck() { // Vérifier les verrous toutes les 30 secondes this.lockCheckInterval = setInterval(() => { this.checkLocks(); }, 30000); } async checkLocks() { for (const [key, lock] of this.locks) { if (new Date() > lock.expiresAt) { // Verrou expiré, le libérer this.locks.delete(key); } else { // Prolonger le verrou await this.extendLock(lock.entityType, lock.entityId); } } } showLockMessage(cell) { const message = document.createElement('div'); message.className = 'alert alert-warning lock-message'; message.textContent = 'Cet élément est en cours de modification par un autre utilisateur'; cell.appendChild(message); setTimeout(() => { message.remove(); }, 3000); } showSuccessMessage(message) { this.showMessage(message, 'success'); } showErrorMessage(message) { this.showMessage(message, 'danger'); } showMessage(message, type) { const alert = document.createElement('div'); alert.className = `alert alert-${type} alert-dismissible fade show position-fixed`; alert.style.top = '20px'; alert.style.right = '20px'; alert.style.zIndex = '9999'; alert.innerHTML = ` ${message} `; document.body.appendChild(alert); setTimeout(() => { alert.remove(); }, 5000); } destroy() { if (this.lockCheckInterval) { clearInterval(this.lockCheckInterval); } // Libérer tous les verrous for (const [key, lock] of this.locks) { this.releaseLock(lock.entityType, lock.entityId); } } } // Initialiser l'édition inline quand le DOM est prêt document.addEventListener('DOMContentLoaded', () => { window.inlineEditing = new InlineEditing(); }); // Nettoyer à la fermeture de la page window.addEventListener('beforeunload', () => { if (window.inlineEditing) { window.inlineEditing.destroy(); } });