316 lines
9.8 KiB
JavaScript
316 lines
9.8 KiB
JavaScript
/**
|
|
* 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}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
|