harmo
This commit is contained in:
+47
-36
@@ -32,13 +32,13 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalHolidays" class="fc-modal-overlay" style="display: none;">
|
||||
<div class="fc-modal-content">
|
||||
<div class="fc-modal-header">
|
||||
<h2><?= tr('fc_modal_holidays_title') ?></h2>
|
||||
<button id="btnCloseHolidays" class="fc-modal-close">×</button>
|
||||
<div id="modalHolidays" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title"><?= tr('fc_modal_holidays_title') ?></h3>
|
||||
<button id="btnCloseHolidays" class="pf-modal-close" onclick="document.getElementById('modalHolidays').classList.remove('is-open'); document.body.classList.remove('no-scroll');">×</button>
|
||||
</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;">
|
||||
<table id="schoolHolidaysTable" class="fc-holidays-table" style="min-width: 400px;">
|
||||
<thead>
|
||||
@@ -55,39 +55,50 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalSnapshot" class="fc-modal-overlay" style="display: none;">
|
||||
<div class="fc-modal-content" style="max-width: 400px;">
|
||||
<div class="fc-modal-header">
|
||||
<h2><?= tr('fc_modal_snap_title') ?></h2>
|
||||
<button id="btnCloseSnapshot" class="fc-modal-close">×</button>
|
||||
</div>
|
||||
<div class="fc-modal-body" style="padding: 24px;">
|
||||
<form id="formSnapshot">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
||||
<select id="snapPerson" class="pf-input" required>
|
||||
<option value="2">Alex</option>
|
||||
<option value="3">Laia</option>
|
||||
</select>
|
||||
<div id="modalSnapshot" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title">📸 <?= tr('fc_modal_snap_title') ?></h3>
|
||||
<button type="button" id="btnCloseSnapshot" class="pf-modal-close" onclick="document.getElementById('modalSnapshot').classList.remove('is-open'); document.body.classList.remove('no-scroll');">×</button>
|
||||
</div>
|
||||
<div class="pf-modal-body">
|
||||
<form id="formSnapshot">
|
||||
<div class="pf-form-row">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
||||
<select id="snapPerson" class="pf-input" required>
|
||||
<option value="2">Alex</option>
|
||||
<option value="3">Laia</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
||||
<select id="snapType" class="pf-input" required>
|
||||
<option value="CP">CP</option>
|
||||
<option value="JRA">JRA</option>
|
||||
<option value="JA">JA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
||||
<select id="snapType" class="pf-input" required>
|
||||
<option value="CP">CP</option>
|
||||
<option value="JRA">JRA</option>
|
||||
<option value="JA">JA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="pf-label"><?= tr('fc_label_apply_date') ?></label>
|
||||
<div class="pf-form-group">
|
||||
<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') ?>">
|
||||
</div>
|
||||
<div style="margin-bottom: 24px;">
|
||||
<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>
|
||||
<button type="submit" class="pf-btn" style="width: 100%;"><?= tr('fc_btn_save_snap') ?></button>
|
||||
</form>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
|
||||
<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>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
+4
-4
@@ -230,7 +230,7 @@ require __DIR__ . '/header.php';
|
||||
<div class="cl-gift-right">
|
||||
<span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span>
|
||||
<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-year="<?= $year ?>"
|
||||
data-child="<?= htmlspecialchars($childName) ?>"
|
||||
@@ -240,10 +240,10 @@ require __DIR__ . '/header.php';
|
||||
data-desc="<?= htmlspecialchars($gift['gift_description']) ?>"
|
||||
data-amount="<?= htmlspecialchars($gift['amount']) ?>"
|
||||
data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>">
|
||||
✎
|
||||
✏️
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+206
-160
@@ -1,16 +1,25 @@
|
||||
/* global.css */
|
||||
|
||||
:root {
|
||||
/* Couleurs de base */
|
||||
--primary: #3b82f6;
|
||||
--primary-dark: #1d4ed8;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
|
||||
/* Textes & Fonds */
|
||||
--text-main: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--bg-page: #f8fafc;
|
||||
--bg-panel: #ffffff;
|
||||
|
||||
/* Bordures, Arrondis & Ombres */
|
||||
--border-light: #e2e8f0;
|
||||
--danger: #ef4444;
|
||||
--radius: 12px;
|
||||
--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 */
|
||||
@@ -19,13 +28,12 @@
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden; /* Empêche le site entier de glisser de gauche à droite */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -43,15 +51,18 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Images et médias responsive par défaut */
|
||||
img,
|
||||
svg,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
body.no-scroll {
|
||||
overflow: hidden !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
/* === HEADER === */
|
||||
/* === HEADER & NAVIGATION === */
|
||||
.pf-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
@@ -64,7 +75,6 @@ video {
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.pf-logo {
|
||||
font-weight: 800;
|
||||
font-size: 1.25rem;
|
||||
@@ -72,13 +82,10 @@ video {
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
/* Navigation Desktop */
|
||||
.pf-nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pf-nav-link {
|
||||
text-decoration: none;
|
||||
color: var(--text-muted);
|
||||
@@ -88,24 +95,19 @@ video {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pf-nav-link:hover {
|
||||
background: #f1f5f9;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.pf-nav-link--active {
|
||||
background: #eff6ff;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Actions Header (User) */
|
||||
.pf-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pf-user-badge {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
@@ -113,7 +115,6 @@ video {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pf-logout-btn {
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
@@ -127,8 +128,6 @@ video {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
/* Menu Burger (Mobile) */
|
||||
.pf-burger-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
@@ -137,6 +136,7 @@ video {
|
||||
cursor: pointer;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.pf-mobile-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -156,7 +156,7 @@ video {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* === MAIN CONTENT === */
|
||||
/* === MAIN CONTENT & TYPOGRAPHY === */
|
||||
.pf-main {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
@@ -164,14 +164,11 @@ video {
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px;
|
||||
}
|
||||
|
||||
.pf-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* === TYPOGRAPHY === */
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
@@ -185,14 +182,13 @@ p {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* === CARDS (Modules) === */
|
||||
/* === COMPOSANTS GLOBAUX === */
|
||||
.pf-modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.pf-module-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border-light);
|
||||
@@ -206,13 +202,11 @@ p {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pf-module-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow);
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.pf-card-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 4px;
|
||||
@@ -236,54 +230,13 @@ p {
|
||||
align-items: center;
|
||||
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 {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
@@ -292,27 +245,155 @@ p {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.pf-btn:hover {
|
||||
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 {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
/* Formulaires & Inputs Standards */
|
||||
.pf-form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.pf-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
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 {
|
||||
overflow: hidden !important;
|
||||
height: 100vh !important;
|
||||
/* Tableaux Standards */
|
||||
.pf-table {
|
||||
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 {
|
||||
border-top: 1px solid var(--border-light);
|
||||
background: white;
|
||||
@@ -322,25 +403,6 @@ body.no-scroll {
|
||||
color: var(--text-muted);
|
||||
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 {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
@@ -359,24 +421,13 @@ body.no-scroll {
|
||||
.pf-toast.is-visible {
|
||||
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) {
|
||||
.pf-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.pf-nav {
|
||||
display: none;
|
||||
}
|
||||
.pf-nav,
|
||||
.pf-header-actions {
|
||||
display: none;
|
||||
}
|
||||
@@ -385,18 +436,7 @@ body.no-scroll {
|
||||
}
|
||||
.pf-main {
|
||||
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 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
@@ -405,44 +445,20 @@ body.no-scroll {
|
||||
padding: 12px 0;
|
||||
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 {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
border-radius: 24px 24px 0 0 !important; /* Arrondi seulement en haut */
|
||||
padding: 24px 20px !important;
|
||||
padding-bottom: env(
|
||||
safe-area-inset-bottom,
|
||||
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;
|
||||
border-radius: 20px !important; /* On garde les arrondis partout */
|
||||
transform: none !important;
|
||||
animation: pfModalPop 0.3s ease !important; /* On remet l'animation desktop */
|
||||
margin: auto !important;
|
||||
}
|
||||
.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 {
|
||||
content: "";
|
||||
display: block;
|
||||
@@ -452,12 +468,12 @@ body.no-scroll {
|
||||
border-radius: 10px;
|
||||
margin: -10px auto 20px auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column-reverse; /* Empilement propre sur mobile */
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.modal-footer .pf-btn {
|
||||
width: 100%; /* Largeur totale uniquement sur mobile */
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@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
File diff suppressed because it is too large
Load Diff
@@ -73,6 +73,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
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) ---
|
||||
if ($action === 'save_expense_manual') {
|
||||
header('Content-Type: application/json'); // On force le retour JSON pour notre script JS
|
||||
|
||||
@@ -237,8 +237,7 @@ function getTranslatedMonthName($dateString) {
|
||||
|
||||
<button type="button"
|
||||
onclick="deleteCategory(<?= $cat['id'] ?>)"
|
||||
class="btn-icon-action delete" title="<?= tr('delete') ?>">
|
||||
×
|
||||
class="btn-icon-action delete" title="<?= tr('delete') ?>">🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -180,8 +180,8 @@ $totalRevenus = 0;
|
||||
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<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" 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 edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -100,12 +100,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') {
|
||||
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
|
||||
@@ -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;">
|
||||
<?php foreach ($categoriesConfig as $key => $conf): ?>
|
||||
<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>
|
||||
<div class="cat-card-header" style="background:<?= $conf['color'] ?>15; border-bottom:1px solid <?= $conf['color'] ?>30;">
|
||||
<div class="cat-card-title-group">
|
||||
<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']; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -510,9 +504,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?>
|
||||
</td>
|
||||
<?php if (!$isClosed): ?>
|
||||
<td style="width:60px; 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 btn-safe-click delete" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button> </td>
|
||||
<td style="width:70px; padding-right:10px; text-align:right; white-space:nowrap;">
|
||||
<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 delete btn-safe-click" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -606,20 +601,29 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
|
||||
<div id="snapshotModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width:350px;">
|
||||
<h3 style="margin-top:0;"><?= tr('bud_update_balance') ?></h3>
|
||||
<form method="POST">
|
||||
<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>
|
||||
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" style="margin-bottom:15px;" required>
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title">🏦 <?= tr('bud_update_balance') ?></h3>
|
||||
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
<div class="pf-modal-body">
|
||||
<form id="snapshotForm" method="POST">
|
||||
<input type="hidden" name="action" value="save_snapshot">
|
||||
<div class="pf-form-group">
|
||||
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" required>
|
||||
</div>
|
||||
</form>
|
||||
</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 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 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>
|
||||
@@ -729,8 +733,8 @@ window.I18N = {
|
||||
|
||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||
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 closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; document.body.classList.remove('no-scroll');}
|
||||
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('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)) ?>;
|
||||
|
||||
@@ -826,11 +830,11 @@ async function deleteExpense(id) {
|
||||
formData.append("id", id);
|
||||
|
||||
try {
|
||||
await pachaFetch("budget.php?tab=suivi", { method: "POST", body: formData });
|
||||
showToast(tr('bud_toast_deleted')); //
|
||||
await pachaFetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
|
||||
showToast(window.I18N['bud_toast_deleted'] || "Supprimé");
|
||||
setTimeout(() => window.location.reload(), 800);
|
||||
} catch (err) {
|
||||
showToast(tr('error_occured'), "error");
|
||||
showToast(window.I18N['error_occured'] || "Erreur", "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1191,8 +1191,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const formSnap = document.getElementById("formSnapshot");
|
||||
|
||||
if (btnSnap && modalSnap) {
|
||||
modalSnap.classList.add("is-open");
|
||||
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");
|
||||
// Pré-remplir la date avec le 1er jour du mois en cours
|
||||
const today = new Date();
|
||||
|
||||
@@ -191,8 +191,8 @@ function addItem(category, name = "", amount = "", isPaid = 0) {
|
||||
<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>
|
||||
</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")}">
|
||||
🗑️
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -604,8 +604,7 @@ function addCpExpenseLine(
|
||||
<input type="hidden" name="items[paid][]" value="${isPaid}">
|
||||
<span class="hol-form-paid-text">${tr("hdl_paid")}</span>
|
||||
</label>
|
||||
<button type="button" class="btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">×</button>
|
||||
</div>
|
||||
<button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button> </div>
|
||||
<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}">
|
||||
</div>
|
||||
|
||||
+1346
-2923
File diff suppressed because it is too large
Load Diff
+240
-102
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 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 = {
|
||||
@@ -9,7 +9,7 @@ const PachaTestEngine = {
|
||||
doc: null,
|
||||
|
||||
// ==========================================
|
||||
// 🛠️ HELPERS
|
||||
// 🛠️ HELPERS (BLINDÉS POUR LE QA)
|
||||
// ==========================================
|
||||
|
||||
wait: (ms) => new Promise((r) => setTimeout(r, ms)),
|
||||
@@ -23,26 +23,42 @@ const PachaTestEngine = {
|
||||
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) {
|
||||
return new Promise(async (resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
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;
|
||||
this.injectIframeErrorCatcher();
|
||||
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(() => {
|
||||
if (!reloaded) {
|
||||
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();
|
||||
@@ -58,6 +74,7 @@ const PachaTestEngine = {
|
||||
this.doc = this.arena.contentWindow.document;
|
||||
this.arena.contentWindow.confirm = () => true;
|
||||
this.arena.contentWindow.alert = () => true;
|
||||
this.injectIframeErrorCatcher();
|
||||
resolve();
|
||||
};
|
||||
this.arena.src = url;
|
||||
@@ -68,107 +85,251 @@ const PachaTestEngine = {
|
||||
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) {
|
||||
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;
|
||||
if (!el) {
|
||||
throw new Error(
|
||||
`Clic impossible : l'élément '${typeof sel === "string" ? sel : "DOM Object"}' est introuvable.`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
||||
this.arena.contentWindow.confirm = () => true;
|
||||
this.arena.contentWindow.alert = () => true;
|
||||
el.click();
|
||||
await this.wait(delay);
|
||||
return true;
|
||||
},
|
||||
|
||||
fill: function (sel, val) {
|
||||
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) {
|
||||
const el = this.get(sel);
|
||||
if (el) {
|
||||
el.value = val;
|
||||
el.dispatchEvent(new Event("change"));
|
||||
await this.wait(delay);
|
||||
}
|
||||
if (!el) throw new Error(`Sélection impossible : '${sel}' introuvable.`);
|
||||
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) =>
|
||||
return Array.from(this.doc.querySelectorAll("td")).find((el) =>
|
||||
el.textContent.includes(text),
|
||||
);
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 🧪 SCÉNARIO 1 : SUIVI MENSUEL
|
||||
// 🧪 SCÉNARIO EXHAUSTIF : SUIVI MENSUEL (MODALES & CRUD)
|
||||
// ==========================================
|
||||
|
||||
runBudgetTests: async function () {
|
||||
testBudgetSuiviExhaustive: async function () {
|
||||
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.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",
|
||||
// --- 1. GESTION DE L'ÉTAT (CLÔTURE / RÉOUVERTURE) ---
|
||||
this.log("🔍 Test 1 : État du mois (Clôture / Réouverture)");
|
||||
let btnReopenInitial = this.get(
|
||||
'form input[value="reopen_month"] ~ button[type="submit"]',
|
||||
);
|
||||
|
||||
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"),
|
||||
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(
|
||||
checkGone === undefined,
|
||||
"Base de données nettoyée avec succès.",
|
||||
this.get('form input[value="close_month"]') !== null,
|
||||
"Mois rouvert.",
|
||||
);
|
||||
}
|
||||
|
||||
this.log("=== 🏁 FIN DU SCÉNARIO ===", "INFO");
|
||||
// --- 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(
|
||||
pendingList && pendingList.style.display !== "none",
|
||||
"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.",
|
||||
);
|
||||
|
||||
// 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");
|
||||
this.fill("#modalAmount", "77.77");
|
||||
this.fill("#modalLabelInput", "E2E_QA_TEST_EXPENSE");
|
||||
|
||||
await this.actionAndWaitForReload(async () => {
|
||||
await this.click('#manualExpenseModal button[type="submit"]');
|
||||
});
|
||||
|
||||
let newExpense = this.findInTable("E2E_QA_TEST_EXPENSE");
|
||||
this.assert(
|
||||
newExpense !== undefined,
|
||||
"Dépense 'E2E_QA_TEST_EXPENSE' trouvée dans le DOM.",
|
||||
);
|
||||
|
||||
if (newExpense) {
|
||||
// 5B. ÉDITION
|
||||
this.log("✏️ Édition de la dépense...");
|
||||
const row = newExpense.closest("tr");
|
||||
|
||||
// 🛡️ NOUVEAU : Sécurité anti-crash
|
||||
if (!row) {
|
||||
throw new Error(
|
||||
"L'élément trouvé n'est pas dans une ligne de tableau (<tr> manquant). Le test est corrompu.",
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
// ==========================================
|
||||
|
||||
testBudgetPrev: async function () {
|
||||
this.reportBox.value = "";
|
||||
this.log("=== 🚀 DÉBUT DU SCÉNARIO : BUDGET PRÉVISIONNEL ===", "INFO");
|
||||
@@ -181,21 +342,15 @@ const PachaTestEngine = {
|
||||
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é.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,11 +368,7 @@ const PachaTestEngine = {
|
||||
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.assert(indicator !== null, "Indicateur de sauvegarde affiché.");
|
||||
|
||||
this.fill("#monthNoteArea", oldNote);
|
||||
await this.click('button[onclick^="saveGenericNote"]', 500);
|
||||
@@ -230,16 +381,9 @@ const PachaTestEngine = {
|
||||
);
|
||||
|
||||
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"]');
|
||||
});
|
||||
@@ -249,8 +393,7 @@ const PachaTestEngine = {
|
||||
).find((el) => el.textContent.includes(uniqueCatName));
|
||||
this.assert(
|
||||
newCatCell !== undefined,
|
||||
`SUCCÈS : '${uniqueCatName}' insérée au tableau !`,
|
||||
"ÉCHEC : Ligne introuvable après rechargement.",
|
||||
`SUCCÈS : '${uniqueCatName}' insérée !`,
|
||||
);
|
||||
|
||||
if (newCatCell) {
|
||||
@@ -259,7 +402,6 @@ const PachaTestEngine = {
|
||||
const delBtn = row.querySelector(".delete");
|
||||
|
||||
if (delBtn) {
|
||||
// 🔄 UTILISATION DU NOUVEAU HELPER SYNCHRONE
|
||||
await this.actionAndWaitForReload(async () => {
|
||||
await this.click(delBtn);
|
||||
});
|
||||
@@ -270,10 +412,7 @@ const PachaTestEngine = {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +420,9 @@ const PachaTestEngine = {
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ==========================================
|
||||
// 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");
|
||||
@@ -294,8 +432,8 @@ document.getElementById("btn-run-test")?.addEventListener("click", async () => {
|
||||
|
||||
try {
|
||||
switch (selectedTest) {
|
||||
case "budget_suivi":
|
||||
await PachaTestEngine.runBudgetTests();
|
||||
case "budget_suivi_exhaustive":
|
||||
await PachaTestEngine.testBudgetSuiviExhaustive();
|
||||
break;
|
||||
case "budget_prev":
|
||||
await PachaTestEngine.testBudgetPrev();
|
||||
|
||||
@@ -105,7 +105,7 @@ require_once 'header.php';
|
||||
<div class="lab-controls" style="display: flex; align-items: center; gap: 10px;">
|
||||
<select id="test-selector" class="lab-select">
|
||||
<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_epargne" disabled>Épargne (Bientôt...)</option>
|
||||
<option value="budget_recap" disabled>Récapitulatif (Bientôt...)</option>
|
||||
|
||||
Reference in New Issue
Block a user