Centralisation des constantes
This commit is contained in:
@@ -1,64 +1,58 @@
|
|||||||
# 🦙 PachaFamily - Dashboard Familial Sur-Mesure
|
🦙 PachaFamily OS
|
||||||
|
Système de gestion familiale sur-mesure : Budget, Calendrier, Voyages et Cadeaux.
|
||||||
|
|
||||||
PachaFamily est une application web privée de gestion familiale. Développée sans framework lourd (Vanilla JS / PHP Natif), elle garantit des performances optimales, une maintenance simplifiée et une personnalisation totale.
|
📝 Présentation du projet
|
||||||
|
PachaFamily est une application web privée conçue pour centraliser l'organisation de la tribu. Elle repose sur une stack légère (PHP natif / Vanilla JS) pour garantir rapidité et portabilité, avec un support complet de l'internationalisation (FR/CA).
|
||||||
|
|
||||||
## 🌟 Modules & Fonctionnalités
|
🛠️ Fonctionnalités Principales
|
||||||
|
💰 Module Budget
|
||||||
|
Suivi Mensuel : Interface de gestion des dépenses réelles avec import CSV bancaire et catégorisation automatique.
|
||||||
|
|
||||||
### 💰 1. Module Budget (`/modules/budget/`)
|
Budget Prévisionnel : Planification des revenus et répartition automatique vers les différents comptes (Commune, Livrets, Épargne).
|
||||||
|
|
||||||
Gestion asynchrone (SPA-like) des finances de la famille.
|
Épargne : Visualisation des soldes bancaires et ventilation par postes de dépenses avec mode "Addition Rapide".
|
||||||
|
|
||||||
- **Suivi Mensuel** : Saisie rapide des dépenses avec répartition automatique (Alex/Laia/Commun).
|
📅 Calendrier Familial
|
||||||
- **Budget Prévisionnel** : Configuration des salaires, charges fixes, et calcul des restes à vivre. Mode "Calculatrice" flottant intégré.
|
Planning Hebdomadaire : Gestion visuelle des modes de garde, des congés enfants et de la maladie.
|
||||||
- **Épargne** : Suivi des cagnottes par personne et par catégorie (Vacances, Nens, Perso).
|
|
||||||
- **Récapitulatif** : Bilan annuel (À venir).
|
|
||||||
|
|
||||||
### 📅 2. Module Calendrier (`/modules/family-calendar/`)
|
Gestion des Congés (CP/JRA/JA) : Calcul automatique des soldes restants avec snapshots de correction.
|
||||||
|
|
||||||
Agenda partagé pour toute la famille.
|
Vacances Scolaires : Intégration automatique via l'API Data.Education (Zone C).
|
||||||
|
|
||||||
- Affichage des événements et vacances scolaires.
|
🏖️ Holidays & Roadtrips
|
||||||
- Gestion des jours de congés avec compteurs annuels par personne.
|
Gestion de Voyages : Planification avec statuts (Brouillon, Confirmé, Terminé).
|
||||||
|
|
||||||
### 🌴 3. Module Voyages (`/modules/holidays/`)
|
Moteur de Roadtrip : Cartographie interactive (Leaflet), itinéraires (OSRM) et gestion des étapes.
|
||||||
|
|
||||||
Planificateur de roadtrips et vacances.
|
Météo Intelligente : Prévisions réelles ou archives historiques (Open-Meteo).
|
||||||
|
|
||||||
- **Cartographie** : Intégration de Leaflet.js.
|
🚀 Roadmap d'Optimisation & Debug (Incrémental)
|
||||||
- **Itinéraires** : Calcul des trajets via OSRM et géocodage via Nominatim.
|
Étape 1 : Fondations & Robustesse (Terminé ✅)
|
||||||
- Checkpoints, notes et liaisons directes avec le budget d'épargne.
|
[x] Moteur AJAX : Création de pachaFetch pour gérer proprement le JSON et les erreurs PHP.
|
||||||
|
|
||||||
### 🎁 4. Module Cadeaux (`/modules/gift-list/`)
|
[x] Correction "Action" : Sécurisation des formulaires via getAttribute('action').
|
||||||
|
|
||||||
Listes de souhaits pour les événements (Noël, Anniversaires, Rois Mages...).
|
[x] Centralisation : Création de config.php pour les IDs (Alex/Laia) et constantes globales.
|
||||||
|
|
||||||
---
|
Étape 2 : UX & Mobile (En cours 🏃♂️)
|
||||||
|
[ ] Harmonisation Mobile : Généralisation des "Bottom Sheets" pour les modales sur smartphone.
|
||||||
|
|
||||||
## 🚀 Plan d'Optimisation & Debugging (Roadmap)
|
[ ] Z-Index Master : Audit des priorités d'affichage des menus contextuels.
|
||||||
|
|
||||||
Cette section trace les étapes pour épurer la dette technique et optimiser l'application.
|
Étape 3 : Performance & Backend
|
||||||
|
[ ] Audit SQL : Remplacement des requêtes en boucle par des jointures.
|
||||||
|
|
||||||
### 🟢 Phase 1 : Quick Wins (Gains rapides)
|
[ ] Cache Cleanup : Script de nettoyage pour le cache de géocodage.
|
||||||
|
|
||||||
- [x] **Refonte Asynchrone Budget** : Remplacement des formulaires classiques par des appels `fetch()` silencieux sur `suivi.php`, `budget_prev.php` et `epargne.php`.
|
📌 Patch / Versioning
|
||||||
- [ ] **Nettoyage des Vues** : Extraire le JavaScript et le CSS encore présents dans les fichiers `.php` vers leurs fichiers `.js` et `.css` dédiés.
|
|
||||||
- [ ] **Corrections des liens GET** : Remplacer les `<a href="?action=delete">` par des `<button onclick="deleteItem()">` asynchrones partout.
|
|
||||||
|
|
||||||
### 🟡 Phase 2 : Robustesse & UX (Effort moyen)
|
- **v1.1.0 :** Refonte de l'expérience mobile. Toutes les modales de l'application sont désormais des "Bottom Sheets" sur smartphone.
|
||||||
|
v1.0.4 : Centralisation des constantes (IDs, devises) dans includes/config.php et injection dans window.CONFIG.
|
||||||
|
|
||||||
- [ ] **Déploiement des Tests E2E** : Étendre `test-engine.js` pour couvrir le Calendrier et les Voyages.
|
v1.0.3 : Migration du module Calendrier vers pachaFetch.
|
||||||
- [ ] **Uniformisation Mobile** : Vérifier tous les `hover` et `z-index` pour garantir que les actions (édition/suppression) soient cliquables sur écrans tactiles.
|
|
||||||
- [ ] **Gestion des Erreurs JSON** : Appliquer systématiquement le `.text()` avant `JSON.parse()` dans les fetchs pour capturer les erreurs PHP (Warnings).
|
|
||||||
|
|
||||||
### 🔴 Phase 3 : Architecture & Performances (Heavy lifting)
|
v1.0.2 : Correction systématique du bug form.action dans les fichiers Budget.
|
||||||
|
|
||||||
- [ ] **Optimisation SQL (N+1)** : Réviser les requêtes du module Calendrier pour réduire le nombre d'appels à la base de données.
|
v1.0.1 : Création de l'utilitaire global pachaFetch dans le header.
|
||||||
- [ ] **Mise en cache API Map** : Implémenter un système de cache local pour les requêtes OSRM/Nominatim afin d'éviter les limites de requêtes (Rate Limits) sur le module Voyages.
|
|
||||||
- [ ] **Routeur PHP Global** : Remplacer la navigation par `?tab=` par un routeur natif centralisé (Front Controller).
|
|
||||||
|
|
||||||
---
|
v1.0.0 : Version stable initiale.
|
||||||
|
|
||||||
## 🏷️ Patch / Versions
|
|
||||||
|
|
||||||
- **v1.1.0 - Le Grand Bond Asynchrone** (Mai 2026) : Transformation des vues Budget (Prévisionnel & Épargne) en interfaces asynchrones (Fetch API). Création d'un moteur de tests E2E maison (`test-engine.js`). Résolution des conflits de `z-index` sur mobile. Sécurisation des JSON face aux Warnings PHP.
|
|
||||||
- **v1.0.0 - MVP Initial** : Mise en place de la structure de base, base de données MariaDB, modules Budget (Suivi), Calendrier, Cadeaux et Voyages (Leaflet).
|
|
||||||
|
|||||||
+9
-7
@@ -39,16 +39,18 @@ require __DIR__ . '/header.php';
|
|||||||
<button id="btnCloseHolidays" class="fc-modal-close">×</button>
|
<button id="btnCloseHolidays" class="fc-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="fc-modal-body">
|
<div class="fc-modal-body">
|
||||||
<table id="schoolHolidaysTable" class="fc-holidays-table">
|
<div style="width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;">
|
||||||
<thead>
|
<table id="schoolHolidaysTable" class="fc-holidays-table" style="min-width: 400px;">
|
||||||
<tr>
|
<thead>
|
||||||
|
<tr>
|
||||||
<th><?= tr('fc_col_period') ?></th>
|
<th><?= tr('fc_col_period') ?></th>
|
||||||
<th><?= tr('fc_col_from') ?></th>
|
<th><?= tr('fc_col_from') ?></th>
|
||||||
<th><?= tr('fc_col_to') ?></th>
|
<th><?= tr('fc_col_to') ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
// generate_prompt.php - Script pour extraire le code de PachaFamily
|
||||||
|
// À lancer via le navigateur (http://localhost:8082/generate_prompt.php) ou en ligne de commande.
|
||||||
|
|
||||||
|
$rootDir = __DIR__;
|
||||||
|
$outputFile = $rootDir . '/pachafamily_source.md';
|
||||||
|
|
||||||
|
// Dossiers à ignorer (pour ne pas polluer le cerveau de l'IA avec des choses inutiles)
|
||||||
|
$ignoredDirs = ['.git', '.devtools', '.gitea', 'assets'];
|
||||||
|
|
||||||
|
// Extensions autorisées (on ne veut que du code, pas d'images)
|
||||||
|
$allowedExtensions = ['php', 'css', 'js', 'html', 'sh'];
|
||||||
|
|
||||||
|
$output = "# 🦙 Source Code PachaFamily\n\n";
|
||||||
|
$output .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
||||||
|
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($rootDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
$fileCount = 0;
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->isFile()) {
|
||||||
|
// Chemin relatif propre
|
||||||
|
$relativePath = str_replace($rootDir . DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||||
|
$relativePath = str_replace('\\', '/', $relativePath); // Uniformiser pour Windows
|
||||||
|
|
||||||
|
// Vérification des dossiers ignorés
|
||||||
|
$skip = false;
|
||||||
|
foreach ($ignoredDirs as $ignored) {
|
||||||
|
if (strpos($relativePath, $ignored) === 0) {
|
||||||
|
$skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($skip) continue;
|
||||||
|
|
||||||
|
// Vérification de l'extension
|
||||||
|
$ext = strtolower($file->getExtension());
|
||||||
|
if (!in_array($ext, $allowedExtensions)) continue;
|
||||||
|
|
||||||
|
// On ignore ce script lui-même
|
||||||
|
if ($file->getFilename() === 'generate_prompt.php') continue;
|
||||||
|
|
||||||
|
// Lecture du contenu
|
||||||
|
$content = file_get_contents($file->getPathname());
|
||||||
|
|
||||||
|
// Détermination du langage pour le bloc Markdown
|
||||||
|
$lang = $ext === 'php' ? 'php' : ($ext === 'js' ? 'javascript' : $ext);
|
||||||
|
|
||||||
|
// Formatage pour l'IA
|
||||||
|
$output .= "### 📄 Fichier : `" . $relativePath . "`\n";
|
||||||
|
$output .= "```" . $lang . "\n";
|
||||||
|
$output .= $content . "\n";
|
||||||
|
$output .= "```\n\n";
|
||||||
|
$output .= "---\n\n";
|
||||||
|
|
||||||
|
$fileCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($outputFile, $output);
|
||||||
|
|
||||||
|
echo "<div style='font-family:sans-serif; padding:20px; background:#f0fdf4; color:#16a34a; border-radius:8px; border:1px solid #bbf7d0;'>";
|
||||||
|
echo "<h2>✅ Succès !</h2>";
|
||||||
|
echo "<p>Le fichier <strong>pachafamily_source.md</strong> a été généré à la racine de ton projet.</p>";
|
||||||
|
echo "<p><strong>$fileCount fichiers</strong> de code ont été fusionnés.</p>";
|
||||||
|
echo "<p><em>Tu peux maintenant donner ce fichier à ta Gem PachaFamily !</em></p>";
|
||||||
|
echo "</div>";
|
||||||
|
?>
|
||||||
+36
@@ -362,4 +362,40 @@ body.no-scroll {
|
|||||||
.pf-mobile-logout {
|
.pf-mobile-logout {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
/* ✅ Transformation de toutes les modales en "Bottom Sheets" */
|
||||||
|
.pf-modal {
|
||||||
|
align-items: flex-end; /* Aligne la modale en bas */
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-modal-content {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
border-radius: 24px 24px 0 0 !important; /* Arrondi seulement en haut */
|
||||||
|
padding: 24px 20px !important;
|
||||||
|
padding-bottom: env(
|
||||||
|
safe-area-inset-bottom,
|
||||||
|
30px
|
||||||
|
) !important; /* Gère l'encoche iPhone */
|
||||||
|
transform: translateY(100%);
|
||||||
|
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
max-height: 92vh; /* Laisse un petit espace en haut */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barre de drag visuelle en haut de la modale */
|
||||||
|
.pf-modal-content::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 40px;
|
||||||
|
height: 5px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: -10px auto 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUpSheet {
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-16
@@ -92,30 +92,63 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* Pont d'Internationalisation pour le Javascript
|
* Pont d'Internationalisation et Configuration
|
||||||
*/
|
*/
|
||||||
window.I18N = <?php echo json_encode($current_translations_array ?? []); ?>;
|
window.I18N = <?php echo json_encode($current_translations_array ?? []); ?>;
|
||||||
|
|
||||||
|
// ✅ Injection sécurisée (évite le crash si config.php n'est pas chargé)
|
||||||
|
window.CONFIG = {
|
||||||
|
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>,
|
||||||
|
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
|
||||||
|
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>'
|
||||||
|
};
|
||||||
|
|
||||||
function tr(key) {
|
function tr(key) {
|
||||||
return window.I18N[key] || key;
|
return window.I18N[key] || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion du menu mobile (Uniquement si le bouton Burger est présent dans le DOM)
|
// Gestion du menu mobile
|
||||||
<?php if (isset($_SESSION['user'])): ?>
|
<?php if (isset($_SESSION['user'])): ?>
|
||||||
const burgerBtn = document.querySelector('.pf-burger-btn');
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mobileMenu = document.querySelector('.pf-mobile-menu');
|
const burgerBtn = document.querySelector('.pf-burger-btn');
|
||||||
|
const mobileMenu = document.querySelector('.pf-mobile-menu');
|
||||||
if(burgerBtn && mobileMenu) {
|
|
||||||
burgerBtn.addEventListener('click', () => {
|
|
||||||
const isOpen = mobileMenu.classList.toggle('is-open');
|
|
||||||
burgerBtn.textContent = isOpen ? '✕' : '☰';
|
|
||||||
|
|
||||||
if (isOpen) {
|
if(burgerBtn && mobileMenu) {
|
||||||
document.body.classList.add('no-scroll');
|
burgerBtn.addEventListener('click', () => {
|
||||||
} else {
|
const isOpen = mobileMenu.classList.toggle('is-open');
|
||||||
document.body.classList.remove('no-scroll');
|
burgerBtn.textContent = isOpen ? '✕' : '☰';
|
||||||
|
document.body.classList.toggle('no-scroll', isOpen);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</script>
|
|
||||||
|
/**
|
||||||
|
* pachaFetch : Utilitaire de requête robuste
|
||||||
|
*/
|
||||||
|
async function pachaFetch(url, options = {}) {
|
||||||
|
const finalUrl = url.startsWith('/') ? url.substring(1) : url;
|
||||||
|
|
||||||
|
options.credentials = 'same-origin';
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(finalUrl, options);
|
||||||
|
const rawText = await response.text();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawText);
|
||||||
|
} catch (jsonErr) {
|
||||||
|
console.error("Réponse corrompue (HTML reçu au lieu de JSON) :", rawText);
|
||||||
|
throw new Error("Erreur serveur : format JSON invalide.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Erreur pachaFetch [${finalUrl}] :`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+7
-3
@@ -17,10 +17,14 @@ function require_login(?string $loginPage = '/login.php'): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (empty($_SESSION['user'])) {
|
if (empty($_SESSION['user'])) {
|
||||||
// URL de la page courante (ex: /gift-list.php?foo=bar)
|
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
|
||||||
$currentUrl = $_SERVER['REQUEST_URI'] ?? '/';
|
header('Content-Type: application/json');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Session expirée. Veuillez vous reconnecter.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// On redirige vers la page de login en ajoutant ?redirect=...
|
$currentUrl = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
$redirectParam = urlencode($currentUrl);
|
$redirectParam = urlencode($currentUrl);
|
||||||
$target = ($loginPage ?? '/login.php') . '?redirect=' . $redirectParam;
|
$target = ($loginPage ?? '/login.php') . '?redirect=' . $redirectParam;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
// PachaFamily Config - Version sans erreur
|
||||||
|
if (!defined('ID_ALEX')) {
|
||||||
|
define('ID_ALEX', 2);
|
||||||
|
define('ID_LAIA', 3);
|
||||||
|
define('CURRENCY', '€');
|
||||||
|
define('ZONE_SCOLAIRE', 'C');
|
||||||
|
}
|
||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
if (file_exists(__DIR__ . '/config.php')) {
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
}
|
||||||
// Activer erreurs
|
// Activer erreurs
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
ini_set('display_startup_errors', 1);
|
ini_set('display_startup_errors', 1);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
// Protection de la page : nécessite d'être connecté
|
// Protection de la page : nécessite d'être connecté
|
||||||
|
|
||||||
require __DIR__ . '/includes/auth.php';
|
require __DIR__ . '/includes/auth.php';
|
||||||
require_login('/login.php');
|
require_login('/login.php');
|
||||||
|
|
||||||
// Inclusion i18n (déjà fait dans header, mais par sécurité si besoin de tr() avant)
|
// Inclusion i18n (déjà fait dans header, mais par sécurité si besoin de tr() avant)
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/i18n.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
|
||||||
// Configuration de la page
|
// Configuration de la page
|
||||||
|
|||||||
@@ -109,10 +109,14 @@ function getTranslatedMonthName($dateString) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach (['Alex', 'Laia'] as $p):
|
<?php
|
||||||
|
// Définition des noms à partir de la config
|
||||||
|
$family_members = [ID_ALEX => 'Alex', ID_LAIA => 'Laia'];
|
||||||
|
|
||||||
|
foreach ($family_members as $id => $p):
|
||||||
$d = $salaryConfig[$p];
|
$d = $salaryConfig[$p];
|
||||||
$restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']);
|
$restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']);
|
||||||
$borderColor = ($p === 'Alex') ? '#0891b2' : '#f59e0b';
|
$borderColor = ($id === ID_ALEX) ? '#0891b2' : '#f59e0b';
|
||||||
?>
|
?>
|
||||||
<tr data-person="<?= $p ?>">
|
<tr data-person="<?= $p ?>">
|
||||||
<td style="text-align:left; font-weight:bold; color:var(--text-main); border-left:4px solid <?= $borderColor ?>;">
|
<td style="text-align:left; font-weight:bold; color:var(--text-main); border-left:4px solid <?= $borderColor ?>;">
|
||||||
@@ -399,7 +403,7 @@ function getTranslatedMonthName($dateString) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="addCatModal" class="pf-modal" style="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;">
|
<div id="addCatModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
|
<div class="pf-modal-content" style="background:white; width:95%; max-width:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
|
||||||
<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; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3>
|
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3>
|
||||||
@@ -789,33 +793,23 @@ function updateSumResult() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCategory(id) {
|
async function deleteCategory(id) {
|
||||||
if (!confirm(window.I18N['bud_prev_confirm_del_line'] || "Confirmer la suppression ?")) return;
|
if (!confirm(tr('bud_prev_confirm_del_line'))) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'delete_category');
|
formData.append('action', 'delete_category');
|
||||||
formData.append('id', id);
|
formData.append('id', id);
|
||||||
formData.append('ajax', '1');
|
formData.append('ajax', '1'); //
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 💡 CORRECTION : Chemin relatif (sans le "/" au début) pour s'adapter à ton localhost
|
const result = await pachaFetch('modules/budget/includes/api/save-budget.php', {
|
||||||
const response = await fetch('modules/budget/includes/api/save-budget.php', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`Erreur réseau HTTP ${response.status}`);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
|
||||||
alert((window.I18N['bud_err_tech'] || 'Erreur technique') + " : Suppression impossible.");
|
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) { console.error(e); }
|
||||||
console.error("Erreur Fetch Suppression:", e);
|
|
||||||
alert("Erreur de communication avec le serveur. Regarde la console (F12) pour plus de détails.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
@@ -843,7 +837,7 @@ document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
|||||||
// --- INTERCEPTION ASYNCHRONE DES FORMULAIRES DE MODALES ---
|
// --- INTERCEPTION ASYNCHRONE DES FORMULAIRES DE MODALES ---
|
||||||
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault(); // 🛑 Bloque le rechargement HTML par défaut
|
e.preventDefault();
|
||||||
|
|
||||||
const submitBtn = form.querySelector('button[type="submit"]');
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
const originalText = submitBtn.innerText;
|
const originalText = submitBtn.innerText;
|
||||||
@@ -855,31 +849,24 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
|||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('ajax', '1');
|
formData.append('ajax', '1');
|
||||||
|
|
||||||
// 💡 CORRECTION DU PIÈGE : On utilise getAttribute() pour forcer la lecture de l'URL !
|
|
||||||
const actionUrl = form.getAttribute('action');
|
const actionUrl = form.getAttribute('action');
|
||||||
|
|
||||||
// On s'assure du bon chemin (chemin relatif depuis budget.php)
|
// On utilise pachaFetch au lieu de fetch
|
||||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
const result = await pachaFetch(actionUrl, {
|
||||||
|
|
||||||
const response = await fetch(finalUrl, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
form.closest('.pf-modal').style.display = 'none';
|
form.closest('.pf-modal').style.display = 'none';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert((window.I18N['bud_err_tech'] || 'Erreur technique') + " : " + (result.error || "Inconnue"));
|
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur Fetch Modale:", error);
|
console.error("Erreur Fetch Modale:", error);
|
||||||
alert("Une erreur technique est survenue lors de l'enregistrement.");
|
alert("Une erreur technique est survenue.");
|
||||||
} finally {
|
} finally {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.innerText = originalText;
|
submitBtn.innerText = originalText;
|
||||||
|
|||||||
@@ -859,31 +859,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
submitBtn.innerText = '⏳ ...';
|
submitBtn.innerText = '⏳ ...';
|
||||||
|
|
||||||
const formData = new FormData(formExpense);
|
const formData = new FormData(formExpense);
|
||||||
|
// Force l'action ici pour être sûr
|
||||||
formData.set('action', 'save_expense_manual');
|
formData.set('action', 'save_expense_manual');
|
||||||
|
|
||||||
// 💡 Chemin relatif propre !
|
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
||||||
const response = await fetch('modules/budget/includes/api/manage-item.php', {
|
|
||||||
|
const result = await pachaFetch(actionUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
|
if (result.success) {
|
||||||
|
closeSuiviModal('manualExpenseModal');
|
||||||
// 🛡️ Double vérification JSON contre les warnings PHP
|
formExpense.reset();
|
||||||
const textResult = await response.text();
|
window.location.reload();
|
||||||
try {
|
} else {
|
||||||
const result = JSON.parse(textResult);
|
alert((window.I18N['error_occured'] || 'Erreur') + ' : ' + (result.error || 'Inconnue'));
|
||||||
if (result.success) {
|
|
||||||
closeSuiviModal('manualExpenseModal');
|
|
||||||
formExpense.reset();
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
const errorMsg = result.error || 'Erreur inconnue';
|
|
||||||
alert((window.I18N['error_occured'] || 'Erreur') + ' : ' + errorMsg);
|
|
||||||
}
|
|
||||||
} catch (jsonErr) {
|
|
||||||
console.error("Réponse non-JSON :", textResult);
|
|
||||||
alert("Erreur PHP. Vérifie la console (F12).");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur réseau :", error);
|
console.error("Erreur réseau :", error);
|
||||||
|
|||||||
@@ -899,38 +899,111 @@ td.col-day {
|
|||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Plein écran (Full Bleed) pour le tableau sur mobile */
|
/* 1. PLEIN ÉCRAN POUR GAGNER DE L'ESPACE */
|
||||||
.pf-family-calendar .pf-container {
|
.pf-family-calendar .pf-container {
|
||||||
padding: 0 !important; /* Retire le padding de la page */
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.pf-family-calendar .pf-main {
|
||||||
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 2. EN-TÊTES ET BOUTONS GLOBAUX (Empilement propre) */
|
||||||
|
.fc-header-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 0 10px; /* On remet un peu de padding juste pour les textes */
|
||||||
|
}
|
||||||
|
.fc-header-row > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Les boutons s'empilent */
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.pf-btn-icon-text {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. CONTRÔLES DU CALENDRIER MENSUEL */
|
||||||
|
.fc-month-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.fc-view-controls {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
.fc-view-button {
|
||||||
|
flex: 1; /* Les onglets 1 Mois / 2 Mois se partagent l'espace */
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
#fc-current-month-year {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.fc-nav-controls {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between; /* Les flèches de chaque côté */
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. CONTRÔLES DU PLANNING HEBDO */
|
||||||
|
.fc-week-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.fc-week-nav-controls {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. LE GRAND TABLEAU (Suppression des bordures extérieures) */
|
||||||
#planningTable-wrapper {
|
#planningTable-wrapper {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
max-height: 75vh; /* Ajusté pour laisser voir un peu le bas */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ajustements des panneaux du bas */
|
/* Ajustements pour que les colonnes figées (Mois + Semaine) ne prennent pas tout l'écran */
|
||||||
|
.col-month {
|
||||||
|
width: 40px !important;
|
||||||
|
min-width: 40px !important;
|
||||||
|
max-width: 40px !important;
|
||||||
|
font-size: 0.75rem; /* On réduit le texte "Avril" etc. */
|
||||||
|
}
|
||||||
|
#planningTable tbody td.col-sticky-sem {
|
||||||
|
left: 40px !important; /* Adapté à la nouvelle largeur de la 1ère colonne */
|
||||||
|
}
|
||||||
|
#planningTable thead tr th.col-sticky-sem {
|
||||||
|
left: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. PANNEAUX DU BAS (Légendes et Résumés) */
|
||||||
.fc-bottom-grid {
|
.fc-bottom-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
.fc-two-months-container,
|
||||||
.fc-two-months-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc-year-container {
|
.fc-year-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-summary-header {
|
.fc-summary-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.fc-summary-controls {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modale Bottom Sheet */
|
/* 7. MODALE BOTTOM SHEET (SÉCURITÉ Z-INDEX) */
|
||||||
.fc-selection-menu {
|
.fc-selection-menu {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: auto !important;
|
top: auto !important;
|
||||||
@@ -938,13 +1011,14 @@ td.col-day {
|
|||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
border-radius: 24px 24px 0 0;
|
border-radius: 24px 24px 0 0;
|
||||||
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.3);
|
||||||
padding: 24px 20px;
|
padding: 24px 20px;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 24px);
|
padding-bottom: env(safe-area-inset-bottom, 24px);
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
z-index: 999999 !important; /* Doit passer AU-DESSUS des colonnes figées du tableau ! */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUpSheet {
|
@keyframes slideUpSheet {
|
||||||
@@ -960,16 +1034,15 @@ td.col-day {
|
|||||||
height: 5px;
|
height: 5px;
|
||||||
background: #cbd5e1;
|
background: #cbd5e1;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin: 0 auto 10px auto;
|
margin: 0 auto 15px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-menu-btn {
|
.fc-menu-btn {
|
||||||
padding: 10px;
|
padding: 12px 10px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Réduction des icônes pour gagner de la place */
|
/* Réduction des icônes pour le tableau hyper dense */
|
||||||
.fc-day--centre::after {
|
.fc-day--centre::after {
|
||||||
font-size: 9px !important;
|
font-size: 9px !important;
|
||||||
top: 1px !important;
|
top: 1px !important;
|
||||||
|
|||||||
@@ -633,9 +633,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
||||||
if (dayLeaves.length) {
|
if (dayLeaves.length) {
|
||||||
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
|
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
|
||||||
if (dayLeaves.some((l) => l.person_id === 2))
|
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_ALEX))
|
||||||
html += `<span style="color:#0f766e; font-weight:800;">A</span>`;
|
html += `<span style="color:#0f766e; font-weight:800;">A</span>`;
|
||||||
if (dayLeaves.some((l) => l.person_id === 3))
|
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_LAIA))
|
||||||
html += `<span style="color:#b45309; font-weight:800;">L</span>`;
|
html += `<span style="color:#b45309; font-weight:800;">L</span>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
td.innerHTML += html;
|
td.innerHTML += html;
|
||||||
@@ -995,17 +995,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchApi(url) {
|
async fetchApi(url) {
|
||||||
return fetch(url).then((r) => r.json());
|
return pachaFetch(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async postApi(url, data) {
|
async postApi(url, data) {
|
||||||
const res = await fetch(url, {
|
return pachaFetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(await res.text());
|
|
||||||
return res.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeSchoolYear(delta) {
|
async changeSchoolYear(delta) {
|
||||||
@@ -1422,7 +1422,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
} else if (action === "clear-leaves-person") {
|
} else if (action === "clear-leaves-person") {
|
||||||
// --- NOUVELLE ACTION : Suppression ciblée (Alex OU Laia selon le pid) ---
|
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||||
{
|
{
|
||||||
@@ -1432,14 +1431,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (action === "clear-leaves") {
|
} else if (action === "clear-leaves") {
|
||||||
// --- ACTION GLOBALE : Gardée au cas où vous en auriez besoin ailleurs ---
|
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||||
{ action: "bulk_delete_day_person", dates, person_id: 2 },
|
{
|
||||||
|
action: "bulk_delete_day_person",
|
||||||
|
dates,
|
||||||
|
person_id: window.CONFIG.ID_ALEX,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||||
{ action: "bulk_delete_day_person", dates, person_id: 3 },
|
{
|
||||||
|
action: "bulk_delete_day_person",
|
||||||
|
dates,
|
||||||
|
person_id: window.CONFIG.ID_LAIA,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16038
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user