diff --git a/streetup/interventions/static/interventions/detail/cms_edit_mode.js b/streetup/interventions/static/interventions/detail/cms_edit_mode.js index f5f5768..b4f2c18 100644 --- a/streetup/interventions/static/interventions/detail/cms_edit_mode.js +++ b/streetup/interventions/static/interventions/detail/cms_edit_mode.js @@ -10,13 +10,18 @@ let isEditMode = false; let originalValues = {}; let originalContractOptions = null; // Store original contract options for cancel + let isSavingInProgress = false; + let isAutoSavingAndNavigating = false; + let isSubmittingAutoSavedForm = false; // Initialize if (editBtn) { editBtn.addEventListener('click', enterEditMode); } if (saveBtn) { - saveBtn.addEventListener('click', saveChanges); + saveBtn.addEventListener('click', function() { + saveChanges({ reloadOnSuccess: true }); + }); } if (cancelBtn) { cancelBtn.addEventListener('click', cancelChanges); @@ -130,7 +135,7 @@ const fieldName = field.dataset.field; const displayDiv = field.querySelector('.field-display'); const editDiv = field.querySelector('.field-edit'); - const input = editDiv.querySelector('input, select'); + const input = editDiv ? editDiv.querySelector('input, select') : null; if (input) { // Handle checkboxes specially @@ -195,7 +200,7 @@ const fieldName = field.dataset.field; const displayDiv = field.querySelector('.field-display'); const editDiv = field.querySelector('.field-edit'); - const input = editDiv.querySelector('input, select'); + const input = editDiv ? editDiv.querySelector('input, select') : null; if (input && originalValues[fieldName] !== undefined) { // Handle checkboxes specially @@ -227,96 +232,107 @@ originalContractOptions = null; } - async function saveChanges() { - // Emit event to allow other components to save their changes - // Collect promises from other components that need to save - const savePromises = []; - const saveEvent = new CustomEvent('cms-save-before', { - detail: { - addSavePromise: (promise) => savePromises.push(promise) - } - }); - document.dispatchEvent(saveEvent); - - // Track if other components had changes - let otherComponentsHadChanges = false; - - // Wait for all other components to finish saving - if (savePromises.length > 0) { - try { - const results = await Promise.all(savePromises); - // Check if any save failed - const failed = results.some(r => r && r.success === false); - if (failed) { - showErrorMessage('Erreur lors de la sauvegarde de certains éléments'); - return; - } - // Check if any component had changes - otherComponentsHadChanges = results.some(r => r && r.saved_count > 0); - } catch (error) { - console.error('Error saving components:', error); - showErrorMessage('Erreur lors de la sauvegarde'); - return; - } - } - - // Collect changed values - const changes = {}; - let hasChanges = false; - - editableFields.forEach(field => { - const canEdit = field.dataset.canEdit === 'true'; - if (!canEdit) return; - - const fieldName = field.dataset.field; - const editDiv = field.querySelector('.field-edit'); - const input = editDiv.querySelector('input, select'); - - if (input) { - let newValue; - // Handle checkboxes specially - if (input.type === 'checkbox') { - newValue = input.checked ? 'true' : 'false'; - } else { - newValue = input.value; - } - const oldValue = originalValues[fieldName]; - - if (newValue !== oldValue) { - changes[fieldName] = newValue; - hasChanges = true; - } - } - }); + async function saveChanges(options = {}) { + const reloadOnSuccess = options.reloadOnSuccess !== false; - // Collect note changes if collectNoteChanges function exists - let noteChanges = null; - if (typeof window.collectNoteChanges === 'function') { - noteChanges = window.collectNoteChanges(); - if (noteChanges.new_notes.length > 0 || noteChanges.updated_notes.length > 0 || noteChanges.deleted_notes.length > 0) { - hasChanges = true; - changes.notes = noteChanges; - } + if (isSavingInProgress) { + return false; } - - if (!hasChanges && !otherComponentsHadChanges) { - // No changes anywhere, just exit edit mode - exitEditMode(); - return; - } - - if (!hasChanges && otherComponentsHadChanges) { - // Only other components had changes (e.g., operations), reload to show updated data - isEditMode = false; - window.location.reload(); - return; - } - - // Show loading state - saveBtn.disabled = true; - saveBtn.innerHTML = ' Enregistrement...'; - + isSavingInProgress = true; + try { + // Emit event to allow other components to save their changes + // Collect promises from other components that need to save + const savePromises = []; + const saveEvent = new CustomEvent('cms-save-before', { + detail: { + addSavePromise: (promise) => savePromises.push(promise) + } + }); + document.dispatchEvent(saveEvent); + + // Track if other components had changes + let otherComponentsHadChanges = false; + + // Wait for all other components to finish saving + if (savePromises.length > 0) { + try { + const results = await Promise.all(savePromises); + // Check if any save failed + const failed = results.some(r => r && r.success === false); + if (failed) { + showErrorMessage('Erreur lors de la sauvegarde de certains éléments'); + return false; + } + // Check if any component had changes + otherComponentsHadChanges = results.some(r => r && r.saved_count > 0); + } catch (error) { + console.error('Error saving components:', error); + showErrorMessage('Erreur lors de la sauvegarde'); + return false; + } + } + + // Collect changed values + const changes = {}; + let hasChanges = false; + + editableFields.forEach(field => { + const canEdit = field.dataset.canEdit === 'true'; + if (!canEdit) return; + + const fieldName = field.dataset.field; + const editDiv = field.querySelector('.field-edit'); + const input = editDiv ? editDiv.querySelector('input, select') : null; + + if (input) { + let newValue; + // Handle checkboxes specially + if (input.type === 'checkbox') { + newValue = input.checked ? 'true' : 'false'; + } else { + newValue = input.value; + } + const oldValue = originalValues[fieldName]; + + if (newValue !== oldValue) { + changes[fieldName] = newValue; + hasChanges = true; + } + } + }); + + // Collect note changes if collectNoteChanges function exists + let noteChanges = null; + if (typeof window.collectNoteChanges === 'function') { + noteChanges = window.collectNoteChanges(); + if (noteChanges.new_notes.length > 0 || noteChanges.updated_notes.length > 0 || noteChanges.deleted_notes.length > 0) { + hasChanges = true; + changes.notes = noteChanges; + } + } + + if (!hasChanges && !otherComponentsHadChanges) { + // No changes anywhere, just exit edit mode + exitEditMode(); + return true; + } + + if (!hasChanges && otherComponentsHadChanges) { + // Only other components had changes (e.g., operations) + isEditMode = false; + if (reloadOnSuccess) { + window.location.reload(); + } + return true; + } + + // Show loading state + if (saveBtn) { + saveBtn.disabled = true; + saveBtn.innerHTML = ' Enregistrement...'; + } + // Get intervention ID from URL or page const interventionId = window.location.pathname.match(/\/interventions\/(\d+)\//)?.[1]; if (!interventionId) { @@ -340,20 +356,26 @@ const result = await response.json(); - // Exit edit mode to prevent "leave page" warning + // Exit edit mode to prevent any warning isEditMode = false; - // Reload page immediately to reflect all changes (especially notes) - // The backend can use Django messages framework to show success message after reload - window.location.reload(); + if (reloadOnSuccess) { + window.location.reload(); + } + return true; } catch (error) { console.error('Save error:', error); showErrorMessage(error.message || 'Erreur lors de la sauvegarde'); // Restore button state - saveBtn.disabled = false; - saveBtn.innerHTML = ' Sauvegarder'; + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.innerHTML = ' Sauvegarder'; + } + return false; + } finally { + isSavingInProgress = false; } } @@ -399,8 +421,10 @@ cancelBtn.classList.add('d-none'); // Restore button state - saveBtn.disabled = false; - saveBtn.innerHTML = ' Sauvegarder'; + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.innerHTML = ' Sauvegarder'; + } originalValues = {}; } @@ -451,12 +475,114 @@ return cookieValue; } - // Warn before leaving page if in edit mode - window.addEventListener('beforeunload', (e) => { - if (isEditMode) { - e.preventDefault(); - e.returnValue = ''; - return ''; + // Auto-save before navigation when in edit mode + document.addEventListener('click', async function(e) { + if (!isEditMode || isAutoSavingAndNavigating) return; + + const anchor = e.target.closest('a'); + if (!anchor) return; + + const href = anchor.getAttribute('href'); + if (!href || href === '#' || href.startsWith('javascript:') || href.startsWith('#')) return; + if (anchor.getAttribute('target') === '_blank') return; + if (anchor.hasAttribute('data-bs-toggle') || anchor.hasAttribute('data-bs-dismiss') || anchor.hasAttribute('data-bs-target')) return; + if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; + + e.preventDefault(); + e.stopPropagation(); + + isAutoSavingAndNavigating = true; + const targetUrl = anchor.href; + + try { + const success = await saveChanges({ reloadOnSuccess: false }); + if (success !== false) { + window.location.href = targetUrl; + } else { + isAutoSavingAndNavigating = false; + } + } catch (err) { + console.error('Auto-save navigation error:', err); + isAutoSavingAndNavigating = false; } - }); + }, true); + + // Intercept form submissions to auto-save before submitting + document.addEventListener('submit', async function(e) { + if (!isEditMode || isSubmittingAutoSavedForm || isAutoSavingAndNavigating) return; + + const form = e.target; + if (form.getAttribute('target') === '_blank') return; + + e.preventDefault(); + e.stopPropagation(); + + isAutoSavingAndNavigating = true; + + try { + const success = await saveChanges({ reloadOnSuccess: false }); + if (success !== false) { + isSubmittingAutoSavedForm = true; + form.submit(); + } else { + isAutoSavingAndNavigating = false; + } + } catch (err) { + console.error('Auto-save form submit error:', err); + isAutoSavingAndNavigating = false; + } + }, true); + + // Background auto-save on tab close / browser reload without showing alert + function sendKeepAliveSave() { + if (!isEditMode) return; + + const changes = {}; + let hasChanges = false; + + editableFields.forEach(field => { + const canEdit = field.dataset.canEdit === 'true'; + if (!canEdit) return; + + const fieldName = field.dataset.field; + const editDiv = field.querySelector('.field-edit'); + const input = editDiv ? editDiv.querySelector('input, select') : null; + + if (input) { + let newValue = input.type === 'checkbox' ? (input.checked ? 'true' : 'false') : input.value; + const oldValue = originalValues[fieldName]; + if (newValue !== oldValue) { + changes[fieldName] = newValue; + hasChanges = true; + } + } + }); + + if (typeof window.collectNoteChanges === 'function') { + const noteChanges = window.collectNoteChanges(); + if (noteChanges.new_notes.length > 0 || noteChanges.updated_notes.length > 0 || noteChanges.deleted_notes.length > 0) { + hasChanges = true; + changes.notes = noteChanges; + } + } + + if (hasChanges) { + const interventionId = window.location.pathname.match(/\/interventions\/(\d+)\//)?.[1]; + if (interventionId) { + fetch(`/interventions/${interventionId}/update/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify(changes), + keepalive: true + }); + } + } + } + + window.addEventListener('pagehide', sendKeepAliveSave); + window.addEventListener('beforeunload', sendKeepAliveSave); })(); +