From 2087cfb9458f49c3def99ceb548288fb36c3c056 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Mon, 4 May 2026 11:34:33 +0200 Subject: [PATCH] fix bugs --- README.md | 65 +++- global.css | 5 + includes/lang/ca.php | 13 + includes/lang/fr.php | 14 + modules/budget/budget.css | 169 ++++------ modules/budget/includes/api/manage-item.php | 53 +++ modules/budget/includes/api/save-budget.php | 25 +- modules/budget/views/budget_prev.php | 112 ++++++- modules/budget/views/epargne.php | 65 ++-- modules/budget/views/suivi.php | 90 ++++-- test-engine.js | 339 ++++++++++++++++++++ tests.php | 139 ++++++++ 12 files changed, 895 insertions(+), 194 deletions(-) create mode 100644 test-engine.js create mode 100644 tests.php diff --git a/README.md b/README.md index b038f62..72949ab 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -pachafamily +# 🦙 PachaFamily - Dashboard Familial Sur-Mesure + +PachaFamily est une application web privée de gestion familiale. Développée sans framework lourd (Vanilla JS / PHP Natif), elle garantit des performances optimales, une maintenance simplifiée et une personnalisation totale. + +## 🌟 Modules & Fonctionnalités + +### 💰 1. Module Budget (`/modules/budget/`) + +Gestion asynchrone (SPA-like) des finances de la famille. + +- **Suivi Mensuel** : Saisie rapide des dépenses avec répartition automatique (Alex/Laia/Commun). +- **Budget Prévisionnel** : Configuration des salaires, charges fixes, et calcul des restes à vivre. Mode "Calculatrice" flottant intégré. +- **Épargne** : Suivi des cagnottes par personne et par catégorie (Vacances, Nens, Perso). +- **Récapitulatif** : Bilan annuel (À venir). + +### 📅 2. Module Calendrier (`/modules/family-calendar/`) + +Agenda partagé pour toute la famille. + +- Affichage des événements et vacances scolaires. +- Gestion des jours de congés avec compteurs annuels par personne. + +### 🌴 3. Module Voyages (`/modules/holidays/`) + +Planificateur de roadtrips et vacances. + +- **Cartographie** : Intégration de Leaflet.js. +- **Itinéraires** : Calcul des trajets via OSRM et géocodage via Nominatim. +- Checkpoints, notes et liaisons directes avec le budget d'épargne. + +### 🎁 4. Module Cadeaux (`/modules/gift-list/`) + +Listes de souhaits pour les événements (Noël, Anniversaires, Rois Mages...). + +--- + +## 🚀 Plan d'Optimisation & Debugging (Roadmap) + +Cette section trace les étapes pour épurer la dette technique et optimiser l'application. + +### 🟢 Phase 1 : Quick Wins (Gains rapides) + +- [x] **Refonte Asynchrone Budget** : Remplacement des formulaires classiques par des appels `fetch()` silencieux sur `suivi.php`, `budget_prev.php` et `epargne.php`. +- [ ] **Nettoyage des Vues** : Extraire le JavaScript et le CSS encore présents dans les fichiers `.php` vers leurs fichiers `.js` et `.css` dédiés. +- [ ] **Corrections des liens GET** : Remplacer les `` par des ` - - × - + ", - 'bud_prev_err_no_history': "", - 'bud_prev_confirm_copy': "", - 'bud_prev_confirm_transfers': "", - 'bud_err_tech': "" + 'bud_prev_label_name': , + 'bud_prev_err_no_history': , + 'bud_prev_confirm_copy': , + 'bud_prev_confirm_transfers': , + 'bud_err_tech': }; + const currentYear = ; const months = ; @@ -627,7 +628,7 @@ function duplicateMonth() { const sourceDateStr = months[1]; if (!sourceDateStr) { - alert(tr("bud_prev_err_no_history")); + alert(window.I18N['bud_prev_err_no_history']); return; } @@ -639,7 +640,7 @@ function duplicateMonth() { const sourceName = formatMonth(sourceDateStr); const targetName = formatMonth(targetDateStr); - const message = tr("bud_prev_confirm_copy").replace('%s', sourceName).replace('%t', targetName); + const message = window.I18N['bud_prev_confirm_copy'].replace('%s', sourceName).replace('%t', targetName); if(!confirm(message)) return; @@ -762,11 +763,11 @@ function saveData(action, data) { const formData = new FormData(); formData.append('action', action); for (const key in data) formData.append(key, data[key]); - fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData }); + fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData }); } function validateTransfers(person, month) { - const msg = tr("bud_prev_confirm_transfers").replace('%p', person).replace('%m', month); + const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', person).replace('%m', month); if (!confirm(msg)) return; const formData = new FormData(); @@ -774,7 +775,7 @@ function validateTransfers(person, month) { formData.append('person', person); formData.append('month_date', month); - fetch('/modules/budget/includes/api/save-budget.php', { + fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData }) @@ -783,7 +784,7 @@ function validateTransfers(person, month) { if(data.success) window.location.reload(); else alert("Erreur: " + data.error); }) - .catch(e => alert(tr("bud_err_tech"))); + .catch(e => alert(window.I18N['bud_err_tech'])); } function saveGenericNote(noteType, refId, content) { @@ -793,7 +794,7 @@ function saveGenericNote(noteType, refId, content) { formData.append('reference_id', refId); formData.append('content', content); - fetch('/modules/budget/includes/api/save-budget.php', { + fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData }) @@ -869,6 +870,36 @@ function updateSumResult() { document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(window.appLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total); } +async function deleteCategory(id) { + if (!confirm(window.I18N['bud_prev_confirm_del_line'] || "Confirmer la suppression ?")) return; + + const formData = new FormData(); + formData.append('action', 'delete_category'); + formData.append('id', id); + formData.append('ajax', '1'); + + try { + // 💡 CORRECTION : Chemin relatif (sans le "/" au début) pour s'adapter à ton localhost + const response = await fetch('modules/budget/includes/api/save-budget.php', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error(`Erreur réseau HTTP ${response.status}`); + + const result = await response.json(); + + if (result.success) { + window.location.reload(); + } else { + alert((window.I18N['bud_err_tech'] || 'Erreur technique') + " : Suppression impossible."); + } + } catch(e) { + console.error("Erreur Fetch Suppression:", e); + alert("Erreur de communication avec le serveur. Regarde la console (F12) pour plus de détails."); + } +} + document.addEventListener('click', function(e) { if (!isSumModeActive) return; @@ -890,4 +921,51 @@ document.addEventListener('click', function(e) { }, true); document.addEventListener('DOMContentLoaded', recalcAllAllocations); + +// --- INTERCEPTION ASYNCHRONE DES FORMULAIRES DE MODALES --- +document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => { + form.addEventListener('submit', async (e) => { + e.preventDefault(); // 🛑 Bloque le rechargement HTML par défaut + + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerText; + + try { + submitBtn.disabled = true; + submitBtn.innerText = '⏳ ...'; + + const formData = new FormData(form); + formData.append('ajax', '1'); + + // 💡 CORRECTION DU PIÈGE : On utilise getAttribute() pour forcer la lecture de l'URL ! + const actionUrl = form.getAttribute('action'); + + // On s'assure du bon chemin (chemin relatif depuis budget.php) + const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl; + + const response = await fetch(finalUrl, { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`); + + const result = await response.json(); + + if (result.success) { + form.closest('.pf-modal').style.display = 'none'; + document.body.classList.remove('no-scroll'); + window.location.reload(); + } else { + alert((window.I18N['bud_err_tech'] || 'Erreur technique') + " : " + (result.error || "Inconnue")); + } + } catch (error) { + console.error("Erreur Fetch Modale:", error); + alert("Une erreur technique est survenue lors de l'enregistrement."); + } finally { + submitBtn.disabled = false; + submitBtn.innerText = originalText; + } + }); +}); \ No newline at end of file diff --git a/modules/budget/views/epargne.php b/modules/budget/views/epargne.php index 5c7f150..55ee6a8 100644 --- a/modules/budget/views/epargne.php +++ b/modules/budget/views/epargne.php @@ -104,12 +104,12 @@ function getMonthName($dateString) { ?>
- -
-
+ @@ -248,16 +248,16 @@ function getMonthName($dateString) { window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; window.I18N = { ...(window.I18N || {}), - 'bud_sav_modal_title_add': "", - 'bud_sav_modal_title_edit': "", - 'bud_sav_ph_name': "", - 'bud_sav_confirm_delete_month': "", - 'bud_sav_prompt_duplicate': "", - 'bud_err_tech': "", - 'bud_err_server': "", - 'bud_err_network_dup': "", - 'bud_sav_saving': "", - 'bud_err_delete': "" + 'bud_sav_modal_title_add': , + 'bud_sav_modal_title_edit': , + 'bud_sav_ph_name': , + 'bud_sav_confirm_delete_month': , + 'bud_sav_prompt_duplicate': , + 'bud_err_tech': , + 'bud_err_server': , + 'bud_err_network_dup': , + 'bud_sav_saving': , + 'bud_err_delete': }; // --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT --- @@ -272,10 +272,10 @@ function updateEpargneCell(month, category, owner, inputEl) { formData.append('owner', owner); formData.append('amount', val); - fetch('/modules/budget/includes/api/save-savings.php', { + fetch('modules/budget/includes/api/save-savings.php', { method: 'POST', body: formData - }).catch(err => alert(tr("bud_err_tech"))); + }).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique')); const totalInput = document.querySelector(`.total-input-${owner}-${month}`); const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0; @@ -304,7 +304,7 @@ function addCustomEpargneLine(catName = '', amount = '') { const html = `
- +
@@ -348,7 +348,7 @@ function editCustomSavingsMonth(monthDate, owner, rowData) { const dateObj = new Date(monthDate); const monthName = dateObj.toLocaleDateString(window.appLang, { month: 'long', year: 'numeric' }); - document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_edit") + " " + monthName + " (" + owner + ")"; + document.getElementById('savingsModalTitle').innerText = (window.I18N['bud_sav_modal_title_edit'] || 'Editer') + " " + monthName + " (" + owner + ")"; document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || ''; @@ -370,7 +370,7 @@ function openCustomSavingsModal(owner) { document.getElementById('sav_month').value = ''; document.getElementById('sav_total').value = ''; - document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_add") + " (" + owner + ")"; + document.getElementById('savingsModalTitle').innerText = (window.I18N['bud_sav_modal_title_add'] || 'Ajouter') + " (" + owner + ")"; const container = document.getElementById('linesContainer'); container.innerHTML = ''; @@ -400,17 +400,20 @@ if (savingsForm) { const submitBtn = this.querySelector('button[type="submit"]'); const originalText = submitBtn.innerText; - submitBtn.innerText = tr('bud_sav_saving'); + submitBtn.innerText = window.I18N['bud_sav_saving'] || 'Sauvegarde...'; submitBtn.disabled = true; const formData = new FormData(this); - fetch(this.action, { method: 'POST', body: formData }) + const actionUrl = this.getAttribute('action'); + const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl; + + fetch(finalUrl, { method: 'POST', body: formData }) .then(response => response.text()) .then(text => { window.location.reload(); }) .catch(error => { console.error("Erreur:", error); - alert(tr("bud_err_tech")); + alert(window.I18N['bud_err_tech'] || 'Erreur Technique'); submitBtn.innerText = originalText; submitBtn.disabled = false; }); @@ -418,16 +421,18 @@ if (savingsForm) { } function deleteEntireMonth(monthDate, owner) { - const msg = tr('bud_sav_confirm_delete_month').replace('%m', monthDate).replace('%o', owner); + const rawMsg = window.I18N['bud_sav_confirm_delete_month'] || "Supprimer %m pour %o ?"; + const msg = rawMsg.replace('%m', monthDate).replace('%o', owner); if (!confirm(msg)) return; const formData = new FormData(); formData.append("action", "delete_month_global"); formData.append("month_date", monthDate); formData.append("owner", owner); - fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData }) + + fetch("modules/budget/includes/api/save-savings.php", { method: "POST", body: formData }) .then(() => window.location.reload()) - .catch(err => alert(tr('bud_err_delete'))); + .catch(err => alert(window.I18N['bud_err_delete'] || 'Erreur lors de la suppression')); } function duplicateLastMonth(lastMonthDate, owner) { @@ -450,10 +455,8 @@ function duplicateLastMonth(lastMonthDate, owner) { defaultTotal = cycleConfigs[nextMonthStr].start_balance; } - const message = tr('bud_sav_prompt_duplicate') - .replace('%s', sourceName) - .replace('%t1', targetName) - .replace('%t2', targetName); + const rawMsg = window.I18N['bud_sav_prompt_duplicate'] || "Dupliquer %s vers %t1 ?"; + const message = rawMsg.replace('%s', sourceName).replace('%t1', targetName).replace('%t2', targetName); let newTotal = prompt(message, defaultTotal); @@ -465,20 +468,20 @@ function duplicateLastMonth(lastMonthDate, owner) { formData.append("new_total", newTotal); formData.append("owner", owner); - fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData }) + fetch("modules/budget/includes/api/save-savings.php", { method: "POST", body: formData }) .then(async r => { const text = await r.text(); try { const d = JSON.parse(text); if (d.success) window.location.reload(); - else alert(tr('bud_err_server') + (d.error || "Inconnue")); + else alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (d.error || "Inconnue")); } catch(e) { window.location.reload(); } }) .catch(err => { console.error(err); - alert(tr("bud_err_network_dup")); + alert(window.I18N['bud_err_network_dup'] || 'Erreur réseau.'); }); } } diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index 3db75c1..e1fe792 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -57,36 +57,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') { exit; } -if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') { - $id = !empty($_POST['expense_id']) ? (int)$_POST['expense_id'] : null; - $cat = $_POST['category']; - $amount = floatval($_POST['amount']); - $date = $_POST['date']; - $gestionMonth = $_POST['gestion_month']; - $label = trim($_POST['label']); - $budgetItemId = null; - $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; - if ($cat === 'School' && !empty($_POST['label_select'])) $label = trim($_POST['label_select']); - elseif (($cat === 'Frais' || $cat === 'Income') && !empty($_POST['budget_item_id'])) $budgetItemId = (int)$_POST['budget_item_id']; - - if ($label && $amount > 0) { - $is_credit = isset($_POST['is_credit']) ? (int)$_POST['is_credit'] : 0; - $finalAmount = $is_credit ? abs($amount) : -abs($amount); - - if ($id) { - $pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?") - ->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $id]); - } else { - $uniqueRef = "MANUAL_" . uniqid(); - $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") - ->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId]); - } - - echo ""; - exit; - } -} if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $count = 0; @@ -766,4 +737,65 @@ function checkValidation() { } if(document.getElementById('formMapping')) { document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); checkValidation(); } + +// Ferme la modale si on clique sur le background assombri (.pf-modal) +window.addEventListener('click', (e) => { + if (e.target.classList.contains('pf-modal')) { + e.target.style.display = 'none'; + document.body.classList.remove('no-scroll'); + } +}); + +document.addEventListener('DOMContentLoaded', () => { + const formExpense = document.querySelector('#manualExpenseModal form'); + + if (formExpense) { + formExpense.addEventListener('submit', async (e) => { + e.preventDefault(); // 🛑 On bloque le rechargement brutal de la page + + const submitBtn = formExpense.querySelector('button[type="submit"]'); + const originalBtnText = submitBtn.innerText; + + try { + // 🛡️ Sécurité UI : on désactive le bouton pour éviter les doubles clics (et les doublons en BDD) + submitBtn.disabled = true; + submitBtn.innerText = '⏳ ...'; + + const formData = new FormData(formExpense); + // On s'assure que l'action est bien définie pour notre API + formData.set('action', 'save_expense_manual'); + + const response = await fetch('modules/budget/includes/api/manage-item.php', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`); + + const result = await response.json(); + + if (result.success) { + // ✅ Succès : Fermeture propre de la modale + closeSuiviModal('manualExpenseModal'); + formExpense.reset(); + + // Pour l'instant, on fait un rechargement propre pour voir les calculs à jour. + // Dans une V2 ultra-opti, on mettra à jour le DOM ligne par ligne ici ! + window.location.reload(); + } else { + // ❌ Erreur renvoyée par le PHP + const errorMsg = result.error || 'Erreur inconnue'; + alert(window.I18N ? window.I18N.tr('error_occured') + ' : ' + errorMsg : errorMsg); + } + } catch (error) { + console.error("Erreur lors de l'enregistrement :", error); + alert("Une erreur critique est survenue côté réseau ou serveur."); + } finally { + // 🔄 On restaure le bouton dans tous les cas (succès ou échec) + submitBtn.disabled = false; + submitBtn.innerText = originalBtnText; + } + }); + } + }); \ No newline at end of file diff --git a/test-engine.js b/test-engine.js new file mode 100644 index 0000000..4978237 --- /dev/null +++ b/test-engine.js @@ -0,0 +1,339 @@ +/** + * PachaFamily - Test Engine 🦙 + * Moteur de tests automatisés (E2E) - Version Anti-Flakiness + */ + +const PachaTestEngine = { + arena: document.getElementById("test-arena"), + reportBox: document.getElementById("test-report"), + doc: null, + + // ========================================== + // 🛠️ HELPERS + // ========================================== + + wait: (ms) => new Promise((r) => setTimeout(r, ms)), + + log: function (msg, icon = "ℹ️") { + this.reportBox.value += `[${new Date().toLocaleTimeString()}] ${icon} ${msg}\n`; + this.reportBox.scrollTop = this.reportBox.scrollHeight; + }, + + assert: function (cond, msgPass, msgFail) { + this.log(cond ? msgPass : msgFail || msgPass, cond ? "✅" : "❌"); + }, + + // 🛡️ NOUVEAU : Attend le rechargement de la page après une action + actionAndWaitForReload: async function (actionFn, timeout = 5000) { + return new Promise(async (resolve) => { + let reloaded = false; + // On écoute le prochain rechargement de l'iframe + this.arena.onload = () => { + reloaded = true; + this.doc = this.arena.contentWindow.document; + this.arena.contentWindow.confirm = () => true; + this.arena.contentWindow.alert = () => true; + resolve(); + }; + + await actionFn(); // On lance le clic ou la soumission + + // Sécurité anti-blocage (si le fetch échoue et ne recharge pas la page) + setTimeout(() => { + if (!reloaded) { + this.log( + "⚠️ Le rechargement de la page n'a pas eu lieu dans le temps imparti.", + "⏱️", + ); + resolve(); + } + }, timeout); + }); + }, + + load: async function (url) { + this.log(`Chargement de la page : ${url}`, "🔄"); + return new Promise((resolve) => { + this.arena.onload = () => { + this.doc = this.arena.contentWindow.document; + this.arena.contentWindow.confirm = () => true; + this.arena.contentWindow.alert = () => true; + resolve(); + }; + this.arena.src = url; + }); + }, + + get: function (sel) { + return this.doc.querySelector(sel); + }, + + // 🖱️ Clic unique et infaillible + click: async function (sel, delay = 500) { + const el = typeof sel === "string" ? this.get(sel) : sel; + if (el) { + // 1. Coupe les popups bloquantes + this.arena.contentWindow.confirm = () => true; + this.arena.contentWindow.alert = () => true; + + // 2. Un seul et unique clic ! + el.click(); + + await this.wait(delay); + return true; + } + return false; + }, + + fill: function (sel, val) { + const el = this.get(sel); + if (el) el.value = val; + }, + + select: async function (sel, val, delay = 300) { + const el = this.get(sel); + if (el) { + el.value = val; + el.dispatchEvent(new Event("change")); + await this.wait(delay); + } + }, + + findInTable: function (text) { + return Array.from(this.doc.querySelectorAll("td, div")).find((el) => + el.textContent.includes(text), + ); + }, + + // ========================================== + // 🧪 SCÉNARIO 1 : SUIVI MENSUEL + // ========================================== + + runBudgetTests: async function () { + this.reportBox.value = ""; + this.log("=== 🚀 DÉBUT DU SCÉNARIO : SUIVI BUDGET ===", "INFO"); + + await this.load("budget.php?tab=suivi"); + await this.wait(1000); + + if (!this.get(".btn-add-item")) { + this.log("Mois clôturé, test annulé. Déverrouille-le !", "⚠️"); + return; + } + + await this.click("button[onclick=\"toggleDiv('pendingDetailsList')\"]"); + this.assert( + this.get("#pendingDetailsList").style.display !== "none", + "Déploiement : Charges à venir", + ); + + await this.click(".btn-add-item"); + await this.select("#modalCatSelect", "Autres"); + this.fill("#modalAmount", "42.42"); + this.fill("#modalLabelInput", "TEST_AUTO_PACHA"); + + this.log("Enregistrement de la dépense...", "WARN"); + await this.actionAndWaitForReload(async () => { + await this.click('#manualExpenseModal button[type="submit"]'); + }); + + let targetTd = Array.from(this.doc.querySelectorAll("td")).find((el) => + el.textContent.includes("TEST_AUTO_PACHA"), + ); + this.assert( + targetTd !== undefined, + "Dépense créée avec succès (42.42€).", + "Échec : Dépense introuvable.", + ); + + if (targetTd) { + this.log("Nettoyage de la base de données...", "WARN"); + await this.actionAndWaitForReload(async () => { + await this.click( + targetTd.closest("tr").querySelector('a[href*="delete_expense"]'), + ); + }); + const checkGone = Array.from(this.doc.querySelectorAll("td")).find((el) => + el.textContent.includes("TEST_AUTO_PACHA"), + ); + this.assert( + checkGone === undefined, + "Base de données nettoyée avec succès.", + ); + } + + this.log("=== 🏁 FIN DU SCÉNARIO ===", "INFO"); + }, + + // ========================================== + // 🧪 SCÉNARIO 2 : BUDGET PRÉVISIONNEL + // ========================================== + + testBudgetPrev: async function () { + this.reportBox.value = ""; + this.log("=== 🚀 DÉBUT DU SCÉNARIO : BUDGET PRÉVISIONNEL ===", "INFO"); + + await this.load("budget.php?tab=budget_prev"); + await this.wait(1000); + + this.log("🔍 Étape 1 : Test du mode Somme flottant...", "INFO"); + await this.click("#fabSumMode"); + this.assert( + this.doc.body.classList.contains("sum-mode-active"), + "Le mode somme s'active.", + "Le bouton somme ne répond pas.", + ); + + const firstSalaryInput = this.get('input[data-field="salary"]'); + if (firstSalaryInput) { + await this.click(firstSalaryInput, 500); + this.assert( + firstSalaryInput.classList.contains("sum-selected"), + "La cellule est bien sélectionnée.", + ); + const sumValue = this.get("#sumResultValue").innerText; + this.assert( + sumValue !== "0,00 €" && sumValue !== "0 €", + `Le total interactif affiche : ${sumValue}`, + "Le calcul de la somme a échoué.", + ); + } + + await this.click(".pf-sum-close"); + this.assert( + !this.doc.body.classList.contains("sum-mode-active"), + "Le mode somme se désactive.", + ); + + this.log("🔍 Étape 2 : Test de sauvegarde de la note...", "INFO"); + const noteArea = this.get("#monthNoteArea"); + if (noteArea) { + const oldNote = noteArea.value; + this.fill("#monthNoteArea", "🤖 Test Auto"); + await this.click('button[onclick^="saveGenericNote"]', 1500); + + const indicator = this.get("#note-save-indicator"); + this.assert( + indicator !== null, + "Sauvegarde asynchrone exécutée.", + "L'indicateur de sauvegarde ne s'est pas affiché.", + ); + + this.fill("#monthNoteArea", oldNote); + await this.click('button[onclick^="saveGenericNote"]', 500); + } + + const uniqueCatName = "TEST_CAT_" + Math.floor(Math.random() * 10000); + this.log( + `🔍 Étape 3 : Création de la catégorie '${uniqueCatName}'...`, + "INFO", + ); + + await this.click('button[onclick*="addCatModal"]'); + this.assert( + this.get("#addCatModal").style.display === "flex", + "Ouverture de la modale d'ajout.", + ); + + this.fill('#addCatModal input[name="name"]', uniqueCatName); + await this.select('#addCatModal select[name="target"]', "vers commune"); + + this.log("Attente du rechargement après création...", "WARN"); + // 🔄 UTILISATION DU NOUVEAU HELPER SYNCHRONE + await this.actionAndWaitForReload(async () => { + await this.click('#addCatModal button[type="submit"]'); + }); + + const newCatCell = Array.from( + this.doc.querySelectorAll(".prev-alloc-table td, .prev-alloc-table div"), + ).find((el) => el.textContent.includes(uniqueCatName)); + this.assert( + newCatCell !== undefined, + `SUCCÈS : '${uniqueCatName}' insérée au tableau !`, + "ÉCHEC : Ligne introuvable après rechargement.", + ); + + if (newCatCell) { + this.log("🧹 Étape 4 : Nettoyage de la BDD...", "WARN"); + const row = newCatCell.closest("tr"); + const delBtn = row.querySelector(".delete"); + + if (delBtn) { + // 🔄 UTILISATION DU NOUVEAU HELPER SYNCHRONE + await this.actionAndWaitForReload(async () => { + await this.click(delBtn); + }); + + const checkGone = Array.from( + this.doc.querySelectorAll(".prev-alloc-table td"), + ).find((td) => td.textContent.includes(uniqueCatName)); + this.assert( + checkGone === undefined, + "NETTOYAGE PARFAIT : Ligne effacée.", + "La ligne est restée dans le tableau.", + ); + } else { + this.log("Bouton de suppression (.delete) introuvable.", "FAIL"); + } + } + + this.log("=== 🏁 FIN DU SCÉNARIO ===", "INFO"); + }, +}; + +// ============================================================================ +// ROUTEUR DU LABORATOIRE ET BOUTON COPIER +// ============================================================================ + +document.getElementById("btn-run-test")?.addEventListener("click", async () => { + const selectedTest = document.getElementById("test-selector").value; + const btnRun = document.getElementById("btn-run-test"); + + btnRun.disabled = true; + btnRun.style.opacity = "0.5"; + + try { + switch (selectedTest) { + case "budget_suivi": + await PachaTestEngine.runBudgetTests(); + break; + case "budget_prev": + await PachaTestEngine.testBudgetPrev(); + break; + default: + PachaTestEngine.log(`Scénario non implémenté : ${selectedTest}`, "⚠️"); + } + } catch (error) { + PachaTestEngine.log(`Erreur JS critique : ${error.message}`, "❌"); + console.error(error); + } + + btnRun.disabled = false; + btnRun.style.opacity = "1"; +}); + +// 📋 Bouton "Copier" +document + .getElementById("btn-copy-report") + ?.addEventListener("click", async () => { + const textArea = document.getElementById("test-report"); + const reportText = textArea.value; + const successMsg = window.I18N + ? window.I18N.tests_report_copied || "Rapport copié !" + : "Rapport copié !"; + + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(reportText); + alert(successMsg); + } else { + textArea.select(); + document.execCommand("copy"); + window.getSelection().removeAllRanges(); + alert(successMsg); + } + } catch (err) { + alert("Erreur lors de la copie du rapport."); + console.error(err); + } + }); diff --git a/tests.php b/tests.php new file mode 100644 index 0000000..5db96e3 --- /dev/null +++ b/tests.php @@ -0,0 +1,139 @@ + + + + +
+ +
+
+

🧪

+
+ + + + + +
+
+ +
+ +
+
+ Live Preview +
+ +
+ +
+ + + + \ No newline at end of file