This commit is contained in:
2026-05-05 17:13:12 +02:00
parent 4763e6a947
commit 9c5620b3e3
14 changed files with 2250 additions and 4419 deletions
+29 -18
View File
@@ -32,13 +32,13 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<div id="modalHolidays" class="fc-modal-overlay" style="display: none;"> <div id="modalHolidays" class="pf-modal">
<div class="fc-modal-content"> <div class="pf-modal-content">
<div class="fc-modal-header"> <div class="pf-modal-header">
<h2><?= tr('fc_modal_holidays_title') ?></h2> <h3 class="pf-modal-title"><?= tr('fc_modal_holidays_title') ?></h3>
<button id="btnCloseHolidays" class="fc-modal-close">×</button> <button id="btnCloseHolidays" class="pf-modal-close" onclick="document.getElementById('modalHolidays').classList.remove('is-open'); document.body.classList.remove('no-scroll');">&times;</button>
</div> </div>
<div class="fc-modal-body"> <div class="pf-modal-body" style="padding:0;">
<div style="width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;"> <div style="width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;">
<table id="schoolHolidaysTable" class="fc-holidays-table" style="min-width: 400px;"> <table id="schoolHolidaysTable" class="fc-holidays-table" style="min-width: 400px;">
<thead> <thead>
@@ -55,22 +55,23 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<div id="modalSnapshot" class="fc-modal-overlay" style="display: none;"> <div id="modalSnapshot" class="pf-modal">
<div class="fc-modal-content" style="max-width: 400px;"> <div class="pf-modal-content">
<div class="fc-modal-header"> <div class="pf-modal-header">
<h2><?= tr('fc_modal_snap_title') ?></h2> <h3 class="pf-modal-title">📸 <?= tr('fc_modal_snap_title') ?></h3>
<button id="btnCloseSnapshot" class="fc-modal-close">×</button> <button type="button" id="btnCloseSnapshot" class="pf-modal-close" onclick="document.getElementById('modalSnapshot').classList.remove('is-open'); document.body.classList.remove('no-scroll');">&times;</button>
</div> </div>
<div class="fc-modal-body" style="padding: 24px;"> <div class="pf-modal-body">
<form id="formSnapshot"> <form id="formSnapshot">
<div style="margin-bottom: 16px;"> <div class="pf-form-row">
<div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_person') ?></label> <label class="pf-label"><?= tr('fc_label_person') ?></label>
<select id="snapPerson" class="pf-input" required> <select id="snapPerson" class="pf-input" required>
<option value="2">Alex</option> <option value="2">Alex</option>
<option value="3">Laia</option> <option value="3">Laia</option>
</select> </select>
</div> </div>
<div style="margin-bottom: 16px;"> <div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label> <label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
<select id="snapType" class="pf-input" required> <select id="snapType" class="pf-input" required>
<option value="CP">CP</option> <option value="CP">CP</option>
@@ -78,17 +79,27 @@ require __DIR__ . '/header.php';
<option value="JA">JA</option> <option value="JA">JA</option>
</select> </select>
</div> </div>
<div style="margin-bottom: 16px;"> </div>
<div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_apply_date') ?></label> <label class="pf-label"><?= tr('fc_label_apply_date') ?></label>
<input type="date" id="snapDate" class="pf-input" required title="<?= tr('fc_snap_date_hint') ?>"> <input type="date" id="snapDate" class="pf-input" required title="<?= tr('fc_snap_date_hint') ?>">
</div> </div>
<div style="margin-bottom: 24px;"> <div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label> <label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" required> <div style="display:flex; align-items:center;">
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--pf-primary);" required>
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
<?= tr('fc_unit_days') ?>
</span>
</div>
</div> </div>
<button type="submit" class="pf-btn" style="width: 100%;"><?= tr('fc_btn_save_snap') ?></button>
</form> </form>
</div> </div>
<div class="pf-modal-footer">
<button type="button" class="pf-btn pf-btn-secondary" onclick="document.getElementById('modalSnapshot').classList.remove('is-open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
<button type="submit" form="formSnapshot" class="pf-btn pf-btn-primary"><?= tr('fc_btn_save_snap') ?></button>
</div>
</div>
</div> </div>
</div> </div>
+4 -4
View File
@@ -230,7 +230,7 @@ require __DIR__ . '/header.php';
<div class="cl-gift-right"> <div class="cl-gift-right">
<span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span> <span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span>
<span class="cl-gift-actions"> <span class="cl-gift-actions">
<button type="button" class="cl-gift-action-btn cl-gift-edit" aria-label="<?= tr('edit') ?>" <button type="button" class="btn-icon-action edit cl-gift-edit" aria-label="<?= tr('edit') ?>"
data-id="<?= $giftId ?>" data-id="<?= $giftId ?>"
data-year="<?= $year ?>" data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>" data-child="<?= htmlspecialchars($childName) ?>"
@@ -240,10 +240,10 @@ require __DIR__ . '/header.php';
data-desc="<?= htmlspecialchars($gift['gift_description']) ?>" data-desc="<?= htmlspecialchars($gift['gift_description']) ?>"
data-amount="<?= htmlspecialchars($gift['amount']) ?>" data-amount="<?= htmlspecialchars($gift['amount']) ?>"
data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>"> data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>">
✏️
</button> </button>
<button type="button" class="cl-gift-action-btn cl-gift-delete" aria-label="<?= tr('delete') ?>" data-id="<?= $giftId ?>"> <button type="button" class="btn-icon-action delete cl-gift-delete" aria-label="<?= tr('delete') ?>" data-id="<?= $giftId ?>">
× 🗑️
</button> </button>
</span> </span>
</div> </div>
+206 -160
View File
@@ -1,16 +1,25 @@
/* global.css */ /* global.css */
:root { :root {
/* Couleurs de base */
--primary: #3b82f6; --primary: #3b82f6;
--primary-dark: #1d4ed8; --primary-dark: #1d4ed8;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
/* Textes & Fonds */
--text-main: #0f172a; --text-main: #0f172a;
--text-muted: #64748b; --text-muted: #64748b;
--bg-page: #f8fafc; --bg-page: #f8fafc;
--bg-panel: #ffffff; --bg-panel: #ffffff;
/* Bordures, Arrondis & Ombres */
--border-light: #e2e8f0; --border-light: #e2e8f0;
--danger: #ef4444;
--radius: 12px; --radius: 12px;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg:
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
} }
/* 🛡️ RESET MOBILE UNIVERSEL */ /* 🛡️ RESET MOBILE UNIVERSEL */
@@ -19,13 +28,12 @@
*::after { *::after {
box-sizing: border-box; box-sizing: border-box;
} }
html, html,
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
max-width: 100vw; max-width: 100vw;
overflow-x: hidden; /* Empêche le site entier de glisser de gauche à droite */ overflow-x: hidden;
} }
body { body {
@@ -43,15 +51,18 @@ body {
flex-direction: column; flex-direction: column;
} }
/* Images et médias responsive par défaut */
img, img,
svg, svg,
video { video {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
body.no-scroll {
overflow: hidden !important;
height: 100vh !important;
}
/* === HEADER === */ /* === HEADER & NAVIGATION === */
.pf-header { .pf-header {
background: white; background: white;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
@@ -64,7 +75,6 @@ video {
top: 0; top: 0;
z-index: 100; z-index: 100;
} }
.pf-logo { .pf-logo {
font-weight: 800; font-weight: 800;
font-size: 1.25rem; font-size: 1.25rem;
@@ -72,13 +82,10 @@ video {
text-decoration: none; text-decoration: none;
letter-spacing: -0.03em; letter-spacing: -0.03em;
} }
/* Navigation Desktop */
.pf-nav { .pf-nav {
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.pf-nav-link { .pf-nav-link {
text-decoration: none; text-decoration: none;
color: var(--text-muted); color: var(--text-muted);
@@ -88,24 +95,19 @@ video {
border-radius: 8px; border-radius: 8px;
transition: all 0.2s; transition: all 0.2s;
} }
.pf-nav-link:hover { .pf-nav-link:hover {
background: #f1f5f9; background: #f1f5f9;
color: var(--text-main); color: var(--text-main);
} }
.pf-nav-link--active { .pf-nav-link--active {
background: #eff6ff; background: #eff6ff;
color: var(--primary); color: var(--primary);
} }
/* Actions Header (User) */
.pf-header-actions { .pf-header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
} }
.pf-user-badge { .pf-user-badge {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-muted); color: var(--text-muted);
@@ -113,7 +115,6 @@ video {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.pf-logout-btn { .pf-logout-btn {
color: var(--danger); color: var(--danger);
font-weight: 600; font-weight: 600;
@@ -127,8 +128,6 @@ video {
background: #fef2f2; background: #fef2f2;
border-color: #fecaca; border-color: #fecaca;
} }
/* Menu Burger (Mobile) */
.pf-burger-btn { .pf-burger-btn {
display: none; display: none;
background: none; background: none;
@@ -137,6 +136,7 @@ video {
cursor: pointer; cursor: pointer;
color: var(--text-main); color: var(--text-main);
} }
.pf-mobile-menu { .pf-mobile-menu {
display: none; display: none;
position: fixed; position: fixed;
@@ -156,7 +156,7 @@ video {
display: flex; display: flex;
} }
/* === MAIN CONTENT === */ /* === MAIN CONTENT & TYPOGRAPHY === */
.pf-main { .pf-main {
flex: 1; flex: 1;
width: 100%; width: 100%;
@@ -164,14 +164,11 @@ video {
margin: 0 auto; margin: 0 auto;
padding: 32px 20px; padding: 32px 20px;
} }
.pf-container { .pf-container {
width: 100%; width: 100%;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
/* === TYPOGRAPHY === */
h1 { h1 {
font-size: 2rem; font-size: 2rem;
font-weight: 800; font-weight: 800;
@@ -185,14 +182,13 @@ p {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
/* === CARDS (Modules) === */ /* === COMPOSANTS GLOBAUX === */
.pf-modules-grid { .pf-modules-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px; gap: 24px;
margin-top: 32px; margin-top: 32px;
} }
.pf-module-card { .pf-module-card {
background: white; background: white;
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
@@ -206,13 +202,11 @@ p {
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
} }
.pf-module-card:hover { .pf-module-card:hover {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: var(--shadow); box-shadow: var(--shadow);
border-color: #cbd5e1; border-color: #cbd5e1;
} }
.pf-card-icon { .pf-card-icon {
font-size: 2rem; font-size: 2rem;
margin-bottom: 4px; margin-bottom: 4px;
@@ -236,54 +230,13 @@ p {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
} }
.pf-card-cta::after {
content: "→";
transition: transform 0.2s;
}
.pf-module-card:hover .pf-card-cta::after {
transform: translateX(4px);
}
.pf-card--disabled {
opacity: 0.6;
pointer-events: none;
filter: grayscale(100%);
}
/* === FORMS (Login) === */
.pf-login-container {
max-width: 400px;
margin: 60px auto;
background: white;
padding: 40px;
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--border-light);
}
.pf-form-group {
margin-bottom: 20px;
}
.pf-form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
font-size: 0.9rem;
}
.pf-form-group input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-light);
border-radius: 8px;
font-size: 1rem;
}
.pf-form-group input:focus {
outline: 2px solid var(--primary);
border-color: var(--primary);
}
/* Boutons Standards */
.pf-btn { .pf-btn {
width: 100%; display: inline-flex;
padding: 12px; align-items: center;
justify-content: center;
gap: 8px;
background: var(--primary); background: var(--primary);
color: white; color: white;
border: none; border: none;
@@ -292,27 +245,155 @@ p {
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
padding: 12px 24px;
text-decoration: none;
} }
.pf-btn:hover { .pf-btn:hover {
background: var(--primary-dark); background: var(--primary-dark);
} }
.pf-btn.btn-secondary {
background: white;
color: var(--text-muted);
border: 1px solid var(--border-light);
}
.pf-btn.btn-secondary:hover {
background: #f8fafc;
color: var(--text-main);
border-color: #94a3b8;
}
.pf-error { /* Formulaires & Inputs Standards */
background: #fef2f2; .pf-form-group {
color: #b91c1c;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
}
.pf-label {
display: block;
font-weight: 600;
margin-bottom: 8px;
font-size: 0.9rem; font-size: 0.9rem;
border: 1px solid #fecaca; color: var(--text-muted);
}
.pf-input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-light);
border-radius: 8px;
font-size: 1rem;
background: #f8fafc;
color: var(--text-main);
transition: all 0.2s;
font-family: inherit;
box-sizing: border-box;
}
.pf-input:focus {
background: white;
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
} }
body.no-scroll { /* Tableaux Standards */
overflow: hidden !important; .pf-table {
height: 100vh !important; width: 100%;
background: var(--bg-panel);
border-radius: var(--radius);
box-shadow: var(--shadow);
border-collapse: separate;
border-spacing: 0;
overflow: hidden;
}
.pf-table th {
background: #f8fafc;
padding: 16px;
text-align: left;
font-weight: 600;
color: var(--text-muted);
font-size: 0.8rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border-light);
white-space: nowrap;
}
.pf-table td {
padding: 10px 16px;
border-bottom: 1px solid var(--bg-page);
vertical-align: middle;
font-size: 0.95rem;
}
.pf-table tbody tr:last-child td {
border-bottom: none;
}
.pf-table tbody tr:hover {
background-color: #f8fafc;
} }
/* === FOOTER === */ /* === 🪟 LE DESIGN SYSTEM DES MODALES (Applicable partout) === */
.pf-modal {
display: none;
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
padding: 20px;
}
.pf-modal.is-active,
.pf-modal.open {
display: flex;
} /* Supporte tes deux syntaxes JS */
.pf-modal-content {
background: var(--bg-panel);
width: 100%;
max-width: 500px;
border-radius: 20px;
box-shadow: var(--shadow-lg);
padding: 32px;
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
animation: pfModalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
margin: auto;
}
.pf-modal-title {
margin-top: 0;
font-size: 1.5rem;
font-weight: 800;
color: var(--text-main);
border-bottom: 1px solid var(--border-light);
padding-bottom: 15px;
margin-bottom: 8px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border-light);
width: 100%;
box-sizing: border-box;
}
.modal-footer .pf-btn {
width: auto;
min-width: 100px;
padding: 10px 20px;
} /* Ajustement pour modale */
@keyframes pfModalPop {
from {
transform: scale(0.95) translateY(10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* === FOOTER & TOASTS === */
.pf-footer { .pf-footer {
border-top: 1px solid var(--border-light); border-top: 1px solid var(--border-light);
background: white; background: white;
@@ -322,25 +403,6 @@ body.no-scroll {
color: var(--text-muted); color: var(--text-muted);
margin-top: auto; margin-top: auto;
} }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border-light);
width: 100%; /* Empêche le débordement */
box-sizing: border-box;
}
/* On s'assure que les boutons ne sont pas géants par défaut */
.modal-footer .pf-btn {
width: auto;
min-width: 100px;
}
/* Notifications Toasts */
.pf-toast { .pf-toast {
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
@@ -359,24 +421,13 @@ body.no-scroll {
.pf-toast.is-visible { .pf-toast.is-visible {
transform: translateX(0); transform: translateX(0);
} }
.pf-toast--success {
border-left-color: #10b981;
}
.pf-toast--error {
border-left-color: #ef4444;
}
.pf-toast--warning {
border-left-color: #f59e0b;
}
/* === MOBILE RESPONSIVE === */ /* === RESPONSIVE MOBILE === */
@media (max-width: 768px) { @media (max-width: 768px) {
.pf-header { .pf-header {
padding: 0 16px; padding: 0 16px;
} }
.pf-nav { .pf-nav,
display: none;
}
.pf-header-actions { .pf-header-actions {
display: none; display: none;
} }
@@ -385,18 +436,7 @@ body.no-scroll {
} }
.pf-main { .pf-main {
padding: 16px 10px; padding: 16px 10px;
} /* Moins de padding latéral sur mobile */
.pf-modules-grid {
grid-template-columns: 1fr;
gap: 16px;
} }
.pf-login-container {
margin: 30px 16px;
padding: 24px;
}
.pf-mobile-nav-link { .pf-mobile-nav-link {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
@@ -405,44 +445,20 @@ body.no-scroll {
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
} }
.pf-mobile-logout {
color: var(--danger);
}
/* ✅ Transformation de toutes les modales en "Bottom Sheets" */
.pf-modal {
align-items: flex-end; /* Aligne la modale en bas */
padding: 0;
z-index: 1000 !important;
background: rgba(15, 23, 42, 0.75);
}
/* 📱 BOTTOM SHEET MODALES MOBILE */
.pf-modal {
align-items: center !important; /* On remet au centre */
padding: 20px;
}
.pf-modal-content { .pf-modal-content {
width: 100% !important; width: 100% !important;
max-width: none !important; max-width: none !important;
border-radius: 24px 24px 0 0 !important; /* Arrondi seulement en haut */ border-radius: 20px !important; /* On garde les arrondis partout */
padding: 24px 20px !important; transform: none !important;
padding-bottom: env( animation: pfModalPop 0.3s ease !important; /* On remet l'animation desktop */
safe-area-inset-bottom, margin: auto !important;
30px
) !important; /* Gère l'encoche iPhone */
transform: translateY(100%);
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
max-height: 92vh; /* Laisse un petit espace en haut */
z-index: 1001;
overflow: hidden;
} }
.prev-section-header,
.view-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap; /* Important pour le mobile */
gap: 15px;
margin-bottom: 20px;
width: 100%;
}
/* Barre de drag visuelle en haut de la modale */
.pf-modal-content::before { .pf-modal-content::before {
content: ""; content: "";
display: block; display: block;
@@ -452,12 +468,12 @@ body.no-scroll {
border-radius: 10px; border-radius: 10px;
margin: -10px auto 20px auto; margin: -10px auto 20px auto;
} }
.modal-footer { .modal-footer {
flex-direction: column-reverse; /* Empilement propre sur mobile */ flex-direction: column-reverse;
} }
.modal-footer .pf-btn { .modal-footer .pf-btn {
width: 100%; /* Largeur totale uniquement sur mobile */ width: 100%;
margin-top: 8px;
} }
@keyframes slideUpSheet { @keyframes slideUpSheet {
@@ -466,3 +482,33 @@ body.no-scroll {
} }
} }
} }
/* === BOUTONS D'ACTION ICÔNES UNIVERSELS (Édition / Suppression) === */
.btn-icon-action {
border: none;
background: transparent;
cursor: pointer;
font-size: 1.1rem;
line-height: 1;
padding: 6px;
border-radius: 6px;
transition: all 0.2s;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.btn-icon-action.edit {
color: var(--primary);
}
.btn-icon-action.edit:hover {
background: #eff6ff; /* Fond bleu très clair au survol */
}
.btn-icon-action.delete {
color: var(--danger);
}
.btn-icon-action.delete:hover {
background: #fef2f2; /* Fond rouge très clair au survol */
}
+237 -573
View File
File diff suppressed because it is too large Load Diff
@@ -73,6 +73,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit; exit;
} }
// --- ACTION : SUPPRIMER UNE DÉPENSE RÉELLE (Depuis l'onglet Suivi) ---
if ($action === 'delete_expense') {
$id = (int)$_POST['id'];
$pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([$id]);
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
// --- ACTION : SAUVEGARDER UNE DÉPENSE MANUELLE (FETCH) --- // --- ACTION : SAUVEGARDER UNE DÉPENSE MANUELLE (FETCH) ---
if ($action === 'save_expense_manual') { if ($action === 'save_expense_manual') {
header('Content-Type: application/json'); // On force le retour JSON pour notre script JS header('Content-Type: application/json'); // On force le retour JSON pour notre script JS
+1 -2
View File
@@ -237,8 +237,7 @@ function getTranslatedMonthName($dateString) {
<button type="button" <button type="button"
onclick="deleteCategory(<?= $cat['id'] ?>)" onclick="deleteCategory(<?= $cat['id'] ?>)"
class="btn-icon-action delete" title="<?= tr('delete') ?>"> class="btn-icon-action delete" title="<?= tr('delete') ?>">🗑️
&times;
</button> </button>
</div> </div>
</td> </td>
+2 -2
View File
@@ -180,8 +180,8 @@ $totalRevenus = 0;
<td style="padding:15px; text-align:right;"> <td style="padding:15px; text-align:right;">
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;"> <div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
<button class="btn-icon" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">✏️</button> <button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
<button class="btn-icon" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button> <button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
</div> </div>
</td> </td>
</tr> </tr>
+29 -25
View File
@@ -100,12 +100,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') {
exit; exit;
} }
if (isset($_POST['action']) && $_POST['action'] === 'delete_expense') {
$pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([(int)$_POST['id']]);
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
// ============================================================================ // ============================================================================
// E-Bis. LECTURE ET PRÉVISUALISATION DU FICHIER CSV // E-Bis. LECTURE ET PRÉVISUALISATION DU FICHIER CSV
@@ -478,10 +472,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<div class="categories-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:24px;"> <div class="categories-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:24px;">
<?php foreach ($categoriesConfig as $key => $conf): ?> <?php foreach ($categoriesConfig as $key => $conf): ?>
<div class="cat-card" <?= $isClosed ? 'style="opacity:0.8;"' : '' ?>> <div class="cat-card" <?= $isClosed ? 'style="opacity:0.8;"' : '' ?>>
<div style="background:<?= $conf['color'] ?>15; padding:15px; border-bottom:1px solid <?= $conf['color'] ?>30; display:flex; justify-content:space-between; align-items:center;"> <div class="cat-card-header" style="background:<?= $conf['color'] ?>15; border-bottom:1px solid <?= $conf['color'] ?>30;">
<div> <div class="cat-card-title-group">
<h3 style="margin:0; font-size:1rem; color:<?= $conf['color'] ?>;"><?= $conf['label'] ?></h3> <h3 style="margin:0; font-size:1rem; color:<?= $conf['color'] ?>;"><?= $conf['label'] ?></h3>
<div style="font-size:0.85rem; color:#64748b; font-weight:600; margin-top:2px;"> <div class="cat-card-subtitle">
<?php $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']); echo $logic['text']; ?> <?php $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']); echo $logic['text']; ?>
</div> </div>
</div> </div>
@@ -510,9 +504,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?> <?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?>
</td> </td>
<?php if (!$isClosed): ?> <?php if (!$isClosed): ?>
<td style="width:60px; padding-right:10px; text-align:right; white-space:nowrap;"> <td style="width:70px; padding-right:10px; text-align:right; white-space:nowrap;">
<button onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' style="background:none; border:none; cursor:pointer; font-size:1rem; margin-right:8px;">✏️</button> <button class="btn-icon-action edit" onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' title="<?= tr('edit') ?>">✏️</button>
<button class="btn-icon-action btn-safe-click delete" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button> </td> <button class="btn-icon-action delete btn-safe-click" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
</td>
<?php endif; ?> <?php endif; ?>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -606,20 +601,29 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<div id="snapshotModal" class="pf-modal"> <div id="snapshotModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:350px;"> <div class="pf-modal-content" style="max-width:350px;">
<h3 style="margin-top:0;"><?= tr('bud_update_balance') ?></h3> <div class="pf-modal-header">
<form method="POST"> <h3 class="pf-modal-title">🏦 <?= tr('bud_update_balance') ?></h3>
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-modal-close">&times;</button>
</div>
<div class="pf-modal-body">
<form id="snapshotForm" method="POST">
<input type="hidden" name="action" value="save_snapshot"> <input type="hidden" name="action" value="save_snapshot">
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" style="margin-bottom:10px;" required> <div class="pf-form-group">
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" style="margin-bottom:15px;" required> <input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" required>
<div class="modal-footer"> </div>
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button> <div class="pf-form-group">
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button> <input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" required>
</div> </div>
</form> </form>
</div> </div>
<div class="pf-modal-footer">
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn pf-btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" form="snapshotForm" class="pf-btn pf-btn-primary"><?= tr('btn_save') ?></button>
</div>
</div>
</div> </div>
<div id="importCsvModal" class="pf-modal" style="display: <?= $showPreview ? 'flex' : 'none' ?>;"> <div id="importCsvModal" class="pf-modal <?= $showPreview ? 'open' : '' ?>">
<div class="pf-modal-content <?= $showPreview ? 'modal-large' : '' ?>"> <div class="pf-modal-content <?= $showPreview ? 'modal-large' : '' ?>">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; padding:0;"><?= tr('bud_import_csv') ?></h3> <h3 class="pf-modal-title" style="margin:0; border:none; padding:0;"><?= tr('bud_import_csv') ?></h3>
@@ -729,8 +733,8 @@ window.I18N = {
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>'; const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; } function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
function openSuiviModal(id) { document.getElementById(id).style.display = 'flex'; document.body.classList.add('no-scroll'); } function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
function closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; document.body.classList.remove('no-scroll');} function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>; const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
@@ -826,11 +830,11 @@ async function deleteExpense(id) {
formData.append("id", id); formData.append("id", id);
try { try {
await pachaFetch("budget.php?tab=suivi", { method: "POST", body: formData }); await pachaFetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
showToast(tr('bud_toast_deleted')); // showToast(window.I18N['bud_toast_deleted'] || "Supprimé");
setTimeout(() => window.location.reload(), 800); setTimeout(() => window.location.reload(), 800);
} catch (err) { } catch (err) {
showToast(tr('error_occured'), "error"); showToast(window.I18N['error_occured'] || "Erreur", "error");
} }
} }
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1191,8 +1191,9 @@ document.addEventListener("DOMContentLoaded", () => {
const formSnap = document.getElementById("formSnapshot"); const formSnap = document.getElementById("formSnapshot");
if (btnSnap && modalSnap) { if (btnSnap && modalSnap) {
modalSnap.classList.add("is-open");
btnSnap.addEventListener("click", () => { btnSnap.addEventListener("click", () => {
modalSnap.style.display = "flex"; // Utilise flex au lieu de block pour le centrage modalSnap.classList.add("is-open"); // Utilise flex au lieu de block pour le centrage
document.body.classList.add("no-scroll"); document.body.classList.add("no-scroll");
// Pré-remplir la date avec le 1er jour du mois en cours // Pré-remplir la date avec le 1er jour du mois en cours
const today = new Date(); const today = new Date();
+3 -4
View File
@@ -191,8 +191,8 @@ function addItem(category, name = "", amount = "", isPaid = 0) {
<input type="hidden" name="items[paid][]" value="${isPaid}"> <input type="hidden" name="items[paid][]" value="${isPaid}">
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">${tr("hdl_paid")}</span> <span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">${tr("hdl_paid")}</span>
</label> </label>
<button type="button" onclick="this.parentElement.remove()" title="${tr("btn_delete")}" style="width: 28px; height: 28px; border: none; background: #fee2e2; color: #ef4444; border-radius: 6px; cursor: pointer; font-weight: bold; display:flex; align-items:center; justify-content:center;"> <button type="button" onclick="this.parentElement.remove()" class="btn-icon-action delete" title="${tr("btn_delete")}">
&times; 🗑️
</button> </button>
`; `;
@@ -604,8 +604,7 @@ function addCpExpenseLine(
<input type="hidden" name="items[paid][]" value="${isPaid}"> <input type="hidden" name="items[paid][]" value="${isPaid}">
<span class="hol-form-paid-text">${tr("hdl_paid")}</span> <span class="hol-form-paid-text">${tr("hdl_paid")}</span>
</label> </label>
<button type="button" class="btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">&times;</button> <button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button> </div>
</div>
<div class="hol-form-subrow"> <div class="hol-form-subrow">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="${tr("hdl_ph_notes")}" value="${notes}"> <input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="${tr("hdl_ph_notes")}" value="${notes}">
</div> </div>
+1313 -2890
View File
File diff suppressed because it is too large Load Diff
+222 -84
View File
@@ -1,6 +1,6 @@
/** /**
* PachaFamily - Test Engine 🦙 * PachaFamily - Test Engine 🦙
* Moteur de tests automatisés (E2E) - Version Anti-Flakiness * Moteur de tests automatisés (E2E) - Version Anti-Flakiness & QA Exhaustive
*/ */
const PachaTestEngine = { const PachaTestEngine = {
@@ -9,7 +9,7 @@ const PachaTestEngine = {
doc: null, doc: null,
// ========================================== // ==========================================
// 🛠️ HELPERS // 🛠️ HELPERS (BLINDÉS POUR LE QA)
// ========================================== // ==========================================
wait: (ms) => new Promise((r) => setTimeout(r, ms)), wait: (ms) => new Promise((r) => setTimeout(r, ms)),
@@ -23,26 +23,42 @@ const PachaTestEngine = {
this.log(cond ? msgPass : msgFail || msgPass, cond ? "✅" : "❌"); this.log(cond ? msgPass : msgFail || msgPass, cond ? "✅" : "❌");
}, },
// 🛡️ NOUVEAU : Attend le rechargement de la page après une action // 📡 Intercepte les erreurs de l'application dans l'iframe pour les afficher dans le rapport
injectIframeErrorCatcher: function () {
if (!this.arena.contentWindow) return;
this.arena.contentWindow.onerror = (msg, url, line) => {
this.log(`Erreur App (Ligne ${line}): ${msg}`, "🔥");
};
const origError = this.arena.contentWindow.console.error;
this.arena.contentWindow.console.error = (...args) => {
this.log(`Console Error: ${args.join(" ")}`, "🔥");
origError.apply(this.arena.contentWindow.console, args);
};
},
// 🛡️ Attend le rechargement de la page de manière stricte
actionAndWaitForReload: async function (actionFn, timeout = 5000) { actionAndWaitForReload: async function (actionFn, timeout = 5000) {
return new Promise(async (resolve) => { return new Promise((resolve, reject) => {
let reloaded = false; let reloaded = false;
// On écoute le prochain rechargement de l'iframe
this.arena.onload = () => { this.arena.onload = () => {
reloaded = true; reloaded = true;
this.doc = this.arena.contentWindow.document; this.doc = this.arena.contentWindow.document;
this.arena.contentWindow.confirm = () => true; this.arena.contentWindow.confirm = () => true;
this.arena.contentWindow.alert = () => true; this.arena.contentWindow.alert = () => true;
this.injectIframeErrorCatcher();
resolve(); resolve();
}; };
await actionFn(); // On lance le clic ou la soumission // Exécute l'action et gère le rejet si elle plante
Promise.resolve(actionFn()).catch((err) => reject(err));
// Sécurité anti-blocage (si le fetch échoue et ne recharge pas la page)
setTimeout(() => { setTimeout(() => {
if (!reloaded) { if (!reloaded) {
this.log( this.log(
"⚠️ Le rechargement de la page n'a pas eu lieu dans le temps imparti.", "⚠️ Le rechargement n'a pas eu lieu. Suite du test...",
"⏱️", "⏱️",
); );
resolve(); resolve();
@@ -58,6 +74,7 @@ const PachaTestEngine = {
this.doc = this.arena.contentWindow.document; this.doc = this.arena.contentWindow.document;
this.arena.contentWindow.confirm = () => true; this.arena.contentWindow.confirm = () => true;
this.arena.contentWindow.alert = () => true; this.arena.contentWindow.alert = () => true;
this.injectIframeErrorCatcher();
resolve(); resolve();
}; };
this.arena.src = url; this.arena.src = url;
@@ -68,107 +85,251 @@ const PachaTestEngine = {
return this.doc.querySelector(sel); return this.doc.querySelector(sel);
}, },
// 🖱️ Clic unique et infaillible // 🖱️ Clic strict : Si l'élément n'existe pas, on DÉCLENCHE UNE ERREUR FATALE
click: async function (sel, delay = 500) { click: async function (sel, delay = 500) {
const el = typeof sel === "string" ? this.get(sel) : sel; const el = typeof sel === "string" ? this.get(sel) : sel;
if (el) { if (!el) {
// 1. Coupe les popups bloquantes throw new Error(
`Clic impossible : l'élément '${typeof sel === "string" ? sel : "DOM Object"}' est introuvable.`,
);
}
this.arena.contentWindow.confirm = () => true; this.arena.contentWindow.confirm = () => true;
this.arena.contentWindow.alert = () => true; this.arena.contentWindow.alert = () => true;
// 2. Un seul et unique clic !
el.click(); el.click();
await this.wait(delay); await this.wait(delay);
return true; return true;
}
return false;
}, },
fill: function (sel, val) { fill: function (sel, val) {
const el = this.get(sel); const el = this.get(sel);
if (el) el.value = val; if (!el) throw new Error(`Remplissage impossible : '${sel}' introuvable.`);
el.value = val;
}, },
select: async function (sel, val, delay = 300) { select: async function (sel, val, delay = 300) {
const el = this.get(sel); const el = this.get(sel);
if (el) { if (!el) throw new Error(`Sélection impossible : '${sel}' introuvable.`);
el.value = val; el.value = val;
el.dispatchEvent(new Event("change")); el.dispatchEvent(new Event("change"));
await this.wait(delay); await this.wait(delay);
}
}, },
findInTable: function (text) { findInTable: function (text) {
return Array.from(this.doc.querySelectorAll("td, div")).find((el) => return Array.from(this.doc.querySelectorAll("td")).find((el) =>
el.textContent.includes(text), el.textContent.includes(text),
); );
}, },
// ========================================== // ==========================================
// 🧪 SCÉNARIO 1 : SUIVI MENSUEL // 🧪 SCÉNARIO EXHAUSTIF : SUIVI MENSUEL (MODALES & CRUD)
// ========================================== // ==========================================
testBudgetSuiviExhaustive: async function () {
runBudgetTests: async function () {
this.reportBox.value = ""; this.reportBox.value = "";
this.log("=== 🚀 DÉBUT DU SCÉNARIO : SUIVI BUDGET ===", "INFO"); this.log("=== 🚀 DÉBUT EXHAUSTIF : SUIVI MENSUEL ===", "INFO");
await this.load("budget.php?tab=suivi"); await this.load("budget.php?tab=suivi");
await this.wait(1000); await this.wait(1000);
if (!this.get(".btn-add-item")) { // --- 1. GESTION DE L'ÉTAT (CLÔTURE / RÉOUVERTURE) ---
this.log("Mois clôturé, test annulé. Déverrouille-le !", "⚠️"); this.log("🔍 Test 1 : État du mois (Clôture / Réouverture)");
return; let btnReopenInitial = this.get(
'form input[value="reopen_month"] ~ button[type="submit"]',
);
if (btnReopenInitial) {
this.log(
"🔒 Mois actuellement fermé. Réouverture forcée pour les tests...",
"WARN",
);
await this.actionAndWaitForReload(
async () => await this.click(btnReopenInitial),
);
await this.wait(1500); // ⏱️ Attente de la 2e redirection JS du PHP
this.assert(
this.get('form input[value="close_month"]') !== null,
"Mois rouvert.",
);
} }
await this.click("button[onclick=\"toggleDiv('pendingDetailsList')\"]"); // --- 2. UI TOGGLES ---
this.log("🔍 Test 2 : Toggle 'Charges à venir'");
const toggleBtn = this.get(
"button[onclick*=\"toggleDiv('pendingDetailsList')\"]",
);
if (toggleBtn) {
await this.click(toggleBtn);
const pendingList = this.get("#pendingDetailsList");
this.assert( this.assert(
this.get("#pendingDetailsList").style.display !== "none", pendingList && pendingList.style.display !== "none",
"Déploiement : Charges à venir", "Liste des charges déployée.",
);
}
// --- 3. MODALE CSV ---
this.log("🔍 Test 3 : Modale Import CSV");
const btnCsv = this.get('button[onclick*="importCsvModal"]');
if (btnCsv) {
await this.click(btnCsv);
const csvModal = this.get("#importCsvModal");
this.assert(
csvModal &&
(csvModal.classList.contains("is-open") ||
csvModal.style.display === "flex"),
"Modale CSV ouverte.",
); );
await this.click(".btn-add-item"); // Fermeture
const closeCsvBtn = csvModal.querySelector(
".pf-modal-close, button[onclick*='closeSuiviModal']",
);
if (closeCsvBtn) await this.click(closeCsvBtn);
this.assert(
!csvModal.classList.contains("is-open") &&
csvModal.style.display !== "flex",
"Modale CSV fermée.",
);
}
// --- 4. MODALE SNAPSHOT (UPDATE BALANCE) ---
this.log("🔍 Test 4 : Modale Snapshot (Solde Bancaire)");
const btnSnapshot = this.get('button[onclick*="snapshotModal"]');
if (btnSnapshot) {
await this.click(btnSnapshot);
this.assert(this.get("#snapshotModal"), "Modale Snapshot ouverte.");
this.fill('#snapshotModal input[name="snapshot_amount"]', "1337.00");
this.log("Soumission du nouveau solde...", "INFO");
await this.actionAndWaitForReload(async () => {
await this.click('#snapshotModal button[type="submit"]');
});
const pageText = this.doc.body.innerText;
this.assert(
pageText.includes("1 337"),
"Solde mis à jour et formaté à 1 337 €.",
);
}
// --- 5. CRUD DÉPENSE (AJOUT / ÉDITION / SUPPRESSION) ---
this.log("🔍 Test 5 : CRUD Dépense Manuelle");
const btnAdd = this.get(".btn-add-item");
if (btnAdd) {
// 5A. AJOUT
this.log("Création d'une nouvelle dépense...");
await this.click(btnAdd);
await this.select("#modalCatSelect", "Autres"); await this.select("#modalCatSelect", "Autres");
this.fill("#modalAmount", "42.42"); this.fill("#modalAmount", "77.77");
this.fill("#modalLabelInput", "TEST_AUTO_PACHA"); this.fill("#modalLabelInput", "E2E_QA_TEST_EXPENSE");
this.log("Enregistrement de la dépense...", "WARN");
await this.actionAndWaitForReload(async () => { await this.actionAndWaitForReload(async () => {
await this.click('#manualExpenseModal button[type="submit"]'); await this.click('#manualExpenseModal button[type="submit"]');
}); });
let targetTd = Array.from(this.doc.querySelectorAll("td")).find((el) => let newExpense = this.findInTable("E2E_QA_TEST_EXPENSE");
el.textContent.includes("TEST_AUTO_PACHA"),
);
this.assert( this.assert(
targetTd !== undefined, newExpense !== undefined,
"Dépense créée avec succès (42.42€).", "Dépense 'E2E_QA_TEST_EXPENSE' trouvée dans le DOM.",
"Échec : Dépense introuvable.",
); );
if (targetTd) { if (newExpense) {
this.log("Nettoyage de la base de données...", "WARN"); // 5B. ÉDITION
await this.actionAndWaitForReload(async () => { this.log("✏️ Édition de la dépense...");
await this.click( const row = newExpense.closest("tr");
targetTd.closest("tr").querySelector('a[href*="delete_expense"]'),
); // 🛡️ NOUVEAU : Sécurité anti-crash
}); if (!row) {
const checkGone = Array.from(this.doc.querySelectorAll("td")).find((el) => throw new Error(
el.textContent.includes("TEST_AUTO_PACHA"), "L'élément trouvé n'est pas dans une ligne de tableau (<tr> manquant). Le test est corrompu.",
);
this.assert(
checkGone === undefined,
"Base de données nettoyée avec succès.",
); );
} }
this.log("=== 🏁 FIN DU SCÉNARIO ===", "INFO"); const btnEdit = row.querySelector('button[onclick*="openEditModal"]');
if (btnEdit) {
await this.click(btnEdit);
this.fill("#modalAmount", "88.88");
await this.actionAndWaitForReload(async () => {
await this.click('#manualExpenseModal button[type="submit"]');
});
const editedExpense = this.findInTable("E2E_QA_TEST_EXPENSE");
this.assert(
editedExpense &&
editedExpense.closest("tr").innerHTML.includes("88"),
"Montant mis à jour à 88.88€.",
);
}
// 5C. SUPPRESSION (Gère pachaConfirm custom modal)
this.log("🧹 Teardown : Suppression de la dépense...");
const rowToDel = this.findInTable("E2E_QA_TEST_EXPENSE").closest("tr");
const btnDel = rowToDel.querySelector("button.delete");
if (btnDel) {
await this.actionAndWaitForReload(async () => {
await this.click(btnDel); // Déclenche pachaConfirm
await this.wait(500); // Attend l'animation de la modale custom
const confirmBtn = this.get("#confirm-ok");
if (confirmBtn) {
await this.click(confirmBtn); // Valide la suppression (fetch AJAX)
}
}, 3000);
const checkGone = this.findInTable("E2E_QA_TEST_EXPENSE");
this.assert(
checkGone === undefined,
"Base de données propre ! Dépense supprimée.",
);
}
}
} else {
this.log("Bouton '.btn-add-item' introuvable.", "❌");
}
// --- 6. CLÔTURE ET RÉOUVERTURE DU MOIS (TEARDOWN STATE) ---
this.log("🔍 Test 6 : Clôture du mois");
let btnClose = this.get(
'form input[value="close_month"] ~ button[type="submit"]',
);
if (btnClose) {
this.log("Verrouillage du mois...");
await this.actionAndWaitForReload(async () => await this.click(btnClose));
await this.wait(1500); // ⏱️ Attente de la 2e redirection JS du PHP (Mois suivant)
this.log("Retour au mois précédent pour vérification...");
await this.actionAndWaitForReload(
async () =>
await this.click(".suivi-nav-group a.suivi-btn-nav:first-child"),
);
this.assert(
this.get('form input[value="reopen_month"]') !== null,
"Mois clôturé avec succès.",
);
// Teardown
this.log("🧹 Teardown : Restauration du mois (Réouverture)");
await this.actionAndWaitForReload(
async () =>
await this.click(
'form input[value="reopen_month"] ~ button[type="submit"]',
),
);
await this.wait(1500); // ⏱️ Attente de la 2e redirection JS du PHP
this.assert(
this.get('form input[value="close_month"]') !== null,
"Mois rouvert avec succès.",
);
}
this.log("=== 🏁 FIN DU SCÉNARIO EXHAUSTIF ===", "INFO");
}, },
// ========================================== // ==========================================
// 🧪 SCÉNARIO 2 : BUDGET PRÉVISIONNEL // 🧪 SCÉNARIO 2 : BUDGET PRÉVISIONNEL
// ========================================== // ==========================================
testBudgetPrev: async function () { testBudgetPrev: async function () {
this.reportBox.value = ""; this.reportBox.value = "";
this.log("=== 🚀 DÉBUT DU SCÉNARIO : BUDGET PRÉVISIONNEL ===", "INFO"); this.log("=== 🚀 DÉBUT DU SCÉNARIO : BUDGET PRÉVISIONNEL ===", "INFO");
@@ -181,21 +342,15 @@ const PachaTestEngine = {
this.assert( this.assert(
this.doc.body.classList.contains("sum-mode-active"), this.doc.body.classList.contains("sum-mode-active"),
"Le mode somme s'active.", "Le mode somme s'active.",
"Le bouton somme ne répond pas.",
); );
const firstSalaryInput = this.get('input[data-field="salary"]'); const firstSalaryInput = this.get('input[data-field="salary"]');
if (firstSalaryInput) { if (firstSalaryInput) {
await this.click(firstSalaryInput, 500); 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; const sumValue = this.get("#sumResultValue").innerText;
this.assert( this.assert(
sumValue !== "0,00 €" && sumValue !== "0 €", sumValue !== "0,00 €" && sumValue !== "0 €",
`Le total interactif affiche : ${sumValue}`, `Le total interactif affiche : ${sumValue}`,
"Le calcul de la somme a échoué.",
); );
} }
@@ -213,11 +368,7 @@ const PachaTestEngine = {
await this.click('button[onclick^="saveGenericNote"]', 1500); await this.click('button[onclick^="saveGenericNote"]', 1500);
const indicator = this.get("#note-save-indicator"); const indicator = this.get("#note-save-indicator");
this.assert( this.assert(indicator !== null, "Indicateur de sauvegarde affiché.");
indicator !== null,
"Sauvegarde asynchrone exécutée.",
"L'indicateur de sauvegarde ne s'est pas affiché.",
);
this.fill("#monthNoteArea", oldNote); this.fill("#monthNoteArea", oldNote);
await this.click('button[onclick^="saveGenericNote"]', 500); await this.click('button[onclick^="saveGenericNote"]', 500);
@@ -230,16 +381,9 @@ const PachaTestEngine = {
); );
await this.click('button[onclick*="addCatModal"]'); 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); this.fill('#addCatModal input[name="name"]', uniqueCatName);
await this.select('#addCatModal select[name="target"]', "vers commune"); 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.actionAndWaitForReload(async () => {
await this.click('#addCatModal button[type="submit"]'); await this.click('#addCatModal button[type="submit"]');
}); });
@@ -249,8 +393,7 @@ const PachaTestEngine = {
).find((el) => el.textContent.includes(uniqueCatName)); ).find((el) => el.textContent.includes(uniqueCatName));
this.assert( this.assert(
newCatCell !== undefined, newCatCell !== undefined,
`SUCCÈS : '${uniqueCatName}' insérée au tableau !`, `SUCCÈS : '${uniqueCatName}' insérée !`,
"ÉCHEC : Ligne introuvable après rechargement.",
); );
if (newCatCell) { if (newCatCell) {
@@ -259,7 +402,6 @@ const PachaTestEngine = {
const delBtn = row.querySelector(".delete"); const delBtn = row.querySelector(".delete");
if (delBtn) { if (delBtn) {
// 🔄 UTILISATION DU NOUVEAU HELPER SYNCHRONE
await this.actionAndWaitForReload(async () => { await this.actionAndWaitForReload(async () => {
await this.click(delBtn); await this.click(delBtn);
}); });
@@ -270,10 +412,7 @@ const PachaTestEngine = {
this.assert( this.assert(
checkGone === undefined, checkGone === undefined,
"NETTOYAGE PARFAIT : Ligne effacée.", "NETTOYAGE PARFAIT : Ligne effacée.",
"La ligne est restée dans le tableau.",
); );
} else {
this.log("Bouton de suppression (.delete) introuvable.", "FAIL");
} }
} }
@@ -281,10 +420,9 @@ const PachaTestEngine = {
}, },
}; };
// ============================================================================ // ==========================================
// ROUTEUR DU LABORATOIRE ET BOUTON COPIER // ROUTEUR DU LABORATOIRE ET BOUTON COPIER
// ============================================================================ // ==========================================
document.getElementById("btn-run-test")?.addEventListener("click", async () => { document.getElementById("btn-run-test")?.addEventListener("click", async () => {
const selectedTest = document.getElementById("test-selector").value; const selectedTest = document.getElementById("test-selector").value;
const btnRun = document.getElementById("btn-run-test"); const btnRun = document.getElementById("btn-run-test");
@@ -294,8 +432,8 @@ document.getElementById("btn-run-test")?.addEventListener("click", async () => {
try { try {
switch (selectedTest) { switch (selectedTest) {
case "budget_suivi": case "budget_suivi_exhaustive":
await PachaTestEngine.runBudgetTests(); await PachaTestEngine.testBudgetSuiviExhaustive();
break; break;
case "budget_prev": case "budget_prev":
await PachaTestEngine.testBudgetPrev(); await PachaTestEngine.testBudgetPrev();
+1 -1
View File
@@ -105,7 +105,7 @@ require_once 'header.php';
<div class="lab-controls" style="display: flex; align-items: center; gap: 10px;"> <div class="lab-controls" style="display: flex; align-items: center; gap: 10px;">
<select id="test-selector" class="lab-select"> <select id="test-selector" class="lab-select">
<optgroup label="💰 Module Budget"> <optgroup label="💰 Module Budget">
<option value="budget_suivi">Suivi Mensuel (Dépenses)</option> <option value="budget_suivi_exhaustive">Suivi Mensuel (Modales & CRUD)</option>
<option value="budget_prev">Budget Prévisionnel</option> <option value="budget_prev">Budget Prévisionnel</option>
<option value="budget_epargne" disabled>Épargne (Bientôt...)</option> <option value="budget_epargne" disabled>Épargne (Bientôt...)</option>
<option value="budget_recap" disabled>Récapitulatif (Bientôt...)</option> <option value="budget_recap" disabled>Récapitulatif (Bientôt...)</option>