1 parent
815266383b
commit
165a0b371c
8 files changed
+366
-76
No files matched your search
+128
-48
@@ -23,6 +23,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/favicon.png">
|
||||
<link rel="stylesheet" href="/global.css">
|
||||
@@ -122,19 +123,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<main class="pf-main<?= ($mainClass ?? '') ? ' '.htmlspecialchars($mainClass) : '' ?>">
|
||||
|
||||
<script>
|
||||
// CSRF avant I18N : le parseur HTML coupe le script dès la séquence de fin de balise « script » (même dans un commentaire JS).
|
||||
// CSRF avant I18N
|
||||
window.CSRF_TOKEN = "<?= htmlspecialchars(function_exists('csrf_token') ? csrf_token() : '', ENT_QUOTES, 'UTF-8') ?>";
|
||||
|
||||
/**
|
||||
* Pont d'Internationalisation et Configuration
|
||||
*/
|
||||
window.I18N = <?php echo json_encode($current_translations_array ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
// ✅ Injection sécurisée
|
||||
window.CONFIG = {
|
||||
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
|
||||
PEOPLE: <?= isset($familyPeople) ? json_encode($familyPeople) : '[]' ?>,
|
||||
// ID du user global connecté (pour savoir "qui" clique)
|
||||
CURRENT_USER_ID: <?= isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'null' ?>,
|
||||
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
|
||||
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
||||
@@ -144,14 +139,20 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
return window.I18N[key] || key;
|
||||
}
|
||||
|
||||
// Thème clair/sombre — état visuel géré entièrement par CSS via [data-theme="dark"]
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(err => {
|
||||
console.error('Service Worker registration failed:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTheme(){
|
||||
const dark=document.documentElement.getAttribute('data-theme')==='dark';
|
||||
document.documentElement.setAttribute('data-theme',dark?'light':'dark');
|
||||
localStorage.setItem('hh-theme',dark?'light':'dark');
|
||||
}
|
||||
|
||||
// Gestion du menu mobile (Off-Canvas Sidebar)
|
||||
<?php if (isset($_SESSION['user'])): ?>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const burgerBtn = document.querySelector('.pf-burger-btn');
|
||||
@@ -174,14 +175,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
if(burgerBtn && mobileMenu && overlay && closeBtn) {
|
||||
burgerBtn.addEventListener('click', openMenu);
|
||||
closeBtn.addEventListener('click', closeMenu);
|
||||
// Fermer le menu si on clique en dehors (sur l'overlay sombre)
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
}
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
/**
|
||||
* pachaFetch : Utilitaire de requête robuste
|
||||
* pachaFetch : Utilitaire de requête robuste avec gestion Offline Cross-Browser
|
||||
*/
|
||||
async function pachaFetch(url, options = {}) {
|
||||
const finalUrl = url.startsWith('/') ? url.substring(1) : url;
|
||||
@@ -200,53 +200,133 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
try {
|
||||
return JSON.parse(rawText);
|
||||
} catch (jsonErr) {
|
||||
console.error("Réponse corrompue (HTML reçu au lieu de JSON) :", rawText);
|
||||
console.error("Réponse corrompue (HTML) :", rawText);
|
||||
throw new Error("Erreur serveur : format JSON invalide.");
|
||||
}
|
||||
} catch (err) {
|
||||
// Si hors ligne ET modification (POST/PUT/DELETE)
|
||||
if (!navigator.onLine && options.method && options.method.toUpperCase() !== 'GET') {
|
||||
await enqueueOfflineAction(finalUrl, options);
|
||||
|
||||
// Chrome/Android : Déclenche le Background Sync
|
||||
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.sync.register('househub-sync').catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
showToast("Mémorisé hors ligne 📡", "success");
|
||||
return { success: true, offline: true };
|
||||
}
|
||||
|
||||
console.error(`Erreur pachaFetch [${finalUrl}] :`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Utility : Toasts (Notifications)
|
||||
*/
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `pf-toast pf-toast--${type}`;
|
||||
toast.innerHTML = message;
|
||||
document.body.appendChild(toast);
|
||||
* Utilitaire PWA : Ajout à la file d'attente (IndexedDB)
|
||||
*/
|
||||
function enqueueOfflineAction(url, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('househub-offline-sync', 1);
|
||||
request.onupgradeneeded = event => {
|
||||
event.target.result.createObjectStore('sync-queue', { autoIncrement: true });
|
||||
};
|
||||
request.onsuccess = event => {
|
||||
const db = event.target.result;
|
||||
const tx = db.transaction('sync-queue', 'readwrite');
|
||||
const store = tx.objectStore('sync-queue');
|
||||
|
||||
// Animation et suppression
|
||||
setTimeout(() => toast.classList.add('is-visible'), 100);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('is-visible');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
let bodyData = options.body;
|
||||
if (bodyData instanceof FormData) {
|
||||
bodyData = new URLSearchParams(bodyData).toString();
|
||||
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Utility : Confirmation stylisée (Remplace confirm())
|
||||
*/
|
||||
async function pachaConfirm(title, message) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'pf-modal open';
|
||||
modal.innerHTML = `
|
||||
<div class="pf-modal-content" style="max-width: 400px; align-self: center;">
|
||||
<h3 style="margin-top:0;">${title}</h3>
|
||||
<p style="color:var(--text-muted);">${message}</p>
|
||||
<div class="modal-footer">
|
||||
<button class="pf-btn btn-secondary" id="confirm-cancel">${tr('btn_cancel')}</button>
|
||||
<button class="pf-btn" id="confirm-ok" style="background:var(--danger);">${tr('btn_delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
store.add({
|
||||
url: url,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
body: bodyData,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
request.onerror = event => reject(event.target.error);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('confirm-cancel').onclick = () => { modal.remove(); resolve(false); };
|
||||
document.getElementById('confirm-ok').onclick = () => { modal.remove(); resolve(true); };
|
||||
/**
|
||||
* Fallback pour Firefox et iOS Safari (Dépilement manuel au retour réseau)
|
||||
*/
|
||||
window.addEventListener('online', () => {
|
||||
const request = indexedDB.open('househub-offline-sync', 1);
|
||||
request.onsuccess = event => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('sync-queue')) return;
|
||||
|
||||
const tx = db.transaction('sync-queue', 'readonly');
|
||||
const store = tx.objectStore('sync-queue');
|
||||
const getAll = store.getAll();
|
||||
|
||||
getAll.onsuccess = async () => {
|
||||
const actions = getAll.result;
|
||||
if (actions && actions.length > 0) {
|
||||
showToast("Synchronisation des données... 🔄", "success");
|
||||
for (const action of actions) {
|
||||
try {
|
||||
await fetch(action.url, {
|
||||
method: action.method,
|
||||
headers: action.headers,
|
||||
body: action.body
|
||||
});
|
||||
const delTx = db.transaction('sync-queue', 'readwrite');
|
||||
delTx.objectStore('sync-queue').delete(action.id);
|
||||
} catch (e) {
|
||||
console.error("Échec de la synchro", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
showToast("Synchronisation terminée ! ✅", "success");
|
||||
// Rafraîchissement léger recommandé après synchro de masse
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `pf-toast pf-toast--${type}`;
|
||||
toast.innerHTML = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => toast.classList.add('is-visible'), 100);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('is-visible');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function pachaConfirm(title, message) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'pf-modal open';
|
||||
modal.innerHTML = `
|
||||
<div class="pf-modal-content" style="max-width: 400px; align-self: center;">
|
||||
<h3 style="margin-top:0;">${title}</h3>
|
||||
<p style="color:var(--text-muted);">${message}</p>
|
||||
<div class="modal-footer">
|
||||
<button class="pf-btn btn-secondary" id="confirm-cancel">${tr('btn_cancel')}</button>
|
||||
<button class="pf-btn" id="confirm-ok" style="background:var(--danger);">${tr('btn_delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
document.getElementById('confirm-cancel').onclick = () => { modal.remove(); resolve(false); };
|
||||
document.getElementById('confirm-ok').onclick = () => { modal.remove(); resolve(true); };
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1036,4 +1036,9 @@ return [
|
||||
'food_err_no_views_desc' => 'Heu desactivat totes les funcions d\'aquest mòdul. Aneu a la configuració per reactivar-les.',
|
||||
'meal_paste_success' => 'Graella enganxada amb èxit!',
|
||||
'meal_err_filename' => 'Format del nom del fitxer no reconegut. Exemple esperat: "Menus-du-28-septembre-au-4-octobre-2026.pdf".',
|
||||
|
||||
'offline_title' => 'Mode fora de línia',
|
||||
'offline_heading' => 'Estàs fora de línia',
|
||||
'offline_message' => 'L\'aplicació HouseHub no està disponible sense connexió a internet ara mateix.',
|
||||
'offline_retry_btn' => 'Tornar-ho a intentar',
|
||||
];
|
||||
@@ -1040,4 +1040,9 @@ return [
|
||||
'meal_paste_success' => 'Grid pasted successfully!',
|
||||
'meal_err_filename' => 'Unrecognized file name format. Expected example: "Menus-du-28-septembre-au-4-octobre-2026.pdf".',
|
||||
|
||||
'offline_title' => 'Offline Mode',
|
||||
'offline_heading' => 'You are offline',
|
||||
'offline_message' => 'The HouseHub application cannot be reached without an internet connection at the moment.',
|
||||
'offline_retry_btn' => 'Retry connection',
|
||||
|
||||
];
|
||||
@@ -1036,4 +1036,9 @@ return [
|
||||
'meal_err_filename' => 'Format de nom de fichier non reconnu. Exemple attendu : "Menus-du-28-septembre-au-4-octobre-2026.pdf".',
|
||||
'meal_paste_success' => 'Grille collée avec succès !',
|
||||
|
||||
'offline_title' => 'Mode hors ligne',
|
||||
'offline_heading' => 'Vous êtes hors ligne',
|
||||
'offline_message' => 'L\'application HouseHub n\'est pas joignable sans connexion internet pour le moment.',
|
||||
'offline_retry_btn' => 'Réessayer de se connecter',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "HouseHub OS",
|
||||
"short_name": "HouseHub",
|
||||
"description": "Système de gestion familiale",
|
||||
"start_url": "/index.php",
|
||||
"display": "standalone",
|
||||
"background_color": "#f8fafc",
|
||||
"theme_color": "#3b82f6",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -238,11 +238,28 @@ $fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
||||
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||
// ============================================================================
|
||||
// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
|
||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
||||
$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
|
||||
|
||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$absAmount = abs((float)$item['amount']);
|
||||
|
||||
// --- 🚀 INTERCEPTION DYNAMIQUE ---
|
||||
$calcDetails = "";
|
||||
if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) {
|
||||
require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php';
|
||||
// Facturation à terme échu : on recule d'un mois par rapport au mois actif du suivi
|
||||
$prevDate = date('Y-m-d', strtotime('-1 month', strtotime($viewMonthDate)));
|
||||
$calcYear = date('Y', strtotime($prevDate));
|
||||
$calcMonth = date('m', strtotime($prevDate));
|
||||
|
||||
$calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth);
|
||||
$estimateData = $calc->getEstimate($item['dynamic_code']);
|
||||
$absAmount = $estimateData['amount'];
|
||||
$calcDetails = $estimateData['details'];
|
||||
}
|
||||
// --------------------------------
|
||||
|
||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||
$name = trim($item['name']);
|
||||
$catCode = $item['category'];
|
||||
@@ -280,39 +297,23 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
}
|
||||
if (!$isPaid) {
|
||||
$reste_a_venir_calc += $absAmount;
|
||||
// Ajouté dans le tableau avec un tooltip vide
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
|
||||
// On inclut les détails de l'interception dans le tooltip natif du suivi
|
||||
$tooltip = !empty($calcDetails) ? $calcDetails : 'Charge fixe en attente';
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => $tooltip];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. C'est une estimation (Variable)
|
||||
if ((int)$item['is_estimate'] === 1) {
|
||||
$calcDetails = "";
|
||||
|
||||
// --- 🚀 INTERCEPTION DYNAMIQUE ---
|
||||
if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) {
|
||||
require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php';
|
||||
// Facturation à terme échu : on recule d'un mois
|
||||
$prevDate = date('Y-m-d', strtotime('-1 month', strtotime("$viewY-$viewM-01")));
|
||||
$calcYear = date('Y', strtotime($prevDate));
|
||||
$calcMonth = date('m', strtotime($prevDate));
|
||||
|
||||
$calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth);
|
||||
$estimateData = $calc->getEstimate($item['dynamic_code']);
|
||||
$amt = $estimateData['amount'];
|
||||
$calcDetails = $estimateData['details'];
|
||||
}
|
||||
// --------------------------------
|
||||
|
||||
$estimatesList[] = [
|
||||
'name' => $name,
|
||||
'amount' => $amt,
|
||||
'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : [],
|
||||
'details' => $calcDetails
|
||||
'calc_details' => $calcDetails // Transmission pour le second bloc
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Attribution du budget aux jauges visuelles de la page
|
||||
// Pour ne pas tout casser, on donne tout le plafond visuel à la 1ère catégorie de la liste
|
||||
if (!empty($catCode)) {
|
||||
$catCodesArray = array_map('trim', explode(',', $catCode));
|
||||
$firstCat = $catCodesArray[0];
|
||||
@@ -369,9 +370,11 @@ foreach ($estimatesList as $est) {
|
||||
$spentForEstimate = 0;
|
||||
$detailsHover = [];
|
||||
|
||||
// On additionne les dépenses de toutes les catégories liées à cette estimation
|
||||
foreach ($est['categories'] as $cCode) {
|
||||
if (!empty($cCode) && isset($totals[$cCode])) {
|
||||
$spentForEstimate += $totals[$cCode];
|
||||
// On prépare le détail pour la petite bulle d'info (hover)
|
||||
if ($totals[$cCode] > 0 && isset($categoriesConfig[$cCode])) {
|
||||
$detailsHover[] = strip_tags($categoriesConfig[$cCode]['label']) . " : " . number_format($totals[$cCode], 0) . "€";
|
||||
}
|
||||
@@ -381,12 +384,9 @@ foreach ($estimatesList as $est) {
|
||||
$rem = max(0, $est['amount'] - $spentForEstimate);
|
||||
if ($rem > 0) {
|
||||
$reste_a_venir_calc += $rem;
|
||||
$tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||
|
||||
// --- INJECTION DU DÉTAIL DYNAMIQUE ---
|
||||
if (!empty($est['details'])) {
|
||||
$tooltip .= "\n\n📊 Détail du calcul :\n" . $est['details'];
|
||||
}
|
||||
// On fusionne les détails du calcul dynamique (s'il y en a) avec les détails des dépenses réelles
|
||||
$baseTooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||
$tooltip = !empty($est['calc_details']) ? $est['calc_details'] . " | " . $baseTooltip : $baseTooltip;
|
||||
|
||||
$pending_charges[] = [
|
||||
'name' => 'Reste ' . $est['name'],
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
// offline.php
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
|
||||
$pageTitle = tr('offline_title');
|
||||
$activePage = "offline";
|
||||
require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="pf-container" style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; text-align: center; padding: 20px;">
|
||||
<div style="font-size: 5rem; margin-bottom: 20px; filter: grayscale(1); opacity: 0.7;">📡</div>
|
||||
<h1 style="color: var(--text-main); margin-bottom: 10px;"><?= tr('offline_heading') ?></h1>
|
||||
<p style="color: var(--text-muted); font-size: 1.1rem; max-width: 400px; margin-bottom: 30px;">
|
||||
<?= tr('offline_message') ?>
|
||||
</p>
|
||||
<button onclick="window.location.reload();" class="pf-btn">
|
||||
🔄 <?= tr('offline_retry_btn') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
@@ -0,0 +1,153 @@
|
||||
const CACHE_NAME = "househub-static-v3";
|
||||
const API_CACHE_NAME = "househub-api-v1";
|
||||
const ASSETS_TO_CACHE = [
|
||||
"/offline.php",
|
||||
"/global.css",
|
||||
"/dark-mode.css",
|
||||
"/favicon.png",
|
||||
];
|
||||
|
||||
// --- Gestion de la file d'attente (IndexedDB) pour les actions hors ligne ---
|
||||
function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("househub-offline-sync", 1);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains("sync-queue")) {
|
||||
db.createObjectStore("sync-queue", { autoIncrement: true });
|
||||
}
|
||||
};
|
||||
request.onsuccess = (event) => resolve(event.target.result);
|
||||
request.onerror = (event) => reject(event.target.error);
|
||||
});
|
||||
}
|
||||
|
||||
async function dequeueSyncActions() {
|
||||
try {
|
||||
const db = await openDB();
|
||||
const transaction = db.transaction("sync-queue", "readwrite");
|
||||
const store = transaction.objectStore("sync-queue");
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = async () => {
|
||||
const actions = request.result;
|
||||
for (const action of actions) {
|
||||
try {
|
||||
await fetch(action.url, {
|
||||
method: action.method,
|
||||
headers: action.headers,
|
||||
body: action.body,
|
||||
});
|
||||
|
||||
// Suppression si succès
|
||||
const delTx = db.transaction("sync-queue", "readwrite");
|
||||
delTx.objectStore("sync-queue").delete(action.id);
|
||||
} catch (err) {
|
||||
console.error("Erreur de synchronisation en arrière-plan :", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Erreur ouverture DB dans le Service Worker", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Background Sync exclusif à Chromium
|
||||
self.addEventListener("sync", (event) => {
|
||||
if (event.tag === "househub-sync") {
|
||||
event.waitUntil(dequeueSyncActions());
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
// Utilise catch pour éviter qu'un fichier manquant (ex: favicon) fasse crasher l'installation
|
||||
return cache
|
||||
.addAll(ASSETS_TO_CACHE)
|
||||
.catch((err) => console.error("Erreur de cache initial", err));
|
||||
}),
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const requestUrl = new URL(event.request.url);
|
||||
|
||||
if (
|
||||
requestUrl.pathname.includes("/api/") ||
|
||||
requestUrl.pathname.includes("api.php")
|
||||
) {
|
||||
if (event.request.method === "GET") {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
const responseClone = response.clone();
|
||||
caches
|
||||
.open(API_CACHE_NAME)
|
||||
.then((cache) => cache.put(event.request, responseClone));
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(event.request)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
const responseClone = response.clone();
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.put(event.request, responseClone));
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(event.request).then((cachedResponse) => {
|
||||
return cachedResponse || caches.match("/offline.php");
|
||||
});
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
if (response) return response;
|
||||
return fetch(event.request)
|
||||
.then((networkResponse) => {
|
||||
if (
|
||||
networkResponse &&
|
||||
networkResponse.status === 200 &&
|
||||
(networkResponse.type === "basic" ||
|
||||
networkResponse.type === "cors")
|
||||
) {
|
||||
const responseToCache = networkResponse.clone();
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.put(event.request, responseToCache));
|
||||
}
|
||||
return networkResponse;
|
||||
})
|
||||
.catch(() => null);
|
||||
}),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user