From 6459c225ac51b3ffa1bbcba1299601704221ad57 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Thu, 11 Jun 2026 17:57:09 +0200 Subject: [PATCH] settings calendar --- budget.php | 657 ++++++- family-calendar.php | 288 +++- global.css | 23 + includes/lang/ca.php | 58 + includes/lang/en.php | 58 + includes/lang/fr.php | 59 + migrate.php | 40 +- migrate_leaves_meta.php | 90 + modules/budget/budget.css | 220 +++ .../budget/includes/api/get-fixed-costs.php | 0 modules/budget/includes/api/settings.php | 179 ++ modules/family-calendar/family-calendar.css | 36 + modules/family-calendar/family-calendar.js | 1504 ++++++++--------- .../family-calendar/includes/api/settings.php | 106 ++ 14 files changed, 2435 insertions(+), 883 deletions(-) create mode 100644 migrate_leaves_meta.php delete mode 100644 modules/budget/includes/api/get-fixed-costs.php create mode 100644 modules/budget/includes/api/settings.php create mode 100644 modules/family-calendar/includes/api/settings.php diff --git a/budget.php b/budget.php index b2c82d1..5e3de33 100644 --- a/budget.php +++ b/budget.php @@ -16,7 +16,12 @@ require __DIR__ . '/header.php';
+ + \ No newline at end of file diff --git a/family-calendar.php b/family-calendar.php index 6f2e94f..f03a5ce 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -9,7 +9,7 @@ require __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/i18n.php'; // 1. Récupération de toutes les personnes avec leurs modes de garde -$stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people ORDER BY id ASC"); +$stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC"); $familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC); $parents = []; @@ -21,6 +21,23 @@ $activeCareModes = []; foreach ($familyPeople as $p) { $role = strtolower($p['role'] ?? ''); if ($role === 'parent') { + // --- 🟢 NOUVEAU : Récupération dynamique des types de congés du parent --- + $stmtLT = $pdo->prepare(" + SELECT DISTINCT leave_type FROM ( + SELECT leave_type FROM pf_leave_balances WHERE person_id = ? + UNION + SELECT leave_type FROM pf_leave_snapshots WHERE person_id = ? + ) as t ORDER BY leave_type = 'CP' DESC, leave_type = 'JRA' DESC, leave_type = 'JA' DESC, leave_type ASC + "); + $stmtLT->execute([$p['id'], $p['id']]); + $lTypes = $stmtLT->fetchAll(PDO::FETCH_COLUMN); + + // Fallback de sécurité si aucun compteur n'est encore paramétré + if (empty($lTypes)) { + $lTypes = ['CP', 'JRA', 'JA']; + } + $p['leave_types'] = $lTypes; + $parents[] = $p; } elseif ($role === 'enfant') { // Décodage sécurisé des modes de garde @@ -28,7 +45,7 @@ foreach ($familyPeople as $p) { if (!is_array($modes)) $modes = []; $p['modes'] = $modes; $kids[] = $p; - + foreach ($modes as $m) { $activeCareModes[$m] = true; } @@ -45,10 +62,18 @@ require __DIR__ . '/header.php'; ?>
-
-

+
-
+ +
+

+ +
+ + +
-
-
+ +
+
+ + +
+

+ ⚙️ +

+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+

+
+ +
+
+ + + +
+ +
+ +
+
+
+ + +
+
+ +
+ +
+ + +
+
+
+ + + + +
+
+
+
+ +
-
- -
- - - +
+ +
+ + + +
+ +
+ +
+ +
+ + +
- -
- -
-
- - - -
-
- +
+
-
-
+
-
-
-

-
- - - +
+
+

+
+ + + +
-
-
- - - - - - - - - - +
+
+ + + + + + + + + - - - + + + - - - + + + - $parent): - $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; - ?> - - - - - $parent): - $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; - ?> - - - - - - - $parent): - $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; - ?> - - - - - - - - - - -
Maladie Maladie
CPJRAJA
- -
+ + $parent): + $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; + $colspan = count($parent['leave_types']) * 2; + ?> + + + + + + $parent): + $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; + foreach ($parent['leave_types'] as $lt): + ?> + + + + + + $parent): + $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; + foreach ($parent['leave_types'] as $lt): + ?> + + + + + + + + +
-
+
-
@@ -258,7 +367,6 @@ require __DIR__ . '/header.php';
$mode): - // Génération d'une couleur pseudo-aléatoire mais fixe par mode $hue = ($index * 137) % 360; ?>
@@ -281,11 +389,15 @@ require __DIR__ . '/header.php';
diff --git a/global.css b/global.css index e0f38ce..7b95568 100644 --- a/global.css +++ b/global.css @@ -1303,3 +1303,26 @@ details[open].pf-accordion > .pf-accordion-summary::after { flex-direction: column; gap: 15px; } + +/* Bouton Paramètres avec icône rotative (Roue crantée) */ +.btn-settings-gear { + background: transparent; + border: none; + box-shadow: none; + outline: none; + font-size: 1.4rem; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; + line-height: 1; + color: inherit; +} + +.btn-settings-gear:hover { + transform: rotate( + 90deg + ); /* 90deg rend mieux sur une roue crantée qu'un 45deg ! */ +} diff --git a/includes/lang/ca.php b/includes/lang/ca.php index 666c863..c5234b5 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -213,6 +213,30 @@ return [ 'fc_err_action' => 'Error en l\'acció: ', 'fc_today_short' => 'Avui', 'fc_today_title' => 'Anar a avui', + 'fc_tab_foyer' => '🏡 Llar', + 'fc_tab_members' => '👥 Membres i Drets', + 'fc_select_member' => 'Selecciona un membre de la família:', + 'fc_role_adult' => '👨‍👩‍👦 Adult', + 'fc_role_child' => '👶 Nen', + 'fc_care_modes_title' => '👶 Modes de cura associats', + 'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.', + 'fc_no_care_mode' => 'Cap mode configurat.', + 'fc_matrix_title' => '👨‍👩‍👦 Matriu de Drets de Permís', + 'fc_matrix_desc' => 'Declara els codis de comptadors i les dates de renovació.', + 'fc_matrix_code' => 'Codi', + 'fc_matrix_date' => 'Data de Reset', + 'fc_matrix_empty' => 'Cap comptador configurat.', + 'fc_matrix_saved' => 'Matriu de drets actualitzada! 💼', + 'fc_child_saved' => 'Subscripció del nen desada!', + 'btn_add_counter' => '➕ Afegir', + 'btn_save_rights' => '💾 Desar', + 'fc_modal_settings_title' => 'Configuració del Calendari', + 'fc_zone_label' => 'Zona Escolar (Vacances)', + 'fc_zone_desc' => 'Determina el calendari automàtic de vacances escolars.', + 'fc_care_modes_label' => 'Modes de Cura de la Llar', + 'fc_care_modes_desc' => 'Gestiona la llista global de modes de cura possibles per a la família.', + 'fc_add_mode_placeholder' => 'Ex: Mainadera, Casal, Avis...', + 'fc_settings_updated' => 'Configuració actualitzada correctament!', 'leg_presence' => 'Presència Pep', 'leg_school_holidays' => 'Vacances Escolars', @@ -403,6 +427,40 @@ return [ 'cat_others' => 'Altres / Imprevistos', 'cat_savings' => 'Estalvis', 'budget_tab_kids' => 'Nens', + 'budget_settings_title' => 'Configuració del Pressupost', + 'bs_tab_accounts' => '💳 Comptes', + 'bs_tab_categories' => '🏷️ Categories', + 'bs_tab_rules' => '🤖 Regles d\'importació', + 'bs_tab_salaries' => '💼 Salaris', + 'bs_empty_accounts' => 'Cap compte configurat.', + 'bs_type_checking' => 'Corrent', + 'bs_type_savings' => 'Estalvi', + 'bs_badge_default' => 'Per defecte', + 'bs_owner' => 'Propietari', + 'bs_shared_account' => 'Compte conjunt', + 'bs_empty_categories' => 'Cap categoria configurada.', + 'bs_type_income' => 'Ingrés', + 'bs_type_expense' => 'Despesa', + 'bs_type_savings_cat' => 'Estalvi', + 'bs_empty_rules' => 'Cap regla d\'importació.', + 'bs_empty_salaries' => 'Cap salari definit per a l\'any', + 'bs_salaries_year_desc' => 'Configuracions per a l\'any', + 'bs_salary_net' => 'Salari net', + 'bs_salary_fees' => 'Mensualitat / Despeses', + 'bs_add_account_title' => 'Afegir un compte', + 'bs_account_name_ph' => 'Nom (ex: Estalvi Bebè)', + 'bs_confirm_delete' => 'Segur que voleu suprimir aquest compte?', + 'bs_add_category_title' => '+ Afegir una categoria', + 'bs_cat_code_ph' => 'Codi (ex: FOOD)', + 'bs_cat_label_ph' => 'Etiqueta (ex: Compres)', + 'bs_cat_icon_ph' => 'Icona (ex: 🛒)', + 'bs_confirm_delete_cat' => 'Segur que voleu suprimir aquesta categoria?', + 'bs_add_rule_title' => 'Afegir una regla', + 'bs_rule_keyword_ph' => 'Paraula clau (ex: AUCHAN)', + 'bs_confirm_delete_rule'=> 'Segur que voleu suprimir aquesta regla?', + 'bs_currency_label' => 'Divisa de la llar', + 'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.', + 'bs_currency_updated' => 'Divisa actualitzada correctament!', // --- BUDGET : SUIVI --- 'bud_rem_school' => 'Escola (Resta estimada)', diff --git a/includes/lang/en.php b/includes/lang/en.php index 2b71bad..8923bee 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -214,6 +214,30 @@ return [ 'fc_err_action' => 'Error during action: ', 'fc_today_short' => 'Today', 'fc_today_title' => 'Today', + 'fc_tab_foyer' => '🏡 Household', + 'fc_tab_members' => '👥 Members & Rights', + 'fc_select_member' => 'Select a family member:', + 'fc_role_adult' => '👨‍👩‍👦 Adult', + 'fc_role_child' => '👶 Child', + 'fc_care_modes_title' => '👶 Associated Care Modes', + 'fc_care_modes_desc' => 'Check the care modes that apply to this child.', + 'fc_no_care_mode' => 'No care mode configured.', + 'fc_matrix_title' => '👨‍👩‍👦 Leave Rights Matrix', + 'fc_matrix_desc' => 'Declare your counter codes and renewal dates.', + 'fc_matrix_code' => 'Code', + 'fc_matrix_date' => 'Reset Date', + 'fc_matrix_empty' => 'No counter configured.', + 'fc_matrix_saved' => 'Rights matrix updated successfully! 💼', + 'fc_child_saved' => 'Child subscription saved!', + 'btn_add_counter' => '➕ Add', + 'btn_save_rights' => '💾 Save', + 'fc_modal_settings_title' => 'Calendar Settings', + 'fc_zone_label' => 'School Zone (Holidays)', + 'fc_zone_desc' => 'Determines the automatic school holiday calendar.', + 'fc_care_modes_label' => 'Household Care Modes', + 'fc_care_modes_desc' => 'Manage the global list of possible care modes for the family.', + 'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...', + 'fc_settings_updated' => 'Settings updated successfully!', 'leg_presence' => 'Pep presence', 'leg_school_holidays' => 'School holidays', @@ -401,6 +425,40 @@ return [ 'cat_others' => 'Other / Unexpected', 'cat_savings' => 'Savings', 'budget_tab_kids' => 'Kids', + 'budget_settings_title' => 'Budget Settings', + 'bs_tab_accounts' => '💳 Accounts', + 'bs_tab_categories' => '🏷️ Categories', + 'bs_tab_rules' => '🤖 Import Rules', + 'bs_tab_salaries' => '💼 Salaries', + 'bs_empty_accounts' => 'No accounts configured.', + 'bs_type_checking' => 'Checking', + 'bs_type_savings' => 'Savings', + 'bs_badge_default' => 'Default', + 'bs_owner' => 'Owner', + 'bs_shared_account' => 'Joint Account', + 'bs_empty_categories' => 'No categories configured.', + 'bs_type_income' => 'Income', + 'bs_type_expense' => 'Expense', + 'bs_type_savings_cat' => 'Savings', + 'bs_empty_rules' => 'No import rules configured.', + 'bs_empty_salaries' => 'No salary defined for the year', + 'bs_salaries_year_desc' => 'Configurations for the year', + 'bs_salary_net' => 'Net salary', + 'bs_salary_fees' => 'Monthly payment / Fees', + 'bs_add_account_title' => 'Add an account', + 'bs_account_name_ph' => 'Name (e.g., Baby Savings)', + 'bs_confirm_delete' => 'Are you sure you want to delete this account?', + 'bs_add_category_title' => '+ Add a category', + 'bs_cat_code_ph' => 'Code (e.g., FOOD)', + 'bs_cat_label_ph' => 'Label (e.g., Groceries)', + 'bs_cat_icon_ph' => 'Icon (e.g., 🛒)', + 'bs_confirm_delete_cat' => 'Are you sure you want to delete this category?', + 'bs_add_rule_title' => 'Add a rule', + 'bs_rule_keyword_ph' => 'Keyword (e.g., WALMART)', + 'bs_confirm_delete_rule'=> 'Are you sure you want to delete this rule?', + 'bs_currency_label' => 'Household Currency', + 'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.', + 'bs_currency_updated' => 'Currency updated successfully!', // --- BUDGET: TRACKING --- 'bud_rem_school' => 'School (estimated remaining)', diff --git a/includes/lang/fr.php b/includes/lang/fr.php index 13b6641..4957bc2 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -216,6 +216,31 @@ return [ 'care_nounou' => 'Nounou', 'care_centre' => 'Centre de loisirs', 'care_avis' => 'Avis', + 'fc_tab_foyer' => '🏡 Foyer', + 'fc_tab_members' => '👥 Membres & Droits', + 'fc_select_member' => 'Sélectionner un membre de la famille :', + 'fc_role_adult' => '👨‍👩‍👦 Adulte', + 'fc_role_child' => '👶 Enfant', + 'fc_care_modes_title' => '👶 Modes de garde associés', + 'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.', + 'fc_no_care_mode' => 'Aucun mode configuré.', + 'fc_matrix_title' => '👨‍👩‍👦 Matrice des Droits aux Congés', + 'fc_matrix_desc' => 'Déclarez les codes de compteurs et dates de renouvellement.', + 'fc_matrix_code' => 'Code', + 'fc_matrix_date' => 'Date Reset', + 'fc_matrix_empty' => 'Aucun compteur configuré.', + 'fc_matrix_saved' => 'Matrice des droits mise à jour ! 💼', + 'fc_child_saved' => 'Abonnement enfant enregistré !', + 'btn_add_counter' => '➕ Ajouter', + 'btn_save_rights' => '💾 Enregistrer', + 'fc_modal_settings_title' => 'Paramètres du Calendrier', + 'fc_zone_label' => 'Zone Scolaire (Vacances)', + 'fc_zone_desc' => 'Détermine le calendrier automatique des vacances scolaires.', + 'fc_care_modes_label' => 'Modes de Garde du Foyer', + 'fc_care_modes_desc' => 'Gérez la liste globale des modes de garde possibles pour la famille.', + 'fc_add_mode_placeholder' => 'Ex: Nounou, Casal, Grands-parents...', + 'fc_settings_updated' => 'Paramètres mis à jour avec succès !', + 'leg_presence' => 'Présence Pep', 'leg_school_holidays' => 'Vacances Scolaires', @@ -403,6 +428,40 @@ return [ 'cat_others' => 'Autres / Imprévus', 'cat_savings' => 'Épargne', 'budget_tab_kids' => 'Enfants', + 'budget_settings_title' => 'Paramètres du Budget', + 'bs_tab_accounts' => '💳 Comptes', + 'bs_tab_categories' => '🏷️ Catégories', + 'bs_tab_rules' => '🤖 Règles d\'import', + 'bs_tab_salaries' => '💼 Salaires', + 'bs_empty_accounts' => 'Aucun compte configuré.', + 'bs_type_checking' => 'Courant', + 'bs_type_savings' => 'Épargne', + 'bs_badge_default' => 'Défaut', + 'bs_owner' => 'Propriétaire', + 'bs_shared_account' => 'Compte Commun', + 'bs_empty_categories' => 'Aucune catégorie configurée.', + 'bs_type_income' => 'Revenu', + 'bs_type_expense' => 'Dépense', + 'bs_type_savings_cat' => 'Épargne', + 'bs_empty_rules' => 'Aucune règle d\'importation.', + 'bs_empty_salaries' => 'Aucun salaire défini pour l\'année', + 'bs_salaries_year_desc' => 'Configurations pour l\'année', + 'bs_salary_net' => 'Salaire net', + 'bs_salary_fees' => 'Mensualité / Frais', + 'bs_add_account_title' => '+ Ajouter un compte', + 'bs_account_name_ph' => 'Nom (ex: Livret A Bébé)', + 'bs_confirm_delete' => 'Voulez-vous vraiment supprimer ce compte ?', + 'bs_add_category_title' => '+ Ajouter une catégorie', + 'bs_cat_code_ph' => 'Code (ex: FOOD)', + 'bs_cat_label_ph' => 'Libellé (ex: Courses)', + 'bs_cat_icon_ph' => 'Icône (ex: 🛒)', + 'bs_confirm_delete_cat' => 'Voulez-vous vraiment supprimer cette catégorie ?', + 'bs_add_rule_title' => 'Ajouter une règle', + 'bs_rule_keyword_ph' => 'Mot-clé (ex: AUCHAN)', + 'bs_confirm_delete_rule' => 'Voulez-vous vraiment supprimer cette règle ?', + 'bs_currency_label' => 'Devise du Foyer', + 'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.', + 'bs_currency_updated' => 'Devise mise à jour avec succès !', // --- BUDGET : SUIVI --- 'bud_rem_school' => 'École (Reste estimé)', diff --git a/migrate.php b/migrate.php index 45446ba..2a197d3 100644 --- a/migrate.php +++ b/migrate.php @@ -88,8 +88,7 @@ try { UNIQUE KEY (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); - // Règles Cadeaux (Qui paie pour quel enfant à quelle occasion) - // L'ERREUR DE SYNTAXE ÉTAIT ICI : UNIQUE KEY adult_child_occ + // Règles Cadeaux $pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules ( id INT AUTO_INCREMENT PRIMARY KEY, adult_person_id INT NOT NULL, @@ -145,6 +144,43 @@ try { echo "✅ Données de base injectées (si nécessaire).
"; + // --------------------------------------------------------- + // 4. MIGRATION SPECIFIQUE : MODULE CALENDRIER (CONGÉS GRANULAIRES) + // --------------------------------------------------------- + $tableExists = $pdo->query("SHOW TABLES LIKE 'pf_person_leave_meta'")->rowCount() > 0; + + if (!$tableExists) { + $pdo->exec(" + CREATE TABLE pf_person_leave_meta ( + id INT AUTO_INCREMENT PRIMARY KEY, + person_id INT NOT NULL, + leave_type VARCHAR(50) NOT NULL, + anniversary_date DATE NOT NULL, + UNIQUE KEY uq_person_leave (person_id, leave_type), + FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + echo "✅ Table pf_person_leave_meta créée (Nouvelle installation).
"; + } else { + $checkColumn = $pdo->query("SHOW COLUMNS FROM pf_person_leave_meta LIKE 'leave_type'")->rowCount(); + if ($checkColumn === 0) { + $pdo->exec("ALTER TABLE pf_person_leave_meta ADD COLUMN leave_type VARCHAR(50) NULL AFTER person_id"); + $pdo->exec("UPDATE pf_person_leave_meta SET leave_type = 'CP' WHERE leave_type IS NULL"); + $pdo->exec("ALTER TABLE pf_person_leave_meta MODIFY COLUMN leave_type VARCHAR(50) NOT NULL"); + + try { + $pdo->exec("ALTER TABLE pf_person_leave_meta DROP INDEX person_id"); + } catch (\Throwable $e) { + // On ignore si l'index n'avait pas ce nom + } + + $pdo->exec("ALTER TABLE pf_person_leave_meta ADD UNIQUE KEY uq_person_leave (person_id, leave_type)"); + echo "✅ Table pf_person_leave_meta mise à jour avec le type granulaire.
"; + } else { + echo "➖ Table pf_person_leave_meta déjà à jour.
"; + } + } + } catch (PDOException $e) { echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "
"; } diff --git a/migrate_leaves_meta.php b/migrate_leaves_meta.php new file mode 100644 index 0000000..1eb69f8 --- /dev/null +++ b/migrate_leaves_meta.php @@ -0,0 +1,90 @@ +🚀 Début de la restauration des compteurs de congés"; + +try { + $stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1"); + $families = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($families as $family) { + $dbName = $family['db_name']; + echo "

Famille : {$family['name']} ($dbName)

"; + + try { + $pdo = new PDO( + "mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", + $db_user, $db_pass, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + // 1. Création de la table de configuration (si elle n'existe pas) + $pdo->exec(" + CREATE TABLE IF NOT EXISTS pf_person_leave_meta ( + id INT AUTO_INCREMENT PRIMARY KEY, + person_id INT NOT NULL, + leave_type VARCHAR(50) NOT NULL, + anniversary_date DATE NOT NULL, + UNIQUE KEY uq_person_leave (person_id, leave_type), + FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + // 2. On scanne l'historique des soldes et snapshots pour trouver TOUS les types existants + $stmtLeaves = $pdo->query(" + SELECT DISTINCT person_id, leave_type + FROM ( + SELECT person_id, leave_type FROM pf_leave_balances + UNION + SELECT person_id, leave_type FROM pf_leave_snapshots + ) as combined + WHERE leave_type IS NOT NULL AND leave_type != '' + "); + + $existingLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC); + $inserted = 0; + + $stmtInsert = $pdo->prepare(" + INSERT IGNORE INTO pf_person_leave_meta (person_id, leave_type, anniversary_date) + VALUES (?, ?, ?) + "); + + // 3. On injecte les données dans la configuration avec les dates par défaut de ton ancien JS + foreach ($existingLeaves as $leave) { + $pid = $leave['person_id']; + $type = strtoupper(trim($leave['leave_type'])); + + // Dates d'anniversaire par défaut (année 2000 arbitraire, seul le mois/jour compte) + $anniversary = '2000-01-01'; + if ($type === 'CP') $anniversary = '2000-06-01'; // 1er Juin + if ($type === 'JRA') $anniversary = '2000-01-01'; // 1er Janvier + if ($type === 'JA') $anniversary = '2000-04-29'; // 29 Avril (issu de ton ancien JS) + + $stmtInsert->execute([$pid, $type, $anniversary]); + if ($stmtInsert->rowCount() > 0) { + $inserted++; + } + } + + echo "✅ $inserted compteurs historiques (CP, JRA, JA...) détectés et configurés avec succès pour la modale.
"; + + } catch (PDOException $e) { + echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "
"; + } + } + + echo "

🎉 Restauration terminée avec succès !

"; + +} catch (Exception $e) { + die("❌ Erreur fatale Meta DB : " . $e->getMessage()); +} +?> \ No newline at end of file diff --git a/modules/budget/budget.css b/modules/budget/budget.css index 5d9325a..f152a71 100644 --- a/modules/budget/budget.css +++ b/modules/budget/budget.css @@ -1268,3 +1268,223 @@ body.sum-mode-active .sum-target { padding: 12px 10px; } } + +/* ========================================================================== + MODALE PARAMÈTRES BUDGET (Design System HouseHub) + ========================================================================== */ +.budget-settings-backdrop { + display: none; + position: fixed; + inset: 0; + z-index: 200; + background: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(4px); + align-items: center; + justify-content: center; + padding: 1rem; +} +.budget-settings-backdrop.show { + display: flex; +} + +.budget-settings-modal { + background: var(--bg-panel); + border: 1px solid var(--border-light); + border-radius: 14px; + width: 100%; + max-width: 800px; + height: 80vh; + max-height: 700px; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + animation: modalIn 0.18s ease; +} + +.bs-header { + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-light); + display: flex; + justify-content: space-between; + align-items: center; +} +.bs-header h3 { + margin: 0; + font-size: 1.1rem; + color: var(--text-main); +} +.bs-close { + background: none; + border: none; + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + color: var(--text-muted); + border-radius: 6px; + padding: 0 0.5rem; +} +.bs-close:hover { + background: var(--bg-page); + color: var(--text-main); +} + +.bs-layout { + display: flex; + flex: 1; + overflow: hidden; +} +.bs-sidebar { + width: 200px; + border-right: 1px solid var(--border-light); + background: var(--bg-page); + display: flex; + flex-direction: column; + padding: 0.75rem; + gap: 0.4rem; + overflow-y: auto; +} +.bs-tab-btn { + text-align: left; + padding: 0.6rem 1rem; + border-radius: 8px; + border: none; + background: transparent; + color: var(--text-muted); + font-weight: 600; + cursor: pointer; + transition: 0.15s; + font-size: 0.9rem; +} +.bs-tab-btn:hover { + background: var(--border-light); + color: var(--text-main); +} +.bs-tab-btn.active { + background: var(--text-main); + color: var(--bg-panel); +} + +.bs-content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; + background: var(--bg-panel); +} +.bs-pane { + display: none; +} +.bs-pane.active { + display: block; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Mobile Responsive */ +@media (max-width: 650px) { + .bs-layout { + flex-direction: column; + } + .bs-sidebar { + width: 100%; + flex-direction: row; + overflow-x: auto; + border-right: none; + border-bottom: 1px solid var(--border-light); + padding: 0.5rem; + white-space: nowrap; + } + .bs-tab-btn { + flex-shrink: 0; + } + .bs-content { + padding: 1rem; + } +} + +/* Utilitaires pour les listes de paramètres */ +.bs-list-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + border: 1px solid var(--border-light); + border-radius: 8px; + margin-bottom: 0.5rem; +} +.bs-section-title { + font-size: 1.1rem; + color: var(--text-main); + margin-top: 0; + margin-bottom: 1rem; +} + +/* --- AFFICHAGE SMART DES RÈGLES (TAG CLOUD) --- */ +.bs-rule-search { + width: 100%; + padding: 0.5rem 0.75rem; + border-radius: 8px; + border: 1px solid var(--border-light); + background: var(--bg-page); + margin-bottom: 1rem; + color: var(--text-main); + font-family: inherit; +} +.bs-rule-group { + margin-bottom: 1.2rem; +} +.bs-rule-group-title { + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 0.5rem; + display: flex; + align-items: center; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border-light); + padding-bottom: 4px; +} +.bs-rule-tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} +.bs-rule-tag { + background: var(--bg-page); + border: 1px solid var(--border-light); + padding: 3px 10px; + border-radius: 14px; + font-size: 0.8rem; + display: flex; + align-items: center; + gap: 6px; + transition: 0.15s; +} +.bs-rule-tag:hover { + border-color: var(--danger); + background: #fef2f2; +} +.bs-rule-tag-del { + color: var(--text-muted); + cursor: pointer; + border: none; + background: none; + padding: 0; + font-size: 1rem; + line-height: 1; + margin-top: -2px; +} +.bs-rule-tag:hover .bs-rule-tag-del { + color: var(--danger); +} diff --git a/modules/budget/includes/api/get-fixed-costs.php b/modules/budget/includes/api/get-fixed-costs.php deleted file mode 100644 index e69de29..0000000 diff --git a/modules/budget/includes/api/settings.php b/modules/budget/includes/api/settings.php new file mode 100644 index 0000000..cb4454f --- /dev/null +++ b/modules/budget/includes/api/settings.php @@ -0,0 +1,179 @@ +query(" + SELECT a.*, p.name as owner_name + FROM pf_bank_accounts a + LEFT JOIN pf_people p ON a.owner_person_id = p.id + ORDER BY a.is_default DESC, a.name ASC + ")->fetchAll(PDO::FETCH_ASSOC); + + // 2. Catégories + $categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC); + + // 3. Règles d'import (avec le nom de la catégorie correspondante) + // Utilisation de COLLATE pour éviter l'erreur 1267 de mix de collations + $rules = $pdo->query(" + SELECT r.*, c.label as cat_label + FROM pf_import_rules r + LEFT JOIN pf_budget_categories c ON r.category COLLATE utf8mb4_unicode_ci = c.code COLLATE utf8mb4_unicode_ci + ORDER BY r.keyword ASC + ")->fetchAll(PDO::FETCH_ASSOC); + + // 4. Salaires (Année en cours) + $currentYear = (int)date('Y'); + $salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC); + + // Récupération de la devise du foyer (par défaut € si non définie) + // Lecture directe de la colonne currency dans pf_foyer_settings + $currencySetting = $pdo->query("SELECT currency FROM pf_foyer_settings LIMIT 1")->fetchColumn() ?: '€'; + + echo json_encode([ + 'success' => true, + 'data' => [ + 'accounts' => $accounts, + 'categories' => $categories, + 'rules' => $rules, + 'salaries' => $salaries, + 'year' => $currentYear, + 'currency' => $currencySetting + ] + ]); + exit; + } + + // --- GESTION DES COMPTES BANCAIRES --- + + if ($action === 'add_account') { + $name = trim($_POST['name'] ?? ''); + $type = $_POST['type'] ?? 'checking'; + // On met is_default à 0 par défaut pour les nouveaux comptes + + if (empty($name)) throw new Exception("Le nom du compte est obligatoire."); + + $stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)"); + $stmt->execute([$name, $type]); + + echo json_encode(['success' => true]); + exit; + } + + if ($action === 'delete_account') { + $id = (int)($_POST['id'] ?? 0); + + // Sécurité : on empêche de supprimer un compte s'il n'en reste qu'un seul + $count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn(); + if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte."); + + $stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?"); + $stmt->execute([$id]); + + echo json_encode(['success' => true]); + exit; + } + + // --- GESTION DES CATÉGORIES --- + + if ($action === 'add_category') { + $code = strtoupper(trim($_POST['code'] ?? '')); + $label = trim($_POST['label'] ?? ''); + $type = $_POST['type'] ?? 'Expense'; + $color = $_POST['color'] ?? '#cccccc'; + $icon = trim($_POST['icon'] ?? '📌'); + + if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires."); + + // Vérification anti-doublon sur le code + $check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?"); + $check->execute([$code]); + if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà."); + + $stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$code, $label, $type, $color, $icon]); + + echo json_encode(['success' => true]); + exit; + } + + if ($action === 'delete_category') { + $id = (int)($_POST['id'] ?? 0); + $stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?"); + $stmt->execute([$id]); + + echo json_encode(['success' => true]); + exit; + } + + // --- GESTION DES RÈGLES D'IMPORT --- + + if ($action === 'add_rule') { + $keyword = strtoupper(trim($_POST['keyword'] ?? '')); + $category = trim($_POST['category'] ?? ''); // Le code de la catégorie (ex: FMCG) + + if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires."); + + // Vérification anti-doublon + $check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?"); + $check->execute([$keyword]); + if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà."); + + $stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)"); + $stmt->execute([$keyword, $category]); + + echo json_encode(['success' => true]); + exit; + } + + if ($action === 'delete_rule') { + $id = (int)($_POST['id'] ?? 0); + $stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?"); + $stmt->execute([$id]); + + echo json_encode(['success' => true]); + exit; + } + + // --- GESTION DES SALAIRES --- + + if ($action === 'save_salary') { + $id = (int)($_POST['id'] ?? 0); + $salary = (float)($_POST['salary'] ?? 0); + $mensualite = (float)($_POST['mensualite'] ?? 0); + + if ($id <= 0) throw new Exception("ID de configuration de salaire invalide."); + + // Mise à jour de la ligne pour l'année en cours + $stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?"); + $stmt->execute([$salary, $mensualite, $id]); + + echo json_encode(['success' => true]); + exit; + } + // --- GESTION DE LA DEVISE GLOBALE --- + + if ($action === 'save_currency') { + $currency = trim($_POST['currency'] ?? '€'); + + // Mise à jour directe de la colonne currency pour le foyer + $stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?"); + $stmt->execute([$currency]); + + echo json_encode(['success' => true]); + exit; + } + + throw new Exception("Action API inconnue."); + +} catch (Exception $e) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/modules/family-calendar/family-calendar.css b/modules/family-calendar/family-calendar.css index c6faae5..a02c047 100644 --- a/modules/family-calendar/family-calendar.css +++ b/modules/family-calendar/family-calendar.css @@ -955,3 +955,39 @@ td.col-laia-sub { gap: 20px !important; } } + +/* ========================================================================== + MODALE PARAMÈTRES (SIDEBAR STYLE) + ========================================================================== */ +.fc-sidebar-tab { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 1.2rem; + background: transparent; + border: none; + text-align: left; + font-weight: 600; + cursor: pointer; + color: var(--text-main); + transition: all 0.2s; + width: calc(100% - 16px); + margin: 2px 8px; + border-radius: 8px; + font-size: 0.95rem; +} + +.fc-sidebar-tab.active { + background: #111827; /* Gris très foncé typique des sidebars */ + color: #ffffff; +} + +.fc-sidebar-tab:hover:not(.active) { + background: var(--bg-soft, #f1f5f9); +} + +/* Variante Dark Mode native pour l'onglet actif */ +[data-theme="dark"] .fc-sidebar-tab.active { + background: var(--primary); + color: #ffffff; +} diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index 4baec4a..ffd23e9 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -1,42 +1,311 @@ /** - * family-calendar.js (Version Optimisée - API BDD & Décalage Vendredi) + * family-calendar.js (Version Optimisée - API BDD, Décalage Vendredi & Paramètres Dynamiques) */ + +// ============================================================================ +// 1. VARIABLES GLOBALES & LOGIQUE DE CONFIGURATION (SETTINGS) +// ============================================================================ +let calGlobalData = null; +let currentSelectedMemberId = null; +let localCareModes = []; + +// Ouvre et initialise la modale avec les données fraîches de la BDD +async function openCalendarSettings() { + try { + const res = await pachaFetch( + "/modules/family-calendar/includes/api/settings.php?action=get_all", + ); + if (!res.success) throw new Error(res.error); + + calGlobalData = res.data; + + const zoneInput = document.getElementById("setZoneScolaire"); + if (zoneInput) zoneInput.value = calGlobalData.foyer.zone_scolaire; + + localCareModes = calGlobalData.foyer.care_modes || []; + renderCareModeTags(); + + const selectMember = document.getElementById("selectCalMember"); + if (selectMember) { + selectMember.innerHTML = calGlobalData.people + .map( + (p) => + ``, + ) + .join(""); + } + + switchCalendarTab("foyer"); + + const modalSettings = document.getElementById("modalCalendarSettings"); + if (modalSettings) { + modalSettings.classList.add("open"); + document.body.classList.add("no-scroll"); + } + } catch (err) { + alert("Erreur : " + err.message); + } +} + +function closeCalendarSettings() { + document.getElementById("modalCalendarSettings").classList.remove("open"); + document.body.classList.remove("no-scroll"); +} + +function switchCalendarTab(tabName) { + document + .querySelectorAll(".cal-settings-pane") + .forEach((p) => (p.style.display = "none")); + document + .querySelectorAll(".bs-tab-btn") + .forEach((b) => b.classList.remove("active")); + + if (tabName === "foyer") { + document.getElementById("cal-pane-foyer").style.display = "block"; + document.getElementById("tab-btn-foyer").classList.add("active"); + } else { + document.getElementById("cal-pane-membres").style.display = "block"; + document.getElementById("tab-btn-membres").classList.add("active"); + loadMemberConfigView(); + } +} + +function renderCareModeTags() { + const container = document.getElementById("careModesContainer"); + container.innerHTML = localCareModes + .map( + (mode, i) => ` + + ${mode} × + + `, + ) + .join(""); +} + +function addCareModeTag() { + const input = document.getElementById("inputNewCareMode"); + const val = input.value.trim(); + if (val && !localCareModes.includes(val)) { + localCareModes.push(val); + renderCareModeTags(); + input.value = ""; + } +} + +function removeCareModeTag(index) { + localCareModes.splice(index, 1); + renderCareModeTags(); +} + +async function submitCalFoyer(e) { + e.preventDefault(); + try { + const formData = new FormData(); + formData.append("action", "save_foyer"); + formData.append( + "zone_scolaire", + document.getElementById("setZoneScolaire").value, + ); + formData.append("care_modes", JSON.stringify(localCareModes)); + + const res = await pachaFetch( + "/modules/family-calendar/includes/api/settings.php", + { method: "POST", body: formData }, + ); + if (!res.success) throw new Error(res.error); + + alert( + window.FAMILY_CONFIG?.i18n?.settings_updated || + "Configuration enregistrée ! ✨", + ); + window.location.reload(); // On recharge pour appliquer la zone et les colonnes de garde + } catch (err) { + alert("Erreur : " + err.message); + } +} + +function loadMemberConfigView() { + const select = document.getElementById("selectCalMember"); + const zone = document.getElementById("memberConfigZone"); + if (!select || !select.value || !zone) return; + + currentSelectedMemberId = parseInt(select.value); + const role = select.options[select.selectedIndex].dataset.role; + + if (role === "enfant") { + zone.innerHTML = ` +
${tr("fc_care_modes_title")}
+

${tr("fc_care_modes_desc")}

+
+ ${ + localCareModes + .map( + (m) => ` + + `, + ) + .join("") || `${tr("fc_no_care_mode")}` + } +
+ + `; + } else { + // 1. Récupération STRICTE des congés existants en BDD (Aucun forçage) + let userLeaves = calGlobalData.leaves[currentSelectedMemberId] || []; + + // 2. Génération des lignes du tableau dynamiquement (Version compactée) + const tableRows = userLeaves + .map( + (l) => ` + + + + + + + + + + `, + ) + .join(""); + + zone.innerHTML = ` +
+
+
🗓️ Matrice des Congés
+

Définissez les quotas et méthodes d'acquisition.

+
+ +
+ +
+ + + + + + + + + + + + ${tableRows || ``} + +
CodeMéthodeQuotaRenouv. / Fin
Aucun compteur défini.
+
+ +
+ +
+ `; + } +} + +window.addLeaveRowToMatrix = function () { + const tbody = document.getElementById("tbodyLeaveMatrix"); + const empty = document.getElementById("emptyMatrixRow"); + if (empty) empty.remove(); + if (!tbody) return; + + const tr = document.createElement("tr"); + tr.className = "js-leave-row"; + tr.style.borderBottom = "1px solid var(--border-light)"; + tr.innerHTML = ` + + + + + + + + `; + tbody.appendChild(tr); +}; + +async function submitMemberLeaves() { + // 🟢 Sécurité : on s'assure qu'un membre est bien sélectionné + if (!currentSelectedMemberId || isNaN(currentSelectedMemberId)) { + alert("Erreur technique : Aucun membre valide sélectionné."); + return; + } + + try { + const rows = document.querySelectorAll(".js-leave-row"); + + // 🟢 Récupération enrichie avec "method" et "allowance" + const leavesPayload = Array.from(rows) + .map((row) => ({ + type: row.querySelector(".js-leave-type").value.trim().toUpperCase(), + method: row.querySelector(".js-leave-method").value, // <-- NOUVEAU + allowance: row.querySelector(".js-leave-allowance").value, // <-- NOUVEAU + date: row.querySelector(".js-leave-date").value, + })) + .filter((l) => l.type && l.date); // Filtre de sécurité + + const formData = new FormData(); + formData.append("action", "save_member_leaves"); + formData.append("person_id", currentSelectedMemberId); + formData.append("leaves", JSON.stringify(leavesPayload)); + + const res = await pachaFetch( + "/modules/family-calendar/includes/api/settings.php", + { method: "POST", body: formData }, + ); + if (!res.success) throw new Error(res.error); + + if (window.showToast) { + showToast(tr("fc_matrix_saved"), "success"); + } else { + alert(tr("fc_matrix_saved")); + } + + closeCalendarSettings(); + setTimeout(openCalendarSettings, 300); + } catch (err) { + alert("Erreur : " + err.message); + } +} + +// ============================================================================ +// 2. MOTEUR PRINCIPAL DU CALENDRIER (DOM CONTENT LOADED) +// ============================================================================ document.addEventListener("DOMContentLoaded", () => { + // Récupération sécurisée depuis le nouveau format PHP + const parents = window.FAMILY_CONFIG?.parents || []; + const kids = window.FAMILY_CONFIG?.kids || []; + const activeCareModes = window.FAMILY_CONFIG?.activeCareModes || []; + const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"]; const GUARDE_TYPES = ["CENTRE", "AVIS"]; const PEP_TYPES = ["PEP_SICK"]; - // 🟢 Liste globale dynamique des personnes (fini les IDs en dur !) - const peopleList = window.CONFIG.PEOPLE || []; - - // On filtre pour ne garder que les parents pour la logique principale des congés - const parents = peopleList.filter( - (p) => p.role && p.role.toLowerCase() === "parent", - ); - + // (Note: La logique LEAVES_CONFIG restera à basculer vers BDD dans un 2nd temps) const LEAVES_CONFIG = { - CP: { - startMonth: 8, - defaultBalance: 25, - }, + CP: { startMonth: 8, defaultBalance: 25 }, JRA: { - yearlyTotals: { - 2024: 10, - 2025: 10, - 2026: 11, - }, + yearlyTotals: { 2024: 10, 2025: 10, 2026: 11 }, defaultBalance: 10, toleranceMonths: 2, maxReport: 2, }, - JA: {}, // Ce tableau est maintenant vide par défaut, on le remplit dynamiquement juste en dessous + JA: {}, }; - // 🟢 INITIALISATION DYNAMIQUE DES JOURS D'ANNIVERSAIRE (JA) parents.forEach((parent) => { LEAVES_CONFIG.JA[parent.id] = { - startMonth: 4, // Mois par défaut (à faire évoluer en BDD au Niveau 3) - startDay: 29, // Jour par défaut + startMonth: 4, + startDay: 29, defaultBalance: 4, }; }); @@ -56,21 +325,19 @@ document.addEventListener("DOMContentLoaded", () => { if ( this.selectionMenu && this.selectionMenu.parentElement !== document.body - ) { + ) document.body.appendChild(this.selectionMenu); - } if ( this.monthSelectionMenu && this.monthSelectionMenu.parentElement !== document.body - ) { + ) document.body.appendChild(this.monthSelectionMenu); - } this.currentMonth = new Date(); this.currentMonth.setDate(1); this.viewMode = "1month"; this.currentSchoolYearStart = null; - this.modalSelectedYear = null; // Pour la modale des vacances + this.modalSelectedYear = null; this.isSelecting = false; this.selectedCells = []; @@ -78,7 +345,7 @@ document.addEventListener("DOMContentLoaded", () => { this._currentBulkInfo = null; this.dbEvents = []; - this.fixedEvents = []; // Fériés statiques + this.fixedEvents = []; this.events = []; this.leaves = []; this.weeks = []; @@ -88,7 +355,6 @@ document.addEventListener("DOMContentLoaded", () => { }; if (!this.planningBody) return; - this.init(); } @@ -114,34 +380,28 @@ document.addEventListener("DOMContentLoaded", () => { setTimeout(() => this.scrollToCurrentMonth(), 100); } - // Modifie dynamiquement le titre de la modale pour y insérer le selecteur d'année setupModalUI() { const headerTitle = document.querySelector( "#modalHolidays .pf-modal-title", ); - if (headerTitle && !document.getElementById("holidayYearSelect")) { let options = ""; const currentY = new Date().getFullYear(); - // 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 += ``; + options += ``; } - // --- NOUVEAU : Récupération de la zone dynamique --- + // Utilise la globale config ou fallback si non définie const zoneText = - window.CONFIG && - window.CONFIG.ZONE_SCOLAIRE && + window.CONFIG?.ZONE_SCOLAIRE && window.CONFIG.ZONE_SCOLAIRE !== "Autre" ? `(Zone ${window.CONFIG.ZONE_SCOLAIRE})` : ""; - // Injection du texte de la zone dans le header - headerTitle.innerHTML = `🏖️ ${window.I18N["fc_modal_holidays_title"]} ${zoneText} - `; + headerTitle.innerHTML = `🏖️ ${window.I18N?.fc_modal_holidays_title || "Vacances"} ${zoneText} + `; document .getElementById("holidayYearSelect") @@ -159,26 +419,27 @@ document.addEventListener("DOMContentLoaded", () => { 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)); + selectMonth.add( + new Option( + new Intl.DateTimeFormat(lang, { month: "long" }).format( + new Date(2000, i, 1), + ), + 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.currentMonth = new Date( + parseInt(selectYear.value), + parseInt(selectMonth.value), + 1, + ); this.renderMonthCalendar(); }; @@ -188,35 +449,36 @@ document.addEventListener("DOMContentLoaded", () => { async refreshAllData() { try { - const weeksData = await this.fetchApi( - `/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php?school_year_start=${this.currentSchoolYearStart}`, - ); - this.weeks = this.processWeeks(weeksData.weeks || []); + const [ + weeksData, + eventsData, + fixedEventsData, + leavesData, + balancesData, + snapshotsData, + ] = await Promise.all([ + this.fetchApi( + `/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php?school_year_start=${this.currentSchoolYearStart}`, + ), + this.fetchApi("/modules/family-calendar/includes/api/get-events.php"), + this.fetchPublicHolidays(), + this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"), + this.fetchApi( + "/modules/family-calendar/includes/api/get-leave-balances.php", + ), + this.fetchApi( + "/modules/family-calendar/includes/api/get-leave-snapshots.php", + ), + ]); - const eventsData = await this.fetchApi( - "/modules/family-calendar/includes/api/get-events.php", - ); + this.weeks = this.processWeeks(weeksData.weeks || []); this.dbEvents = (eventsData.events || []).map((e) => ({ ...e, duration: parseFloat(e.duration), })); - - this.fixedEvents = await this.fetchPublicHolidays(); - - const leavesData = await this.fetchApi( - "/modules/family-calendar/includes/api/get-leaves.php", - ); + this.fixedEvents = fixedEventsData; this.leaves = leavesData.leaves || []; - - const balancesData = await this.fetchApi( - "/modules/family-calendar/includes/api/get-leave-balances.php", - ); this.leaveBalances = balancesData.balances || []; - - // --- NOUVEAU : Chargement des correctifs de congés --- - const snapshotsData = await this.fetchApi( - "/modules/family-calendar/includes/api/get-leave-snapshots.php", - ); this.leaveSnapshots = snapshotsData.snapshots || []; this.events = [...this.dbEvents, ...this.fixedEvents]; @@ -233,38 +495,26 @@ document.addEventListener("DOMContentLoaded", () => { } } - // Les jours fériés restent hardcodés car ils sont fixes. async fetchPublicHolidays() { try { - // Appel à l'API officielle des jours fériés en métropole const res = await fetch( "https://calendrier.api.gouv.fr/jours-feries/metropole.json", ); const holidays = await res.json(); - - // L'API renvoie un objet : { "2024-01-01": "Jour de l'an", ... } - // On le transforme en tableau compatible avec ton système d'événements return Object.keys(holidays).map((date, idx) => ({ id: `ph-${idx}`, date: date, - name: holidays[date], // On garde le nom au cas où tu veuilles l'afficher plus tard + name: holidays[date], type: "PUBLIC_HOLIDAY", duration: 1, })); } catch (error) { - console.error( - "Erreur lors de la récupération des jours fériés :", - error, - ); - return []; // Évite de casser le calendrier si l'API de l'État est indisponible + return []; } } - // --- RECONSTRUCTION DE LA MODALE VIA LA BDD (Par blocs consécutifs) --- renderModalHolidays() { if (!this.schoolHolidaysTableBody) return; - - // On utilise bien l'année de la modale, indépendamment du planning de fond const startDate = `${this.modalSelectedYear}-09-01`; const endDate = `${this.modalSelectedYear + 1}-08-31`; @@ -277,13 +527,13 @@ document.addEventListener("DOMContentLoaded", () => { if (yearHolidays.length === 0) { this.schoolHolidaysTableBody.innerHTML = ` - - -

${tr("fc_err_no_data_gov")}

- - - - `; + + +

${window.I18N?.fc_err_no_data_gov || "Aucune donnée"}

+ + + + `; document .getElementById("btnFetchGovHolidays") ?.addEventListener("click", (e) => { @@ -295,7 +545,6 @@ document.addEventListener("DOMContentLoaded", () => { } yearHolidays.sort((a, b) => new Date(a.date) - new Date(b.date)); - const blocks = []; let currentBlock = null; @@ -306,128 +555,93 @@ document.addEventListener("DOMContentLoaded", () => { blocks.push(currentBlock); } else { const diffDays = Math.round( - (d.getTime() - currentBlock.end.getTime()) / (1000 * 60 * 60 * 24), + (d.getTime() - currentBlock.end.getTime()) / 86400000, ); - if (diffDays <= 4) { - currentBlock.end = d; - } else { + if (diffDays <= 4) currentBlock.end = d; + else { currentBlock = { start: d, end: d }; blocks.push(currentBlock); } } }); - let html = ""; - blocks.forEach((block) => { - const m = block.start.getMonth() + 1; - const durationDays = - Math.round( - (block.end.getTime() - block.start.getTime()) / - (1000 * 60 * 60 * 24), - ) + 1; + this.schoolHolidaysTableBody.innerHTML = blocks + .map((block) => { + const m = block.start.getMonth() + 1; + const dur = + Math.round( + (block.end.getTime() - block.start.getTime()) / 86400000, + ) + 1; + let name = window.I18N?.leg_school_holidays || "Vacances"; + if (m === 10 || m === 11) + name = window.I18N?.vac_toussaint || "Toussaint"; + else if (m === 12 || m === 1) name = window.I18N?.vac_noel || "Noël"; + else if (m === 2 || m === 3) name = window.I18N?.vac_hiver || "Hiver"; + else if (m === 4 || (m === 5 && dur > 6)) + name = window.I18N?.vac_printemps || "Printemps"; + else if (m === 5 && dur <= 6) + name = window.I18N?.vac_ascension || "Ascension"; + else if (m === 7 || m === 8) name = window.I18N?.vac_ete || "Eté"; - let name = tr("leg_school_holidays"); - if (m === 10 || m === 11) name = tr("vac_toussaint"); - else if (m === 12 || m === 1) 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"); - else if (m === 5 && durationDays <= 6) name = tr("vac_ascension"); - else if (m === 7 || m === 8) name = tr("vac_ete"); - - html += ` - - ${name} - ${block.start.toLocaleDateString(window.appLang || "fr-FR")} - ${block.end.toLocaleDateString(window.appLang || "fr-FR")} - - `; - }); - - this.schoolHolidaysTableBody.innerHTML = html; + return ` + ${name} + ${block.start.toLocaleDateString(window.appLang || "fr-FR")} + ${block.end.toLocaleDateString(window.appLang || "fr-FR")} + `; + }) + .join(""); } - // --- IMPORTATION DEPUIS L'API & SAUVEGARDE EN BDD --- async fetchAndSaveGovHolidays(yearStart) { try { const yearStr = `${yearStart}-${yearStart + 1}`; const zone = window.CONFIG?.ZONE_SCOLAIRE || "C"; if (zone === "Autre") { - alert( - "L'importation automatique n'est disponible que pour les zones A, B ou C (France).", - ); - const btn = document.getElementById("btnFetchGovHolidays"); - if (btn) { - btn.innerText = "Non disponible"; - btn.disabled = true; - } + alert("Import auto dispo que pour Zones A, B ou C (France)."); return; } const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100`; const res = await fetch(url); const data = await res.json(); - const rawRecords = data.results || []; - // Dédoublonnage const uniqueMap = new Map(); - rawRecords.forEach((r) => { - const key = `${r.description}|${r.start_date}`; - if (!uniqueMap.has(key)) uniqueMap.set(key, r); - }); + (data.results || []).forEach((r) => + uniqueMap.set(`${r.description}|${r.start_date}`, r), + ); const payload = []; - Array.from(uniqueMap.values()).forEach((r) => { - const startDateStr = r.start_date.split("T")[0]; - const endDateStr = r.end_date.split("T")[0]; - - let curr = new Date(startDateStr + "T00:00:00"); - const end = new Date(endDateStr + "T00:00:00"); - - // REGLE METIER : Si le 1er jour est un VENDREDI (jour=5), on décale le début au SAMEDI. - if (curr.getDay() === 5) { - curr.setDate(curr.getDate() + 1); - } + let curr = new Date(r.start_date.split("T")[0] + "T00:00:00"); + const end = new Date(r.end_date.split("T")[0] + "T00:00:00"); + if (curr.getDay() === 5) curr.setDate(curr.getDate() + 1); // Règle: Décale vendredi -> samedi while (curr < end) { - const iso = this.getLocalIsoDate(curr); payload.push({ - date: iso, + date: this.getLocalIsoDate(curr), type: "VACANCES_SCOLAIRES", duration: 1, - person: r.description, // ASTUCE: On stocke le nom de la vacance ici ! + person: r.description, }); curr.setDate(curr.getDate() + 1); } }); if (payload.length > 0) { - // On envoie le gros lot à la base de données via l'API existante await this.postApi( "/modules/family-calendar/includes/api/save-events.php", payload, ); - await this.refreshAllData(); // Recharge tout, ce qui mettra à jour la modale + await this.refreshAllData(); } else { - alert( - "Aucune donnée trouvée sur l'API du gouvernement pour cette année.", - ); - document.getElementById("btnFetchGovHolidays").innerText = - "Réessayer"; - document.getElementById("btnFetchGovHolidays").disabled = false; + alert("Aucune donnée trouvée."); } } catch (e) { - console.error("Erreur API", e); - alert("Erreur lors de la connexion à l'API gouvernementale."); + alert("Erreur API Gouv."); } } - // ================================================================ - // LE RESTE DU CODE (AFFICHAGE) RESTE INCHANGÉ MAIS OPTIMISÉ - // ================================================================ - reprocessAndRender() { this.reprocessEvents(); this.calculateMonthlyBalances(); @@ -465,8 +679,6 @@ document.addEventListener("DOMContentLoaded", () => { const d = new Date(l.leave_date + "T00:00:00"); if (d >= w.dayDates.mon && d <= w.dayDates.fri) { const dur = parseFloat(l.duration) || 1; - - // 🟢 Correspondance dynamique pour le stockage des totaux de la semaine let prefix = null; if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id)) prefix = "alex"; @@ -475,7 +687,6 @@ document.addEventListener("DOMContentLoaded", () => { parseInt(l.person_id) === parseInt(parents[1].id) ) prefix = "laia"; - if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur; } }); @@ -495,7 +706,6 @@ document.addEventListener("DOMContentLoaded", () => { calculateMonthlyBalances() { const balances = {}; - // 🟢 On initialise le tableau des soldes pour chaque parent trouvé parents.forEach((p) => { balances[p.id] = { CP: {}, JRA: {}, JA: {} }; }); @@ -506,10 +716,9 @@ document.addEventListener("DOMContentLoaded", () => { const usageByMonth = {}; this.leaves.forEach((l) => { - const pid = l.person_id; - const type = l.leave_type; - const ym = l.leave_date.substring(0, 7); - + const pid = l.person_id, + type = l.leave_type, + ym = l.leave_date.substring(0, 7); if (!usageByMonth[pid]) usageByMonth[pid] = {}; if (!usageByMonth[pid][type]) usageByMonth[pid][type] = {}; usageByMonth[pid][type][ym] = @@ -521,9 +730,8 @@ document.addEventListener("DOMContentLoaded", () => { ["CP", "JRA", "JA"].forEach((type) => { ymList.forEach((ym) => { const [currYear, currMonth] = ym.split("-").map(Number); - - let cycleStartStr = ""; - let initialBalance = 0; + let cycleStartStr = "", + initialBalance = 0; const latestSnapshot = (this.leaveSnapshots || []) .filter( @@ -560,25 +768,19 @@ document.addEventListener("DOMContentLoaded", () => { initialBalance = LEAVES_CONFIG.JRA.yearlyTotals[currYear] || LEAVES_CONFIG.JRA.defaultBalance; - if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) { const prevYear = currYear - 1; const prevInitial = LEAVES_CONFIG.JRA.yearlyTotals[prevYear] || LEAVES_CONFIG.JRA.defaultBalance; - let usedPrevYear = 0; - for (let m = 1; m <= 12; m++) { - const mStr = `${prevYear}-${String(m).padStart(2, "0")}`; - usedPrevYear += usageByMonth[pid]?.[type]?.[mStr] || 0; - } - - const remainingPrevYear = Math.max( - 0, - prevInitial - usedPrevYear, - ); + for (let m = 1; m <= 12; m++) + usedPrevYear += + usageByMonth[pid]?.[type]?.[ + `${prevYear}-${String(m).padStart(2, "0")}` + ] || 0; initialBalance += Math.min( - remainingPrevYear, + Math.max(0, prevInitial - usedPrevYear), LEAVES_CONFIG.JRA.maxReport, ); } @@ -604,25 +806,20 @@ document.addEventListener("DOMContentLoaded", () => { let usedBeforeCurrentMonth = 0; Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => { - if (usedYm >= cycleStartStr && usedYm < ym) { + if (usedYm >= cycleStartStr && usedYm < ym) usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm]; - } }); - const available = Math.max( - 0, - initialBalance - usedBeforeCurrentMonth, - ); - const usedInMonth = usageByMonth[pid]?.[type]?.[ym] || 0; - balances[pid][type][ym] = { - availableAtMonthStart: available, - usedInMonth: usedInMonth, + availableAtMonthStart: Math.max( + 0, + initialBalance - usedBeforeCurrentMonth, + ), + usedInMonth: usageByMonth[pid]?.[type]?.[ym] || 0, }; }); }); }); - this.monthlyLeaveBalances = balances; } @@ -688,8 +885,7 @@ document.addEventListener("DOMContentLoaded", () => { const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); if (dayLeaves.length) { - let html = `
`; - + let html = `
`; const colors = [ "#0f766e", "#b45309", @@ -697,258 +893,193 @@ document.addEventListener("DOMContentLoaded", () => { "#4338ca", "#b91c1c", ]; - if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) { - window.CONFIG.PEOPLE.forEach((person, index) => { - if ( - dayLeaves.some( - (l) => parseInt(l.person_id) === parseInt(person.id), - ) - ) { - const color = colors[index % colors.length]; - const initial = person.name.charAt(0).toUpperCase(); - html += `${initial}`; - } - }); - } - - html += `
`; - td.innerHTML += html; + parents.forEach((person, index) => { + if ( + dayLeaves.some( + (l) => parseInt(l.person_id) === parseInt(person.id), + ) + ) { + html += `${person.name.charAt(0).toUpperCase()}`; + } + }); + td.innerHTML += html + `
`; } tr.appendChild(td); }); - [ - "offCarole", - "extraOffCarole", - "centre", - "avis", - "pepSick", - "presencePep", - ].forEach((k) => { + // --- 🟢 CORRECTION 1 : Colonnes du milieu (Modes de garde & Maladie) --- + const activeCareModes = window.FAMILY_CONFIG?.activeCareModes || []; + const kids = window.FAMILY_CONFIG?.kids || []; + + // 1A. Modes de garde + activeCareModes.forEach((mode) => { const td = document.createElement("td"); td.className = "col-total"; - td.textContent = fmt(w.totals[k]); + // On normalise le nom (ex: "Centre" -> "centre") pour matcher ton objet w.totals + const key = mode.toLowerCase(); + td.textContent = fmt(w.totals[key] || 0); tr.appendChild(td); }); + // 1B. Enfants (Maladie) + kids.forEach((kid) => { + const td = document.createElement("td"); + td.className = "col-total"; + // Rétrocompatibilité avec ton ancien code "pepSick" + const key = + kid.name.toLowerCase() === "pep" + ? "pepSick" + : `${kid.name.toLowerCase()}Sick`; + td.textContent = fmt(w.totals[key] || 0); + tr.appendChild(td); + }); + + // --- 🟢 CORRECTION 2 : Colonnes des congés Parents (Dynamique !) --- if (!processedLeavesCols[w.monthKey]) { processedLeavesCols[w.monthKey] = true; - const span = monthSpans[w.monthKey]; - const ym = w.monthKey; - const renderPersonCols = (pid, prefix) => { - ["CP", "JRA", "JA"].forEach((type) => { - const info = this.monthlyLeaveBalances[pid][type][ym]; - const tdAv = document.createElement("td"); - tdAv.className = `${prefix}-sub ${prefix}-av`; - tdAv.rowSpan = span; - tdAv.textContent = info ? fmt(info.availableAtMonthStart) : "-"; - tr.appendChild(tdAv); - const tdUse = document.createElement("td"); - tdUse.className = `${prefix}-sub ${prefix}-use`; - tdUse.rowSpan = span; - tdUse.textContent = info ? fmt(info.usedInMonth) : ""; - tr.appendChild(tdUse); - }); - }; - parents.forEach((parent, index) => { + + const parentsList = window.FAMILY_CONFIG?.parents || []; + parentsList.forEach((parent, index) => { const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia"; - renderPersonCols(parent.id, cssPrefix); + + // On récupère les compteurs réels du parent envoyés par le PHP + const parentLeaveTypes = + parent.leave_types && parent.leave_types.length > 0 + ? parent.leave_types + : ["CP", "JRA", "JA"]; + + parentLeaveTypes.forEach((type) => { + const info = + this.monthlyLeaveBalances[parent.id]?.[type]?.[w.monthKey]; + tr.innerHTML += `${info ? fmt(info.availableAtMonthStart) : "-"}`; + tr.innerHTML += `${info ? fmt(info.usedInMonth) : ""}`; + }); }); } + this.planningBody.appendChild(tr); }); } - // --- Auto-scroll vers le mois en cours --- scrollToCurrentMonth() { const wrapper = document.getElementById("planningTable-wrapper"); - const thead = document.querySelector("#planningTable thead"); - if (!wrapper || !thead) return; - - const now = new Date(); - // Construit la clé au format "YYYY-MM" - const currentYm = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; - - // Cherche la TOUTE PREMIÈRE ligne qui correspond à ce mois const targetRow = document.querySelector( - `#planningTable tbody tr[data-month="${currentYm}"]`, + `#planningTable tbody tr[data-month="${this.getLocalIsoDate(new Date()).slice(0, 7)}"]`, ); - - if (targetRow) { - // On donne 50ms au navigateur pour finir son rendu graphique avant de calculer les hauteurs - setTimeout(() => { - const scrollPos = targetRow.offsetTop - thead.offsetHeight; - wrapper.scrollTo({ - top: scrollPos > 0 ? scrollPos : 0, - behavior: "smooth", - }); - }, 50); - } + if (wrapper && targetRow) + setTimeout( + () => + wrapper.scrollTo({ + top: Math.max(0, targetRow.offsetTop - 50), + behavior: "smooth", + }), + 50, + ); } renderMonthCalendar() { if (!this.monthCalendar) return; - this.monthCalendar.innerHTML = ""; - - const y = this.currentMonth.getFullYear(); - const m = this.currentMonth.getMonth(); - + const y = this.currentMonth.getFullYear(), + m = this.currentMonth.getMonth(); 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") { - this.renderThreeMonthsView(); - } else if (this.viewMode === "2months") { - this.renderTwoMonthsView(); - } else { - this.monthCalendar.innerHTML = this.generateMonthHTML(y, m); - } + if (this.viewMode === "3months") this.renderThreeMonthsView(); + else if (this.viewMode === "2months") this.renderTwoMonthsView(); + else this.monthCalendar.innerHTML = this.generateMonthHTML(y, m); } - this.renderMonthBalances(); this.syncSummaryWithMonth(); } renderTwoMonthsView() { - const y = this.currentMonth.getFullYear(); - const m = this.currentMonth.getMonth(); + const y = this.currentMonth.getFullYear(), + m = this.currentMonth.getMonth(); const nextDate = new Date(y, m + 1, 1); const lang = window.I18N_LANG || "fr-FR"; - - let html = `
`; - html += `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(this.currentMonth)}
${this.generateMonthHTML(y, m)}
`; - html += `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(nextDate)}
${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}
`; - html += `
`; - - this.monthCalendar.innerHTML = html; + this.monthCalendar.innerHTML = `
+
${new Intl.DateTimeFormat(lang, { month: "long" }).format(this.currentMonth)}
${this.generateMonthHTML(y, m)}
+
${new Intl.DateTimeFormat(lang, { month: "long" }).format(nextDate)}
${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}
+
`; } renderThreeMonthsView() { - const y = this.currentMonth.getFullYear(); - const m = this.currentMonth.getMonth(); - const d1 = this.currentMonth; - const d2 = new Date(y, m + 1, 1); - const d3 = new Date(y, m + 2, 1); - + const y = this.currentMonth.getFullYear(), + m = this.currentMonth.getMonth(); + const [d1, d2, d3] = [ + this.currentMonth, + new Date(y, m + 1, 1), + new Date(y, m + 2, 1), + ]; let html = `
`; - [d1, d2, d3].forEach((d) => { - html += `
${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(d)}
${this.generateMonthHTML(d.getFullYear(), d.getMonth())}
`; - }); - html += `
`; - this.monthCalendar.innerHTML = html; + [d1, d2, d3].forEach( + (d) => + (html += `
${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(d)}
${this.generateMonthHTML(d.getFullYear(), d.getMonth())}
`), + ); + this.monthCalendar.innerHTML = html + `
`; } renderMonthBalances() { const container = document.getElementById("fc-month-balances"); if (!container) return; - - const monthsToDisplay = []; - const y = this.currentMonth.getFullYear(); - const m = this.currentMonth.getMonth(); - + const monthsToDisplay = [], + y = this.currentMonth.getFullYear(), + m = this.currentMonth.getMonth(); let numMonths = this.viewMode === "2months" ? 2 : this.viewMode === "3months" ? 3 : 1; - - for (let i = 0; i < numMonths; i++) { - const d = new Date(y, m + i, 1); - monthsToDisplay.push( - `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`, - ); - } + for (let i = 0; i < numMonths; i++) + monthsToDisplay.push(`${y}-${String(m + i + 1).padStart(2, "0")}`); container.style.display = "flex"; - let html = ""; + container.innerHTML = parents + .map((person) => { + let cards = `
${person.name.toUpperCase()}
`; + ["CP", "JRA", "JA"].forEach((type) => { + const startBal = + this.monthlyLeaveBalances[person.id]?.[type]?.[monthsToDisplay[0]] + ?.availableAtMonthStart || 0; + let totalUsed = 0; + monthsToDisplay.forEach( + (ym) => + (totalUsed += + this.monthlyLeaveBalances[person.id]?.[type]?.[ym] + ?.usedInMonth || 0), + ); + const endBal = Math.max(0, startBal - totalUsed); + const fmt = (n) => + n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0"; - parents.forEach((person) => { - html += `
- ${person.name.toUpperCase()} -
`; - - ["CP", "JRA", "JA"].forEach((type) => { - const startInfo = - this.monthlyLeaveBalances[person.id]?.[type]?.[monthsToDisplay[0]]; - const startBal = startInfo ? startInfo.availableAtMonthStart : 0; - - let totalUsed = 0; - monthsToDisplay.forEach((ym) => { - const info = this.monthlyLeaveBalances[person.id]?.[type]?.[ym]; - if (info) totalUsed += info.usedInMonth; - }); - - const endBal = Math.max(0, startBal - totalUsed); - const fmt = (n) => - n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0"; - - const usedHtml = - totalUsed > 0 - ? `-${fmt(totalUsed)}` - : ""; - - // --- LOGIQUE D'ALERTE FIRE 🔥 --- - let alertHtml = ""; - const currentYm = monthsToDisplay[0]; - const [cYear, cMonth] = currentYm.split("-").map(Number); - - if (endBal > 0) { - if (type === "CP" && cMonth >= 6 && cMonth <= 7) { - // CP : Alerte en Juin et Juillet uniquement - let msg = ( - window.I18N["fc_alert_burn_days"] || "Perte: %s j avant le %s" + let alertHtml = ""; + const cMonth = parseInt(monthsToDisplay[0].split("-")[1]); + if (endBal > 0) { + if (type === "CP" && cMonth >= 6 && cMonth <= 7) + alertHtml = `
🔥
`; + else if ( + type === "JRA" && + (cMonth === 1 || cMonth === 2) && + endBal > 2 ) - .replace("%s", fmt(endBal)) - .replace("%s", "31/07"); - alertHtml = `
🔥
`; - } else if (type === "JRA" && (cMonth === 1 || cMonth === 2)) { - // JRA : Alerte Janvier/Février si > 2 jours - if (endBal > 2) { - let msg = - window.I18N["fc_alert_burn_jra"] || "Seuls 2j reportables"; - alertHtml = `
🔥 ${fmt(endBal - 2)}
`; - } - } else if (type === "JA") { - const configJA = LEAVES_CONFIG.JA[person.id]; - let limitMonth = configJA.startMonth - 1 || 12; - if (cMonth === limitMonth || cMonth === configJA.startMonth) { - let msg = (window.I18N["fc_alert_burn_days"] || "Perte: %s j") - .replace("%s", fmt(endBal)) - .replace("%s", window.I18N["ANNIV"] || "Anniv"); - alertHtml = `
🔥
`; - } + alertHtml = `
🔥
`; } - } - - html += `
- ${type} - ${fmt(endBal)} - ${usedHtml} - ${alertHtml} -
`; - }); - html += `
`; - }); - container.innerHTML = html; + cards += `
${type}${fmt(endBal)}${totalUsed > 0 ? `-${fmt(totalUsed)}` : ""}${alertHtml}
`; + }); + return cards + `
`; + }) + .join(""); } syncSummaryWithMonth() { - const typeSelect = document.getElementById("summType"); - const valueSelect = document.getElementById("summValue"); - + const typeSelect = document.getElementById("summType"), + valueSelect = document.getElementById("summValue"); if (typeSelect && valueSelect) { - // Le récap se synchronise avec le PREMIER mois affiché de la période - const ym = `${this.currentMonth.getFullYear()}-${String(this.currentMonth.getMonth() + 1).padStart(2, "0")}`; - if (typeSelect.value !== "month") { typeSelect.value = "month"; typeSelect.dispatchEvent(new Event("change")); } - valueSelect.value = ym; + valueSelect.value = `${this.currentMonth.getFullYear()}-${String(this.currentMonth.getMonth() + 1).padStart(2, "0")}`; this.updateGlobalSummary(); } } @@ -959,10 +1090,8 @@ document.addEventListener("DOMContentLoaded", () => { html += ``; const daysInMonth = new Date(year, month + 1, 0).getDate(); - let currentRenderedCols = 0; - - const firstDay = new Date(year, month, 1); - let startDay = (firstDay.getDay() + 6) % 7; + let currentRenderedCols = 0, + startDay = (new Date(year, month, 1).getDay() + 6) % 7; if (startDay < 5) { for (let i = 0; i < startDay; i++) { @@ -971,22 +1100,19 @@ document.addEventListener("DOMContentLoaded", () => { } } - for (let dayCounter = 1; dayCounter <= daysInMonth; dayCounter++) { - const dateObj = new Date(year, month, dayCounter); - const dayOfWeek = dateObj.getDay(); - + for (let d = 1; d <= daysInMonth; d++) { + const dateObj = new Date(year, month, d), + dayOfWeek = dateObj.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) continue; if (currentRenderedCols === 5) { html += ``; currentRenderedCols = 0; } + const iso = this.getLocalIsoDate(dateObj), + todayIso = this.getLocalIsoDate(new Date()); - const iso = this.getLocalIsoDate(dateObj); - const todayIso = this.getLocalIsoDate(new Date()); // Date du jour - - let cls = "fc-month-day"; - if (iso === todayIso) cls += " fc-day--today"; // On applique la classe ! + let cls = "fc-month-day" + (iso === todayIso ? " fc-day--today" : ""); const dayEvts = this.events.filter((e) => e.date === iso); if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES")) @@ -997,105 +1123,81 @@ document.addEventListener("DOMContentLoaded", () => { cls += " fc-day--off-carole"; if (dayEvts.some((e) => e.type === "EXTRA_OFF_CAROLE")) cls += " fc-day--extra-off-carole"; - if (dayEvts.some((e) => ["CENTRE", "AVIS"].includes(e.type))) { + if (dayEvts.some((e) => ["CENTRE", "AVIS"].includes(e.type))) cls += " fc-day--has-guard"; - if (dayEvts.some((e) => e.type === "CENTRE")) - cls += " fc-day--centre"; - if (dayEvts.some((e) => e.type === "AVIS")) cls += " fc-day--avis"; - } - let content = `
${dayCounter}`; + let content = `
${d}`; if (dayEvts.some((e) => e.type === "PEP_SICK")) content += `🤒`; + const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); if (dayLeaves.length) { content += `
`; - parents.forEach((parent, index) => { if ( dayLeaves.some( (l) => parseInt(l.person_id) === parseInt(parent.id), ) ) { - const color = index % 2 === 0 ? "#0f766e" : "#b45309"; - const initial = parent.name.charAt(0).toUpperCase(); - content += `${initial} `; + content += `${parent.name.charAt(0).toUpperCase()} `; } }); - content += `
`; } - content += `
`; - html += `${content}`; + html += `${content}
`; currentRenderedCols++; } - while (currentRenderedCols < 5 && currentRenderedCols > 0) { html += ``; currentRenderedCols++; } - - html += ``; - return html; + return html + ``; } initSummaryControls() { - const typeSelect = document.getElementById("summType"); - const valueSelect = document.getElementById("summValue"); + const typeSelect = document.getElementById("summType"), + valueSelect = document.getElementById("summValue"); if (!typeSelect || !valueSelect) return; - const years = new Set(); - const months = new Set(); - - this.weeks.forEach((w) => { + const years = new Set(), + months = new Set(); + this.weeks.forEach((w) => Object.values(w.dayDates).forEach((d) => { years.add(d.getFullYear()); - const mKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; - months.add(mKey); - }); - }); - - const sortedYears = Array.from(years).sort(); - const sortedMonths = Array.from(months).sort(); + months.add( + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`, + ); + }), + ); const populateValues = () => { - const currentType = typeSelect.value; - const previousValue = valueSelect.value; + const currentType = typeSelect.value, + prevValue = valueSelect.value; valueSelect.innerHTML = ""; - if (currentType === "year") { - sortedYears.forEach((y) => { - const opt = document.createElement("option"); - opt.value = y; - opt.textContent = y; - valueSelect.appendChild(opt); - }); - if (sortedYears.includes(parseInt(previousValue))) { - valueSelect.value = previousValue; - } else { - const currentYear = new Date().getFullYear(); - if (sortedYears.includes(currentYear)) - valueSelect.value = currentYear; - } + Array.from(years) + .sort() + .forEach((y) => valueSelect.add(new Option(y, y))); + valueSelect.value = years.has(parseInt(prevValue)) + ? prevValue + : new Date().getFullYear(); } else { - sortedMonths.forEach((m) => { - const [y, mo] = m.split("-"); - const dateObj = new Date(y, mo - 1, 1); - const label = new Intl.DateTimeFormat("fr-FR", { - month: "long", - year: "numeric", - }).format(dateObj); - const opt = document.createElement("option"); - opt.value = m; - opt.textContent = label; - valueSelect.appendChild(opt); - }); - - const now = new Date(); - const nowIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; - if (sortedMonths.includes(nowIso)) { - valueSelect.value = nowIso; - } + Array.from(months) + .sort() + .forEach((m) => { + const [y, mo] = m.split("-"); + valueSelect.add( + new Option( + new Intl.DateTimeFormat("fr-FR", { + month: "long", + year: "numeric", + }).format(new Date(y, mo - 1, 1)), + m, + ), + ); + }); + const nowIso = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`; + if (months.has(nowIso)) valueSelect.value = nowIso; } this.updateGlobalSummary(); }; @@ -1111,50 +1213,41 @@ document.addEventListener("DOMContentLoaded", () => { } updateGlobalSummary() { - const div = document.getElementById("globalSummary"); - const typeSelect = document.getElementById("summType"); - const valueSelect = document.getElementById("summValue"); + const div = document.getElementById("globalSummary"), + typeSelect = document.getElementById("summType"), + valueSelect = document.getElementById("summValue"); + if (!div || !valueSelect?.value) return; - if (!div || (typeSelect && valueSelect && !valueSelect.value)) return; - - const filterType = typeSelect ? typeSelect.value : "year"; - const filterValue = valueSelect - ? valueSelect.value - : new Date().getFullYear().toString(); - - const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 }; + const filterType = typeSelect.value, + filterValue = valueSelect.value; + const stats = { off: 0, extra: 0, sick: 0, pep: 0 }; this.weeks.forEach((w) => { - Object.entries(w.dayDates).forEach(([dayName, dateObj]) => { - let match = false; - if (filterType === "year") { - if (dateObj.getFullYear().toString() === filterValue) match = true; - } else { - const isoMonth = this.getLocalIsoDate(dateObj).slice(0, 7); - if (isoMonth === filterValue) match = true; - } - - if (match) { - const dayEvents = w.dayFlags[dayName].events; + Object.values(w.dayDates).forEach((dateObj) => { + if ( + (filterType === "year" && + dateObj.getFullYear().toString() === filterValue) || + (filterType === "month" && + this.getLocalIsoDate(dateObj).slice(0, 7) === filterValue) + ) { + const dayEvents = this.events.filter( + (e) => e.date === this.getLocalIsoDate(dateObj), + ); dayEvents.forEach((e) => { const dur = parseFloat(e.duration) || 1; if (e.type === "OFF_CAROLE") stats.off += dur; if (e.type === "EXTRA_OFF_CAROLE") stats.extra += dur; if (e.type === "PEP_SICK") stats.sick += dur; }); - - const dateIso = this.getLocalIsoDate(dateObj); - if (!this.publicHolidayDates.has(dateIso)) { - stats.totalWorking++; + if (!this.publicHolidayDates.has(this.getLocalIsoDate(dateObj))) { let dayAbsence = 0; dayEvents.forEach((e) => { if ( ["OFF_CAROLE", "EXTRA_OFF_CAROLE", "PEP_SICK"].includes( e.type, ) - ) { + ) dayAbsence += parseFloat(e.duration) || 1; - } }); stats.pep += Math.max(0, 1 - dayAbsence); } @@ -1162,12 +1255,13 @@ document.addEventListener("DOMContentLoaded", () => { }); }); + const tr = window.I18N || {}; div.innerHTML = ` -
${tr("leg_off_carole")}${parseFloat(stats.off.toFixed(1))} ${tr("fc_unit_days")}
-
${tr("leg_extra_off")}${parseFloat(stats.extra.toFixed(1))} ${tr("fc_unit_days")}
-
${tr("leg_pep_sick")}${parseFloat(stats.sick.toFixed(1))} ${tr("fc_unit_days")}
-
${tr("leg_presence")}${parseFloat(stats.pep.toFixed(1))} ${tr("fc_unit_days")}
- `; +
${tr.leg_off_carole || "Off"}${parseFloat(stats.off.toFixed(1))} j
+
${tr.leg_extra_off || "Extra"}${parseFloat(stats.extra.toFixed(1))} j
+
${tr.leg_pep_sick || "Maladie"}${parseFloat(stats.sick.toFixed(1))} j
+
${tr.leg_presence || "Présence"}${parseFloat(stats.pep.toFixed(1))} j
+ `; } updateSchoolYearLabel() { @@ -1216,17 +1310,13 @@ document.addEventListener("DOMContentLoaded", () => { async fetchApi(url) { return pachaFetch(url); } - async postApi(url, data) { return pachaFetch(url, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); } - async changeSchoolYear(delta) { this.currentSchoolYearStart += delta; this.updateSchoolYearLabel(); @@ -1234,15 +1324,17 @@ document.addEventListener("DOMContentLoaded", () => { } setupEventListeners() { + // -- BRANCHEMENT MODALE DE CONFIGURATION (NOUVEAU) -- + const btnSettings = document.getElementById("btnOpenCalendarSettings"); + if (btnSettings) + btnSettings.addEventListener("click", openCalendarSettings); + if (this.planningBody) { - // Souris (PC) this.planningBody.addEventListener("mousedown", (e) => this.handleMouseDown(e), ); document.addEventListener("mousemove", (e) => this.handleMouseMove(e)); document.addEventListener("mouseup", (e) => this.handleMouseUp(e)); - - // Tactile (Mobile/Tablette) this.planningBody.addEventListener( "touchstart", (e) => this.handleTouchStart(e), @@ -1256,26 +1348,18 @@ document.addEventListener("DOMContentLoaded", () => { 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) { + } 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); @@ -1283,103 +1367,75 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // --- GESTION MODALE VACANCES SCOLAIRES --- - const btnOpen = document.getElementById("btnOpenHolidays"); - const btnClose = document.getElementById("btnCloseHolidays"); - const modal = document.getElementById("modalHolidays"); - - if (btnOpen && modal) { + const btnOpen = document.getElementById("btnOpenHolidays"), + btnClose = document.getElementById("btnCloseHolidays"), + modal = document.getElementById("modalHolidays"); + if (btnOpen && modal) btnOpen.addEventListener("click", () => { - modal.classList.add("open"); // Utilisation propre de la classe CSS + modal.classList.add("open"); document.body.classList.add("no-scroll"); - - const yearSelect = document.getElementById("holidayYearSelect"); - if (yearSelect) yearSelect.value = this.currentSchoolYearStart; - this.modalSelectedYear = this.currentSchoolYearStart; this.renderModalHolidays(); }); - } - if (btnClose && modal) { + if (btnClose && modal) btnClose.addEventListener("click", () => { modal.classList.remove("open"); document.body.classList.remove("no-scroll"); }); - } - if (modal) { + if (modal) modal.addEventListener("click", (e) => { if (e.target === modal) { modal.classList.remove("open"); document.body.classList.remove("no-scroll"); } }); - } - // --- GESTION MODALE CORRECTIF DES SOLDES (SNAPSHOTS) --- - const btnSnap = document.getElementById("btnOpenSnapshotModal"); - const modalSnap = document.getElementById("modalSnapshot"); - const btnCloseSnap = document.getElementById("btnCloseSnapshot"); - const formSnap = document.getElementById("formSnapshot"); - - if (btnSnap && modalSnap) { + const btnSnap = document.getElementById("btnOpenSnapshotModal"), + modalSnap = document.getElementById("modalSnapshot"), + btnCloseSnap = document.getElementById("btnCloseSnapshot"), + formSnap = document.getElementById("formSnapshot"); + if (btnSnap && modalSnap) btnSnap.addEventListener("click", () => { - modalSnap.classList.add("open"); // On ouvre la modale correctement + modalSnap.classList.add("open"); document.body.classList.add("no-scroll"); - - // Pré-remplir la date avec le 1er jour du mois en cours - const today = new Date(); - const y = today.getFullYear(); - const m = String(today.getMonth() + 1).padStart(2, "0"); const snapDateInput = document.getElementById("snapDate"); - if (snapDateInput) snapDateInput.value = `${y}-${m}-01`; + if (snapDateInput) + snapDateInput.value = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}-01`; }); - } - - if (btnCloseSnap && modalSnap) { + if (btnCloseSnap && modalSnap) btnCloseSnap.addEventListener("click", () => { modalSnap.classList.remove("open"); document.body.classList.remove("no-scroll"); }); - } - - if (modalSnap) { + if (modalSnap) modalSnap.addEventListener("click", (e) => { if (e.target === modalSnap) { modalSnap.classList.remove("open"); document.body.classList.remove("no-scroll"); } }); - } - - if (formSnap) { + if (formSnap) formSnap.addEventListener("submit", async (e) => { e.preventDefault(); - - const payload = { - person_id: document.getElementById("snapPerson").value, - leave_type: document.getElementById("snapType").value, - snapshot_date: document.getElementById("snapDate").value, - remaining_balance: document.getElementById("snapBalance").value, - }; - try { await this.postApi( "/modules/family-calendar/includes/api/save-leave-snapshot.php", - payload, + { + person_id: document.getElementById("snapPerson").value, + leave_type: document.getElementById("snapType").value, + snapshot_date: document.getElementById("snapDate").value, + remaining_balance: document.getElementById("snapBalance").value, + }, ); - - modalSnap.classList.remove("open"); // Fermeture propre + modalSnap.classList.remove("open"); document.body.classList.remove("no-scroll"); formSnap.reset(); - await this.refreshAllData(); } catch (error) { - alert("Erreur lors de la sauvegarde du correctif."); + alert("Erreur lors de la sauvegarde."); } }); - } - // --- GESTION DU RESTE DE L'INTERFACE --- - if (this.monthCalendar) { + if (this.monthCalendar) this.monthCalendar.addEventListener("click", (e) => { const td = e.target.closest("td[data-date]"); if (td && td.dataset.date) { @@ -1387,14 +1443,11 @@ document.addEventListener("DOMContentLoaded", () => { this.showMenu(e.pageX, e.pageY, [td.dataset.date], true); } }); - } - document.addEventListener("click", (e) => this.closeMenusIfOutside(e)); const handleMenu = (e) => { const btn = e.target.closest("button"); if (btn && btn.dataset.action) this.handleMenuAction(btn.dataset); }; - if (this.selectionMenu) this.selectionMenu.addEventListener("click", handleMenu); if (this.monthSelectionMenu) @@ -1412,61 +1465,39 @@ document.addEventListener("DOMContentLoaded", () => { this.currentMonth.setMonth(this.currentMonth.getMonth() + 1); 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.currentMonth.setDate(1); this.renderMonthCalendar(); }); - // --- GESTION DU SWIPE MENSUEL (MOBILE) --- if (this.monthCalendar) { - let touchStartX = 0; - let touchStartY = 0; - - // On écoute le début du toucher + let startX = 0, + startY = 0; this.monthCalendar.addEventListener( "touchstart", (e) => { - touchStartX = e.changedTouches[0].screenX; - touchStartY = e.changedTouches[0].screenY; + startX = e.changedTouches[0].screenX; + startY = 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 = + const dX = e.changedTouches[0].screenX - startX, + dY = e.changedTouches[0].screenY - startY; + if (Math.abs(dX) > Math.abs(dY) && Math.abs(dX) > 50) { + const num = 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 + this.currentMonth.setMonth( + this.currentMonth.getMonth() + (dX < 0 ? num : -num), + ); + this.renderMonthCalendar(); } }, { passive: true }, @@ -1478,8 +1509,7 @@ document.addEventListener("DOMContentLoaded", () => { document .getElementById("fc-next-school-year") ?.addEventListener("click", () => this.changeSchoolYear(1)); - - document.querySelectorAll(".fc-view-button").forEach((btn) => { + document.querySelectorAll(".fc-view-button").forEach((btn) => btn.addEventListener("click", (e) => { document .querySelectorAll(".fc-view-button") @@ -1487,8 +1517,8 @@ document.addEventListener("DOMContentLoaded", () => { e.target.classList.add("fc-view-button--active"); this.viewMode = e.target.dataset.view; this.renderMonthCalendar(); - }); - }); + }), + ); } handleMouseDown(e) { @@ -1499,64 +1529,57 @@ document.addEventListener("DOMContentLoaded", () => { this.isSelecting = true; this.selectCell(td); } - handleMouseMove(e) { if (!this.isSelecting) return; const td = e.target.closest("#planningTable td[data-date]"); if (td && !this.selectedCells.includes(td)) this.selectCell(td); } - handleMouseUp(e) { if (!this.isSelecting) return; const td = e.target.closest("#planningTable td[data-date]"); if (td && !this.selectedCells.includes(td)) this.selectCell(td); this.isSelecting = false; - if (!this.selectedCells.length) return; - const dates = this.selectedCells.map((c) => c.dataset.date); - this.showMenu(e.pageX, e.pageY, dates, false); + if (this.selectedCells.length) + this.showMenu( + e.pageX, + e.pageY, + this.selectedCells.map((c) => c.dataset.date), + false, + ); } - - // --- GESTION DU TACTILE (Swipe pour sélectionner) --- handleTouchStart(e) { const td = e.target.closest("#planningTable td[data-date]"); if (!td) return; - - // Si l'utilisateur touche une case, on bloque le scroll de la page pour le laisser sélectionner e.preventDefault(); - this.clearSelection(); this.isSelecting = true; this.selectCell(td); } - handleTouchMove(e) { if (!this.isSelecting) return; - e.preventDefault(); // Bloque le défilement de l'écran pendant qu'on glisse le doigt - - const touch = e.touches[0]; - // elementFromPoint permet de savoir sur quelle case se trouve le doigt actuellement - const target = document.elementFromPoint(touch.clientX, touch.clientY); + e.preventDefault(); + const touch = e.touches[0], + target = document.elementFromPoint(touch.clientX, touch.clientY); if (!target) return; - const td = target.closest("#planningTable td[data-date]"); if (td && !this.selectedCells.includes(td)) this.selectCell(td); } - handleTouchEnd(e) { if (!this.isSelecting) return; this.isSelecting = false; - if (!this.selectedCells.length) return; - - const dates = this.selectedCells.map((c) => c.dataset.date); - // Sur mobile, on s'en fiche de X et Y car le menu s'affiche en bas de l'écran - this.showMenu(0, 0, dates, false); + if (this.selectedCells.length) + this.showMenu( + 0, + 0, + this.selectedCells.map((c) => c.dataset.date), + false, + ); } selectCell(cell) { cell.classList.add("fc-day--selected"); this.selectedCells.push(cell); } - clearSelection() { this.selectedCells.forEach((c) => c.classList.remove("fc-day--selected")); this.selectedCells = []; @@ -1564,24 +1587,20 @@ document.addEventListener("DOMContentLoaded", () => { if (this.monthSelectionMenu) this.monthSelectionMenu.style.display = "none"; } - closeMenusIfOutside(e) { if ( !this.selectionMenu?.contains(e.target) && !this.monthSelectionMenu?.contains(e.target) && !this.menuJustOpened - ) { + ) this.clearSelection(); - } } showMenu(x, y, dates, isMonthView) { const menu = isMonthView ? this.monthSelectionMenu : this.selectionMenu; if (!menu) return; - 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); @@ -1589,117 +1608,54 @@ document.addEventListener("DOMContentLoaded", () => { const activeLeaves = {}; parents.forEach((p) => (activeLeaves[p.id] = 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); - } + if (dates.includes(l.leave_date) && 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) + const getBtnClass = (type, pid = null) => + (pid ? activeLeaves[pid]?.has(type) : activeEvents.has(type)) + ? "fc-menu-btn--active" + : ""; + const getBtnIcon = (type, pid = null) => + (pid ? activeLeaves[pid]?.has(type) : activeEvents.has(type)) + ? "✓ " + : ""; + const tr = window.I18N || {}; const dateLabel = dates.length > 1 - ? `${dates.length} ${tr("fc_unit_days")}` + ? `${dates.length} Jours` : new Date(dates[0]).toLocaleDateString(window.I18N_LANG || "fr-FR"); - - // Icône SVG Poubelle const trashSvg = ``; + const buildHeader = (title, action, cat) => + `
${title}${action ? `` : ""}
`; - const buildHeader = (title, action, cat) => ` -
- ${title} - ${action ? `` : ""} -
- `; - - let html = `
- ${dateLabel} -
`; - - // 2. Section Carole - html += `
- ${buildHeader(tr("fc_menu_carole"), "clear-type", "CONGE")} -
- - -
-
`; - - // 3. Section Garde - html += `
- ${buildHeader(tr("leg_centre"), "clear-type", "GARDE")} -
- - -
-
`; - - // 4. Section Pep - html += `
- ${buildHeader("Pep", "clear-type", "PEP")} - -
`; - - // 5. Section Parents (Congés) - html += `
- ${buildHeader(tr("fc_menu_kids_leaves"), null, null)} -
- - - `; - - // En-têtes dynamiques (Noms + Bouton Poubelle) - parents.forEach((parent) => { - html += ``; - }); - html += ` - - `; - - // Lignes de congés (CP, JRA, JA) + let html = `
${dateLabel}
`; + html += `
${buildHeader(tr.fc_menu_carole || "Carole", "clear-type", "CONGE")}
`; + html += `
${buildHeader(tr.leg_centre || "Centre", "clear-type", "GARDE")}
`; + html += `
${buildHeader("Pep", "clear-type", "PEP")}
`; + html += `
${buildHeader(tr.fc_menu_kids_leaves || "Congés Parents", null, null)}
-
- ${parent.name} -
-
`; + parents.forEach( + (p) => + (html += ``), + ); + html += ``; ["CP", "JRA", "JA"].forEach((t) => { html += ``; - parents.forEach((parent) => { - html += ``; - }); + parents.forEach( + (p) => + (html += ``), + ); html += ``; }); - - html += ` -
${p.name}
-
-
`; + html += `
`; menu.innerHTML = html; - - // Positionnement (Gestion des bords d'écran) - const menuWidth = 240; let left = x + 10; - if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10; - + if (left + 240 > window.innerWidth) left = x - 250; menu.style.left = `${left}px`; menu.style.top = `${y + 10}px`; menu.style.display = "block"; - this.menuJustOpened = true; setTimeout(() => (this.menuJustOpened = false), 100); } @@ -1708,9 +1664,8 @@ document.addEventListener("DOMContentLoaded", () => { if (this.selectionMenu) this.selectionMenu.style.display = "none"; if (this.monthSelectionMenu) this.monthSelectionMenu.style.display = "none"; - - const { action, type, pid, cat } = dataset; - const dates = this._currentBulkInfo.dates; + const { action, type, pid, cat } = dataset, + dates = this._currentBulkInfo.dates; try { if (action === "add") { @@ -1720,28 +1675,29 @@ document.addEventListener("DOMContentLoaded", () => { if (["CENTRE", "AVIS"].includes(type)) typesToClear = GUARDE_TYPES; if (type === "PEP_SICK") typesToClear = PEP_TYPES; - if (typesToClear.length) { + if (typesToClear.length) await this.postApi( "/modules/family-calendar/includes/api/manage-event.php", { action: "bulk_delete_day_types", dates, types: typesToClear }, ); - } - const payload = dates.map((d) => ({ - date: d, - type: type, - duration: 1, - person: "Carole", - })); await this.postApi( "/modules/family-calendar/includes/api/save-events.php", - payload, + dates.map((d) => ({ + date: d, + type: type, + duration: 1, + person: "Carole", + })), ); } else if (action === "clear-type") { - let typesToClear = []; - if (cat === "CONGE") typesToClear = CONGE_TYPES; - if (cat === "GARDE") typesToClear = GUARDE_TYPES; - if (cat === "PEP") typesToClear = PEP_TYPES; - + let typesToClear = + cat === "CONGE" + ? CONGE_TYPES + : cat === "GARDE" + ? GUARDE_TYPES + : cat === "PEP" + ? PEP_TYPES + : []; await this.postApi( "/modules/family-calendar/includes/api/manage-event.php", { action: "bulk_delete_day_types", dates, types: typesToClear }, @@ -1755,16 +1711,14 @@ document.addEventListener("DOMContentLoaded", () => { person_id: parseInt(pid), }, ); - - const payload = dates.map((d) => ({ - date: d, - person_id: parseInt(pid), // On s'assure que c'est bien un format numérique - leave_type: type, - duration: 1, - })); await this.postApi( "/modules/family-calendar/includes/api/save-leaves.php", - payload, + dates.map((d) => ({ + date: d, + person_id: parseInt(pid), + leave_type: type, + duration: 1, + })), ); } else if (action === "clear-leaves-person") { await this.postApi( @@ -1775,24 +1729,10 @@ document.addEventListener("DOMContentLoaded", () => { person_id: parseInt(pid), }, ); - } else if (action === "clear-leaves") { - if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) { - for (const person of window.CONFIG.PEOPLE) { - await this.postApi( - "/modules/family-calendar/includes/api/manage-leaf.php", - { - action: "bulk_delete_day_person", - dates, - person_id: person.id, - }, - ); - } - } } - await this.refreshAllData(); } catch (e) { - alert("Erreur action: " + e.message); + alert("Erreur: " + e.message); } } } diff --git a/modules/family-calendar/includes/api/settings.php b/modules/family-calendar/includes/api/settings.php new file mode 100644 index 0000000..04110b1 --- /dev/null +++ b/modules/family-calendar/includes/api/settings.php @@ -0,0 +1,106 @@ +query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1"); + $foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '["Nounou","Centre"]']; + + // B. Liste des membres de la famille + $stmtPeople = $pdo->query("SELECT id, name, role FROM pf_people WHERE is_active = 1 ORDER BY role DESC, name ASC"); + $people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC); + + // C. Matrice des congés par personne + $stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date FROM pf_person_leave_meta"); + $leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC); + + // On organise les congés par ID de personne pour faciliter le traitement côté JS + $leavesMap = []; + foreach ($leavesRaw as $leave) { + $leavesMap[$leave['person_id']][] = [ + 'type' => $leave['leave_type'], + 'method' => $leave['method'] ?? 'FIXED', + 'allowance' => $leave['allowance'] ?? 0, + 'date' => $leave['anniversary_date'] + ]; + } + + echo json_encode([ + 'success' => true, + 'data' => [ + 'foyer' => [ + 'zone_scolaire' => $foyer['zone_scolaire'], + 'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true) + ], + 'people' => $people, + 'leaves' => $leavesMap + ] + ]); + exit; + } + + // ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ─── + if ($action === 'save_foyer') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée"); + + $zone = trim($_POST['zone_scolaire'] ?? 'C'); + $modesRaw = $_POST['care_modes'] ?? '[]'; + + $modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true))))); + + $stmt = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1"); + $stmt->execute([$zone, $modesArray]); + + echo json_encode(['success' => true]); + exit; + } + + // ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ─── + if ($action === 'save_member_leaves') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée"); + + $personId = (int)($_POST['person_id'] ?? 0); + $leavesData = json_decode($_POST['leaves'] ?? '[]', true); + + if ($personId <= 0) throw new Exception("Identifiant de membre invalide."); + + $pdo->beginTransaction(); + + // On nettoie les anciennes configurations de congés de cette personne + $stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?"); + $stmtDelete->execute([$personId]); + + // On réinsère la nouvelle matrice propre + if (!empty($leavesData)) { + $stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)"); + foreach ($leavesData as $leave) { + $type = strtoupper(trim($leave['type'])); + $method = in_array($leave['method'], ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED'; + $allowance = (float)($leave['allowance'] ?? 0); + $date = trim($leave['date']); + + if (!empty($type) && !empty($date)) { + $stmtInsert->execute([$personId, $type, $method, $allowance, $date]); + } + } + } + + $pdo->commit(); + echo json_encode(['success' => true]); + exit; + } + + throw new Exception("Action inconnue"); + +} catch (\Throwable $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + exit; +} \ No newline at end of file