harmo
This commit is contained in:
+47
-36
@@ -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');">×</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,39 +55,50 @@ 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');">×</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">
|
||||||
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
<div class="pf-form-group">
|
||||||
<select id="snapPerson" class="pf-input" required>
|
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
||||||
<option value="2">Alex</option>
|
<select id="snapPerson" class="pf-input" required>
|
||||||
<option value="3">Laia</option>
|
<option value="2">Alex</option>
|
||||||
</select>
|
<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>
|
||||||
<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_apply_date') ?></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>
|
|
||||||
<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 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>
|
</div>
|
||||||
<button type="submit" class="pf-btn" style="width: 100%;"><?= tr('fc_btn_save_snap') ?></button>
|
<div class="pf-form-group">
|
||||||
</form>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+4
-4
@@ -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
@@ -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
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
|
||||||
|
|||||||
@@ -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') ?>">🗑️
|
||||||
×
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
<input type="hidden" name="action" value="save_snapshot">
|
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-modal-close">×</button>
|
||||||
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" style="margin-bottom:10px;" required>
|
</div>
|
||||||
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" style="margin-bottom:15px;" required>
|
<div class="pf-modal-body">
|
||||||
<div class="modal-footer">
|
<form id="snapshotForm" method="POST">
|
||||||
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
<input type="hidden" name="action" value="save_snapshot">
|
||||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
<div class="pf-form-group">
|
||||||
</div>
|
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" required>
|
||||||
</form>
|
</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>
|
</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
@@ -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();
|
||||||
|
|||||||
@@ -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")}">
|
||||||
×
|
🗑️
|
||||||
</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")}">×</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>
|
||||||
|
|||||||
+1346
-2923
File diff suppressed because it is too large
Load Diff
+240
-102
@@ -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(
|
||||||
this.arena.contentWindow.confirm = () => true;
|
`Clic impossible : l'élément '${typeof sel === "string" ? sel : "DOM Object"}' est introuvable.`,
|
||||||
this.arena.contentWindow.alert = () => true;
|
);
|
||||||
|
|
||||||
// 2. Un seul et unique clic !
|
|
||||||
el.click();
|
|
||||||
|
|
||||||
await this.wait(delay);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
this.arena.contentWindow.confirm = () => true;
|
||||||
|
this.arena.contentWindow.alert = () => true;
|
||||||
|
el.click();
|
||||||
|
await this.wait(delay);
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
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"]',
|
||||||
|
|
||||||
await this.click("button[onclick=\"toggleDiv('pendingDetailsList')\"]");
|
|
||||||
this.assert(
|
|
||||||
this.get("#pendingDetailsList").style.display !== "none",
|
|
||||||
"Déploiement : Charges à venir",
|
|
||||||
);
|
);
|
||||||
|
if (btnReopenInitial) {
|
||||||
await this.click(".btn-add-item");
|
this.log(
|
||||||
await this.select("#modalCatSelect", "Autres");
|
"🔒 Mois actuellement fermé. Réouverture forcée pour les tests...",
|
||||||
this.fill("#modalAmount", "42.42");
|
"WARN",
|
||||||
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"),
|
|
||||||
);
|
);
|
||||||
|
await this.actionAndWaitForReload(
|
||||||
|
async () => await this.click(btnReopenInitial),
|
||||||
|
);
|
||||||
|
await this.wait(1500); // ⏱️ Attente de la 2e redirection JS du PHP
|
||||||
this.assert(
|
this.assert(
|
||||||
checkGone === undefined,
|
this.get('form input[value="close_month"]') !== null,
|
||||||
"Base de données nettoyée avec succès.",
|
"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
|
// 🧪 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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user