Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29f341b052 | ||
|
|
8792e44f69 | ||
|
|
e099fb80cd | ||
|
|
8a6abe5f8e | ||
|
|
2b2150870c | ||
|
|
31f03b7182 | ||
|
|
f2af3541f6 | ||
|
|
ab1374472d | ||
|
|
55a2f8a730 | ||
|
|
3bfd25d566 |
+10
-3
@@ -110,14 +110,21 @@ require __DIR__ . '/header.php';
|
|||||||
|
|
||||||
<div class="fc-month-nav-row">
|
<div class="fc-month-nav-row">
|
||||||
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
||||||
<h3 id="fc-current-month-year"></h3>
|
|
||||||
|
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
||||||
|
<select id="fc-select-month" class="fc-smart-select"></select>
|
||||||
|
<select id="fc-select-year" class="fc-smart-select"></select>
|
||||||
|
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="fc-next-month" class="fc-nav-button">›</button>
|
<button id="fc-next-month" class="fc-nav-button">›</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fc-view-controls">
|
<div class="fc-view-controls">
|
||||||
|
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button> <div class="fc-view-divider"></div>
|
||||||
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
|
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
|
||||||
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
||||||
<button class="fc-view-button" data-view="3months">3 Mois</button>
|
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +236,7 @@ window.I18N = {
|
|||||||
'leg_pep_sick': "<?= tr('leg_pep_sick') ?>",
|
'leg_pep_sick': "<?= tr('leg_pep_sick') ?>",
|
||||||
'leg_off_carole': "<?= tr('leg_off_carole') ?>",
|
'leg_off_carole': "<?= tr('leg_off_carole') ?>",
|
||||||
'leg_extra_off': "<?= tr('leg_extra_off') ?>",
|
'leg_extra_off': "<?= tr('leg_extra_off') ?>",
|
||||||
'leg_presence Pep': "<?= tr('leg_presence Pep') ?>",
|
'leg_presence': "<?= tr('leg_presence') ?>",
|
||||||
'fc_menu_kids_leaves': "<?= tr('fc_menu_kids_leaves') ?>",
|
'fc_menu_kids_leaves': "<?= tr('fc_menu_kids_leaves') ?>",
|
||||||
'fc_clear': "<?= tr('fc_clear') ?>",
|
'fc_clear': "<?= tr('fc_clear') ?>",
|
||||||
'fc_unit_days': "<?= tr('fc_unit_days') ?>",
|
'fc_unit_days': "<?= tr('fc_unit_days') ?>",
|
||||||
|
|||||||
+125
-13
@@ -137,23 +137,107 @@ body.no-scroll {
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-mobile-menu {
|
/* === MENU MOBILE (Sidebar "Off-Canvas") === */
|
||||||
|
.pf-mobile-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 64px;
|
inset: 0;
|
||||||
left: 0;
|
background: rgba(15, 23, 42, 0.6);
|
||||||
right: 0;
|
backdrop-filter: blur(4px);
|
||||||
height: calc(100vh - 64px);
|
z-index: 998;
|
||||||
overflow-y: auto;
|
opacity: 0;
|
||||||
-webkit-overflow-scrolling: touch;
|
transition: opacity 0.3s ease;
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
z-index: 99;
|
|
||||||
}
|
}
|
||||||
.pf-mobile-menu.is-open {
|
.pf-mobile-overlay.is-visible {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -300px;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh; /* Fallback (sécurité) */
|
||||||
|
height: 100dvh;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
box-shadow: -4px 0 25px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 999;
|
||||||
|
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-menu.is-open {
|
||||||
|
right: 0; /* Le menu glisse dans l'écran */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-menu-header {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
background: var(--bg-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-menu-body {
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-nav-link {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-main);
|
||||||
|
padding: 14px 16px; /* Plus large pour le tactile */
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-nav-link:active {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-logout {
|
||||||
|
color: var(--danger);
|
||||||
|
background: #fef2f2;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-menu-header .pf-logo,
|
||||||
|
body.pf-home .pf-mobile-menu-header .pf-logo {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force la croix en gris, MÊME sur la page d'accueil */
|
||||||
|
.pf-mobile-close-btn,
|
||||||
|
body.pf-home .pf-mobile-close-btn {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-mobile-close-btn:hover {
|
||||||
|
color: var(--danger); /* Touche de rouge au survol */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === MAIN CONTENT & TYPOGRAPHY === */
|
/* === MAIN CONTENT & TYPOGRAPHY === */
|
||||||
@@ -575,3 +659,31 @@ p {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Optimisation Login Mobile --- */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.pf-login-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
align-items: flex-start; /* Aligné en haut pour laisser de la place au clavier */
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-login-card {
|
||||||
|
border-radius: 0; /* Supprime les arrondis pour un look plein écran */
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 60px 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ⚠️ IMPORTANT : Éviter le zoom auto sur iOS */
|
||||||
|
.pf-login-card .pf-input {
|
||||||
|
font-size: 16px; /* En dessous de 16px, Safari iOS zoome automatiquement sur le champ */
|
||||||
|
padding: 14px; /* Zone de clic plus large pour les doigts */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-btn-block {
|
||||||
|
padding: 16px; /* Bouton plus haut pour un appui facile au pouce */
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|||||||
+38
-16
@@ -77,14 +77,23 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<?php if (isset($_SESSION['user'])): ?>
|
<?php if (isset($_SESSION['user'])): ?>
|
||||||
<div class="pf-mobile-menu">
|
<div class="pf-mobile-overlay" id="mobileOverlay"></div>
|
||||||
<a href="/index.php" class="pf-mobile-nav-link"><?= tr('menu_home') ?></a>
|
|
||||||
<a href="/family-calendar.php" class="pf-mobile-nav-link"><?= tr('menu_calendar') ?></a>
|
<div class="pf-mobile-menu" id="mobileMenu">
|
||||||
<a href="/budget.php" class="pf-mobile-nav-link"><?= tr('menu_budget') ?></a>
|
<div class="pf-mobile-menu-header">
|
||||||
<a href="/holidays.php" class="pf-mobile-nav-link"><?= tr('menu_holidays') ?></a>
|
<span class="pf-logo">PachaFamily</span>
|
||||||
<a href="/gift-list.php" class="pf-mobile-nav-link"><?= tr('menu_gifts') ?></a>
|
<button class="pf-mobile-close-btn" id="closeMobileMenu">×</button>
|
||||||
<hr style="width: 80%; border: 0; border-top: 1px solid #eee; margin: 10px 0;">
|
</div>
|
||||||
<a href="/logout.php" class="pf-mobile-nav-link pf-mobile-logout"><?= tr('btn_logout') ?></a>
|
|
||||||
|
<div class="pf-mobile-menu-body">
|
||||||
|
<a href="/index.php" class="pf-mobile-nav-link">🏠 <?= tr('menu_home') ?></a>
|
||||||
|
<a href="/family-calendar.php" class="pf-mobile-nav-link">📅 <?= tr('menu_calendar') ?></a>
|
||||||
|
<a href="/budget.php" class="pf-mobile-nav-link">💰 <?= tr('menu_budget') ?></a>
|
||||||
|
<a href="/holidays.php" class="pf-mobile-nav-link">🏖️ <?= tr('menu_holidays') ?></a>
|
||||||
|
<a href="/gift-list.php" class="pf-mobile-nav-link">🎁 <?= tr('menu_gifts') ?></a>
|
||||||
|
|
||||||
|
<a href="/logout.php" class="pf-mobile-nav-link pf-mobile-logout" style="margin-top: auto;">🚪 <?= tr('btn_logout') ?></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -107,18 +116,31 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
return window.I18N[key] || key;
|
return window.I18N[key] || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion du menu mobile
|
// Gestion du menu mobile (Off-Canvas Sidebar)
|
||||||
<?php if (isset($_SESSION['user'])): ?>
|
<?php if (isset($_SESSION['user'])): ?>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const burgerBtn = document.querySelector('.pf-burger-btn');
|
const burgerBtn = document.querySelector('.pf-burger-btn');
|
||||||
const mobileMenu = document.querySelector('.pf-mobile-menu');
|
const mobileMenu = document.getElementById('mobileMenu');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
const closeBtn = document.getElementById('closeMobileMenu');
|
||||||
|
|
||||||
if(burgerBtn && mobileMenu) {
|
function openMenu() {
|
||||||
burgerBtn.addEventListener('click', () => {
|
mobileMenu.classList.add('is-open');
|
||||||
const isOpen = mobileMenu.classList.toggle('is-open');
|
overlay.classList.add('is-visible');
|
||||||
burgerBtn.textContent = isOpen ? '✕' : '☰';
|
document.body.classList.add('no-scroll');
|
||||||
document.body.classList.toggle('no-scroll', isOpen);
|
}
|
||||||
});
|
|
||||||
|
function closeMenu() {
|
||||||
|
mobileMenu.classList.remove('is-open');
|
||||||
|
overlay.classList.remove('is-visible');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(burgerBtn && mobileMenu && overlay && closeBtn) {
|
||||||
|
burgerBtn.addEventListener('click', openMenu);
|
||||||
|
closeBtn.addEventListener('click', closeMenu);
|
||||||
|
// Fermer le menu si on clique en dehors (sur l'overlay sombre)
|
||||||
|
overlay.addEventListener('click', closeMenu);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -134,8 +134,10 @@ return [
|
|||||||
'fc_err_fetch_gov' => 'Error en la connexió amb l’API governamental.',
|
'fc_err_fetch_gov' => 'Error en la connexió amb l’API governamental.',
|
||||||
'fc_err_no_data_gov' => 'No s’han trobat dades a l’API del govern per a aquest any.',
|
'fc_err_no_data_gov' => 'No s’han trobat dades a l’API del govern per a aquest any.',
|
||||||
'fc_err_action' => 'Error en l’acció: ',
|
'fc_err_action' => 'Error en l’acció: ',
|
||||||
'leg_presence_pep' => 'Presència Pep',
|
'fc_today_short' => 'Avui',
|
||||||
|
'fc_today_title' => 'Anar a avui',
|
||||||
|
|
||||||
|
'leg_presence' => 'Presència Pep',
|
||||||
'leg_school_holidays' => 'Vacances Escolars',
|
'leg_school_holidays' => 'Vacances Escolars',
|
||||||
'leg_public_holiday' => 'Festiu',
|
'leg_public_holiday' => 'Festiu',
|
||||||
'leg_off_carole' => 'Off Carole',
|
'leg_off_carole' => 'Off Carole',
|
||||||
@@ -143,7 +145,6 @@ return [
|
|||||||
'leg_centre' => 'Casal',
|
'leg_centre' => 'Casal',
|
||||||
'leg_avis' => 'Avís',
|
'leg_avis' => 'Avís',
|
||||||
'leg_pep_sick' => 'Pep Malalt',
|
'leg_pep_sick' => 'Pep Malalt',
|
||||||
'leg_presence' => 'Presència',
|
|
||||||
|
|
||||||
'vac_toussaint' => 'Vacances de Tots Sants',
|
'vac_toussaint' => 'Vacances de Tots Sants',
|
||||||
'vac_noel' => 'Vacances de Nadal',
|
'vac_noel' => 'Vacances de Nadal',
|
||||||
|
|||||||
@@ -137,8 +137,10 @@ return [
|
|||||||
'fc_err_fetch_gov' => 'Erreur lors de la connexion à l’API gouvernementale.',
|
'fc_err_fetch_gov' => 'Erreur lors de la connexion à l’API gouvernementale.',
|
||||||
'fc_err_no_data_gov' => 'Aucune donnée trouvée sur l’API du gouvernement pour cette année.',
|
'fc_err_no_data_gov' => 'Aucune donnée trouvée sur l’API du gouvernement pour cette année.',
|
||||||
'fc_err_action' => 'Erreur lors de l’action : ',
|
'fc_err_action' => 'Erreur lors de l’action : ',
|
||||||
'leg_presence_pep' => 'Présence Pep',
|
'fc_today_short' => 'Auj.',
|
||||||
|
'fc_today_title' => "Aujourd'hui",
|
||||||
|
|
||||||
|
'leg_presence' => 'Présence Pep',
|
||||||
'leg_school_holidays' => 'Vacances Scolaires',
|
'leg_school_holidays' => 'Vacances Scolaires',
|
||||||
'leg_public_holiday' => 'Férié',
|
'leg_public_holiday' => 'Férié',
|
||||||
'leg_off_carole' => 'Off Carole',
|
'leg_off_carole' => 'Off Carole',
|
||||||
@@ -146,7 +148,6 @@ return [
|
|||||||
'leg_centre' => 'Centre',
|
'leg_centre' => 'Centre',
|
||||||
'leg_avis' => 'Avis',
|
'leg_avis' => 'Avis',
|
||||||
'leg_pep_sick' => 'Pep Malade',
|
'leg_pep_sick' => 'Pep Malade',
|
||||||
'leg_presence' => 'Présence',
|
|
||||||
|
|
||||||
'vac_toussaint' => 'Vacances de la Toussaint',
|
'vac_toussaint' => 'Vacances de la Toussaint',
|
||||||
'vac_noel' => 'Vacances de Noël',
|
'vac_noel' => 'Vacances de Noël',
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-container pf-login-wrapper">
|
<div class="pf-container pf-login-wrapper">
|
||||||
<div class="pf-login-card">
|
<div class="pf-login-card">
|
||||||
|
|
||||||
<div class="pf-login-header">
|
<header class="pf-login-header">
|
||||||
<img src="/favicon.png" alt="PachaFamily Logo" class="pf-login-icon">
|
<img src="/favicon.png" alt="PachaFamily Logo" class="pf-login-icon">
|
||||||
<h1><?= tr('login_header') ?></h1>
|
<h1><?= tr('login_header') ?></h1>
|
||||||
<p><?= tr('login_subtitle') ?></p>
|
<p><?= tr('login_subtitle') ?></p>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<?php if ($error): ?>
|
<?php if ($error): ?>
|
||||||
<div class="pf-login-error">
|
<div class="pf-login-error">
|
||||||
@@ -60,12 +60,15 @@ require __DIR__ . '/header.php';
|
|||||||
<form method="post" action="/login.php<?= isset($_GET['redirect']) ? '?redirect=' . urlencode($_GET['redirect']) : '' ?>">
|
<form method="post" action="/login.php<?= isset($_GET['redirect']) ? '?redirect=' . urlencode($_GET['redirect']) : '' ?>">
|
||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label" for="username"><?= tr('label_username') ?></label>
|
<label class="pf-label" for="username"><?= tr('label_username') ?></label>
|
||||||
<input type="text" id="username" name="username" class="pf-input" required autofocus placeholder="<?= tr('placeholder_username') ?>">
|
<input type="text" id="username" name="username" class="pf-input"
|
||||||
|
required autofocus placeholder="<?= tr('placeholder_username') ?>"
|
||||||
|
autocomplete="username" autocapitalize="none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label" for="password"><?= tr('label_password') ?></label>
|
<label class="pf-label" for="password"><?= tr('label_password') ?></label>
|
||||||
<input type="password" id="password" name="password" class="pf-input" required placeholder="••••••">
|
<input type="password" id="password" name="password" class="pf-input"
|
||||||
|
required placeholder="••••••" autocomplete="current-password">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="pf-btn pf-btn-block">
|
<button type="submit" class="pf-btn pf-btn-block">
|
||||||
@@ -77,7 +80,3 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
|
||||||
@@ -601,10 +601,11 @@ $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;">
|
||||||
<div class="pf-modal-header">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||||
<h3 class="pf-modal-title">🏦 <?= tr('bud_update_balance') ?></h3>
|
<h3 class="pf-modal-title" style="margin:0; border:none; padding:0;">🏦 <?= tr('bud_update_balance') ?></h3>
|
||||||
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-modal-close">×</button>
|
<button type="button" onclick="closeSuiviModal('snapshotModal')" style="background:none; border:none; font-size:1.8rem; cursor:pointer; color:#94a3b8; line-height:1;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-modal-body">
|
<div class="pf-modal-body">
|
||||||
<form id="snapshotForm" method="POST">
|
<form id="snapshotForm" method="POST">
|
||||||
<input type="hidden" name="action" value="save_snapshot">
|
<input type="hidden" name="action" value="save_snapshot">
|
||||||
@@ -616,9 +617,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-modal-footer">
|
|
||||||
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn pf-btn-secondary"><?= tr('btn_cancel') ?></button>
|
<div class="modal-footer">
|
||||||
<button type="submit" form="snapshotForm" class="pf-btn pf-btn-primary"><?= tr('btn_save') ?></button>
|
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" form="snapshotForm" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -728,7 +730,9 @@ window.I18N = {
|
|||||||
'bud_confirm_delete': <?= json_encode(tr('bud_confirm_delete')) ?>,
|
'bud_confirm_delete': <?= json_encode(tr('bud_confirm_delete')) ?>,
|
||||||
'bud_to_define_js': <?= json_encode(tr('bud_to_define_js')) ?>,
|
'bud_to_define_js': <?= json_encode(tr('bud_to_define_js')) ?>,
|
||||||
'error_occured': <?= json_encode(tr('error_occured')) ?>,
|
'error_occured': <?= json_encode(tr('error_occured')) ?>,
|
||||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
|
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||||
|
'btn_cancel': <?= json_encode(tr('btn_cancel')) ?>,
|
||||||
|
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Couleurs sémantiques Calendrier */
|
/* Couleurs sémantiques Calendrier */
|
||||||
--c-school-holiday: #e5d9f2;
|
--c-school-holiday: #ede9fe;
|
||||||
--c-public-holiday: #e2e8f0; /* Plus marqué que l'original */
|
--c-public-holiday: repeating-linear-gradient(
|
||||||
--c-off-carole: #ffedd5;
|
45deg,
|
||||||
|
#f8fafc,
|
||||||
|
#f8fafc 8px,
|
||||||
|
#f1f5f9 8px,
|
||||||
|
#f1f5f9 16px
|
||||||
|
);
|
||||||
|
--c-off-carole: #fef3c7;
|
||||||
--c-extra-off: #fee2e2;
|
--c-extra-off: #fee2e2;
|
||||||
--c-selected: #bfdbfe;
|
--c-selected: #dbeafe;
|
||||||
|
|
||||||
/* Thèmes Parents (Sync Budget) */
|
/* Thèmes Parents (Sync Budget) */
|
||||||
--bg-alex: #ecfeff;
|
--bg-alex: #ecfeff;
|
||||||
@@ -575,7 +581,6 @@ td.col-laia-sub {
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
.fc-legend-school-holiday {
|
.fc-legend-school-holiday {
|
||||||
background: var(--c-school-holiday);
|
background: var(--c-school-holiday);
|
||||||
@@ -735,6 +740,15 @@ td.col-laia-sub {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- BOUTON DE MENU ACTIF (Élément déjà enregistré) --- */
|
||||||
|
.fc-menu-btn--active {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: var(--pf-primary);
|
||||||
|
color: var(--pf-primary);
|
||||||
|
/* Un léger inset shadow pour rendre la bordure un peu plus épaisse sans casser la taille */
|
||||||
|
box-shadow: inset 0 0 0 1px var(--pf-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Table specifique modale vacances */
|
/* Table specifique modale vacances */
|
||||||
.fc-holidays-table {
|
.fc-holidays-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -790,6 +804,94 @@ td.col-laia-sub {
|
|||||||
color: #ef4444; /* Devient rouge au survol */
|
color: #ef4444; /* Devient rouge au survol */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
SMART DATE SELECTOR
|
||||||
|
========================================================= */
|
||||||
|
.fc-smart-select {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
|
||||||
|
/* Padding standard */
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-transform: capitalize;
|
||||||
|
box-shadow: var(--pf-shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-smart-select:hover {
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-smart-select:focus {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- BOUTON AUJOURD'HUI & CONTROLES --- */
|
||||||
|
.fc-view-controls {
|
||||||
|
align-items: center; /* Pour bien centrer le nouveau bouton */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-today-button {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid var(--pf-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--pf-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: var(--pf-shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-today-button:hover {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: var(--pf-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-view-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MISE EN ÉVIDENCE DU JOUR EN COURS --- */
|
||||||
|
.fc-day--today {
|
||||||
|
background: #eff6ff !important;
|
||||||
|
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day--today .fc-day-number {
|
||||||
|
display: inline;
|
||||||
|
background: none;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--pf-primary);
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.fc-smart-select {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile */
|
/* Mobile */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pf-family-calendar .pf-container {
|
.pf-family-calendar .pf-container {
|
||||||
@@ -846,4 +948,10 @@ td.col-laia-sub {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin: 0 auto 15px auto;
|
margin: 0 auto 15px auto;
|
||||||
}
|
}
|
||||||
|
.fc-two-months-container,
|
||||||
|
.fc-three-months-container {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 20px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
|
now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
|
||||||
this.modalSelectedYear = this.currentSchoolYearStart;
|
this.modalSelectedYear = this.currentSchoolYearStart;
|
||||||
|
|
||||||
this.setupModalUI(); // Prépare le selecteur d'année dans la modale
|
this.setupModalUI();
|
||||||
|
this.initSmartSelectors();
|
||||||
await this.refreshAllData();
|
await this.refreshAllData();
|
||||||
this.updateSchoolYearLabel();
|
this.updateSchoolYearLabel();
|
||||||
|
|
||||||
@@ -105,14 +106,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
// Modifie dynamiquement le titre de la modale pour y insérer le selecteur d'année
|
// Modifie dynamiquement le titre de la modale pour y insérer le selecteur d'année
|
||||||
setupModalUI() {
|
setupModalUI() {
|
||||||
const headerH2 = document.querySelector(".fc-modal-header h2");
|
const headerTitle = document.querySelector(
|
||||||
if (headerH2 && !document.getElementById("holidayYearSelect")) {
|
"#modalHolidays .pf-modal-title",
|
||||||
headerH2.innerHTML = `${tr("fc_modal_holidays_title")}
|
);
|
||||||
<select id="holidayYearSelect" style="margin-left:15px; font-size:1rem; padding:4px; border-radius:4px; border:1px solid #cbd5e1;">
|
|
||||||
<option value="${this.currentSchoolYearStart - 1}">${this.currentSchoolYearStart - 1} - ${this.currentSchoolYearStart}</option>
|
if (headerTitle && !document.getElementById("holidayYearSelect")) {
|
||||||
<option value="${this.currentSchoolYearStart}" selected>${this.currentSchoolYearStart} - ${this.currentSchoolYearStart + 1}</option>
|
let options = "";
|
||||||
<option value="${this.currentSchoolYearStart + 1}">${this.currentSchoolYearStart + 1} - ${this.currentSchoolYearStart + 2}</option>
|
const currentY = new Date().getFullYear();
|
||||||
<option value="${this.currentSchoolYearStart + 2}">${this.currentSchoolYearStart + 2} - ${this.currentSchoolYearStart + 3}</option>
|
// On affiche de N-2 à N+3 pour avoir un bel historique/futur
|
||||||
|
for (let y = currentY - 2; y <= currentY + 3; y++) {
|
||||||
|
const selected = y === this.currentSchoolYearStart ? "selected" : "";
|
||||||
|
options += `<option value="${y}" ${selected}>${y} - ${y + 1}</option>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerTitle.innerHTML = `🏖️ ${tr("fc_modal_holidays_title")}
|
||||||
|
<select id="holidayYearSelect" class="pf-input" style="width:auto; display:inline-block; margin-left:10px; padding:4px 10px; height:auto;">
|
||||||
|
${options}
|
||||||
</select>`;
|
</select>`;
|
||||||
|
|
||||||
document
|
document
|
||||||
@@ -124,6 +133,40 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initSmartSelectors() {
|
||||||
|
const selectMonth = document.getElementById("fc-select-month");
|
||||||
|
const selectYear = document.getElementById("fc-select-year");
|
||||||
|
if (!selectMonth || !selectYear) return;
|
||||||
|
|
||||||
|
const lang = window.appLang || "fr-FR";
|
||||||
|
|
||||||
|
// 1. Remplir les mois en tenant compte de la langue locale
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const d = new Date(2000, i, 1);
|
||||||
|
const monthName = new Intl.DateTimeFormat(lang, {
|
||||||
|
month: "long",
|
||||||
|
}).format(d);
|
||||||
|
selectMonth.add(new Option(monthName, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remplir les années (de l'année actuelle -2 à +5)
|
||||||
|
const currentY = new Date().getFullYear();
|
||||||
|
for (let y = currentY - 2; y <= currentY + 5; y++) {
|
||||||
|
selectYear.add(new Option(y, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Écouteurs d'événements pour le rechargement en direct
|
||||||
|
const handleChange = () => {
|
||||||
|
const m = parseInt(selectMonth.value);
|
||||||
|
const y = parseInt(selectYear.value);
|
||||||
|
this.currentMonth = new Date(y, m, 1);
|
||||||
|
this.renderMonthCalendar();
|
||||||
|
};
|
||||||
|
|
||||||
|
selectMonth.addEventListener("change", handleChange);
|
||||||
|
selectYear.addEventListener("change", handleChange);
|
||||||
|
}
|
||||||
|
|
||||||
async refreshAllData() {
|
async refreshAllData() {
|
||||||
try {
|
try {
|
||||||
const weeksData = await this.fetchApi(
|
const weeksData = await this.fetchApi(
|
||||||
@@ -202,7 +245,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
renderModalHolidays() {
|
renderModalHolidays() {
|
||||||
if (!this.schoolHolidaysTableBody) return;
|
if (!this.schoolHolidaysTableBody) return;
|
||||||
|
|
||||||
// Filtrer les événements de type VACANCES_SCOLAIRES pour l'année scolaire sélectionnée
|
// On utilise bien l'année de la modale, indépendamment du planning de fond
|
||||||
const startDate = `${this.modalSelectedYear}-09-01`;
|
const startDate = `${this.modalSelectedYear}-09-01`;
|
||||||
const endDate = `${this.modalSelectedYear + 1}-08-31`;
|
const endDate = `${this.modalSelectedYear + 1}-08-31`;
|
||||||
|
|
||||||
@@ -213,61 +256,50 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
e.date <= endDate,
|
e.date <= endDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
// S'il n'y a pas de vacances en base pour cette année, on affiche le bouton "Générer"
|
|
||||||
if (yearHolidays.length === 0) {
|
if (yearHolidays.length === 0) {
|
||||||
this.schoolHolidaysTableBody.innerHTML = `
|
this.schoolHolidaysTableBody.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" style="text-align:center; padding: 30px;">
|
<td colspan="3" style="text-align:center; padding: 30px;">
|
||||||
<p style="color:#64748b; margin-bottom:15px;">Les vacances de cette année ne sont pas encore enregistrées.</p>
|
<p style="color:#64748b; margin-bottom:15px;">${tr("fc_err_no_data_gov")}</p>
|
||||||
<button id="btnFetchGovHolidays" class="pf-btn">Importer depuis l'API Gouvernement</button>
|
<button id="btnFetchGovHolidays" class="pf-btn">${tr("btn_import")}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("btnFetchGovHolidays")
|
.getElementById("btnFetchGovHolidays")
|
||||||
.addEventListener("click", (e) => {
|
?.addEventListener("click", (e) => {
|
||||||
e.target.innerText = "Téléchargement en cours...";
|
e.target.innerText = "...";
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
this.fetchAndSaveGovHolidays(this.modalSelectedYear);
|
this.fetchAndSaveGovHolidays(this.modalSelectedYear);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Tri chronologique strict des jours
|
|
||||||
yearHolidays.sort((a, b) => new Date(a.date) - new Date(b.date));
|
yearHolidays.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||||
|
|
||||||
// 2. Regroupement par blocs de jours consécutifs
|
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
let currentBlock = null;
|
let currentBlock = null;
|
||||||
|
|
||||||
yearHolidays.forEach((e) => {
|
yearHolidays.forEach((e) => {
|
||||||
const d = new Date(e.date + "T00:00:00"); // Force locale
|
const d = new Date(e.date + "T00:00:00");
|
||||||
|
|
||||||
if (!currentBlock) {
|
if (!currentBlock) {
|
||||||
currentBlock = { start: d, end: d };
|
currentBlock = { start: d, end: d };
|
||||||
blocks.push(currentBlock);
|
blocks.push(currentBlock);
|
||||||
} else {
|
} else {
|
||||||
// Calcul de l'écart en jours entre la date actuelle et la fin du bloc en cours
|
const diffDays = Math.round(
|
||||||
const diffTime = d.getTime() - currentBlock.end.getTime();
|
(d.getTime() - currentBlock.end.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
|
);
|
||||||
|
|
||||||
// Si l'écart est minime (<= 4 jours, pour absorber un éventuel week-end non stocké)
|
|
||||||
// on considère qu'on est toujours dans la même période de vacances.
|
|
||||||
if (diffDays <= 4) {
|
if (diffDays <= 4) {
|
||||||
currentBlock.end = d;
|
currentBlock.end = d;
|
||||||
} else {
|
} else {
|
||||||
// Sinon, l'écart est grand : c'est une NOUVELLE période de vacances
|
|
||||||
currentBlock = { start: d, end: d };
|
currentBlock = { start: d, end: d };
|
||||||
blocks.push(currentBlock);
|
blocks.push(currentBlock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Rendu HTML et déduction des noms
|
|
||||||
let html = "";
|
let html = "";
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
// Déduction intelligente du nom selon le mois de départ ET la durée
|
|
||||||
const m = block.start.getMonth() + 1;
|
const m = block.start.getMonth() + 1;
|
||||||
const durationDays =
|
const durationDays =
|
||||||
Math.round(
|
Math.round(
|
||||||
@@ -276,26 +308,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
) + 1;
|
) + 1;
|
||||||
|
|
||||||
let name = tr("leg_school_holidays");
|
let name = tr("leg_school_holidays");
|
||||||
|
if (m === 10 || m === 11) name = tr("vac_toussaint");
|
||||||
if (m === 10 || m === 11) {
|
else if (m === 12 || m === 1) name = tr("vac_noel");
|
||||||
name = tr("vac_toussaint");
|
else if (m === 2 || m === 3) name = tr("vac_hiver");
|
||||||
} else if (m === 12 || m === 1) {
|
else if (m === 4 || (m === 5 && durationDays > 6))
|
||||||
name = tr("vac_noel");
|
|
||||||
} else if (m === 2 || m === 3) {
|
|
||||||
name = tr("vac_hiver");
|
|
||||||
} else if (m === 4 || (m === 5 && durationDays > 6)) {
|
|
||||||
name = tr("vac_printemps");
|
name = tr("vac_printemps");
|
||||||
} else if (m === 5 && durationDays <= 6) {
|
else if (m === 5 && durationDays <= 6) name = tr("vac_ascension");
|
||||||
name = tr("vac_ascension");
|
else if (m === 7 || m === 8) name = tr("vac_ete");
|
||||||
} else if (m === 7 || m === 8) {
|
|
||||||
name = tr("vac_ete");
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>${name}</strong></td>
|
<td><strong>${name}</strong></td>
|
||||||
<td>${block.start.toLocaleDateString("fr-FR")}</td>
|
<td>${block.start.toLocaleDateString(window.appLang || "fr-FR")}</td>
|
||||||
<td>${block.end.toLocaleDateString("fr-FR")}</td>
|
<td>${block.end.toLocaleDateString(window.appLang || "fr-FR")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
@@ -724,37 +749,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
const y = this.currentMonth.getFullYear();
|
const y = this.currentMonth.getFullYear();
|
||||||
const m = this.currentMonth.getMonth();
|
const m = this.currentMonth.getMonth();
|
||||||
const lang = window.I18N_LANG || "fr-FR";
|
|
||||||
const titleEl = document.querySelector("#fc-current-month-year");
|
|
||||||
|
|
||||||
if (titleEl) {
|
const selectMonth = document.getElementById("fc-select-month");
|
||||||
|
const selectYear = document.getElementById("fc-select-year");
|
||||||
|
// On masque définitivement le suffixe encombrant
|
||||||
|
const suffixEl = document.getElementById("fc-multi-month-suffix");
|
||||||
|
|
||||||
|
if (selectMonth && selectYear) {
|
||||||
|
selectMonth.value = m;
|
||||||
|
selectYear.value = y;
|
||||||
|
if (suffixEl) suffixEl.style.display = "none";
|
||||||
|
|
||||||
if (this.viewMode === "3months") {
|
if (this.viewMode === "3months") {
|
||||||
const nextM2 = new Date(y, m + 2, 1);
|
|
||||||
const m1 = new Intl.DateTimeFormat(lang, { month: "short" }).format(
|
|
||||||
this.currentMonth,
|
|
||||||
);
|
|
||||||
const m2 = new Intl.DateTimeFormat(lang, {
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
}).format(nextM2);
|
|
||||||
titleEl.textContent = `${m1} - ${m2}`;
|
|
||||||
this.renderThreeMonthsView();
|
this.renderThreeMonthsView();
|
||||||
} else if (this.viewMode === "2months") {
|
} else if (this.viewMode === "2months") {
|
||||||
const nextM = new Date(y, m + 1, 1);
|
|
||||||
const m1 = new Intl.DateTimeFormat(lang, { month: "short" }).format(
|
|
||||||
this.currentMonth,
|
|
||||||
);
|
|
||||||
const m2 = new Intl.DateTimeFormat(lang, {
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
}).format(nextM);
|
|
||||||
titleEl.textContent = `${m1} - ${m2}`;
|
|
||||||
this.renderTwoMonthsView();
|
this.renderTwoMonthsView();
|
||||||
} else {
|
} else {
|
||||||
titleEl.textContent = new Intl.DateTimeFormat(lang, {
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}).format(this.currentMonth);
|
|
||||||
this.monthCalendar.innerHTML = this.generateMonthHTML(y, m);
|
this.monthCalendar.innerHTML = this.generateMonthHTML(y, m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -930,7 +940,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const iso = this.getLocalIsoDate(dateObj);
|
const iso = this.getLocalIsoDate(dateObj);
|
||||||
|
const todayIso = this.getLocalIsoDate(new Date()); // Date du jour
|
||||||
|
|
||||||
let cls = "fc-month-day";
|
let cls = "fc-month-day";
|
||||||
|
if (iso === todayIso) cls += " fc-day--today"; // On applique la classe !
|
||||||
const dayEvts = this.events.filter((e) => e.date === iso);
|
const dayEvts = this.events.filter((e) => e.date === iso);
|
||||||
|
|
||||||
if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES"))
|
if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES"))
|
||||||
@@ -948,7 +961,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (dayEvts.some((e) => e.type === "AVIS")) cls += " fc-day--avis";
|
if (dayEvts.some((e) => e.type === "AVIS")) cls += " fc-day--avis";
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = `<div style="position:relative; height:100%;">${dayCounter}`;
|
let content = `<div style="position:relative; height:100%;"><span class="fc-day-number">${dayCounter}</span>`;
|
||||||
if (dayEvts.some((e) => e.type === "PEP_SICK"))
|
if (dayEvts.some((e) => e.type === "PEP_SICK"))
|
||||||
content += `<span style="position:absolute; bottom:2px; right:2px;">🤒</span>`;
|
content += `<span style="position:absolute; bottom:2px; right:2px;">🤒</span>`;
|
||||||
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
||||||
@@ -1101,7 +1114,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_off_carole")}</span><span class="fc-summary-value">${parseFloat(stats.off.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_off_carole")}</span><span class="fc-summary-value">${parseFloat(stats.off.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
||||||
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_extra_off")}</span><span class="fc-summary-value">${parseFloat(stats.extra.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_extra_off")}</span><span class="fc-summary-value">${parseFloat(stats.extra.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
||||||
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_pep_sick")}</span><span class="fc-summary-value">${parseFloat(stats.sick.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_pep_sick")}</span><span class="fc-summary-value">${parseFloat(stats.sick.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
||||||
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_presence")} Pep</span><span class="fc-summary-value">${parseFloat(stats.pep.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
<div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_presence")}</span><span class="fc-summary-value">${parseFloat(stats.pep.toFixed(1))} ${tr("fc_unit_days")}</span></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,6 +1201,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
document.addEventListener("touchend", (e) => this.handleTouchEnd(e));
|
document.addEventListener("touchend", (e) => this.handleTouchEnd(e));
|
||||||
}
|
}
|
||||||
|
const scrollWrapper = document.getElementById("planningTable-wrapper");
|
||||||
|
if (scrollWrapper) {
|
||||||
|
scrollWrapper.addEventListener("scroll", async () => {
|
||||||
|
// Bloquer si on est déjà en train de charger
|
||||||
|
if (this._isAutoLoading) return;
|
||||||
|
|
||||||
|
// Détection du bas (On descend dans le temps : passage à l'année suivante)
|
||||||
|
// On met une tolérance de 5px pour les calculs de pixels décimaux
|
||||||
|
if (
|
||||||
|
scrollWrapper.scrollTop + scrollWrapper.clientHeight >=
|
||||||
|
scrollWrapper.scrollHeight - 5
|
||||||
|
) {
|
||||||
|
this._isAutoLoading = true;
|
||||||
|
await this.changeSchoolYear(1);
|
||||||
|
// On replace le scroll tout en haut pour la continuité visuelle
|
||||||
|
scrollWrapper.scrollTop = 5;
|
||||||
|
setTimeout(() => (this._isAutoLoading = false), 500);
|
||||||
|
}
|
||||||
|
// Détection du haut (On remonte le temps : passage à l'année précédente)
|
||||||
|
else if (scrollWrapper.scrollTop === 0) {
|
||||||
|
this._isAutoLoading = true;
|
||||||
|
await this.changeSchoolYear(-1);
|
||||||
|
// On replace le scroll tout en bas pour la continuité visuelle
|
||||||
|
scrollWrapper.scrollTop =
|
||||||
|
scrollWrapper.scrollHeight - scrollWrapper.clientHeight - 5;
|
||||||
|
setTimeout(() => (this._isAutoLoading = false), 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- GESTION MODALE VACANCES SCOLAIRES ---
|
// --- GESTION MODALE VACANCES SCOLAIRES ---
|
||||||
const btnOpen = document.getElementById("btnOpenHolidays");
|
const btnOpen = document.getElementById("btnOpenHolidays");
|
||||||
@@ -1318,6 +1360,66 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
|
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
|
||||||
this.renderMonthCalendar();
|
this.renderMonthCalendar();
|
||||||
});
|
});
|
||||||
|
// --- BOUTON AUJOURD'HUI ---
|
||||||
|
document.getElementById("fc-today-btn")?.addEventListener("click", () => {
|
||||||
|
this.currentMonth = new Date();
|
||||||
|
this.currentMonth.setDate(1); // Force au 1er du mois pour éviter les bugs de fins de mois
|
||||||
|
this.renderMonthCalendar();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- GESTION DU SWIPE MENSUEL (MOBILE) ---
|
||||||
|
if (this.monthCalendar) {
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
|
||||||
|
// On écoute le début du toucher
|
||||||
|
this.monthCalendar.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
(e) => {
|
||||||
|
touchStartX = e.changedTouches[0].screenX;
|
||||||
|
touchStartY = e.changedTouches[0].screenY;
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// On écoute la fin du toucher
|
||||||
|
this.monthCalendar.addEventListener(
|
||||||
|
"touchend",
|
||||||
|
(e) => {
|
||||||
|
const touchEndX = e.changedTouches[0].screenX;
|
||||||
|
const touchEndY = e.changedTouches[0].screenY;
|
||||||
|
|
||||||
|
const deltaX = touchEndX - touchStartX;
|
||||||
|
const deltaY = touchEndY - touchStartY;
|
||||||
|
|
||||||
|
// Détection d'un swipe horizontal (on exclut les swipes verticaux de défilement)
|
||||||
|
// Seuil de 50px pour éviter les déclenchements par erreur
|
||||||
|
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
|
||||||
|
const numMonths =
|
||||||
|
this.viewMode === "2months"
|
||||||
|
? 2
|
||||||
|
: this.viewMode === "3months"
|
||||||
|
? 3
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
if (deltaX < 0) {
|
||||||
|
// Swipe vers la GAUCHE = Aller dans le FUTUR (Mois suivants)
|
||||||
|
this.currentMonth.setMonth(
|
||||||
|
this.currentMonth.getMonth() + numMonths,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Swipe vers la DROITE = Revenir dans le PASSÉ (Mois précédents)
|
||||||
|
this.currentMonth.setMonth(
|
||||||
|
this.currentMonth.getMonth() - numMonths,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderMonthCalendar(); // Rafraîchit l'UI
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
document
|
document
|
||||||
.getElementById("fc-prev-school-year")
|
.getElementById("fc-prev-school-year")
|
||||||
?.addEventListener("click", () => this.changeSchoolYear(-1));
|
?.addEventListener("click", () => this.changeSchoolYear(-1));
|
||||||
@@ -1427,16 +1529,43 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
this._currentBulkInfo = { dates };
|
this._currentBulkInfo = { dates };
|
||||||
|
|
||||||
|
// --- 🔍 DÉTECTION DES ÉVÉNEMENTS & CONGÉS EXISTANTS ---
|
||||||
|
const activeEvents = new Set();
|
||||||
|
this.events.forEach((e) => {
|
||||||
|
if (dates.includes(e.date)) activeEvents.add(e.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLeaves = { 2: new Set(), 3: new Set() };
|
||||||
|
this.leaves.forEach((l) => {
|
||||||
|
if (dates.includes(l.leave_date)) {
|
||||||
|
if (activeLeaves[l.person_id])
|
||||||
|
activeLeaves[l.person_id].add(l.leave_type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Petites fonctions utilitaires pour gérer l'UI du bouton
|
||||||
|
const getBtnClass = (type, personId = null) => {
|
||||||
|
let isActive = personId
|
||||||
|
? activeLeaves[personId] && activeLeaves[personId].has(type)
|
||||||
|
: activeEvents.has(type);
|
||||||
|
return isActive ? "fc-menu-btn--active" : "";
|
||||||
|
};
|
||||||
|
const getBtnIcon = (type, personId = null) => {
|
||||||
|
let isActive = personId
|
||||||
|
? activeLeaves[personId] && activeLeaves[personId].has(type)
|
||||||
|
: activeEvents.has(type);
|
||||||
|
return isActive ? "✓ " : "";
|
||||||
|
};
|
||||||
|
|
||||||
// Gestion de la date affichée (Singulier/Pluriel)
|
// Gestion de la date affichée (Singulier/Pluriel)
|
||||||
const dateLabel =
|
const dateLabel =
|
||||||
dates.length > 1
|
dates.length > 1
|
||||||
? `${dates.length} ${tr("fc_unit_days")}`
|
? `${dates.length} ${tr("fc_unit_days")}`
|
||||||
: new Date(dates[0]).toLocaleDateString(window.I18N_LANG || "fr-FR");
|
: new Date(dates[0]).toLocaleDateString(window.I18N_LANG || "fr-FR");
|
||||||
|
|
||||||
// Icône SVG Poubelle (Uniformisée)
|
// Icône SVG Poubelle
|
||||||
const trashSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>`;
|
const trashSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>`;
|
||||||
|
|
||||||
// Fonction utilitaire interne pour les en-têtes de section avec suppression
|
|
||||||
const buildHeader = (title, action, cat) => `
|
const buildHeader = (title, action, cat) => `
|
||||||
<div class="fc-menu-header">
|
<div class="fc-menu-header">
|
||||||
<strong>${title}</strong>
|
<strong>${title}</strong>
|
||||||
@@ -1444,36 +1573,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 1. En-tête du menu (Date ou nombre de jours)
|
|
||||||
let html = `<div class="fc-menu-section" style="border-bottom: none; padding-bottom: 0;">
|
let html = `<div class="fc-menu-section" style="border-bottom: none; padding-bottom: 0;">
|
||||||
<strong style="font-size:0.85rem; color:var(--text-main); margin-bottom: 4px;">${dateLabel}</strong>
|
<strong style="font-size:0.85rem; color:var(--text-main); margin-bottom: 4px;">${dateLabel}</strong>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// 2. Section Carole (Congés)
|
// 2. Section Carole
|
||||||
html += `<div class="fc-menu-section">
|
html += `<div class="fc-menu-section">
|
||||||
${buildHeader(tr("fc_menu_carole"), "clear-type", "CONGE")}
|
${buildHeader(tr("fc_menu_carole"), "clear-type", "CONGE")}
|
||||||
<div class="fc-menu-grid">
|
<div class="fc-menu-grid">
|
||||||
<button class="fc-menu-btn" data-action="add" data-type="OFF_CAROLE" data-person="Carole">${tr("btn_off")}</button>
|
<button class="fc-menu-btn ${getBtnClass("OFF_CAROLE")}" data-action="add" data-type="OFF_CAROLE" data-person="Carole">${getBtnIcon("OFF_CAROLE")}${tr("btn_off")}</button>
|
||||||
<button class="fc-menu-btn" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">${tr("btn_extra")}</button>
|
<button class="fc-menu-btn ${getBtnClass("EXTRA_OFF_CAROLE")}" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">${getBtnIcon("EXTRA_OFF_CAROLE")}${tr("btn_extra")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// 3. Section Garde (Centre / Avis)
|
// 3. Section Garde
|
||||||
html += `<div class="fc-menu-section">
|
html += `<div class="fc-menu-section">
|
||||||
${buildHeader(tr("leg_centre"), "clear-type", "GARDE")}
|
${buildHeader(tr("leg_centre"), "clear-type", "GARDE")}
|
||||||
<div class="fc-menu-grid">
|
<div class="fc-menu-grid">
|
||||||
<button class="fc-menu-btn" data-action="add" data-type="CENTRE">${tr("leg_centre")}</button>
|
<button class="fc-menu-btn ${getBtnClass("CENTRE")}" data-action="add" data-type="CENTRE">${getBtnIcon("CENTRE")}${tr("leg_centre")}</button>
|
||||||
<button class="fc-menu-btn" data-action="add" data-type="AVIS">${tr("leg_avis")}</button>
|
<button class="fc-menu-btn ${getBtnClass("AVIS")}" data-action="add" data-type="AVIS">${getBtnIcon("AVIS")}${tr("leg_avis")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// 4. Section Pep (Maladie)
|
// 4. Section Pep
|
||||||
html += `<div class="fc-menu-section">
|
html += `<div class="fc-menu-section">
|
||||||
${buildHeader("Pep", "clear-type", "PEP")}
|
${buildHeader("Pep", "clear-type", "PEP")}
|
||||||
<button class="fc-menu-btn" data-action="add" data-type="PEP_SICK" style="width:100%">${tr("leg_pep_sick")} 🤒</button>
|
<button class="fc-menu-btn ${getBtnClass("PEP_SICK")}" data-action="add" data-type="PEP_SICK" style="width:100%">${getBtnIcon("PEP_SICK")}${tr("leg_pep_sick")} 🤒</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// 5. Section Enfants (Alex & Laia)
|
// 5. Section Enfants
|
||||||
html += `<div class="fc-menu-section">
|
html += `<div class="fc-menu-section">
|
||||||
${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
|
${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
|
||||||
<div class="fc-menu-leaves-table">
|
<div class="fc-menu-leaves-table">
|
||||||
@@ -1482,14 +1610,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<div class="fc-th-inline">
|
<div class="fc-th-inline">
|
||||||
Alex
|
Alex <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="${tr("fc_clear")} Alex">${trashSvg}</button>
|
||||||
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="${tr("fc_clear")} Alex">${trashSvg}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<div class="fc-th-inline">
|
<div class="fc-th-inline">
|
||||||
Laia
|
Laia <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="${tr("fc_clear")} Laia">${trashSvg}</button>
|
||||||
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="${tr("fc_clear")} Laia">${trashSvg}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1498,8 +1624,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
["CP", "JRA", "JA"].forEach((t) => {
|
["CP", "JRA", "JA"].forEach((t) => {
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td><button class="fc-menu-btn" data-action="add-leave" data-pid="2" data-type="${t}">${t}</button></td>
|
<td><button class="fc-menu-btn ${getBtnClass(t, 2)}" data-action="add-leave" data-pid="2" data-type="${t}">${getBtnIcon(t, 2)}${t}</button></td>
|
||||||
<td><button class="fc-menu-btn" data-action="add-leave" data-pid="3" data-type="${t}">${t}</button></td>
|
<td><button class="fc-menu-btn ${getBtnClass(t, 3)}" data-action="add-leave" data-pid="3" data-type="${t}">${getBtnIcon(t, 3)}${t}</button></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,3 +46,8 @@
|
|||||||
[2026-05-06T10:14:34+02:00] RAW INPUT: [{"date":"2026-02-23","type":"CENTRE","duration":1,"person":"Carole"}]
|
[2026-05-06T10:14:34+02:00] RAW INPUT: [{"date":"2026-02-23","type":"CENTRE","duration":1,"person":"Carole"}]
|
||||||
[2026-05-06T10:16:01+02:00] RAW INPUT: [{"date":"2026-01-19","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
[2026-05-06T10:16:01+02:00] RAW INPUT: [{"date":"2026-01-19","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||||
[2026-05-06T10:16:57+02:00] RAW INPUT: [{"date":"2026-03-19","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
[2026-05-06T10:16:57+02:00] RAW INPUT: [{"date":"2026-03-19","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||||
|
[2026-05-06T20:38:19+02:00] RAW INPUT: [{"date":"2026-05-08","type":"AVIS","duration":1,"person":"Carole"}]
|
||||||
|
[2026-05-06T20:38:22+02:00] RAW INPUT: [{"date":"2026-05-08","type":"CENTRE","duration":1,"person":"Carole"}]
|
||||||
|
[2026-05-06T20:38:24+02:00] RAW INPUT: [{"date":"2026-05-08","type":"AVIS","duration":1,"person":"Carole"}]
|
||||||
|
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
|
||||||
|
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||||
|
|||||||
@@ -115,87 +115,80 @@ function closeHolidayModal() {
|
|||||||
function editHoliday(data) {
|
function editHoliday(data) {
|
||||||
const h = data.main;
|
const h = data.main;
|
||||||
const modal = document.getElementById("holidayModal");
|
const modal = document.getElementById("holidayModal");
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
if (!modal) {
|
|
||||||
alert(tr("err_modal_missing"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
|
|
||||||
if (typeof openHolidayModal === "function") {
|
|
||||||
openHolidayModal("edit");
|
openHolidayModal("edit");
|
||||||
}
|
|
||||||
|
|
||||||
modal.classList.add("open");
|
|
||||||
modal.style.setProperty("display", "flex", "important");
|
|
||||||
modal.style.setProperty("z-index", "999999", "important");
|
|
||||||
document.body.classList.add("no-scroll");
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.getElementById("inp_id").value = h.id;
|
document.getElementById("inp_id").value = h.id;
|
||||||
document.getElementById("inp_title").value = h.title;
|
document.getElementById("inp_title").value = h.title;
|
||||||
document.getElementById("inp_status").value = h.status;
|
document.getElementById("inp_status").value = h.status;
|
||||||
document.getElementById("inp_period").value = h.period_hint || "";
|
document.getElementById("inp_period").value = h.period_hint || "";
|
||||||
document.getElementById("inp_start").value = h.start_date || "";
|
document.getElementById("inp_start").value = h.start_date || "";
|
||||||
document.getElementById("inp_end").value = h.end_date || "";
|
document.getElementById("inp_end").value = h.end_date || "";
|
||||||
|
|
||||||
document.getElementById("inp_food").value =
|
document.getElementById("inp_food").value =
|
||||||
h.budget_food > 0 ? h.budget_food : "";
|
h.budget_food > 0 ? h.budget_food : "";
|
||||||
document.getElementById("inp_extra").value =
|
document.getElementById("inp_extra").value =
|
||||||
h.budget_extra > 0 ? h.budget_extra : "";
|
h.budget_extra > 0 ? h.budget_extra : "";
|
||||||
document.getElementById("inp_notes").value = h.notes || "";
|
document.getElementById("inp_notes").value = h.notes || "";
|
||||||
} catch (err) {
|
|
||||||
console.error("Erreur champs textes :", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.getElementById("list_transport").innerHTML = "";
|
document.getElementById("list_transport").innerHTML = "";
|
||||||
document.getElementById("list_accommodation").innerHTML = "";
|
document.getElementById("list_accommodation").innerHTML = "";
|
||||||
document.getElementById("list_activity").innerHTML = "";
|
document.getElementById("list_activity").innerHTML = "";
|
||||||
|
|
||||||
if (data.items && data.items.length > 0) {
|
if (data.items) {
|
||||||
data.items.forEach((item) => {
|
data.items.forEach((item) => {
|
||||||
if (
|
if (item.name !== "PF_TECHNICAL_POINT") {
|
||||||
typeof addItem === "function" &&
|
// On passe maintenant l'ID et le lieu à addItem
|
||||||
item.name !== "PF_TECHNICAL_POINT"
|
addItem(
|
||||||
) {
|
item.category,
|
||||||
addItem(item.category, item.name, item.amount, item.is_paid);
|
item.name,
|
||||||
|
item.amount,
|
||||||
|
item.is_paid,
|
||||||
|
item.id,
|
||||||
|
item.location_name || "",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("Erreur listes :", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE ---
|
// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE ---
|
||||||
|
|
||||||
function addItem(category, name = "", amount = "", isPaid = 0) {
|
function addItem(
|
||||||
|
category,
|
||||||
|
name = "",
|
||||||
|
amount = "",
|
||||||
|
isPaid = 0,
|
||||||
|
id = "",
|
||||||
|
location = "",
|
||||||
|
) {
|
||||||
const container = document.getElementById("list_" + category);
|
const container = document.getElementById("list_" + category);
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
div.className = "savings-line-item"; // Utilisation de ta classe existante
|
||||||
div.style.display = "flex";
|
|
||||||
div.style.gap = "8px";
|
|
||||||
div.style.alignItems = "center";
|
|
||||||
div.style.marginBottom = "10px";
|
div.style.marginBottom = "10px";
|
||||||
|
|
||||||
const checkedAttr = isPaid == 1 ? "checked" : "";
|
const checkedAttr = isPaid == 1 ? "checked" : "";
|
||||||
|
// Optimisation : Affichage du badge d'étape si existant
|
||||||
|
const locationBadge = location
|
||||||
|
? `<span style="font-size:0.65rem; background:#e2e8f0; padding:2px 6px; border-radius:4px; margin-right:5px; color:#64748b; font-weight:bold;">📍 ${location}</span>`
|
||||||
|
: "";
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
|
<input type="hidden" name="items[id][]" value="${id}">
|
||||||
<input type="hidden" name="items[cat][]" value="${category}">
|
<input type="hidden" name="items[cat][]" value="${category}">
|
||||||
<input type="text" name="items[name][]" class="pf-input" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}" style="flex: 2; padding: 8px; font-size:0.9rem;" required>
|
<input type="hidden" name="items[location][]" value="${location}">
|
||||||
<input type="number" step="0.01" name="items[amount][]" class="pf-input" placeholder="0.00" value="${amount}" style="width: 80px; text-align: right; padding: 8px; font-size:0.9rem;">
|
<div style="flex: 2; display:flex; flex-direction:column; gap:4px;">
|
||||||
|
${locationBadge}
|
||||||
|
<input type="text" name="items[name][]" class="pf-input" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}" style="padding: 8px; font-size:0.9rem;" required>
|
||||||
|
</div>
|
||||||
|
<input type="number" step="0.01" name="items[amount][]" class="pf-input" placeholder="0.00" value="${amount}" style="width: 90px; text-align: right; padding: 8px; font-size:0.9rem;">
|
||||||
<label title="${tr("hdl_paid")}" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
|
<label title="${tr("hdl_paid")}" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
|
||||||
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="margin:0;">
|
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="margin: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()" class="btn-icon-action delete" title="${tr("btn_delete")}">
|
<button type="button" onclick="this.parentElement.remove()" class="btn-icon-action delete" title="${tr("btn_delete")}">🗑️</button>
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,24 +39,42 @@ try {
|
|||||||
$id = $pdo->lastInsertId();
|
$id = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GESTION DES ITEMS GLOBAUX (On ne supprime QUE les items qui n'ont pas de lieu défini !)
|
// GESTION INTELLIGENTE DES ITEMS
|
||||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND (location_name IS NULL OR location_name = '')")->execute([$id]);
|
|
||||||
|
|
||||||
if (!empty($_POST['items']['name'])) {
|
if (!empty($_POST['items']['name'])) {
|
||||||
$stmtItem = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
|
|
||||||
$count = count($_POST['items']['name']);
|
$count = count($_POST['items']['name']);
|
||||||
|
// On prépare une requête qui met à jour si l'ID existe, sinon insère
|
||||||
|
$stmtItem = $pdo->prepare("
|
||||||
|
INSERT INTO pf_holidays_items (id, holiday_id, category, name, amount, is_paid, location_name)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
category = VALUES(category),
|
||||||
|
name = VALUES(name),
|
||||||
|
amount = VALUES(amount),
|
||||||
|
is_paid = VALUES(is_paid)
|
||||||
|
");
|
||||||
|
|
||||||
|
$keepIds = [];
|
||||||
for ($i = 0; $i < $count; $i++) {
|
for ($i = 0; $i < $count; $i++) {
|
||||||
// Utilisation de "?? ''" pour sécuriser si la donnée n'est pas envoyée
|
$itemId = !empty($_POST['items']['id'][$i]) ? (int)$_POST['items']['id'][$i] : null;
|
||||||
$cat = $_POST['items']['cat'][$i] ?? '';
|
$cat = $_POST['items']['cat'][$i] ?? 'activity';
|
||||||
$name = trim($_POST['items']['name'][$i] ?? '');
|
$name = trim($_POST['items']['name'][$i] ?? '');
|
||||||
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
|
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
|
||||||
$paid = isset($_POST['items']['paid'][$i]) ? $_POST['items']['paid'][$i] : 0;
|
$paid = (int)($_POST['items']['paid'][$i] ?? 0);
|
||||||
|
$loc = !empty($_POST['items']['location'][$i]) ? $_POST['items']['location'][$i] : null;
|
||||||
|
|
||||||
if (!empty($name)) {
|
if (!empty($name)) {
|
||||||
$stmtItem->execute([$id, $cat, $name, $amount, $paid]);
|
$stmtItem->execute([$itemId, $id, $cat, $name, $amount, $paid, $loc]);
|
||||||
|
$keepIds[] = $itemId ?: $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nettoyage : On supprime les items qui ont été retirés de la modale
|
||||||
|
// (Attention : uniquement ceux du voyage actuel qui ne sont plus dans la liste envoyée)
|
||||||
|
if (!empty($keepIds)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($keepIds), '?'));
|
||||||
|
$sqlDel = "DELETE FROM pf_holidays_items WHERE holiday_id = ? AND id NOT IN ($placeholders)";
|
||||||
|
$pdo->prepare($sqlDel)->execute(array_merge([$id], $keepIds));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ if ($id === 0) {
|
|||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT h.*,
|
SELECT h.*,
|
||||||
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
||||||
((SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) + (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)) as total_paid,
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
||||||
FROM pf_holidays h WHERE h.id = ?
|
FROM pf_holidays h WHERE h.id = ?
|
||||||
");
|
");
|
||||||
@@ -391,6 +391,8 @@ window.I18N = {
|
|||||||
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
|
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
|
||||||
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
|
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
|
||||||
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
||||||
|
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
||||||
|
|
||||||
|
|
||||||
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
||||||
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ if ($selectedYear !== 'all') {
|
|||||||
$sql = "
|
$sql = "
|
||||||
SELECT h.*,
|
SELECT h.*,
|
||||||
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE(SUM(hi.amount), 0)) as total_cost,
|
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE(SUM(hi.amount), 0)) as total_cost,
|
||||||
((SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) +
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)) as total_paid,
|
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
||||||
FROM pf_holidays h
|
FROM pf_holidays h
|
||||||
LEFT JOIN pf_holidays_items hi ON h.id = hi.holiday_id
|
LEFT JOIN pf_holidays_items hi ON h.id = hi.holiday_id
|
||||||
@@ -113,6 +112,7 @@ window.I18N = {
|
|||||||
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
|
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
|
||||||
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
|
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
|
||||||
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
|
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
|
||||||
|
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
||||||
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>"
|
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2324
-1138
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user