672 lines
25 KiB
JavaScript
672 lines
25 KiB
JavaScript
// Gestion du stockage offline pour les remarques
|
||
|
||
class OfflineManager {
|
||
constructor() {
|
||
this.dbName = 'StreetUpOffline';
|
||
this.dbVersion = 2; // Incrémenté pour forcer la création des stores
|
||
this.db = null;
|
||
this.isOnline = navigator.onLine;
|
||
this.isReady = false;
|
||
|
||
// Seuils de qualité réseau en-dessous desquels le mode hors ligne est activé.
|
||
// Équivalent 4G faible : effectiveType != '4g', downlink < 0.5 Mbps ou rtt > 500 ms.
|
||
this.connectionThreshold = {
|
||
minEffectiveType: '4g', // '3g' et inférieurs déclenchent le mode offline
|
||
minDownlink: 0.5, // Mbps
|
||
maxRtt: 500 // ms
|
||
};
|
||
|
||
this.setupEventListeners();
|
||
this._evaluateConnectionQuality();
|
||
}
|
||
|
||
async init() {
|
||
return new Promise((resolve, reject) => {
|
||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||
|
||
request.onerror = () => {
|
||
reject(request.error);
|
||
};
|
||
|
||
request.onsuccess = () => {
|
||
this.db = request.result;
|
||
|
||
// Vérifier que les stores existent
|
||
const hasRemarks = this.db.objectStoreNames.contains('pendingRemarks');
|
||
const hasPhotos = this.db.objectStoreNames.contains('pendingPhotos');
|
||
|
||
if (!hasRemarks || !hasPhotos) {
|
||
this.db.close();
|
||
|
||
// Supprimer et recréer
|
||
const deleteRequest = indexedDB.deleteDatabase(this.dbName);
|
||
deleteRequest.onsuccess = () => {
|
||
// Réessayer
|
||
this.init().then(resolve).catch(reject);
|
||
};
|
||
deleteRequest.onerror = () => reject(deleteRequest.error);
|
||
return;
|
||
}
|
||
|
||
this.isReady = true;
|
||
resolve(this.db);
|
||
};
|
||
|
||
request.onupgradeneeded = (event) => {
|
||
const db = event.target.result;
|
||
|
||
// Store pour les remarques en attente
|
||
if (!db.objectStoreNames.contains('pendingRemarks')) {
|
||
const store = db.createObjectStore('pendingRemarks', {
|
||
keyPath: 'id',
|
||
autoIncrement: true
|
||
});
|
||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||
store.createIndex('controlId', 'controlId', { unique: false });
|
||
}
|
||
|
||
// Store pour les photos (blob)
|
||
if (!db.objectStoreNames.contains('pendingPhotos')) {
|
||
db.createObjectStore('pendingPhotos', {
|
||
keyPath: 'id',
|
||
autoIncrement: true
|
||
});
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// Écouter les changements de connectivité réseau (binaire)
|
||
window.addEventListener('online', () => {
|
||
this._evaluateConnectionQuality();
|
||
if (this.isOnline) {
|
||
this.showNotification('Connexion rétablie', 'success');
|
||
this.syncPendingData();
|
||
} else {
|
||
this.showNotification('Connexion trop faible – mode hors ligne maintenu', 'warning');
|
||
}
|
||
});
|
||
|
||
window.addEventListener('offline', () => {
|
||
this.isOnline = false;
|
||
this.showNotification('Mode hors ligne activé', 'warning');
|
||
});
|
||
|
||
// Network Information API : détecter les changements de qualité réseau
|
||
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||
if (conn) {
|
||
conn.addEventListener('change', () => {
|
||
const wasOnline = this.isOnline;
|
||
this._evaluateConnectionQuality();
|
||
if (!wasOnline && this.isOnline) {
|
||
this.showNotification('Connexion suffisante – retour en ligne', 'success');
|
||
this.syncPendingData();
|
||
} else if (wasOnline && !this.isOnline) {
|
||
this.showNotification('Connexion trop faible – mode hors ligne activé', 'warning');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Retourne true si la connexion actuelle est en-dessous du seuil configuré.
|
||
* Utilise l'API Network Information si disponible, sinon retourne false (pas de dégradation).
|
||
*/
|
||
_isConnectionPoor() {
|
||
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||
if (!conn) return false; // API non disponible : conserver le comportement par défaut
|
||
|
||
const poorTypes = ['slow-2g', '2g', '3g'];
|
||
if (poorTypes.includes(conn.effectiveType)) return true;
|
||
if (conn.rtt > 0 && conn.rtt > this.connectionThreshold.maxRtt) return true;
|
||
if (conn.downlink > 0 && conn.downlink < this.connectionThreshold.minDownlink) return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Évalue la qualité de connexion et met à jour isOnline en conséquence.
|
||
* Dispatch un événement CustomEvent pour notifier les templates.
|
||
*/
|
||
_evaluateConnectionQuality() {
|
||
if (!navigator.onLine) {
|
||
this.isOnline = false;
|
||
return;
|
||
}
|
||
|
||
const isPoor = this._isConnectionPoor();
|
||
const wasOnline = this.isOnline;
|
||
this.isOnline = !isPoor;
|
||
|
||
if (!wasOnline && this.isOnline) {
|
||
window.dispatchEvent(new CustomEvent('effectiveOnline'));
|
||
} else if (wasOnline && !this.isOnline) {
|
||
window.dispatchEvent(new CustomEvent('effectiveOffline', { detail: { reason: 'poor_connection' } }));
|
||
}
|
||
}
|
||
|
||
// Sauvegarder une remarque localement
|
||
async saveRemarkOffline(remarkData, photos = []) {
|
||
if (!this.db || !this.isReady) {
|
||
throw new Error('IndexedDB non initialisé');
|
||
}
|
||
|
||
// IMPORTANT : Convertir les photos en blobs AVANT d'ouvrir la transaction
|
||
// pour éviter que la transaction expire
|
||
const photoRecords = [];
|
||
for (const photo of photos) {
|
||
const photoBlob = await this.fileToBlob(photo);
|
||
photoRecords.push({
|
||
filename: photo.name,
|
||
blob: photoBlob,
|
||
type: photo.type
|
||
});
|
||
}
|
||
|
||
// Maintenant ouvrir la transaction et faire toutes les opérations de manière synchrone
|
||
return new Promise((resolve, reject) => {
|
||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite');
|
||
const remarksStore = transaction.objectStore('pendingRemarks');
|
||
const photosStore = transaction.objectStore('pendingPhotos');
|
||
|
||
const remarkRecord = {
|
||
controlId: remarkData.controlId,
|
||
description: remarkData.description,
|
||
status: remarkData.status,
|
||
latitude: remarkData.latitude,
|
||
longitude: remarkData.longitude,
|
||
coord_x: remarkData.coord_x,
|
||
coord_y: remarkData.coord_y,
|
||
reference_plan: remarkData.reference_plan,
|
||
timestamp: Date.now(),
|
||
photoIds: []
|
||
};
|
||
|
||
// Gérer les erreurs de transaction
|
||
transaction.onerror = () => {
|
||
reject(transaction.error);
|
||
};
|
||
|
||
transaction.oncomplete = () => {
|
||
this.showNotification(
|
||
`Remarque sauvegardée localement. Elle sera synchronisée dès le retour du réseau.`,
|
||
'info'
|
||
);
|
||
resolve(remarkId);
|
||
};
|
||
|
||
// Sauvegarder les photos de manière synchrone
|
||
let photosAdded = 0;
|
||
let remarkId = null;
|
||
|
||
if (photoRecords.length > 0) {
|
||
photoRecords.forEach((photoRecord) => {
|
||
const photoRequest = photosStore.add(photoRecord);
|
||
photoRequest.onsuccess = () => {
|
||
remarkRecord.photoIds.push(photoRequest.result);
|
||
photosAdded++;
|
||
|
||
// Une fois toutes les photos ajoutées, ajouter la remarque
|
||
if (photosAdded === photoRecords.length) {
|
||
const remarkRequest = remarksStore.add(remarkRecord);
|
||
remarkRequest.onsuccess = () => {
|
||
remarkId = remarkRequest.result;
|
||
};
|
||
remarkRequest.onerror = () => reject(remarkRequest.error);
|
||
}
|
||
};
|
||
photoRequest.onerror = () => reject(photoRequest.error);
|
||
});
|
||
} else {
|
||
// Pas de photos, ajouter directement la remarque
|
||
const remarkRequest = remarksStore.add(remarkRecord);
|
||
remarkRequest.onsuccess = () => {
|
||
remarkId = remarkRequest.result;
|
||
};
|
||
remarkRequest.onerror = () => reject(remarkRequest.error);
|
||
}
|
||
});
|
||
}
|
||
|
||
async fileToBlob(file) {
|
||
return new Promise((resolve) => {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => resolve(new Blob([e.target.result], { type: file.type }));
|
||
reader.readAsArrayBuffer(file);
|
||
});
|
||
}
|
||
|
||
addRecord(store, record) {
|
||
return new Promise((resolve, reject) => {
|
||
const request = store.add(record);
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
}
|
||
|
||
// Synchroniser les données en attente
|
||
async syncPendingData() {
|
||
if (!this.isOnline) {
|
||
return;
|
||
}
|
||
|
||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readonly');
|
||
const remarksStore = transaction.objectStore('pendingRemarks');
|
||
|
||
const remarks = await this.getAllRecords(remarksStore);
|
||
|
||
if (remarks.length === 0) {
|
||
return;
|
||
}
|
||
|
||
this.showNotification(
|
||
`Synchronisation de ${remarks.length} remarque(s) en cours...`,
|
||
'info'
|
||
);
|
||
|
||
let syncedCount = 0;
|
||
let failedCount = 0;
|
||
|
||
for (const remark of remarks) {
|
||
try {
|
||
await this.uploadRemark(remark);
|
||
await this.deleteRemark(remark.id);
|
||
syncedCount++;
|
||
} catch (error) {
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
if (syncedCount > 0) {
|
||
this.showNotification(
|
||
`${syncedCount} remarque(s) synchronisée(s) avec succès`,
|
||
'success'
|
||
);
|
||
|
||
// Recharger la page pour afficher les nouvelles remarques
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 1500);
|
||
}
|
||
|
||
if (failedCount > 0) {
|
||
this.showNotification(
|
||
`${failedCount} remarque(s) n'ont pas pu être synchronisées`,
|
||
'error'
|
||
);
|
||
}
|
||
}
|
||
|
||
async getAllRecords(store) {
|
||
return new Promise((resolve, reject) => {
|
||
const request = store.getAll();
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
}
|
||
|
||
async uploadRemark(remark) {
|
||
const photosStore = this.db.transaction('pendingPhotos', 'readonly')
|
||
.objectStore('pendingPhotos');
|
||
|
||
// Récupérer les photos
|
||
const photos = [];
|
||
for (const photoId of remark.photoIds) {
|
||
const photo = await this.getRecord(photosStore, photoId);
|
||
if (photo) {
|
||
photos.push(new File([photo.blob], photo.filename, { type: photo.type }));
|
||
}
|
||
}
|
||
|
||
// Créer FormData
|
||
const formData = new FormData();
|
||
|
||
// IMPORTANT: Ajouter le paramètre submit_remark pour que le backend traite la requête
|
||
formData.append('submit_remark', 'true');
|
||
|
||
// Champ obligatoire : control ID
|
||
formData.append('control', remark.controlId);
|
||
|
||
formData.append('description', remark.description);
|
||
formData.append('status', remark.status);
|
||
formData.append('latitude', remark.latitude || '');
|
||
formData.append('longitude', remark.longitude || '');
|
||
formData.append('coord_x', remark.coord_x || '');
|
||
formData.append('coord_y', remark.coord_y || '');
|
||
formData.append('reference_plan', remark.reference_plan || '');
|
||
formData.append('categories', ''); // Catégories vides par défaut
|
||
|
||
// Ajouter les champs du management form pour le formset (quantity)
|
||
// Django s'attend à ces champs même si le formset est vide
|
||
formData.append('qty-TOTAL_FORMS', '0');
|
||
formData.append('qty-INITIAL_FORMS', '0');
|
||
formData.append('qty-MIN_NUM_FORMS', '0');
|
||
formData.append('qty-MAX_NUM_FORMS', '1000');
|
||
|
||
photos.forEach(photo => {
|
||
formData.append('images', photo);
|
||
});
|
||
|
||
// Envoyer au serveur
|
||
const csrftoken = this.getCookie('csrftoken');
|
||
const currentUrl = window.location.pathname;
|
||
|
||
const response = await fetch(currentUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': csrftoken,
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`);
|
||
}
|
||
|
||
// Supprimer les photos associées
|
||
const photosTransaction = this.db.transaction('pendingPhotos', 'readwrite');
|
||
const photosWriteStore = photosTransaction.objectStore('pendingPhotos');
|
||
for (const photoId of remark.photoIds) {
|
||
await this.deleteRecord(photosWriteStore, photoId);
|
||
}
|
||
|
||
return response;
|
||
}
|
||
|
||
async getRecord(store, id) {
|
||
return new Promise((resolve, reject) => {
|
||
const request = store.get(id);
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
}
|
||
|
||
async deleteRemark(remarkId) {
|
||
const transaction = this.db.transaction('pendingRemarks', 'readwrite');
|
||
const store = transaction.objectStore('pendingRemarks');
|
||
return this.deleteRecord(store, remarkId);
|
||
}
|
||
|
||
deleteRecord(store, id) {
|
||
return new Promise((resolve, reject) => {
|
||
const request = store.delete(id);
|
||
request.onsuccess = () => resolve();
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
}
|
||
|
||
getCookie(name) {
|
||
let cookieValue = null;
|
||
if (document.cookie && document.cookie !== '') {
|
||
const cookies = document.cookie.split(';');
|
||
for (let i = 0; i < cookies.length; i++) {
|
||
const cookie = cookies[i].trim();
|
||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return cookieValue;
|
||
}
|
||
|
||
showNotification(message, type = 'info') {
|
||
// Créer un toast Bootstrap
|
||
const bgClass = type === 'success' ? 'success' :
|
||
type === 'error' ? 'danger' :
|
||
type === 'warning' ? 'warning' : 'info';
|
||
|
||
const toastHtml = `
|
||
<div class="toast align-items-center text-white bg-${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||
<div class="d-flex">
|
||
<div class="toast-body">${message}</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Ajouter au container de toasts
|
||
let container = document.getElementById('toast-container');
|
||
if (!container) {
|
||
container = this.createToastContainer();
|
||
}
|
||
|
||
container.insertAdjacentHTML('beforeend', toastHtml);
|
||
const toastEl = container.lastElementChild;
|
||
|
||
if (typeof bootstrap !== 'undefined' && bootstrap.Toast) {
|
||
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
|
||
toast.show();
|
||
|
||
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
|
||
}
|
||
}
|
||
|
||
createToastContainer() {
|
||
const container = document.createElement('div');
|
||
container.id = 'toast-container';
|
||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||
container.style.zIndex = '9999';
|
||
document.body.appendChild(container);
|
||
return container;
|
||
}
|
||
|
||
// Obtenir le nombre de remarques en attente
|
||
async getPendingCount() {
|
||
if (!this.db) return 0;
|
||
const transaction = this.db.transaction('pendingRemarks', 'readonly');
|
||
const store = transaction.objectStore('pendingRemarks');
|
||
return new Promise((resolve, reject) => {
|
||
const request = store.count();
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
}
|
||
|
||
// Obtenir toutes les remarques en attente pour un contrôle
|
||
async getPendingRemarks(controlId) {
|
||
if (!this.db) return [];
|
||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readonly');
|
||
const remarksStore = transaction.objectStore('pendingRemarks');
|
||
const photosStore = transaction.objectStore('pendingPhotos');
|
||
|
||
const allRemarks = await this.getAllRecords(remarksStore);
|
||
const controlRemarks = allRemarks.filter(r => r.controlId == controlId);
|
||
|
||
// Charger les photos pour chaque remarque
|
||
for (const remark of controlRemarks) {
|
||
remark.photos = [];
|
||
for (const photoId of remark.photoIds || []) {
|
||
const photo = await this.getRecord(photosStore, photoId);
|
||
if (photo) {
|
||
// Créer une URL pour afficher la photo
|
||
remark.photos.push({
|
||
id: photoId,
|
||
url: URL.createObjectURL(photo.blob),
|
||
filename: photo.filename
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return controlRemarks;
|
||
}
|
||
|
||
// Mettre à jour une remarque offline
|
||
async updateRemarkOffline(remarkId, remarkData, photos = []) {
|
||
if (!this.db || !this.isReady) {
|
||
throw new Error('IndexedDB non initialisé');
|
||
}
|
||
|
||
// Convertir les nouvelles photos en blobs
|
||
const photoRecords = [];
|
||
for (const photo of photos) {
|
||
const photoBlob = await this.fileToBlob(photo);
|
||
photoRecords.push({
|
||
filename: photo.name,
|
||
blob: photoBlob,
|
||
type: photo.type
|
||
});
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite');
|
||
const remarksStore = transaction.objectStore('pendingRemarks');
|
||
const photosStore = transaction.objectStore('pendingPhotos');
|
||
|
||
transaction.onerror = () => reject(transaction.error);
|
||
transaction.oncomplete = () => {
|
||
this.showNotification('Remarque mise à jour localement', 'info');
|
||
resolve();
|
||
};
|
||
|
||
// Récupérer la remarque existante
|
||
const getRequest = remarksStore.get(remarkId);
|
||
getRequest.onsuccess = () => {
|
||
const existingRemark = getRequest.result;
|
||
if (!existingRemark) {
|
||
reject(new Error('Remarque non trouvée'));
|
||
return;
|
||
}
|
||
|
||
// Mettre à jour les données
|
||
existingRemark.description = remarkData.description;
|
||
existingRemark.status = remarkData.status;
|
||
existingRemark.latitude = remarkData.latitude;
|
||
existingRemark.longitude = remarkData.longitude;
|
||
existingRemark.coord_x = remarkData.coord_x;
|
||
existingRemark.coord_y = remarkData.coord_y;
|
||
existingRemark.reference_plan = remarkData.reference_plan;
|
||
|
||
// Ajouter les nouvelles photos
|
||
let photosAdded = 0;
|
||
if (photoRecords.length > 0) {
|
||
photoRecords.forEach((photoRecord) => {
|
||
const photoRequest = photosStore.add(photoRecord);
|
||
photoRequest.onsuccess = () => {
|
||
existingRemark.photoIds.push(photoRequest.result);
|
||
photosAdded++;
|
||
|
||
if (photosAdded === photoRecords.length) {
|
||
remarksStore.put(existingRemark);
|
||
}
|
||
};
|
||
});
|
||
} else {
|
||
remarksStore.put(existingRemark);
|
||
}
|
||
};
|
||
getRequest.onerror = () => reject(getRequest.error);
|
||
});
|
||
}
|
||
|
||
// Supprimer une remarque offline
|
||
async deleteRemarkOffline(remarkId) {
|
||
if (!this.db || !this.isReady) {
|
||
throw new Error('IndexedDB non initialisé');
|
||
}
|
||
|
||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite');
|
||
const remarksStore = transaction.objectStore('pendingRemarks');
|
||
const photosStore = transaction.objectStore('pendingPhotos');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// Récupérer la remarque pour obtenir les photoIds
|
||
const getRequest = remarksStore.get(remarkId);
|
||
getRequest.onsuccess = async () => {
|
||
const remark = getRequest.result;
|
||
if (remark && remark.photoIds) {
|
||
// Supprimer les photos associées
|
||
for (const photoId of remark.photoIds) {
|
||
await this.deleteRecord(photosStore, photoId);
|
||
}
|
||
}
|
||
|
||
// Supprimer la remarque
|
||
const deleteRequest = remarksStore.delete(remarkId);
|
||
deleteRequest.onsuccess = () => {
|
||
this.showNotification('Remarque supprimée localement', 'info');
|
||
resolve();
|
||
};
|
||
deleteRequest.onerror = () => reject(deleteRequest.error);
|
||
};
|
||
getRequest.onerror = () => reject(getRequest.error);
|
||
});
|
||
}
|
||
|
||
// Supprimer une photo d'une remarque offline
|
||
async deletePhotoOffline(remarkId, photoId) {
|
||
if (!this.db || !this.isReady) {
|
||
throw new Error('IndexedDB non initialisé');
|
||
}
|
||
|
||
const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite');
|
||
const remarksStore = transaction.objectStore('pendingRemarks');
|
||
const photosStore = transaction.objectStore('pendingPhotos');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const getRequest = remarksStore.get(remarkId);
|
||
getRequest.onsuccess = () => {
|
||
const remark = getRequest.result;
|
||
if (!remark) {
|
||
reject(new Error('Remarque non trouvée'));
|
||
return;
|
||
}
|
||
|
||
// Retirer l'ID de la photo
|
||
remark.photoIds = remark.photoIds.filter(id => id !== photoId);
|
||
|
||
// Mettre à jour la remarque
|
||
const putRequest = remarksStore.put(remark);
|
||
putRequest.onsuccess = () => {
|
||
// Supprimer la photo
|
||
photosStore.delete(photoId);
|
||
resolve();
|
||
};
|
||
putRequest.onerror = () => reject(putRequest.error);
|
||
};
|
||
getRequest.onerror = () => reject(getRequest.error);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Instance globale
|
||
let offlineManager = null;
|
||
let offlineManagerReady = false;
|
||
|
||
// Initialiser immédiatement, pas dans DOMContentLoaded
|
||
(async function initOfflineManager() {
|
||
try {
|
||
offlineManager = new OfflineManager();
|
||
|
||
// Attendre que la DB soit initialisée
|
||
await offlineManager.init();
|
||
|
||
offlineManagerReady = true;
|
||
|
||
// Tenter une synchronisation au chargement si en ligne
|
||
if (navigator.onLine) {
|
||
setTimeout(() => {
|
||
if (offlineManager) {
|
||
offlineManager.syncPendingData();
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// Mettre à jour le badge après initialisation
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (typeof updatePendingBadge === 'function') {
|
||
updatePendingBadge();
|
||
}
|
||
});
|
||
} else {
|
||
if (typeof updatePendingBadge === 'function') {
|
||
updatePendingBadge();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur initialisation offline manager:', error);
|
||
}
|
||
})();
|