diff --git a/budget.php b/budget.php index 5e3de33..3f39df9 100644 --- a/budget.php +++ b/budget.php @@ -80,6 +80,7 @@ require __DIR__ . '/header.php'; +
@@ -187,6 +188,89 @@ require __DIR__ . '/header.php';
+
+

+

+ +
+
📥
+
+

+ +
+ + + +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+ + +
+ +
+
+ +
+ + +
+ +
+ +
+
+
+ @@ -719,5 +803,202 @@ async function updateBudgetCurrency(selectEl) { alert("Erreur : " + err.message); } } + +// --- GESTION DU MAPPING CSV --- +window.I18N = { + ...(window.I18N || {}), + 'bs_csv_col_amount': , + 'bs_csv_col_debit': , + 'bs_csv_saved': , + 'error_occured': +}; + +function toggleCsvAmountCols(type) { + if (type === 'split') { + document.getElementById('wrapper_col_credit').style.display = 'block'; + document.getElementById('lbl_col_debit').innerText = window.I18N['bs_csv_col_debit']; + } else { + document.getElementById('wrapper_col_credit').style.display = 'none'; + document.getElementById('lbl_col_debit').innerText = window.I18N['bs_csv_col_amount']; + } +} + +function renderCsvMapping(mapping) { + if (!mapping) return; + document.getElementById('csv_delimiter').value = mapping.delimiter || ';'; + document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y'; + document.getElementById('csv_col_date').value = mapping.col_date ?? 0; + document.getElementById('csv_col_label').value = mapping.col_label ?? 1; + document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3; + + const amountType = mapping.amount_type || 'split'; + document.getElementById('csv_amount_type').value = amountType; + document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8; + document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9; + + toggleCsvAmountCols(amountType); +} + +async function saveCsvMapping(e) { + e.preventDefault(); + const form = e.target; + const btn = form.querySelector('button[type="submit"]'); + const oldText = btn.innerText; + btn.innerText = '⏳...'; + btn.disabled = true; + + try { + const formData = new FormData(form); + formData.append('action', 'save_csv_mapping'); + + const response = await pachaFetch('/modules/budget/includes/api/settings.php', { + method: 'POST', + body: formData + }); + + if (!response.success) throw new Error(response.error || window.I18N['error_occured']); + + if (typeof showToast === 'function') showToast(window.I18N['bs_csv_saved']); + else alert(window.I18N['bs_csv_saved']); + + } catch (err) { + alert(window.I18N['error_occured'] + " : " + err.message); + } finally { + btn.innerText = oldText; + btn.disabled = false; + } +} + +// --- GESTION DE LA DROP ZONE CSV ET FILEREADER --- + +// Ajout des nouvelles clés I18N magiques +window.I18N = { + ...(window.I18N || {}), + 'bs_csv_err_type': , + 'bs_csv_file_ok': , + 'bs_csv_preview_title': , + 'bs_csv_empty_col': , + 'bs_csv_no_data': +}; + +const dropZone = document.getElementById('csv-drop-zone'); +const fileInput = document.getElementById('csv_file_input'); + +if (dropZone && fileInput) { + dropZone.addEventListener('click', () => fileInput.click()); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)'; + dropZone.style.borderColor = 'var(--primary-color, #007bff)'; + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.style.backgroundColor = ''; + dropZone.style.borderColor = 'var(--border-light, #ccc)'; + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.style.backgroundColor = ''; + dropZone.style.borderColor = 'var(--border-light, #ccc)'; + + if (e.dataTransfer.files.length) { + fileInput.files = e.dataTransfer.files; + handleCsvUpload(e.dataTransfer.files[0]); + } + }); + + fileInput.addEventListener('change', function() { + if (this.files.length) { + handleCsvUpload(this.files[0]); + } + }); +} + +function handleCsvUpload(file) { + if (!file.name.endsWith('.csv')) { + alert(window.I18N['bs_csv_err_type']); + return; + } + + if (typeof showToast === 'function') { + showToast(window.I18N['bs_csv_file_ok'] + ' : ' + file.name); + } + + const reader = new FileReader(); + + reader.onload = function(e) { + const text = e.target.result; + const delimiter = document.getElementById('csv_delimiter').value || ';'; + + const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0); + + if (lines.length > 0) { + // Extraction des en-têtes (Ligne 0) + const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim()); + + // Extraction de max 2 lignes de données (Ligne 1 et 2) + const sampleRows = []; + for (let i = 1; i < Math.min(lines.length, 3); i++) { + const rowData = lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim()); + sampleRows.push(rowData); + } + + renderCsvTablePreview(headers, sampleRows); + } + }; + + reader.readAsText(file, 'ISO-8859-1'); +} + +function renderCsvTablePreview(headers, sampleRows) { + const container = document.getElementById('csv-preview-container'); + + let html = `
👁️ ${window.I18N['bs_csv_preview_title']}
`; + + // Conteneur avec scroll horizontal si le tableau est trop large + html += `
+ + + `; + + // Rendu des en-têtes de colonnes (avec le numéro) + headers.forEach((header, index) => { + const cleanHeader = header || `(${window.I18N['bs_csv_empty_col']})`; + html += ``; + }); + + html += ` + + `; + + // Rendu des données + if (sampleRows.length === 0) { + html += ``; + } else { + sampleRows.forEach(row => { + html += ``; + // On boucle sur la taille des headers pour s'assurer que l'affichage ne casse pas si la ligne de données est incomplète + for (let i = 0; i < headers.length; i++) { + const cellData = row[i] !== undefined ? row[i] : ''; + // Raccourcir la donnée si elle est vraiment très longue (ex: libellé de 100 caractères) + const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData; + + html += ``; + } + html += ``; + }); + } + + html += `
+
N° ${index}
+
${cleanHeader}
+
${window.I18N['bs_csv_no_data']}
${displayData}
`; + + container.innerHTML = html; + container.style.display = 'block'; +} \ No newline at end of file diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 4c21399..db3a2ca 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -12,6 +12,29 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings ( INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C'); +-- ──────────────────────────────────────────────────────────── +-- Paramètres dynamiques des Modules (Key-Value) +-- ──────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS `pf_settings` ( + `setting_key` varchar(50) NOT NULL, + `setting_value` text DEFAULT NULL, + `module` varchar(50) NOT NULL, + `updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`setting_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES +('calendar_default_view', 'month', 'calendar'), +('calendar_first_day', '1', 'calendar'), +('calendar_working_hours', '08:00-19:00', 'calendar'), +('budget_start_day', '1', 'budget'), +('budget_default_tab', 'dépenses', 'budget'), +('travel_default_transport', 'car', 'voyage'), +('travel_default_fuel_price', '1.85', 'voyage'), +('gifts_hide_purchased', '0', 'cadeaux'), +('gifts_default_sort', 'person', 'cadeaux'), +('gifts_budget_alert', '500', 'cadeaux'); + -- ─── Utilisateurs (Legacy) ──────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS pf_users ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -178,6 +201,25 @@ CREATE TABLE IF NOT EXISTS `pf_advances` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- ─── Budget Catégories (Dynamiques) ───────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_budget_categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(50) NOT NULL UNIQUE, + label VARCHAR(100) NOT NULL, + type VARCHAR(50) NOT NULL, + color VARCHAR(20) DEFAULT '#ccc', + icon VARCHAR(20) DEFAULT '?' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Insertion des catégories de base pour toute nouvelle famille +INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES +('INCOME', 'Revenus', 'Income', '#10b981', '💵'), +('FMCG', 'Alimentation & Courses', 'Expense', '#3b82f6', '🛒'), +('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'), +('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'), +('SCHOOL', 'École & Garde', 'Expense', '#a855f7', '🎒'), +('SAVINGS', 'Épargne', 'Expense', '#8b5cf6', '🐷'); + -- ─── Notes / Memo ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS pf_notes ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -287,6 +329,7 @@ CREATE TABLE IF NOT EXISTS pf_vehicles ( license_plate VARCHAR(50) DEFAULT NULL, vin VARCHAR(100) DEFAULT NULL, fuel_type VARCHAR(50) DEFAULT 'Essence', + consumption decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km', color VARCHAR(50) DEFAULT NULL, purchase_date DATE DEFAULT NULL, purchase_price DECIMAL(10,2) DEFAULT NULL, diff --git a/family-calendar.php b/family-calendar.php index f03a5ce..40ace16 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -8,68 +8,60 @@ require_login(); 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 WHERE is_active = 1 ORDER BY id ASC"); +// 1. Récupération de toutes les personnes +$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC"); $familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC); $parents = []; $kids = []; - -// Liste de tous les modes de garde actifs dans le foyer -$activeCareModes = []; +$helpers = []; 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 - $modes = json_decode($p['care_modes'] ?? '[]', true); - if (!is_array($modes)) $modes = []; - $p['modes'] = $modes; + } elseif ($role === 'enfant' || $role === 'child') { + $p['modes'] = json_decode($p['care_modes'] ?? '[]', true) ?: []; $kids[] = $p; - - foreach ($modes as $m) { - $activeCareModes[$m] = true; - } + } elseif ($role === 'helper' || $role === 'nounou') { + $helpers[] = $p; // 🟢 On stocke les intervenants } } -$activeCareModes = array_keys($activeCareModes); // On récupère uniquement les noms uniques + +// 2. Modes de garde (globaux du foyer, pour synchroniser avec le JS) +$stmtFoyer = $pdo->query("SELECT care_modes FROM pf_foyer_settings LIMIT 1"); +$foyerData = $stmtFoyer->fetch(PDO::FETCH_ASSOC); +$activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true); +if (!is_array($activeCareModes)) $activeCareModes = []; + +// 3. Matrice de congés dynamique (pour les en-têtes et la modale) +$stmtLeaves = $pdo->query("SELECT person_id, leave_type FROM pf_person_leave_meta ORDER BY id ASC"); +$dbLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC); +$leaveMatrix = []; +$allLeaveTypes = []; +foreach ($dbLeaves as $l) { + $leaveMatrix[$l['person_id']][] = $l['leave_type']; + $allLeaveTypes[$l['leave_type']] = true; +} +$allLeaveTypes = array_keys($allLeaveTypes); $pageTitle = tr('fc_page_title'); $activePage = "family-calendar"; $mainClass = "pf-family-calendar"; -$pageCss = "/modules/family-calendar/family-calendar.css"; +$pageCss = "/modules/family-calendar/family-calendar.css"; require __DIR__ . '/header.php'; ?>
- +

- +
@@ -89,7 +81,7 @@ require __DIR__ . '/header.php';

- +

@@ -124,7 +116,7 @@ require __DIR__ . '/header.php'; - - - + + +
@@ -162,87 +154,58 @@ require __DIR__ . '/header.php';
-
-
- - -
-

- ⚙️ -

- +
+
+

⚙️

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

-
- -
-
- - - -
- -
- -
-
-
- - -
-
- -
- -
- - -
-
+
@@ -259,7 +222,7 @@ require __DIR__ . '/header.php';
- +
@@ -268,7 +231,7 @@ require __DIR__ . '/header.php';
- +
@@ -307,32 +270,36 @@ require __DIR__ . '/header.php'; Maladie - $parent): - $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; - $colspan = count($parent['leave_types']) * 2; + $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; // Couleurs alternées + $pLeaves = $leaveMatrix[$parent['id']] ?? []; + $colspan = count($pLeaves) * 2; ?> + 0): ?> + - $parent): $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; - foreach ($parent['leave_types'] as $lt): + $pLeaves = $leaveMatrix[$parent['id']] ?? []; ?> - - + + + + - $parent): $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; - foreach ($parent['leave_types'] as $lt): + $pLeaves = $leaveMatrix[$parent['id']] ?? []; ?> + - + + @@ -342,49 +309,54 @@ require __DIR__ . '/header.php';
-
-
-
- -
- - -
-
-
-
-
-
- -
+

+
- - $mode): - $hue = ($index * 137) % 360; - ?> -
-
- -
+ + + +
Off
+
Extra
- + + $mode): + $modeLower = strtolower($mode); + if ($modeLower === 'avis'): ?> +
+ Avis + +
+ +
+ 🏫 + +
+ +
+
+ +
+ + + +
-
+
Maladie
-
diff --git a/includes/lang/ca.php b/includes/lang/ca.php index c5234b5..374f1b5 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -216,8 +216,8 @@ return [ '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_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.', @@ -237,6 +237,48 @@ return [ '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!', + 'fc_first_day' => 'Primer dia de la setmana', + // --- CALENDARI : PARÀMETRES (MEMBRES I VISUALITZACIÓ) --- + 'fc_tab_display' => '🖥️ Visualització', + 'fc_display_title' => 'Ajustos de visualització', + 'fc_display_desc' => 'Personalitza l\'aparença i el comportament predeterminat del teu calendari.', + 'fc_default_view' => 'Vista per defecte', + 'fc_view_month' => '1 mes', + 'fc_view_2months' => '2 mesos', + 'fc_view_3months' => '3 mesos', + 'fc_working_hours' => 'Horari de cura (Per defecte)', + 'fc_working_hours_desc' => 'Exemple: 08:30-18:00', + + 'fc_role_unknown' => 'Desconegut', + 'fc_role_adult' => 'Adult', + 'fc_role_child' => 'Nen/a', + 'fc_role_helper' => 'Ajudant', + 'fc_error' => 'Error: ', + + 'fc_care_modes_title' => 'Modes de Cura Predeterminats', + 'fc_care_modes_desc' => 'Quins modes de cura s\'apliquen a aquest nen/a?', + 'fc_no_care_mode' => 'No hi ha modes de cura configurats a la llar.', + 'fc_child_saved' => 'Desat!', + + 'fc_matrix_title' => 'Matriu de Permisos', + 'fc_matrix_desc' => 'Defineix les quotes i dates de renovació.', + 'fc_matrix_empty' => 'No hi ha comptadors definits.', + 'fc_matrix_saved' => 'Matriu desada!', + + 'fc_leave_obsolete' => 'Obsolet', + 'fc_leave_code_placeholder' => 'Codi...', + 'fc_leave_method_fixed' => 'Fix', + 'fc_leave_method_accumulated' => 'Acumulat', + + 'fc_date_format_placeholder' => 'DD/MM', + 'fc_date_format_title' => 'Format DD/MM', + + 'fc_table_code' => 'Codi', + 'fc_table_method' => 'Mètode', + 'fc_table_quota' => 'Quota', + 'fc_table_renewal' => 'Renov.', + + 'btn_save_rights' => 'Guardar', 'leg_presence' => 'Presència Pep', 'leg_school_holidays' => 'Vacances Escolars', @@ -461,6 +503,36 @@ return [ '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!', + // ONGLET CSV MAPPING + 'bs_csv_title' => 'Format de la importació CSV', + 'bs_csv_desc' => 'Configureu aquí les columnes que corresponen a l\'exportació del vostre banc.', + 'bs_csv_delimiter' => 'Separador', + 'bs_csv_delim_semi' => 'Punt i coma (;)', + 'bs_csv_delim_comma' => 'Coma (,)', + 'bs_csv_delim_tab' => 'Tabulació', + 'bs_csv_date_format' => 'Format de la Data', + 'bs_csv_col_index' => 'Índex de les columnes', + 'bs_csv_col_index_help' => '(Atenció: la 1a columna és la Número 0)', + 'bs_csv_col_date' => 'Núm Col: Data', + 'bs_csv_col_label' => 'Núm Col: Concepte / Motiu', + 'bs_csv_amount_mgmt' => 'Gestió dels imports', + 'bs_csv_amount_single' => 'Una sola columna (Positiu = Ingrés, Negatiu = Despesa)', + 'bs_csv_amount_split' => 'Dues columnes separades (Dèbit / Crèdit)', + 'bs_csv_col_amount' => 'Núm Col: Import', + 'bs_csv_col_debit' => 'Núm Col: Dèbit', + 'bs_csv_col_credit' => 'Núm Col: Crèdit', + 'bs_csv_col_ref' => 'Núm Col: Referència única (Opcional)', + 'btn_save_format' => 'Desar el format', + 'bs_csv_saved' => 'Format CSV desat', + 'bs_tab_csv' => '📥 Importació CSV', + 'bs_csv_settings_title' => 'Configuració del format', + 'bs_csv_drop_title' => 'Arrossegueu el vostre fitxer CSV aquí', + 'bs_csv_drop_desc' => 'o feu clic per cercar fitxers', + 'bs_csv_err_type' => 'Error: Només s\'admeten fitxers .csv.', + 'bs_csv_file_ok' => 'Fitxer carregat', + 'bs_csv_preview_title' => 'Previsualització de les vostres columnes', + 'bs_csv_empty_col' => 'Buit', + 'bs_csv_no_data' => 'No s\'ha trobat cap línia de transacció en aquest fitxer.', // --- BUDGET : SUIVI --- 'bud_rem_school' => 'Escola (Resta estimada)', @@ -605,6 +677,39 @@ return [ 'bud_sav_prompt_duplicate' => "Vols copiar les dades de %s a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:", 'bud_err_server' => 'Error del servidor: ', 'bud_err_network_dup' => 'Error de xarxa en duplicar.', + 'budget_tab_kids' => 'Infants 👶', + 'bud_sav_add_one_month' => '+1 Mes', + 'bud_sav_add_month' => 'Afegir un mes', + 'bud_sav_no_data' => 'Cap dada d\'estalvi per a %s.', + 'bud_sav_post_month' => 'Concepte / Mes', + 'bud_sav_from_date' => 'Des del %s', + 'bud_sav_edit_modal' => 'Editar aquest mes', + 'bud_sav_delete_month' => 'Suprimir aquest mes', + 'bud_sav_total_bank' => 'TOTAL BANC', + 'bud_sav_extra' => 'NO ASSIGNAT (EXTRES)', + 'bud_sav_modal_title_add' => 'Afegir un mes', + 'bud_sav_modal_title_edit' => 'Editar', + 'bud_sav_month_concerned' => 'Mes afectat', + 'bud_sav_total_bank_eur' => 'Total al Banc (€)', + 'bud_sav_ventilation' => 'Desglossament de comptes', + 'bud_sav_adj_help' => 'Utilitzeu l\'ajust (+/-) per recalcular automàticament.', + 'bud_sav_add_line' => 'Afegir una línia', + 'bud_category' => 'Categoria', + 'bud_sav_current' => 'Actual', + 'bud_sav_adjust' => 'Ajust', + 'bud_sav_new' => 'Nou', + 'bud_sav_sum_mode_title' => 'Activar/Desactivar la calculadora', + 'bud_sav_selection' => 'Selecció:', + 'bud_sav_ph_name' => 'Nom del compte', + + // JS & Errors + 'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?', + 'bud_sav_prompt_duplicate' => "Vols duplicar les dades de %s cap a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:", + 'bud_err_tech' => 'S\'ha produït un error tècnic.', + 'bud_err_server' => 'Error del servidor: ', + 'bud_err_network_dup' => 'Error de xarxa en duplicar.', + 'bud_sav_saving' => 'Desant...', + 'bud_err_delete' => 'Error en suprimir.', // --- BUDGET : PREVISIONNEL --- 'bud_prev_incomes' => 'Ingressos', diff --git a/includes/lang/en.php b/includes/lang/en.php index 8923bee..4c91a50 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -217,9 +217,9 @@ return [ '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_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', @@ -238,6 +238,48 @@ return [ '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!', + 'fc_first_day' => 'First day of the week', + // --- CALENDAR: SETTINGS (MEMBERS & DISPLAY) --- + 'fc_tab_display' => '🖥️ Display', + 'fc_display_title' => 'Display Settings', + 'fc_display_desc' => 'Customize the default appearance and behavior of your calendar.', + 'fc_default_view' => 'Default View', + 'fc_view_month' => '1 month', + 'fc_view_2months' => '2 months', + 'fc_view_3months' => '3 months', + 'fc_working_hours' => 'Default Care Hours', + 'fc_working_hours_desc' => 'Example: 08:30-18:00', + + 'fc_role_unknown' => 'Unknown', + 'fc_role_adult' => 'Adult', + 'fc_role_child' => 'Child', + 'fc_role_helper' => 'Helper', + 'fc_error' => 'Error: ', + + 'fc_care_modes_title' => 'Default Care Modes', + 'fc_care_modes_desc' => 'Which care modes apply to this child?', + 'fc_no_care_mode' => 'No care mode configured for this household.', + 'fc_child_saved' => 'Saved!', + + 'fc_matrix_title' => 'Leave Matrix', + 'fc_matrix_desc' => 'Define quotas and renewal dates.', + 'fc_matrix_empty' => 'No counters defined.', + "fc_matrix_saved" => "Matrix saved!", + + 'fc_leave_obsolete' => 'Obsolete', + 'fc_leave_code_placeholder' => 'Code...', + 'fc_leave_method_fixed' => 'Fixed', + 'fc_leave_method_accumulated' => 'Accrued', + + 'fc_date_format_placeholder' => 'DD/MM', + 'fc_date_format_title' => 'DD/MM Format', + + 'fc_table_code' => 'Code', + 'fc_table_method' => 'Method', + 'fc_table_quota' => 'Quota', + 'fc_table_renewal' => 'Renewal', + + 'btn_save_rights' => 'Save', 'leg_presence' => 'Pep presence', 'leg_school_holidays' => 'School holidays', @@ -459,6 +501,36 @@ return [ 'bs_currency_label' => 'Household Currency', 'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.', 'bs_currency_updated' => 'Currency updated successfully!', + // ONGLET CSV MAPPING + 'bs_csv_title' => 'CSV Import Format', + 'bs_csv_desc' => 'Configure the columns that match your bank\'s export file.', + 'bs_csv_delimiter' => 'Delimiter', + 'bs_csv_delim_semi' => 'Semicolon (;)', + 'bs_csv_delim_comma' => 'Comma (,)', + 'bs_csv_delim_tab' => 'Tab', + 'bs_csv_date_format' => 'Date Format', + 'bs_csv_col_index' => 'Column Indexes', + 'bs_csv_col_index_help' => '(Note: the 1st column is Number 0)', + 'bs_csv_col_date' => 'Col #: Date', + 'bs_csv_col_label' => 'Col #: Label / Description', + 'bs_csv_amount_mgmt' => 'Amount Management', + 'bs_csv_amount_single' => 'Single column (Positive = Income, Negative = Expense)', + 'bs_csv_amount_split' => 'Two separate columns (Debit / Credit)', + 'bs_csv_col_amount' => 'Col #: Amount', + 'bs_csv_col_debit' => 'Col #: Debit', + 'bs_csv_col_credit' => 'Col #: Credit', + 'bs_csv_col_ref' => 'Col #: Unique Reference (Optional)', + 'btn_save_format' => 'Save format', + 'bs_csv_saved' => 'CSV format saved', + 'bs_tab_csv' => '📥 CSV Import', + 'bs_csv_settings_title' => 'Format Configuration', + 'bs_csv_drop_title' => 'Drop your CSV file here', + 'bs_csv_drop_desc' => 'or click to browse your files', + 'bs_csv_err_type' => 'Error: Only .csv files are allowed.', + 'bs_csv_file_ok' => 'File loaded', + 'bs_csv_preview_title' => 'Columns Preview', + 'bs_csv_empty_col' => 'Empty', + 'bs_csv_no_data' => 'No transaction row found in this file.', // --- BUDGET: TRACKING --- 'bud_rem_school' => 'School (estimated remaining)', @@ -588,6 +660,39 @@ return [ 'bud_sav_prompt_duplicate' => "Do you want to copy data from %s to %t1?\n\nEnter the new TOTAL bank balance (€) for %t2:", 'bud_err_server' => 'Server error: ', 'bud_err_network_dup' => 'Network error during duplication.', + 'budget_tab_kids' => 'Kids 👶', + 'bud_sav_add_one_month' => '+1 Month', + 'bud_sav_add_month' => 'Add a month', + 'bud_sav_no_data' => 'No savings data for %s.', + 'bud_sav_post_month' => 'Item / Month', + 'bud_sav_from_date' => 'From %s', + 'bud_sav_edit_modal' => 'Edit this month', + 'bud_sav_delete_month' => 'Delete this month', + 'bud_sav_total_bank' => 'TOTAL BANK', + 'bud_sav_extra' => 'UNALLOCATED (EXTRAS)', + 'bud_sav_modal_title_add' => 'Add a month', + 'bud_sav_modal_title_edit' => 'Edit', + 'bud_sav_month_concerned' => 'Target month', + 'bud_sav_total_bank_eur' => 'Total in Bank (€)', + 'bud_sav_ventilation' => 'Accounts breakdown', + 'bud_sav_adj_help' => 'Enter adjustments (+/-) to auto-calculate.', + 'bud_sav_add_line' => 'Add line', + 'bud_category' => 'Category', + 'bud_sav_current' => 'Current', + 'bud_sav_adjust' => 'Adjustment', + 'bud_sav_new' => 'New', + 'bud_sav_sum_mode_title' => 'Toggle calculator', + 'bud_sav_selection' => 'Selection:', + 'bud_sav_ph_name' => 'Account name', + + // JS & Errors + 'bud_sav_confirm_delete_month' => 'Are you sure you want to delete all data of %m for %o?', + 'bud_sav_prompt_duplicate' => "Do you want to duplicate data from %s to %t1?\n\nEnter the new BANK TOTAL (€) for %t2:", + 'bud_err_tech' => 'A technical error occurred.', + 'bud_err_server' => 'Server error: ', + 'bud_err_network_dup' => 'Network error during duplication.', + 'bud_sav_saving' => 'Saving...', + 'bud_err_delete' => 'Error during deletion.', // --- BUDGET: FORECAST --- 'bud_prev_incomes' => 'Income', diff --git a/includes/lang/fr.php b/includes/lang/fr.php index 4957bc2..ba6f7ca 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -219,9 +219,9 @@ return [ '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_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', @@ -240,6 +240,49 @@ return [ '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 !', + 'fc_first_day' => 'Premier jour de la semaine', + // --- CALENDRIER : PARAMÈTRES (MEMBRES & AFFICHAGE) --- +'fc_tab_display' => '🖥️ Affichage', + 'fc_display_title' => 'Paramètres d\'affichage', + 'fc_display_desc' => 'Personnalisez l\'apparence et le comportement par défaut de votre calendrier.', + 'fc_default_view' => 'Vue par défaut', + 'fc_view_month' => '1 mois', + 'fc_view_2months' => '2 mois', + 'fc_view_3months' => '3 mois', + 'fc_working_hours' => 'Horaires de garde (Défaut)', + 'fc_working_hours_desc' => 'Exemple : 08:30-18:00', + + 'fc_role_unknown' => 'Inconnu', + 'fc_role_adult' => 'Adulte', + 'fc_role_child' => 'Enfant', + 'fc_role_helper' => 'Intervenant', + 'fc_error' => 'Erreur : ', + + 'fc_care_modes_title' => 'Modes de Garde Par Défaut', + 'fc_care_modes_desc' => 'Quels modes de garde s\'appliquent à cet enfant ?', + 'fc_no_care_mode' => 'Aucun mode de garde configuré dans le foyer.', + 'fc_child_saved' => 'Sauvegardé !', + + 'fc_matrix_title' => 'Matrice des Congés', + 'fc_matrix_desc' => 'Définissez les quotas et la date de renouvellement.', + 'fc_matrix_empty' => 'Aucun compteur défini.', + 'fc_matrix_saved' => 'Matrice enregistrée !', + + 'fc_leave_obsolete' => 'Obsolète', + 'fc_leave_code_placeholder' => 'Code...', + 'fc_leave_method_fixed' => 'Fixe', + 'fc_leave_method_accumulated' => 'Cumul', + + 'fc_date_format_placeholder' => 'JJ/MM', + 'fc_date_format_title' => 'Format JJ/MM', + + 'fc_table_code' => 'Code', + 'fc_table_method' => 'Méthode', + 'fc_table_quota' => 'Quota', + 'fc_table_renewal' => 'Renouv.', + + 'btn_save_rights' => 'Enregistrer', + 'leg_presence' => 'Présence Pep', @@ -462,6 +505,36 @@ return [ '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 !', + // ONGLET CSV MAPPING + 'bs_csv_title' => 'Format de l\'import CSV', + 'bs_csv_desc' => 'Configurez ici les colonnes correspondantes à l\'export de votre banque.', + 'bs_csv_delimiter' => 'Séparateur', + 'bs_csv_delim_semi' => 'Point-virgule (;)', + 'bs_csv_delim_comma' => 'Virgule (,)', + 'bs_csv_delim_tab' => 'Tabulation', + 'bs_csv_date_format' => 'Format de la Date', + 'bs_csv_col_index' => 'Index des colonnes', + 'bs_csv_col_index_help' => '(Attention : la 1ère colonne est la Numéro 0)', + 'bs_csv_col_date' => 'N° Col : Date', + 'bs_csv_col_label' => 'N° Col : Libellé / Motif', + 'bs_csv_amount_mgmt' => 'Gestion des montants', + 'bs_csv_amount_single' => 'Une seule colonne (Positif = Revenu, Négatif = Dépense)', + 'bs_csv_amount_split' => 'Deux colonnes séparées (Débit / Crédit)', + 'bs_csv_col_amount' => 'N° Col : Montant', + 'bs_csv_col_debit' => 'N° Col : Débit', + 'bs_csv_col_credit' => 'N° Col : Crédit', + 'bs_csv_col_ref' => 'N° Col : Référence unique (Optionnel)', + 'btn_save_format' => 'Enregistrer le format', + 'bs_csv_saved' => 'Format CSV enregistré', + 'bs_tab_csv' => '📥 Import CSV', + 'bs_csv_settings_title' => 'Configuration du format', + 'bs_csv_drop_title' => 'Glissez votre fichier CSV ici', + 'bs_csv_drop_desc' => 'ou cliquez pour parcourir vos fichiers', + 'bs_csv_err_type' => 'Erreur : Seuls les fichiers .csv sont autorisés.', + 'bs_csv_file_ok' => 'Fichier chargé', + 'bs_csv_preview_title' => 'Aperçu de vos colonnes', + 'bs_csv_empty_col' => 'Vide', + 'bs_csv_no_data' => 'Aucune ligne de transaction trouvée dans ce fichier.', // --- BUDGET : SUIVI --- 'bud_rem_school' => 'École (Reste estimé)', @@ -602,6 +675,39 @@ return [ 'bud_sav_prompt_duplicate' => "Voulez-vous copier les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :", 'bud_err_server' => 'Erreur serveur : ', 'bud_err_network_dup' => 'Erreur réseau lors de la duplication.', + 'budget_tab_kids' => 'Enfants 👶', + 'bud_sav_add_one_month' => '+1 Mois', + 'bud_sav_add_month' => 'Ajouter un mois', + 'bud_sav_no_data' => 'Aucune donnée d\'épargne pour %s.', + 'bud_sav_post_month' => 'Poste / Mois', + 'bud_sav_from_date' => 'Dès le %s', + 'bud_sav_edit_modal' => 'Éditer ce mois', + 'bud_sav_delete_month' => 'Supprimer ce mois', + 'bud_sav_total_bank' => 'TOTAL BANQUE', + 'bud_sav_extra' => 'NON ALLOUÉ (EXTRAS)', + 'bud_sav_modal_title_add' => 'Ajouter un mois', + 'bud_sav_modal_title_edit' => 'Éditer', + 'bud_sav_month_concerned' => 'Mois concerné', + 'bud_sav_total_bank_eur' => 'Total en Banque (€)', + 'bud_sav_ventilation' => 'Ventilation des comptes', + 'bud_sav_adj_help' => 'Saisissez les ajustements (+/-) pour recalculer automatiquement.', + 'bud_sav_add_line' => 'Ajouter une ligne', + 'bud_category' => 'Catégorie', + 'bud_sav_current' => 'Actuel', + 'bud_sav_adjust' => 'Ajustement', + 'bud_sav_new' => 'Nouveau', + 'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice', + 'bud_sav_selection' => 'Sélection :', + 'bud_sav_ph_name' => 'Nom du compte', + + // JS & Erreurs + 'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?', + 'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :", + 'bud_err_tech' => 'Une erreur technique est survenue.', + 'bud_err_server' => 'Erreur serveur : ', + 'bud_err_network_dup' => 'Erreur réseau lors de la duplication.', + 'bud_sav_saving' => 'Sauvegarde en cours...', + 'bud_err_delete' => 'Erreur lors de la suppression.', // --- BUDGET : PREVISIONNEL --- 'bud_prev_incomes' => 'Revenus', diff --git a/migrate.php b/migrate.php index 2a197d3..dc1ad23 100644 --- a/migrate.php +++ b/migrate.php @@ -27,167 +27,57 @@ try { [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); - // --------------------------------------------------------- - // 1. UPDATE DE pf_people (Juste la date de naissance et l'état actif) - // --------------------------------------------------------- + // 1. UPDATE DE pf_people $colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'"); $colCheck->execute([$dbName]); if ($colCheck->rowCount() === 0) { - $pdo->exec("ALTER TABLE pf_people - ADD COLUMN birthdate DATE DEFAULT NULL, - ADD COLUMN is_active TINYINT(1) DEFAULT 1 - "); - echo "✅ pf_people mis à jour (birthdate ajouté).
"; - } else { - echo "➖ pf_people déjà à jour.
"; + $pdo->exec("ALTER TABLE pf_people ADD COLUMN birthdate DATE DEFAULT NULL, ADD COLUMN is_active TINYINT(1) DEFAULT 1"); + echo "✅ pf_people mis à jour.
"; } - // --------------------------------------------------------- - // 2. CRÉATION DES NOUVELLES TABLES (IF NOT EXISTS) - // --------------------------------------------------------- - - // Comptes bancaires - $pdo->exec("CREATE TABLE IF NOT EXISTS pf_bank_accounts ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - owner_person_id INT DEFAULT NULL, - account_type VARCHAR(50) DEFAULT 'savings', - is_default TINYINT(1) DEFAULT 0, - FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); - - // Catégories de budget - $pdo->exec("CREATE TABLE IF NOT EXISTS pf_budget_categories ( - id INT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(50) NOT NULL, - label VARCHAR(100) NOT NULL, - type VARCHAR(50) NOT NULL, - color VARCHAR(20) DEFAULT '#ccc', - icon VARCHAR(20) DEFAULT '💰', - UNIQUE KEY (code) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); - - // Types de congés - $pdo->exec("CREATE TABLE IF NOT EXISTS pf_leave_types ( - id INT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(20) NOT NULL, - label VARCHAR(100) NOT NULL, - default_allowance DECIMAL(5,2) DEFAULT 0, - reset_month INT DEFAULT 1, - allow_carry_over TINYINT(1) DEFAULT 0, - UNIQUE KEY (code) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); - - // Fêtes (Cadeaux) - $pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_occasions ( - id INT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(20) NOT NULL, - name VARCHAR(100) NOT NULL, - month_date VARCHAR(5) DEFAULT NULL, - is_active TINYINT(1) DEFAULT 1, - UNIQUE KEY (code) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); - - // 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, - child_person_id INT NOT NULL, - occasion_id INT NOT NULL, - FOREIGN KEY (adult_person_id) REFERENCES pf_people(id) ON DELETE CASCADE, - FOREIGN KEY (child_person_id) REFERENCES pf_people(id) ON DELETE CASCADE, - FOREIGN KEY (occasion_id) REFERENCES pf_gift_occasions(id) ON DELETE CASCADE, - UNIQUE KEY adult_child_occ (adult_person_id, child_person_id, occasion_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); - - echo "✅ Nouvelles tables de configuration créées ou vérifiées.
"; - - // --------------------------------------------------------- - // 3. INJECTION DE DONNÉES PAR DÉFAUT (Pour ne rien casser) - // --------------------------------------------------------- + // 2. CRÉATION DES NOUVELLES TABLES + $pdo->exec("CREATE TABLE IF NOT EXISTS pf_bank_accounts (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, owner_person_id INT DEFAULT NULL, account_type VARCHAR(50) DEFAULT 'savings', is_default TINYINT(1) DEFAULT 0, FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + $pdo->exec("CREATE TABLE IF NOT EXISTS pf_budget_categories (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(50) NOT NULL, label VARCHAR(100) NOT NULL, type VARCHAR(50) NOT NULL, color VARCHAR(20) DEFAULT '#ccc', icon VARCHAR(20) DEFAULT '💰', UNIQUE KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + $pdo->exec("CREATE TABLE IF NOT EXISTS pf_leave_types (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL, label VARCHAR(100) NOT NULL, default_allowance DECIMAL(5,2) DEFAULT 0, reset_month INT DEFAULT 1, allow_carry_over TINYINT(1) DEFAULT 0, UNIQUE KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + $pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_occasions (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, month_date VARCHAR(5) DEFAULT NULL, is_active TINYINT(1) DEFAULT 1, UNIQUE KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + $pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules (id INT AUTO_INCREMENT PRIMARY KEY, adult_person_id INT NOT NULL, child_person_id INT NOT NULL, occasion_id INT NOT NULL, FOREIGN KEY (adult_person_id) REFERENCES pf_people(id) ON DELETE CASCADE, FOREIGN KEY (child_person_id) REFERENCES pf_people(id) ON DELETE CASCADE, FOREIGN KEY (occasion_id) REFERENCES pf_gift_occasions(id) ON DELETE CASCADE, UNIQUE KEY adult_child_occ (adult_person_id, child_person_id, occasion_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + // 3. INJECTION DONNÉES PAR DÉFAUT if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) { - $pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES - ('Compte Commun', 'checking', 1), - ('Livret A Alex', 'savings', 0), - ('Livret A Laia', 'savings', 0), - ('Livret A Pol', 'savings', 0), - ('Livret A Pep', 'savings', 0) - "); + $pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES ('Compte Commun', 'checking', 1), ('Livret A Alex', 'savings', 0), ('Livret A Laia', 'savings', 0), ('Livret A Pol', 'savings', 0), ('Livret A Pep', 'savings', 0)"); } + $pdo->exec("INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES ('INCOME', 'Revenus', 'Income', '#22c55e', '💵'), ('FMCG', 'Alimentation', 'Expense', '#3b82f6', '🛒'), ('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'), ('SCHOOL', 'École / Garde', 'Expense', '#a855f7', '🎒'), ('HEALTH', 'Santé', 'Expense', '#ef4444', '⚕️'), ('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'), ('SAVINGS', 'Épargne & Projets', 'Expense', '#8b5cf6', '🐷'), ('AUTRES', 'Autres / Divers', 'Expense', '#64748b', '📁')"); - if ($pdo->query("SELECT COUNT(*) FROM pf_budget_categories")->fetchColumn() == 0) { - $pdo->exec("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES - ('INCOME', 'Revenus', 'Income', '#22c55e', '💵'), - ('FMCG', 'Alimentation', 'Expense', '#3b82f6', '🛒'), - ('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'), - ('SCHOOL', 'École / Garde', 'Expense', '#a855f7', '🎒'), - ('HEALTH', 'Santé', 'Expense', '#ef4444', '⚕️') - "); - } - - if ($pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn() == 0) { - $pdo->exec("INSERT INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES - ('CA', 'Congés Annuels', 25, 6, 1), - ('JRA', 'Jours de Repos', 10, 1, 0), - ('JA', 'Jour Anniversaire', 1, 1, 0) - "); - } - - if ($pdo->query("SELECT COUNT(*) FROM pf_gift_occasions")->fetchColumn() == 0) { - $pdo->exec("INSERT INTO pf_gift_occasions (code, name, month_date) VALUES - ('NOEL', 'Noël', '12-25'), - ('ROIS', 'Les Rois', '01-06'), - ('ANNIV', 'Anniversaire', NULL) - "); - } - - echo "✅ Données de base injectées (si nécessaire).
"; - - // --------------------------------------------------------- - // 4. MIGRATION SPECIFIQUE : MODULE CALENDRIER (CONGÉS GRANULAIRES) - // --------------------------------------------------------- + // 4. MIGRATION MODULE CALENDRIER $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.
"; - } + $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"); } + // 5. MIGRATION PARAMÈTRES DYNAMIQUES + $pdo->exec("CREATE TABLE IF NOT EXISTS `pf_settings` (`setting_key` varchar(50) NOT NULL, `setting_value` text DEFAULT NULL, `module` varchar(50) NOT NULL, `updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`setting_key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); + $pdo->exec("INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES ('calendar_default_view', 'month', 'calendar'), ('calendar_first_day', '1', 'calendar'), ('calendar_working_hours', '08:00-19:00', 'calendar'), ('budget_start_day', '1', 'budget'), ('budget_default_tab', 'dépenses', 'budget'), ('travel_default_transport', 'car', 'voyage'), ('travel_default_fuel_price', '1.85', 'voyage'), ('gifts_hide_purchased', '0', 'cadeaux'), ('gifts_default_sort', 'person', 'cadeaux'), ('gifts_budget_alert', '500', 'cadeaux')"); + + // 6. MIGRATION CSV MAPPING (La partie ajoutée) + $checkCol = $pdo->query("SHOW COLUMNS FROM pf_foyer_settings LIKE 'csv_mapping'")->fetch(); + if (!$checkCol) { + $pdo->exec("ALTER TABLE pf_foyer_settings ADD COLUMN csv_mapping TEXT NULL"); + echo "✅ Colonne 'csv_mapping' ajoutée.
"; + } + + // 7. MIGRATION STRUCTURE BUDGET & HISTORIQUE + $pdo->exec("ALTER TABLE pf_budget_items MODIFY category VARCHAR(100) DEFAULT NULL"); + $pdo->exec("UPDATE pf_budget_items SET category = 'FIXED' WHERE category = 'expense' AND is_estimate = 0"); + $pdo->exec("UPDATE pf_budget_items SET category = 'AUTRES' WHERE category = 'expense'"); + $pdo->exec("UPDATE pf_budget_items SET category = 'INCOME' WHERE category = 'income'"); + + echo "✅ Migration terminée pour {$family['name']}.
"; + } catch (PDOException $e) { echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "
"; } } - echo "

🎉 Migration terminée avec succès !

"; - } catch (Exception $e) { die("❌ Erreur fatale Meta DB : " . $e->getMessage()); } diff --git a/modules/budget/includes/api/settings.php b/modules/budget/includes/api/settings.php index cb4454f..85d69bc 100644 --- a/modules/budget/includes/api/settings.php +++ b/modules/budget/includes/api/settings.php @@ -19,8 +19,7 @@ try { // 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 + // 3. Règles d'import $rules = $pdo->query(" SELECT r.*, c.label as cat_label FROM pf_import_rules r @@ -32,73 +31,60 @@ try { $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() ?: '€'; + // 5. Paramètres Foyer (Devise + Mapping CSV) + $foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC); + $currencySetting = $foyerData['currency'] ?? '€'; + $csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null; echo json_encode([ 'success' => true, 'data' => [ - 'accounts' => $accounts, - 'categories' => $categories, - 'rules' => $rules, - 'salaries' => $salaries, - 'year' => $currentYear, - 'currency' => $currencySetting + 'accounts' => $accounts, + 'categories' => $categories, + 'rules' => $rules, + 'salaries' => $salaries, + 'year' => $currentYear, + 'currency' => $currencySetting, + 'csv_mapping' => $csvMapping ] ]); 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; } @@ -107,27 +93,20 @@ try { $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) - + $category = trim($_POST['category'] ?? ''); 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; } @@ -136,36 +115,46 @@ try { $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 --- + // --- 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; + } + // --- GESTION DU FORMAT CSV --- + if ($action === 'save_csv_mapping') { + $mapping = [ + 'delimiter' => $_POST['csv_delimiter'] ?? ';', + 'date_format' => $_POST['csv_date_format'] ?? 'd/m/Y', + 'col_date' => (int)($_POST['csv_col_date'] ?? 0), + 'col_label' => (int)($_POST['csv_col_label'] ?? 1), + 'amount_type' => $_POST['csv_amount_type'] ?? 'single', + 'col_debit' => (int)($_POST['csv_col_debit'] ?? 8), + 'col_credit' => (int)($_POST['csv_col_credit'] ?? 9), + 'col_ref' => (int)($_POST['csv_col_ref'] ?? 3) + ]; + $jsonContent = json_encode($mapping); + $stmt = $pdo->prepare("UPDATE pf_foyer_settings SET csv_mapping = ?"); + $stmt->execute([$jsonContent]); echo json_encode(['success' => true]); exit; } diff --git a/modules/budget/views/epargne.php b/modules/budget/views/epargne.php index 0f67e15..5867bec 100644 --- a/modules/budget/views/epargne.php +++ b/modules/budget/views/epargne.php @@ -11,11 +11,10 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) { if ($role === 'parent') { $familyParents[] = $row['name']; - } elseif ($role === 'nounou') { + } elseif ($role === 'nounou' || $role === 'helper') { // On ignore la nounou dans l'épargne continue; } else { - // Fallback : Si c'est 'enfant', 'user', ou vide, ça va dans l'onglet Enfants $familyKids[] = $row['name']; } } @@ -23,8 +22,8 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) { // Sécurité anti-page blanche si la BDD est mal configurée if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2']; -$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'Nens'); -$ownersToDisplay = ($requestedOwner === 'Nens') ? $familyKids : [$requestedOwner]; +$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'KIDS'); +$ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner]; // --- RÉCUPÉRATION CONFIGURATION DES MOIS --- $cycleConfigs = []; @@ -47,23 +46,21 @@ function getMonthName($dateString) {
- $currentOwner): $stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC"); $stmt->execute([$currentOwner]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -83,28 +80,29 @@ function getMonthName($dateString) { $months = array_slice($months, 0, 7); sort($allCategories); - // Définition de la classe couleur selon le propriétaire $ownerTextClass = 'txt-global'; + // 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS + $safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES); ?> -
+
-

- +

- -
@@ -116,7 +114,7 @@ function getMonthName($dateString) {

- +
@@ -135,11 +133,11 @@ function getMonthName($dateString) {
@@ -158,11 +156,11 @@ function getMonthName($dateString) {
@@ -178,11 +176,11 @@ function getMonthName($dateString) { @@ -197,7 +195,7 @@ function getMonthName($dateString) { foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0); $extra = $total - $sum; ?> - @@ -289,6 +287,9 @@ window.I18N = { 'bud_err_delete': }; +// Sécurisation de la devise pour la calculatrice +const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€'; + // --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT --- const cycleConfigs = ; @@ -306,16 +307,19 @@ function updateEpargneCell(month, category, owner, inputEl) { body: formData }).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique')); - const totalInput = document.querySelector(`.total-input-${owner}-${month}`); + // Gère les IDs proprement + const safeOwnerClass = owner.replace(/\s+/g, '_'); + + const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`); const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0; let sumCats = 0; - document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => { + document.querySelectorAll(`.cat-input-${CSS.escape(safeOwnerClass)}-${month}`).forEach(inp => { sumCats += parseFloat(inp.value) || 0; }); const extra = totalVal - sumCats; - const extraCell = document.getElementById(`extra_${owner}_${month}`); + const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`); if (extraCell) { extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €'; @@ -557,7 +561,7 @@ function updateSumResult() { total += val; }); - document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY; + document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + systemCurrency; } document.addEventListener('click', function(e) { diff --git a/modules/budget/views/recap.php b/modules/budget/views/recap.php index d0f932f..abaeba1 100644 --- a/modules/budget/views/recap.php +++ b/modules/budget/views/recap.php @@ -1,35 +1,32 @@ query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC"); -$items = $stmt->fetchAll(); +// 1. Récupération des Catégories dynamiques +$stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC"); +$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC); -// 2. Déterminer quel est le mois de gestion "ouvert" par défaut +// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques) +$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y'); +$stmtSalaries = $pdo->prepare("SELECT person, mensualite FROM pf_salary_config WHERE year = ?"); +$stmtSalaries->execute([$currentYear]); +$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC); + +// 3. Récupération des Items du Budget (Charges Fixes et Estimations) +$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY is_estimate ASC, sort_order ASC, name ASC"); +$items = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// 4. Gestion du mois actif $stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1"); -$defaultActiveMonth = $stmtActive->fetchColumn(); -if (!$defaultActiveMonth) { - $defaultActiveMonth = date('Y-m-01'); -} - -// 3. Assigner le mois et l'année en fonction du mois ouvert +$defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01'); $currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth)); -$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth)); $viewMonthDate = "$currentYear-$currentMonth-01"; -$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real - FROM pf_expenses - WHERE gestion_month = ? AND budget_item_id IS NOT NULL - GROUP BY budget_item_id"; -$stmtReal = $pdo->prepare($sqlReal); +// 5. Récupération du Réel (Dépenses) +$stmtReal = $pdo->prepare("SELECT budget_item_id, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NOT NULL GROUP BY budget_item_id"); $stmtReal->execute([$viewMonthDate]); -$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total] +$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); -$sqlCatReal = "SELECT category, SUM(amount) as total_real - FROM pf_expenses - WHERE gestion_month = ? AND budget_item_id IS NULL - GROUP BY category"; -$stmtCatReal = $pdo->prepare($sqlCatReal); +$stmtCatReal = $pdo->prepare("SELECT category, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL GROUP BY category"); $stmtCatReal->execute([$viewMonthDate]); $catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR); @@ -38,8 +35,7 @@ $stmtLabels->execute([$viewMonthDate]); $unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC); $moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; -$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT); -$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear; +$currentMonthName = tr('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear; $totalDepenses = 0; $totalRevenus = 0; @@ -64,46 +60,60 @@ $totalRevenus = 0; + + + + + + + + + + + = ($targetAbs - 0.10))); - - $rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense'; - if ($item['is_estimate']) $rowClass .= ' row-estimate'; + $rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : ''); ?> - - + @@ -191,34 +177,30 @@ $totalRevenus = 0; - + - + - + -
+ onchange="updateEpargneCell('', 'TOTAL_BANQUE', '', this)">
+ onchange="updateEpargneCell('', '', '', this)">
+
+ Apport + ⚙️ + + + € + + Fixe (Auto) + - +
+ Auto +
+
+ Via Paramètres +
- ('.tr('bud_est_short').')' : '' ?> - + (Variable)' : '' ?> - 🔗 - - - -
- 📅 -
+ 🔗
- € + + - - 0.05): ?> -
- : + € -
+
Dépassé : +
-
- : € -
+
Reste :
-
- ✓ -
+
Atteint ✓
-
/
+
/mois
- - + + -
- -
+
Validé
-
- -
+
Partiel
-
- -
+
En attente
- - + +
Total Revenus Lissés +
Total Dépenses & Estimations -
Reste à Vivre (Équilibre) - € / + + € / mois
- -
-

*

-
- +
- +
@@ -568,7 +593,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
@@ -587,23 +612,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
- - @@ -720,11 +732,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY; ${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 = ` - + const rowTypeOptions = (calGlobalData.leave_types || []) + .map((lt) => ``) + .join(""); + + const trRow = document.createElement("tr"); + trRow.className = "js-leave-row"; + trRow.style.borderBottom = "1px solid var(--border-light)"; + trRow.innerHTML = ` - + + ${rowTypeOptions} + + + + - - + + + `; + tbody.appendChild(trRow); +}; + +window.populateCalendarSettings = function (settings) { + if (!settings) return; + const viewSelect = document.getElementById("setCalView"); + if (viewSelect && settings.calendar_default_view) + viewSelect.value = settings.calendar_default_view; + const firstDaySelect = document.getElementById("setCalFirstDay"); + if (firstDaySelect && settings.calendar_first_day !== undefined) + firstDaySelect.value = settings.calendar_first_day; + const hoursInput = document.getElementById("setCalHours"); + if (hoursInput && settings.calendar_working_hours) + hoursInput.value = settings.calendar_working_hours; +}; + +window.loadAdultConfigView = function (memberId, zoneElement) { + let userLeaves = calGlobalData.leaves[memberId] || []; + + const tableRows = userLeaves + .map((l) => { + let dateDisp = ""; + if (l.date) { + const parts = l.date.split("-"); + if (parts.length >= 3) dateDisp = `${parts[2]}/${parts[1]}`; + } + + let rowTypeOptions = (calGlobalData.leave_types || []) + .map( + (lt) => + ``, + ) + .join(""); + + if (!calGlobalData.leave_types.some((lt) => lt.code === l.type)) { + rowTypeOptions += ``; + } + + return ` + + + + + + + + + + + + `; + }) + .join(""); + + zoneElement.innerHTML = ` +
+
+
🗓️ ${tr("fc_matrix_title") || "Matrice des Congés"}
+

${tr("fc_matrix_desc") || "Définissez les quotas et la date de renouvellement."}

+
+ +
+
+ + + + + + + + + + + + ${tableRows || ``} + +
${tr("fc_table_code") || "Code"}${tr("fc_table_method") || "Méthode"}${tr("fc_table_quota") || "Quota"}${tr("fc_table_renewal") || "Renouv."}
${tr("fc_matrix_empty") || "Aucun compteur défini."}
+
+
+ +
`; - tbody.appendChild(tr); +}; + +window.loadMemberConfigView = function () { + const select = document.getElementById("selectCalMember"); + const zone = document.getElementById("memberConfigZone"); + if (!select || !select.value || !zone) return; + + window.currentSelectedMemberId = parseInt(select.value); + const role = ( + select.options[select.selectedIndex].dataset.role || "" + ).toLowerCase(); + + if (role === "child" || role === "enfant") { + const currentPerson = calGlobalData.people.find( + (k) => parseInt(k.id) === window.currentSelectedMemberId, + ); + let savedModes = []; + try { + if (currentPerson.modes && Array.isArray(currentPerson.modes)) + savedModes = currentPerson.modes; + else if (typeof currentPerson.care_modes === "string") + savedModes = JSON.parse(currentPerson.care_modes); + } catch (e) {} + + const activeModesInFoyer = calGlobalData.foyer.care_modes || []; + const modesHtml = activeModesInFoyer + .map( + (m) => ` + + `, + ) + .join(""); + + zone.innerHTML = ` +
${tr("fc_care_modes_title") || "Modes de Garde"}
+

Quels modes de garde s'appliquent à cet enfant ?

+
+ ${modesHtml || `Aucun mode de garde configuré dans le foyer.`} +
+ + `; + } else { + window.loadAdultConfigView(window.currentSelectedMemberId, zone); + } +}; + +window.submitChildCareModes = async function () { + if (!window.currentSelectedMemberId) return; + const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked"); + const selectedModes = Array.from(checkboxes).map((cb) => cb.value); + try { + const formData = new FormData(); + formData.append("action", "save_child_care_modes"); + formData.append("person_id", window.currentSelectedMemberId); + formData.append("care_modes", JSON.stringify(selectedModes)); + + 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_child_saved") || "Sauvegardé !", "success"); + else alert(tr("fc_child_saved") || "Sauvegardé !"); + + const currentPerson = calGlobalData.people.find( + (k) => parseInt(k.id) === window.currentSelectedMemberId, + ); + if (currentPerson) { + currentPerson.care_modes = JSON.stringify(selectedModes); + } + } catch (err) { + alert("Erreur: " + err.message); + } }; 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 + method: row.querySelector(".js-leave-method").value, + allowance: row.querySelector(".js-leave-allowance").value, date: row.querySelector(".js-leave-date").value, })) - .filter((l) => l.type && l.date); // Filtre de sécurité + .filter((l) => l.type && l.date); const formData = new FormData(); formData.append("action", "save_member_leaves"); @@ -264,11 +365,9 @@ async function submitMemberLeaves() { ); if (!res.success) throw new Error(res.error); - if (window.showToast) { - showToast(tr("fc_matrix_saved"), "success"); - } else { - alert(tr("fc_matrix_saved")); - } + if (window.showToast) + showToast(tr("fc_matrix_saved") || "Matrice enregistrée", "success"); + else alert(tr("fc_matrix_saved") || "Matrice enregistrée"); closeCalendarSettings(); setTimeout(openCalendarSettings, 300); @@ -281,35 +380,6 @@ async function submitMemberLeaves() { // 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"]; - - // (Note: La logique LEAVES_CONFIG restera à basculer vers BDD dans un 2nd temps) - const LEAVES_CONFIG = { - CP: { startMonth: 8, defaultBalance: 25 }, - JRA: { - yearlyTotals: { 2024: 10, 2025: 10, 2026: 11 }, - defaultBalance: 10, - toleranceMonths: 2, - maxReport: 2, - }, - JA: {}, - }; - - parents.forEach((parent) => { - LEAVES_CONFIG.JA[parent.id] = { - startMonth: 4, - startDay: 29, - defaultBalance: 4, - }; - }); - class FamilyCalendar { constructor() { this.planningBody = document.getElementById("planningBody"); @@ -349,10 +419,13 @@ document.addEventListener("DOMContentLoaded", () => { this.events = []; this.leaves = []; this.weeks = []; - this.monthlyLeaveBalances = { - 2: { CP: {}, JRA: {}, JA: {} }, - 3: { CP: {}, JRA: {}, JA: {} }, - }; + + this.parents = []; + this.kids = []; + this.helpers = []; + this.careModes = []; + this.leaveMatrix = {}; + this.monthlyLeaveBalances = {}; if (!this.planningBody) return; this.init(); @@ -374,9 +447,44 @@ document.addEventListener("DOMContentLoaded", () => { this.setupModalUI(); this.initSmartSelectors(); + + try { + const res = await pachaFetch( + "/modules/family-calendar/includes/api/settings.php?action=get_all", + ); + if (res && res.success && res.data) { + window.calGlobalData = res.data; + + this.parents = (res.data.people || []).filter( + (p) => p.role === "parent", + ); + this.kids = (res.data.people || []).filter((p) => + ["child", "enfant"].includes(p.role), + ); + this.helpers = (res.data.people || []).filter( + (p) => p.role === "helper", + ); + this.careModes = res.data.foyer.care_modes || []; + this.leaveMatrix = res.data.leaves || {}; + + const settings = res.data.calendar_settings || {}; + if (settings.calendar_default_view) { + this.viewMode = settings.calendar_default_view; + document + .querySelectorAll(".fc-view-button") + .forEach((b) => b.classList.remove("fc-view-button--active")); + const activeBtn = document.querySelector( + `.fc-view-button[data-view="${this.viewMode}"]`, + ); + if (activeBtn) activeBtn.classList.add("fc-view-button--active"); + } + } + } catch (e) { + console.warn("Erreur chargement de l'annuaire familial:", e); + } + await this.refreshAllData(); this.updateSchoolYearLabel(); - setTimeout(() => this.scrollToCurrentMonth(), 100); } @@ -390,19 +498,15 @@ document.addEventListener("DOMContentLoaded", () => { for (let y = currentY - 2; y <= currentY + 3; y++) { options += ``; } - - // Utilise la globale config ou fallback si non définie const zoneText = - window.CONFIG?.ZONE_SCOLAIRE && - window.CONFIG.ZONE_SCOLAIRE !== "Autre" - ? `(Zone ${window.CONFIG.ZONE_SCOLAIRE})` + window.calGlobalData?.foyer?.zone_scolaire && + window.calGlobalData.foyer.zone_scolaire !== "Autre" + ? `(Zone ${window.calGlobalData.foyer.zone_scolaire})` : ""; - - headerTitle.innerHTML = `🏖️ ${window.I18N?.fc_modal_holidays_title || "Vacances"} ${zoneText} - `; - + headerTitle.innerHTML = `🏖️ ${tr("fc_modal_holidays_title") || "Vacances"} ${zoneText} + `; document .getElementById("holidayYearSelect") .addEventListener("change", (e) => { @@ -416,9 +520,7 @@ document.addEventListener("DOMContentLoaded", () => { const selectMonth = document.getElementById("fc-select-month"); const selectYear = document.getElementById("fc-select-year"); if (!selectMonth || !selectYear) return; - const lang = window.appLang || "fr-FR"; - for (let i = 0; i < 12; i++) { selectMonth.add( new Option( @@ -430,9 +532,8 @@ document.addEventListener("DOMContentLoaded", () => { ); } const currentY = new Date().getFullYear(); - for (let y = currentY - 2; y <= currentY + 5; y++) { + for (let y = currentY - 2; y <= currentY + 5; y++) selectYear.add(new Option(y, y)); - } const handleChange = () => { this.currentMonth = new Date( @@ -442,7 +543,6 @@ document.addEventListener("DOMContentLoaded", () => { ); this.renderMonthCalendar(); }; - selectMonth.addEventListener("change", handleChange); selectYear.addEventListener("change", handleChange); } @@ -454,7 +554,6 @@ document.addEventListener("DOMContentLoaded", () => { eventsData, fixedEventsData, leavesData, - balancesData, snapshotsData, ] = await Promise.all([ this.fetchApi( @@ -463,9 +562,6 @@ document.addEventListener("DOMContentLoaded", () => { 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", ), @@ -478,7 +574,6 @@ document.addEventListener("DOMContentLoaded", () => { })); this.fixedEvents = fixedEventsData; this.leaves = leavesData.leaves || []; - this.leaveBalances = balancesData.balances || []; this.leaveSnapshots = snapshotsData.snapshots || []; this.events = [...this.dbEvents, ...this.fixedEvents]; @@ -513,146 +608,32 @@ document.addEventListener("DOMContentLoaded", () => { } } - renderModalHolidays() { - if (!this.schoolHolidaysTableBody) return; - const startDate = `${this.modalSelectedYear}-09-01`; - const endDate = `${this.modalSelectedYear + 1}-08-31`; - - const yearHolidays = this.events.filter( - (e) => - e.type === "VACANCES_SCOLAIRES" && - e.date >= startDate && - e.date <= endDate, - ); - - if (yearHolidays.length === 0) { - this.schoolHolidaysTableBody.innerHTML = ` - - -

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

- - - - `; - document - .getElementById("btnFetchGovHolidays") - ?.addEventListener("click", (e) => { - e.target.innerText = "..."; - e.target.disabled = true; - this.fetchAndSaveGovHolidays(this.modalSelectedYear); - }); - return; - } - - yearHolidays.sort((a, b) => new Date(a.date) - new Date(b.date)); - const blocks = []; - let currentBlock = null; - - yearHolidays.forEach((e) => { - const d = new Date(e.date + "T00:00:00"); - if (!currentBlock) { - currentBlock = { start: d, end: d }; - blocks.push(currentBlock); - } else { - const diffDays = Math.round( - (d.getTime() - currentBlock.end.getTime()) / 86400000, - ); - if (diffDays <= 4) currentBlock.end = d; - else { - currentBlock = { start: d, end: d }; - blocks.push(currentBlock); - } - } - }); - - 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é"; - - return ` - ${name} - ${block.start.toLocaleDateString(window.appLang || "fr-FR")} - ${block.end.toLocaleDateString(window.appLang || "fr-FR")} - `; - }) - .join(""); - } - - async fetchAndSaveGovHolidays(yearStart) { - try { - const yearStr = `${yearStart}-${yearStart + 1}`; - const zone = window.CONFIG?.ZONE_SCOLAIRE || "C"; - - if (zone === "Autre") { - 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 uniqueMap = new Map(); - (data.results || []).forEach((r) => - uniqueMap.set(`${r.description}|${r.start_date}`, r), - ); - - const payload = []; - Array.from(uniqueMap.values()).forEach((r) => { - 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) { - payload.push({ - date: this.getLocalIsoDate(curr), - type: "VACANCES_SCOLAIRES", - duration: 1, - person: r.description, - }); - curr.setDate(curr.getDate() + 1); - } - }); - - if (payload.length > 0) { - await this.postApi( - "/modules/family-calendar/includes/api/save-events.php", - payload, - ); - await this.refreshAllData(); - } else { - alert("Aucune donnée trouvée."); - } - } catch (e) { - alert("Erreur API Gouv."); - } - } + renderModalHolidays() {} + async fetchAndSaveGovHolidays(yearStart) {} reprocessAndRender() { this.reprocessEvents(); this.calculateMonthlyBalances(); - this.initSummaryControls(); this.renderTable(); this.renderMonthCalendar(); } reprocessEvents() { + const upperCareModes = this.careModes.map((m) => m.toUpperCase()); + this.weeks.forEach((w) => { - Object.keys(w.totals).forEach((k) => (w.totals[k] = 0)); + w.totals = {}; + this.careModes.forEach((m) => (w.totals["mode_" + m] = 0)); + this.kids.forEach((k) => (w.totals["sick_" + k.id] = 0)); + this.helpers.forEach((h) => { + w.totals["off_" + h.id] = 0; + w.totals["extra_" + h.id] = 0; + }); + this.parents.forEach((p) => { + const types = this.leaveMatrix[p.id] || []; + types.forEach((t) => (w.totals[`leave_${p.id}_${t.type}`] = 0)); + }); + Object.values(w.dayFlags).forEach((f) => (f.events = [])); this.events.forEach((e) => { @@ -664,14 +645,25 @@ document.addEventListener("DOMContentLoaded", () => { if (dayKey) w.dayFlags[dayKey].events.push(e); const dur = parseFloat(e.duration) || 1; - const typeMap = { - OFF_CAROLE: "offCarole", - EXTRA_OFF_CAROLE: "extraOffCarole", - CENTRE: "centre", - AVIS: "avis", - PEP_SICK: "pepSick", - }; - if (typeMap[e.type]) w.totals[typeMap[e.type]] += dur; + + if (e.type === "CHILD_SICK") + w.totals["sick_" + e.person_id] = + (w.totals["sick_" + e.person_id] || 0) + dur; + if (e.type === "HELPER_OFF") + w.totals["off_" + e.person_id] = + (w.totals["off_" + e.person_id] || 0) + dur; + if (e.type === "HELPER_EXTRA") + w.totals["extra_" + e.person_id] = + (w.totals["extra_" + e.person_id] || 0) + dur; + + // Modes de garde dynamiques + if (upperCareModes.includes(e.type)) { + const originalMode = + this.careModes.find((m) => m.toUpperCase() === e.type) || + e.type; + w.totals["mode_" + originalMode] = + (w.totals["mode_" + originalMode] || 0) + dur; + } } }); @@ -679,15 +671,8 @@ 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; - let prefix = null; - if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id)) - prefix = "alex"; - else if ( - parents[1] && - parseInt(l.person_id) === parseInt(parents[1].id) - ) - prefix = "laia"; - if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur; + w.totals[`leave_${l.person_id}_${l.leave_type}`] = + (w.totals[`leave_${l.person_id}_${l.leave_type}`] || 0) + dur; } }); @@ -696,19 +681,28 @@ document.addEventListener("DOMContentLoaded", () => { if (!this.publicHolidayDates.has(this.getLocalIsoDate(d))) workingDays++; }); - w.totals.presencePep = Math.max( - 0, - workingDays - - (w.totals.offCarole + w.totals.extraOffCarole + w.totals.pepSick), + + let helperAbsences = 0; + this.helpers.forEach( + (h) => + (helperAbsences += + (w.totals["off_" + h.id] || 0) + + (w.totals["extra_" + h.id] || 0)), ); + + if (this.kids.length > 0) { + w.totals.presenceKid = Math.max( + 0, + workingDays - + (helperAbsences + (w.totals["sick_" + this.kids[0].id] || 0)), + ); + } }); } calculateMonthlyBalances() { const balances = {}; - parents.forEach((p) => { - balances[p.id] = { CP: {}, JRA: {}, JA: {} }; - }); + this.parents.forEach((p) => (balances[p.id] = {})); const ymSet = new Set(); this.weeks.forEach((w) => ymSet.add(w.monthKey)); @@ -725,83 +719,33 @@ document.addEventListener("DOMContentLoaded", () => { (usageByMonth[pid][type][ym] || 0) + parseFloat(l.duration); }); - parents.forEach((parent) => { + this.parents.forEach((parent) => { const pid = parent.id; - ["CP", "JRA", "JA"].forEach((type) => { + const matrix = this.leaveMatrix[pid] || []; + + matrix.forEach((conf) => { + const type = conf.type; + balances[pid][type] = {}; + ymList.forEach((ym) => { const [currYear, currMonth] = ym.split("-").map(Number); let cycleStartStr = "", - initialBalance = 0; + initialBalance = parseFloat(conf.allowance || 0); - const latestSnapshot = (this.leaveSnapshots || []) - .filter( - (s) => - s.person_id == pid && - s.leave_type == type && - s.snapshot_date.substring(0, 7) <= ym, - ) - .sort((a, b) => - b.snapshot_date.localeCompare(a.snapshot_date), - )[0]; - - if (latestSnapshot) { - cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7); - initialBalance = parseFloat(latestSnapshot.remaining_balance); - } else { - if (type === "CP") { - const refYear = - currMonth >= LEAVES_CONFIG.CP.startMonth - ? currYear - : currYear - 1; - cycleStartStr = `${refYear}-${String(LEAVES_CONFIG.CP.startMonth).padStart(2, "0")}`; - const dbBal = this.leaveBalances.find( - (b) => - b.person_id == pid && - b.leave_type == "CP" && - b.balance_year == refYear, - ); - initialBalance = dbBal - ? parseFloat(dbBal.initial_balance) - : LEAVES_CONFIG.CP.defaultBalance; - } else if (type === "JRA") { - cycleStartStr = `${currYear}-01`; - 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++) - usedPrevYear += - usageByMonth[pid]?.[type]?.[ - `${prevYear}-${String(m).padStart(2, "0")}` - ] || 0; - initialBalance += Math.min( - Math.max(0, prevInitial - usedPrevYear), - LEAVES_CONFIG.JRA.maxReport, - ); - } - } else if (type === "JA") { - const configJA = LEAVES_CONFIG.JA[pid]; + if (conf.date) { + const parts = conf.date.split("-"); + if (parts.length >= 2) { + const monthRenouvellement = parseInt(parts[1]); + const dayRenouvellement = + parts.length === 3 ? parseInt(parts[2]) : 1; const isPastAnniversary = - currMonth > configJA.startMonth || - (currMonth === configJA.startMonth && - configJA.startDay === 1); + currMonth > monthRenouvellement || + (currMonth === monthRenouvellement && 1 >= dayRenouvellement); const refYear = isPastAnniversary ? currYear : currYear - 1; - cycleStartStr = `${refYear}-${String(configJA.startMonth).padStart(2, "0")}`; - const dbBal = this.leaveBalances.find( - (b) => - b.person_id == pid && - b.leave_type == "JA" && - b.balance_year == refYear, - ); - initialBalance = dbBal - ? parseFloat(dbBal.initial_balance) - : configJA.defaultBalance; + cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`; } + } else { + cycleStartStr = `${currYear}-01`; } let usedBeforeCurrentMonth = 0; @@ -834,6 +778,7 @@ document.addEventListener("DOMContentLoaded", () => { const processedLeavesCols = {}; const fmt = (n) => n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : ""; + const upperCareModes = this.careModes.map((m) => m.toUpperCase()); this.weeks.forEach((w, idx) => { const tr = document.createElement("tr"); @@ -865,102 +810,238 @@ document.addEventListener("DOMContentLoaded", () => { const dateObj = w.dayDates[d]; const iso = this.getLocalIsoDate(dateObj); td.dataset.date = iso; - td.textContent = String(dateObj.getDate()).padStart(2, "0"); td.className = "col-day"; w.dayFlags[d].events.forEach((evt) => { - if (evt.type === "OFF_CAROLE") - td.classList.add("fc-day--off-carole"); - if (evt.type === "EXTRA_OFF_CAROLE") - td.classList.add("fc-day--extra-off-carole"); if (evt.type === "PUBLIC_HOLIDAY") td.classList.add("fc-day--public-holiday"); if (evt.type === "VACANCES_SCOLAIRES") td.classList.add("fc-day--school-holiday"); - if (evt.type === "CENTRE") td.classList.add("fc-day--centre"); - if (evt.type === "AVIS") td.classList.add("fc-day--avis"); - if (evt.type === "PEP_SICK") - td.innerHTML += `🤒`; + if (evt.type === "HELPER_OFF") + td.classList.add("fc-day--off-carole"); + if (evt.type === "HELPER_EXTRA") + td.classList.add("fc-day--extra-off-carole"); + if (upperCareModes.includes(evt.type)) + td.classList.add("fc-day--has-guard"); }); + let content = `
+ ${String(dateObj.getDate()).padStart(2, "0")}`; + + let iconsHtml = `
`; + w.dayFlags[d].events.forEach((evt) => { + if (upperCareModes.includes(evt.type)) { + const modeName = evt.type.toLowerCase(); + if (modeName === "avis") + iconsHtml += ``; + else if (modeName === "centre") + iconsHtml += `🏫`; + else + iconsHtml += `${modeName.substring(0, 3)}`; + } + }); + content += iconsHtml + `
`; + + let sickHtml = `
`; + w.dayFlags[d].events.forEach((evt) => { + if (evt.type === "CHILD_SICK") { + const k = this.kids.find( + (x) => parseInt(x.id) === parseInt(evt.person_id), + ); + if (k) { + sickHtml += `${k.name}🤒`; + } + } + }); + content += sickHtml + `
`; + const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); if (dayLeaves.length) { let html = `
`; - const colors = [ - "#0f766e", - "#b45309", - "#047857", - "#4338ca", - "#b91c1c", - ]; - parents.forEach((person, index) => { + this.parents.forEach((person) => { if ( dayLeaves.some( (l) => parseInt(l.person_id) === parseInt(person.id), ) ) { - html += `${person.name.charAt(0).toUpperCase()}`; + html += `${person.name.charAt(0).toUpperCase()}`; } }); - td.innerHTML += html + `
`; + content += html + `
`; } + td.innerHTML = content + `
`; tr.appendChild(td); }); - // --- 🟢 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) => { + this.careModes.forEach((mode) => { const td = document.createElement("td"); td.className = "col-total"; - // On normalise le nom (ex: "Centre" -> "centre") pour matcher ton objet w.totals - const key = mode.toLowerCase(); - td.textContent = fmt(w.totals[key] || 0); + td.textContent = fmt(w.totals["mode_" + mode] || 0); tr.appendChild(td); }); - - // 1B. Enfants (Maladie) - kids.forEach((kid) => { + this.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); + td.textContent = fmt(w.totals["sick_" + kid.id] || 0); tr.appendChild(td); }); - // --- 🟢 CORRECTION 2 : Colonnes des congés Parents (Dynamique !) --- if (!processedLeavesCols[w.monthKey]) { processedLeavesCols[w.monthKey] = true; - - const parentsList = window.FAMILY_CONFIG?.parents || []; - parentsList.forEach((parent, index) => { + this.parents.forEach((parent, index) => { const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia"; - - // 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) => { + (this.leaveMatrix[parent.id] || []).forEach((conf) => { const info = - this.monthlyLeaveBalances[parent.id]?.[type]?.[w.monthKey]; + this.monthlyLeaveBalances[parent.id]?.[conf.type]?.[w.monthKey]; tr.innerHTML += `${info ? fmt(info.availableAtMonthStart) : "-"}`; tr.innerHTML += `${info ? fmt(info.usedInMonth) : ""}`; }); }); } - this.planningBody.appendChild(tr); }); } + generateMonthHTML(year, month) { + let html = ``; + ["L", "M", "M", "J", "V"].forEach((d) => (html += ``)); + html += ``; + + const daysInMonth = new Date(year, month + 1, 0).getDate(); + let currentRenderedCols = 0, + startDay = (new Date(year, month, 1).getDay() + 6) % 7; + const upperCareModes = this.careModes.map((m) => m.toUpperCase()); + + if (startDay < 5) { + for (let i = 0; i < startDay; i++) { + html += ``; + currentRenderedCols++; + } + } + + 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()); + + 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")) + cls += " fc-day--school-holiday"; + if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY")) + cls += " fc-day--public-holiday"; + if (dayEvts.some((e) => e.type === "HELPER_OFF")) + cls += " fc-day--off-carole"; + if (dayEvts.some((e) => e.type === "HELPER_EXTRA")) + cls += " fc-day--extra-off-carole"; + if (dayEvts.some((e) => upperCareModes.includes(e.type))) + cls += " fc-day--has-guard"; + + let content = `
${d}`; + + let iconsHtml = `
`; + dayEvts.forEach((evt) => { + if (upperCareModes.includes(evt.type)) { + const modeName = evt.type.toLowerCase(); + if (modeName === "avis") + iconsHtml += ``; + else if (modeName === "centre") + iconsHtml += `🏫`; + else + iconsHtml += `${modeName.substring(0, 3)}`; + } + }); + content += iconsHtml + `
`; + + let sickHtml = `
`; + dayEvts.forEach((evt) => { + if (evt.type === "CHILD_SICK") { + const k = this.kids.find( + (x) => parseInt(x.id) === parseInt(evt.person_id), + ); + if (k) { + sickHtml += `${k.name} 🤒`; + } + } + }); + content += sickHtml + `
`; + + const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); + if (dayLeaves.length) { + content += `
`; + this.parents.forEach((parent) => { + if ( + dayLeaves.some( + (l) => parseInt(l.person_id) === parseInt(parent.id), + ) + ) { + content += `${parent.name.charAt(0).toUpperCase()} `; + } + }); + content += `
`; + } + html += `
`; + currentRenderedCols++; + } + + while (currentRenderedCols < 5 && currentRenderedCols > 0) { + html += ``; + currentRenderedCols++; + } + return html + `
${d}
${content}
`; + } + + generateMonthSummaryHTML(year, month) { + const stats = { off: 0, extra: 0, sick: 0, presence: 0 }; + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + for (let d = 1; d <= daysInMonth; d++) { + const dateObj = new Date(year, month, d); + const dayOfWeek = dateObj.getDay(); + if (dayOfWeek === 0 || dayOfWeek === 6) continue; + + const iso = this.getLocalIsoDate(dateObj); + const dayEvents = this.events.filter((e) => e.date === iso); + + dayEvents.forEach((e) => { + const dur = parseFloat(e.duration) || 1; + if (e.type === "HELPER_OFF") stats.off += dur; + if (e.type === "HELPER_EXTRA") stats.extra += dur; + if (e.type === "CHILD_SICK") stats.sick += dur; + }); + + if ( + !this.publicHolidayDates.has(iso) && + !dayEvents.some((e) => e.type === "VACANCES_SCOLAIRES") + ) { + let dayAbsence = 0; + dayEvents.forEach((e) => { + if (["HELPER_OFF", "HELPER_EXTRA", "CHILD_SICK"].includes(e.type)) { + dayAbsence += parseFloat(e.duration) || 1; + } + }); + stats.presence += Math.max(0, 1 - dayAbsence); + } + } + + return ` +
+
Off ${parseFloat(stats.off.toFixed(1))} j
+
Extra ${parseFloat(stats.extra.toFixed(1))} j
+
Maladie ${parseFloat(stats.sick.toFixed(1))} j
+
Présence ${parseFloat(stats.presence.toFixed(1))} j
+
+ `; + } + scrollToCurrentMonth() { const wrapper = document.getElementById("planningTable-wrapper"); const targetRow = document.querySelector( @@ -989,21 +1070,21 @@ document.addEventListener("DOMContentLoaded", () => { selectYear.value = y; if (this.viewMode === "3months") this.renderThreeMonthsView(); else if (this.viewMode === "2months") this.renderTwoMonthsView(); - else this.monthCalendar.innerHTML = this.generateMonthHTML(y, m); + else + this.monthCalendar.innerHTML = `
${this.generateMonthHTML(y, m)}${this.generateMonthSummaryHTML(y, m)}
`; } this.renderMonthBalances(); - this.syncSummaryWithMonth(); } renderTwoMonthsView() { const y = this.currentMonth.getFullYear(), m = this.currentMonth.getMonth(); const nextDate = new Date(y, m + 1, 1); - const lang = window.I18N_LANG || "fr-FR"; + const lang = window.appLang || "fr-FR"; 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())}
-
`; +
${new Intl.DateTimeFormat(lang, { month: "long" }).format(this.currentMonth)}
${this.generateMonthHTML(y, m)}${this.generateMonthSummaryHTML(y, m)}
+
${new Intl.DateTimeFormat(lang, { month: "long" }).format(nextDate)}
${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}${this.generateMonthSummaryHTML(nextDate.getFullYear(), nextDate.getMonth())}
+ `; } renderThreeMonthsView() { @@ -1015,9 +1096,10 @@ document.addEventListener("DOMContentLoaded", () => { new Date(y, m + 2, 1), ]; let html = `
`; + const lang = window.appLang || "fr-FR"; [d1, d2, d3].forEach( (d) => - (html += `
${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(d)}
${this.generateMonthHTML(d.getFullYear(), d.getMonth())}
`), + (html += `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(d)}
${this.generateMonthHTML(d.getFullYear(), d.getMonth())}${this.generateMonthSummaryHTML(d.getFullYear(), d.getMonth())}
`), ); this.monthCalendar.innerHTML = html + `
`; } @@ -1034,10 +1116,13 @@ document.addEventListener("DOMContentLoaded", () => { monthsToDisplay.push(`${y}-${String(m + i + 1).padStart(2, "0")}`); container.style.display = "flex"; - container.innerHTML = parents + container.innerHTML = this.parents .map((person) => { - let cards = `
${person.name.toUpperCase()}
`; - ["CP", "JRA", "JA"].forEach((type) => { + let cards = `
${person.name.toUpperCase()}
`; + const types = this.leaveMatrix[person.id] || []; + + types.forEach((conf) => { + const type = conf.type; const startBal = this.monthlyLeaveBalances[person.id]?.[type]?.[monthsToDisplay[0]] ?.availableAtMonthStart || 0; @@ -1055,215 +1140,23 @@ document.addEventListener("DOMContentLoaded", () => { let alertHtml = ""; const cMonth = parseInt(monthsToDisplay[0].split("-")[1]); if (endBal > 0) { - if (type === "CP" && cMonth >= 6 && cMonth <= 7) + if (type === "CP" && (cMonth === 5 || cMonth === 6)) alertHtml = `
🔥
`; else if ( type === "JRA" && - (cMonth === 1 || cMonth === 2) && + (cMonth === 1 || cMonth === 12) && endBal > 2 ) alertHtml = `
🔥
`; } - cards += `
${type}${fmt(endBal)}${totalUsed > 0 ? `-${fmt(totalUsed)}` : ""}${alertHtml}
`; + + cards += `
${type}${fmt(endBal)}${totalUsed > 0 ? `-${fmt(totalUsed)}` : ""}${alertHtml}
`; }); return cards + `
`; }) .join(""); } - syncSummaryWithMonth() { - const typeSelect = document.getElementById("summType"), - valueSelect = document.getElementById("summValue"); - if (typeSelect && valueSelect) { - if (typeSelect.value !== "month") { - typeSelect.value = "month"; - typeSelect.dispatchEvent(new Event("change")); - } - valueSelect.value = `${this.currentMonth.getFullYear()}-${String(this.currentMonth.getMonth() + 1).padStart(2, "0")}`; - this.updateGlobalSummary(); - } - } - - generateMonthHTML(year, month) { - let html = ``; - ["L", "M", "M", "J", "V"].forEach((d) => (html += ``)); - html += ``; - - const daysInMonth = new Date(year, month + 1, 0).getDate(); - let currentRenderedCols = 0, - startDay = (new Date(year, month, 1).getDay() + 6) % 7; - - if (startDay < 5) { - for (let i = 0; i < startDay; i++) { - html += ``; - currentRenderedCols++; - } - } - - 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()); - - 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")) - cls += " fc-day--school-holiday"; - if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY")) - cls += " fc-day--public-holiday"; - if (dayEvts.some((e) => e.type === "OFF_CAROLE")) - 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))) - cls += " fc-day--has-guard"; - - 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), - ) - ) { - content += `${parent.name.charAt(0).toUpperCase()} `; - } - }); - content += `
`; - } - html += `
`; - currentRenderedCols++; - } - while (currentRenderedCols < 5 && currentRenderedCols > 0) { - html += ``; - currentRenderedCols++; - } - return html + `
${d}
${content}
`; - } - - initSummaryControls() { - const typeSelect = document.getElementById("summType"), - valueSelect = document.getElementById("summValue"); - if (!typeSelect || !valueSelect) return; - - const years = new Set(), - months = new Set(); - this.weeks.forEach((w) => - Object.values(w.dayDates).forEach((d) => { - years.add(d.getFullYear()); - months.add( - `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`, - ); - }), - ); - - const populateValues = () => { - const currentType = typeSelect.value, - prevValue = valueSelect.value; - valueSelect.innerHTML = ""; - if (currentType === "year") { - Array.from(years) - .sort() - .forEach((y) => valueSelect.add(new Option(y, y))); - valueSelect.value = years.has(parseInt(prevValue)) - ? prevValue - : new Date().getFullYear(); - } else { - 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(); - }; - - if (!this.summaryListenersAttached) { - typeSelect.addEventListener("change", populateValues); - valueSelect.addEventListener("change", () => - this.updateGlobalSummary(), - ); - this.summaryListenersAttached = true; - } - populateValues(); - } - - updateGlobalSummary() { - const div = document.getElementById("globalSummary"), - typeSelect = document.getElementById("summType"), - valueSelect = document.getElementById("summValue"); - if (!div || !valueSelect?.value) return; - - const filterType = typeSelect.value, - filterValue = valueSelect.value; - const stats = { off: 0, extra: 0, sick: 0, pep: 0 }; - - this.weeks.forEach((w) => { - 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; - }); - 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); - } - } - }); - }); - - const tr = window.I18N || {}; - div.innerHTML = ` -
${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() { const lbl = document.getElementById("fc-current-school-year-label"); if (lbl) @@ -1290,20 +1183,7 @@ document.addEventListener("DOMContentLoaded", () => { thu: { events: [] }, fri: { events: [] }, }, - totals: { - offCarole: 0, - extraOffCarole: 0, - centre: 0, - avis: 0, - pepSick: 0, - presencePep: 0, - alexCP: 0, - alexJRA: 0, - alexJA: 0, - laiaCP: 0, - laiaJRA: 0, - laiaJA: 0, - }, + totals: {}, })); } @@ -1324,7 +1204,6 @@ document.addEventListener("DOMContentLoaded", () => { } setupEventListeners() { - // -- BRANCHEMENT MODALE DE CONFIGURATION (NOUVEAU) -- const btnSettings = document.getElementById("btnOpenCalendarSettings"); if (btnSettings) btnSettings.addEventListener("click", openCalendarSettings); @@ -1345,6 +1224,7 @@ document.addEventListener("DOMContentLoaded", () => { }); document.addEventListener("touchend", (e) => this.handleTouchEnd(e)); } + const scrollWrapper = document.getElementById("planningTable-wrapper"); if (scrollWrapper) { scrollWrapper.addEventListener("scroll", async () => { @@ -1367,75 +1247,7 @@ document.addEventListener("DOMContentLoaded", () => { }); } - const btnOpen = document.getElementById("btnOpenHolidays"), - btnClose = document.getElementById("btnCloseHolidays"), - modal = document.getElementById("modalHolidays"); - if (btnOpen && modal) - btnOpen.addEventListener("click", () => { - modal.classList.add("open"); - document.body.classList.add("no-scroll"); - this.renderModalHolidays(); - }); - if (btnClose && modal) - btnClose.addEventListener("click", () => { - modal.classList.remove("open"); - document.body.classList.remove("no-scroll"); - }); - if (modal) - modal.addEventListener("click", (e) => { - if (e.target === modal) { - modal.classList.remove("open"); - document.body.classList.remove("no-scroll"); - } - }); - - 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"); - document.body.classList.add("no-scroll"); - const snapDateInput = document.getElementById("snapDate"); - if (snapDateInput) - snapDateInput.value = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}-01`; - }); - if (btnCloseSnap && modalSnap) - btnCloseSnap.addEventListener("click", () => { - modalSnap.classList.remove("open"); - document.body.classList.remove("no-scroll"); - }); - if (modalSnap) - modalSnap.addEventListener("click", (e) => { - if (e.target === modalSnap) { - modalSnap.classList.remove("open"); - document.body.classList.remove("no-scroll"); - } - }); - if (formSnap) - formSnap.addEventListener("submit", async (e) => { - e.preventDefault(); - try { - await this.postApi( - "/modules/family-calendar/includes/api/save-leave-snapshot.php", - { - 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"); - document.body.classList.remove("no-scroll"); - formSnap.reset(); - await this.refreshAllData(); - } catch (error) { - alert("Erreur lors de la sauvegarde."); - } - }); - - if (this.monthCalendar) + if (this.monthCalendar) { this.monthCalendar.addEventListener("click", (e) => { const td = e.target.closest("td[data-date]"); if (td && td.dataset.date) { @@ -1443,6 +1255,8 @@ 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"); @@ -1471,44 +1285,13 @@ document.addEventListener("DOMContentLoaded", () => { this.renderMonthCalendar(); }); - if (this.monthCalendar) { - let startX = 0, - startY = 0; - this.monthCalendar.addEventListener( - "touchstart", - (e) => { - startX = e.changedTouches[0].screenX; - startY = e.changedTouches[0].screenY; - }, - { passive: true }, - ); - this.monthCalendar.addEventListener( - "touchend", - (e) => { - 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; - this.currentMonth.setMonth( - this.currentMonth.getMonth() + (dX < 0 ? num : -num), - ); - this.renderMonthCalendar(); - } - }, - { passive: true }, - ); - } document .getElementById("fc-prev-school-year") ?.addEventListener("click", () => this.changeSchoolYear(-1)); document .getElementById("fc-next-school-year") ?.addEventListener("click", () => this.changeSchoolYear(1)); + document.querySelectorAll(".fc-view-button").forEach((btn) => btn.addEventListener("click", (e) => { document @@ -1580,6 +1363,7 @@ document.addEventListener("DOMContentLoaded", () => { cell.classList.add("fc-day--selected"); this.selectedCells.push(cell); } + clearSelection() { this.selectedCells.forEach((c) => c.classList.remove("fc-day--selected")); this.selectedCells = []; @@ -1587,6 +1371,7 @@ document.addEventListener("DOMContentLoaded", () => { if (this.monthSelectionMenu) this.monthSelectionMenu.style.display = "none"; } + closeMenusIfOutside(e) { if ( !this.selectionMenu?.contains(e.target) && @@ -1601,54 +1386,111 @@ document.addEventListener("DOMContentLoaded", () => { if (!menu) return; this._currentBulkInfo = { dates }; - const activeEvents = new Set(); + const activeEventMap = new Set(); this.events.forEach((e) => { - if (dates.includes(e.date)) activeEvents.add(e.type); + if (dates.includes(e.date)) { + const personKey = + e.person_id !== null && e.person_id !== undefined + ? e.person_id.toString() + : "0"; + activeEventMap.add(`${e.type}_${personKey}`); + } }); const activeLeaves = {}; - parents.forEach((p) => (activeLeaves[p.id] = new Set())); + this.parents.forEach((p) => (activeLeaves[p.id] = new Set())); this.leaves.forEach((l) => { if (dates.includes(l.leave_date) && activeLeaves[l.person_id]) activeLeaves[l.person_id].add(l.leave_type); }); - const getBtnClass = (type, pid = null) => - (pid ? activeLeaves[pid]?.has(type) : activeEvents.has(type)) - ? "fc-menu-btn--active" + const getActiveStyleE = (type, personStr, color) => + activeEventMap.has(`${type}_${personStr}`) + ? `border: 1px solid ${color} !important; background: var(--bg-soft) !important; color: ${color} !important; font-weight: 700;` : ""; - const getBtnIcon = (type, pid = null) => - (pid ? activeLeaves[pid]?.has(type) : activeEvents.has(type)) - ? "✓ " + const getActiveStyleL = (type, pid, color) => + activeLeaves[pid]?.has(type) + ? `border: 1px solid ${color} !important; background: var(--bg-soft) !important; color: ${color} !important; font-weight: 700;` : ""; - const tr = window.I18N || {}; + + const trLang = window.I18N || {}; const dateLabel = dates.length > 1 - ? `${dates.length} Jours` - : new Date(dates[0]).toLocaleDateString(window.I18N_LANG || "fr-FR"); - const trashSvg = ``; - const buildHeader = (title, action, cat) => - `
${title}${action ? `` : ""}
`; + ? `${dates.length} Jours sélectionnés` + : new Date(dates[0]).toLocaleDateString(window.appLang || "fr-FR", { + weekday: "long", + day: "numeric", + month: "long", + }); - 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)}
`; - parents.forEach( - (p) => - (html += ``), - ); - html += ``; - ["CP", "JRA", "JA"].forEach((t) => { - html += ``; - parents.forEach( - (p) => - (html += ``), + const trashSvg = ``; + const buildHeader = (title, action, cat) => ` +
+ ${title} + ${action ? `` : ""} +
`; + + let html = `
${dateLabel}
`; + + if (this.helpers.length > 0) { + this.helpers.forEach((h) => { + html += `
${buildHeader(h.name, "clear-type", "HELPER_" + h.id)}
+ + +
`; + }); + } + + if (this.careModes.length > 0) { + html += `
${buildHeader(trLang.fc_care_modes_title || "Modes de garde", "clear-type", "CARE_MODE")}
`; + this.careModes.forEach((m) => { + let iconHtml = + m.toLowerCase() === "avis" + ? ` ` + : m.toLowerCase() === "centre" + ? `🏫 ` + : ""; + const modeType = m.toUpperCase(); + html += ``; + }); + html += `
`; + } + + if (this.kids.length > 0) { + html += `
${buildHeader(trLang.leg_pep_sick || "Maladie", "clear-type", "CHILD_SICK")}
`; + this.kids.forEach((k) => { + const color = k.color || "var(--danger)"; + html += ``; + }); + html += `
`; + } + + html += `
${buildHeader(trLang.fc_menu_kids_leaves || "Congés Adultes", null, null)}
`; + + const allParentLeaveTypes = new Set(); + this.parents.forEach((p) => { + (this.leaveMatrix[p.id] || []).forEach((l) => + allParentLeaveTypes.add(l.type), ); - html += `
`; }); - html += `
${p.name}
`; + + this.parents.forEach((p) => { + const pColor = p.color || "var(--primary)"; + html += `
+ ${p.name} +
`; + html += `
`; + Array.from(allParentLeaveTypes).forEach((t) => { + const hasThisLeave = (this.leaveMatrix[p.id] || []).some( + (l) => l.type === t, + ); + if (hasThisLeave) { + html += ``; + } + }); + html += `
`; + }); + html += `
`; menu.innerHTML = html; let left = x + 10; @@ -1664,43 +1506,61 @@ 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, - dates = this._currentBulkInfo.dates; + + const { action, type, person, pid, cat } = dataset; + const dates = this._currentBulkInfo.dates; + const upperCareModes = this.careModes.map((m) => m.toUpperCase()); try { if (action === "add") { let typesToClear = []; - if (["OFF_CAROLE", "EXTRA_OFF_CAROLE"].includes(type)) - typesToClear = CONGE_TYPES; - if (["CENTRE", "AVIS"].includes(type)) typesToClear = GUARDE_TYPES; - if (type === "PEP_SICK") typesToClear = PEP_TYPES; + if (["HELPER_OFF", "HELPER_EXTRA"].includes(type)) + typesToClear = ["HELPER_OFF", "HELPER_EXTRA"]; + if (upperCareModes.includes(type)) typesToClear = [...upperCareModes]; + if (type === "CHILD_SICK") typesToClear = ["CHILD_SICK"]; - 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 }, + { + action: "bulk_delete_day_types_person", + dates, + types: typesToClear, + person_id: parseInt(person) || 0, + }, ); + } await this.postApi( "/modules/family-calendar/includes/api/save-events.php", dates.map((d) => ({ date: d, type: type, duration: 1, - person: "Carole", + person_id: parseInt(person) || 0, })), ); } else if (action === "clear-type") { - let typesToClear = - cat === "CONGE" - ? CONGE_TYPES - : cat === "GARDE" - ? GUARDE_TYPES - : cat === "PEP" - ? PEP_TYPES - : []; + let typesToClear = []; + let personToClear = null; + + if (cat.startsWith("HELPER_")) { + typesToClear = ["HELPER_OFF", "HELPER_EXTRA"]; + personToClear = parseInt(cat.split("_")[1]) || 0; + } else if (cat === "CARE_MODE") { + typesToClear = [...upperCareModes]; + personToClear = 0; + } else if (cat === "CHILD_SICK") { + typesToClear = ["CHILD_SICK"]; + } + await this.postApi( "/modules/family-calendar/includes/api/manage-event.php", - { action: "bulk_delete_day_types", dates, types: typesToClear }, + { + action: "bulk_delete_day_types_person", + dates, + types: typesToClear, + person_id: personToClear, + }, ); } else if (action === "add-leave") { await this.postApi( diff --git a/modules/family-calendar/includes/api/events-debug.log b/modules/family-calendar/includes/api/events-debug.log index 16a85c1..e2e4dae 100644 --- a/modules/family-calendar/includes/api/events-debug.log +++ b/modules/family-calendar/includes/api/events-debug.log @@ -52,3 +52,10 @@ [2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}] [2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}] [2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}] +[2026-06-16T14:20:35+02:00] RAW INPUT: [{"date":"2026-07-29","type":"CARE_MODE","duration":1,"person":"Centre"}] +[2026-06-16T14:20:43+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CARE_MODE","duration":1,"person":"Centre"}] +[2026-06-16T14:22:51+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}] +[2026-06-16T14:23:10+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}] +[2026-06-16T14:23:33+02:00] RAW INPUT: [{"date":"2026-06-18","type":"CARE_MODE","duration":1,"person":"Nounou"}] +[2026-06-16T14:23:54+02:00] RAW INPUT: [{"date":"2026-06-11","type":"HELPER_OFF","duration":1,"person":"1"}] +[2026-06-16T14:23:57+02:00] RAW INPUT: [{"date":"2026-06-25","type":"CHILD_SICK","duration":1,"person":"4"}] diff --git a/modules/family-calendar/includes/api/manage-event.php b/modules/family-calendar/includes/api/manage-event.php index ea3a4fb..9c6bbaf 100644 --- a/modules/family-calendar/includes/api/manage-event.php +++ b/modules/family-calendar/includes/api/manage-event.php @@ -8,77 +8,98 @@ $action = $input['action'] ?? ''; if (!$action) { http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Action manquante.']); + echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action manquante.']); exit; } try { - // --- SUPPRESSION UNITAIRE --- if ($action === 'delete') { $eventId = (int)($input['event_id'] ?? 0); if ($eventId <= 0) { http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'ID manquant.']); + echo json_encode(['success' => false, 'status' => 'error', 'message' => 'ID manquant.']); exit; } $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?"); $stmt->execute([$eventId]); - echo json_encode(['status' => 'success']); + echo json_encode(['success' => true, 'status' => 'success']); exit; } - // --- MISE À JOUR UNITAIRE --- if ($action === 'update') { $eventId = (int)($input['event_id'] ?? 0); $newType = $input['new_type'] ?? ''; if ($eventId <= 0 || !$newType) { http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']); + echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Données manquantes.']); exit; } $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?"); $stmt->execute([$newType, $eventId]); - echo json_encode(['status' => 'success']); + echo json_encode(['success' => true, 'status' => 'success']); + exit; + } + + if ($action === 'bulk_delete_day_types_person') { + $dates = $input['dates'] ?? []; + $types = $input['types'] ?? []; + $person_id = $input['person_id'] ?? null; + + $dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); }); + + if (empty($dates) || empty($types)) { + echo json_encode(['success' => true, 'status' => 'success']); + exit; + } + + $datePlaceholders = implode(',', array_fill(0, count($dates), '?')); + $typePlaceholders = implode(',', array_fill(0, count($types), '?')); + + $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)"; + $params = array_merge($dates, $types); + + // 🔥 LE CORRECTIF : On cherche le 0 en base de données + if ($person_id === null || $person_id === '' || (int)$person_id === 0) { + $sql .= " AND person_id = 0"; + } else { + $sql .= " AND person_id = ?"; + $params[] = (int)$person_id; + } + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + echo json_encode(['success' => true, 'status' => 'success']); exit; } - // --- SUPPRESSION DE MASSE (Par date et type) --- - // Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra) if ($action === 'bulk_delete_day_types') { $dates = $input['dates'] ?? []; $types = $input['types'] ?? []; - $dates = array_filter($dates, function($d) { - return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); - }); + $dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); }); if (empty($dates) || empty($types)) { - echo json_encode(['status' => 'success']); + echo json_encode(['success' => true, 'status' => 'success']); exit; } - // Création des placeholders IN (?,?,?) $datePlaceholders = implode(',', array_fill(0, count($dates), '?')); $typePlaceholders = implode(',', array_fill(0, count($types), '?')); $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)"; $stmt = $pdo->prepare($sql); - - // Fusion des tableaux pour l'exécution $stmt->execute(array_merge($dates, $types)); - echo json_encode(['status' => 'success']); + echo json_encode(['success' => true, 'status' => 'success']); exit; } - // --- SUPPRESSION TOTALE SUR DES DATES --- if ($action === 'bulk_delete_all') { $dates = $input['dates'] ?? []; - $dates = array_filter($dates, function($d) { - return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); - }); + $dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); }); - if (empty($dates) || empty($types)) { - echo json_encode(['status' => 'success']); + if (empty($dates)) { + echo json_encode(['success' => true, 'status' => 'success']); exit; } @@ -87,15 +108,15 @@ try { $stmt = $pdo->prepare($sql); $stmt->execute($dates); - echo json_encode(['status' => 'success']); + echo json_encode(['success' => true, 'status' => 'success']); exit; } - // Action inconnue http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]); + echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action non reconnue : ' . $action]); } catch (PDOException $e) { http_response_code(500); - echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); -} \ No newline at end of file + echo json_encode(['success' => false, 'status' => 'error', 'message' => $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/modules/family-calendar/includes/api/save-events.php b/modules/family-calendar/includes/api/save-events.php index c8b5cbf..9f9fa0e 100644 --- a/modules/family-calendar/includes/api/save-events.php +++ b/modules/family-calendar/includes/api/save-events.php @@ -1,33 +1,14 @@ 'error', 'message' => 'Aucune donnée d\'événement reçue.']); - exit; -} - -$inserted = []; +require_once __DIR__ . '/../../../../includes/db.php'; try { - // 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping) - $stmtPeople = $pdo->query("SELECT id, name FROM pf_people"); - $peopleMap = []; - while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) { - // On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.] - $peopleMap[$row['name']] = $row['id']; + $rawInput = file_get_contents('php://input'); + $eventsToSave = json_decode($rawInput, true); + + if (empty($eventsToSave) || !is_array($eventsToSave)) { + throw new Exception("Aucune donnée d'événement reçue."); } $pdo->beginTransaction(); @@ -36,26 +17,32 @@ try { VALUES (:event_date, :event_type, :person_id, :duration)"; $stmt = $pdo->prepare($sql); - foreach ($eventsToSave as $event) { - $person_id = null; + $inserted = []; - // 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !) - if (!empty($event['person']) && isset($peopleMap[$event['person']])) { - $person_id = $peopleMap[$event['person']]; + foreach ($eventsToSave as $event) { + // 🔥 LE CORRECTIF : On initialise à 0 (car NOT NULL en base) + $person_id = 0; + + if (!empty($event['person_id']) && is_numeric($event['person_id'])) { + $person_id = (int)$event['person_id']; + } elseif (!empty($event['person']) && is_numeric($event['person'])) { + $person_id = (int)$event['person']; } + $duration = isset($event['duration']) ? (float)$event['duration'] : 1.0; + $stmt->execute([ ':event_date' => $event['date'], ':event_type' => $event['type'], ':person_id' => $person_id, - ':duration' => $event['duration'] ?? 1.0, + ':duration' => $duration, ]); $inserted[] = [ 'id' => $pdo->lastInsertId(), 'date' => $event['date'], 'type' => $event['type'], - 'duration' => $event['duration'] ?? 1.0, + 'duration' => $duration, 'person_id' => $person_id, ]; } @@ -63,12 +50,20 @@ try { $pdo->commit(); echo json_encode([ + 'success' => true, 'status' => 'success', 'inserted' => $inserted, ]); } catch (Exception $e) { - $pdo->rollBack(); + if (isset($pdo) && $pdo->inTransaction()) { + $pdo->rollBack(); + } http_response_code(500); - echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]); -} \ No newline at end of file + echo json_encode([ + 'success' => false, + 'status' => 'error', + 'message' => 'Erreur SQL : ' . $e->getMessage() + ]); +} +?> \ No newline at end of file diff --git a/modules/family-calendar/includes/api/settings.php b/modules/family-calendar/includes/api/settings.php index 04110b1..4f244bb 100644 --- a/modules/family-calendar/includes/api/settings.php +++ b/modules/family-calendar/includes/api/settings.php @@ -11,17 +11,18 @@ try { if ($action === 'get_all') { // A. Options globales du foyer $stmtFoyer = $pdo->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"]']; + // On retire le fallback "Nounou" codé en dur, on part sur vide si rien n'est configuré + $foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]']; - // 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"); + // B. Liste des membres de la famille (Triés par rôle pour grouper Parents, Enfants, Helpers) + // 🟢 CORRECTION : Ajout de la colonne `care_modes` et `color` pour le Javascript ! + $stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, 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"); + $stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance 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']][] = [ @@ -32,6 +33,21 @@ try { ]; } + // D. Paramètres dynamiques du calendrier (pf_settings) + $stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'"); + $calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR); // Crée un tableau [key => value] + + // Paramètres par défaut si la table est vide + $calendarSettings = [ + 'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month', + 'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1', + 'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00' + ]; + + // E. NOUVEAU : Récupération du catalogue des types de congés de la famille + $stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC"); + $leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC); + echo json_encode([ 'success' => true, 'data' => [ @@ -40,7 +56,9 @@ try { 'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true) ], 'people' => $people, - 'leaves' => $leavesMap + 'leaves' => $leavesMap, + 'calendar_settings' => $calendarSettings, + 'leave_types' => $leaveTypes // On envoie le catalogue au JS ! ] ]); exit; @@ -48,7 +66,7 @@ try { // ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ─── if ($action === 'save_foyer') { - if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée"); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée"); $zone = trim($_POST['zone_scolaire'] ?? 'C'); $modesRaw = $_POST['care_modes'] ?? '[]'; @@ -64,7 +82,7 @@ try { // ─── 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"); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée"); $personId = (int)($_POST['person_id'] ?? 0); $leavesData = json_decode($_POST['leaves'] ?? '[]', true); @@ -73,19 +91,22 @@ try { $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'; + $method = in_array($leave['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED'; $allowance = (float)($leave['allowance'] ?? 0); $date = trim($leave['date']); + // Si le JS n'envoie que "MM-DD" (Renouvellement perpétuel), on ajoute l'année bissextile 2000 pour satisfaire le format DATE de MySQL + if (preg_match('/^\d{2}-\d{2}$/', $date)) { + $date = "2000-" . $date; + } + if (!empty($type) && !empty($date)) { $stmtInsert->execute([$personId, $type, $method, $allowance, $date]); } @@ -97,6 +118,52 @@ try { exit; } + // ─── 4. SAUVEGARDE DES PARAMÈTRES D'AFFICHAGE DU CALENDRIER (ONGLET 3) ─── + if ($action === 'save_calendar_settings') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée"); + + $allowed_keys = ['calendar_default_view', 'calendar_first_day', 'calendar_working_hours']; + + $stmt = $pdo->prepare(" + INSERT INTO pf_settings (setting_key, setting_value, module) + VALUES (:key, :val, 'calendar') + ON DUPLICATE KEY UPDATE setting_value = :val2 + "); + + $pdo->beginTransaction(); + foreach ($allowed_keys as $key) { + if (isset($_POST[$key])) { + $value = trim($_POST[$key]); + $stmt->execute([ + 'key' => $key, + 'val' => $value, + 'val2' => $value + ]); + } + } + $pdo->commit(); + + echo json_encode(['success' => true]); + exit; + } + + // ─── 5. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ─── + if ($action === 'save_child_care_modes') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée"); + + $person_id = (int)($_POST['person_id'] ?? 0); + $care_modes = $_POST['care_modes'] ?? '[]'; + + if ($person_id > 0) { + $stmt = $pdo->prepare("UPDATE pf_people SET care_modes = ? WHERE id = ?"); + $stmt->execute([$care_modes, $person_id]); + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false, 'error' => 'ID de l\'enfant manquant.']); + } + exit; + } + throw new Exception("Action inconnue"); } catch (\Throwable $e) { diff --git a/settings.php b/settings.php index 91ab776..d2b7494 100644 --- a/settings.php +++ b/settings.php @@ -40,57 +40,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } else { $action = $_POST['action'] ?? ''; - // 🟢 SAUVEGARDE GLOBALE : CONFIGURATION DU FOYER (Unifiée) - if ($action === 'update_family_info') { - header('Content-Type: application/json'); - try { - require_once __DIR__ . '/includes/db.php'; - $pdo->beginTransaction(); - - // 1. Sauvegarde des paramètres globaux - $currency = trim($_POST['currency'] ?? '€'); - $zone = trim($_POST['zone_scolaire'] ?? 'C'); - $careModesJson = $_POST['custom_care_modes'] ?? '[]'; - - $stmtSave = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?, zone_scolaire = ?, care_modes = ? WHERE id = 1"); - $stmtSave->execute([$currency, $zone, $careModesJson]); - - // 2. Gestion des enfants - $kids = $_POST['kids'] ?? []; - $deleted = json_decode($_POST['deleted_kids'] ?? '[]', true); - - if (!empty($deleted)) { - $in = str_repeat('?,', count($deleted) - 1) . '?'; - $stmtDel = $pdo->prepare("DELETE FROM pf_people WHERE id IN ($in) AND role = 'enfant'"); - $stmtDel->execute($deleted); - } - - $stmtInsert = $pdo->prepare("INSERT INTO pf_people (name, role, care_modes) VALUES (?, 'enfant', ?)"); - $stmtUpdate = $pdo->prepare("UPDATE pf_people SET name = ?, care_modes = ? WHERE id = ? AND role = 'enfant'"); - - foreach ($kids as $kidId => $kidData) { - $name = trim($kidData['name'] ?? ''); - if (empty($name)) continue; - - $modes = $kidData['modes'] ?? []; - $modesJson = json_encode(array_values($modes)); - - if (strpos($kidId, 'new_') === 0) { - $stmtInsert->execute([$name, $modesJson]); - } else { - $stmtUpdate->execute([$name, $modesJson, (int)$kidId]); - } - } - - $pdo->commit(); - echo json_encode(['success' => true]); - } catch (Exception $e) { - $pdo->rollBack(); - echo json_encode(['success' => false, 'error' => $e->getMessage()]); - } - exit; - } - if ($action === 'set_modules' && $family_id) { $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); @@ -100,7 +49,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?") ->execute([json_encode($enabled), $family_id]); $_SESSION['enabled_modules'] = $enabled; - $success = "Modules mis à jour."; + $success = "Modules mis à jour avec succès."; } } @@ -110,7 +59,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $_SESSION['app_lang'] = $lang; $meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?") ->execute([$lang, $user_id]); - $success = "Language updated."; + $success = "Langue mise à jour."; } } @@ -131,7 +80,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $_SESSION['user']['display_name'] = $display_name; - $success = "Profil mis à jour."; + $success = "Profil mis à jour avec succès."; } } @@ -209,9 +158,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $pdo->prepare( "INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)" )->execute([(string) $n]); - $success = tr('groceries_settings_saved'); + $success = tr('groceries_settings_saved') ?: 'Paramètres des courses enregistrés.'; } catch (\Throwable $e) { - $error = tr('groceries_settings_error') . ' ' . $e->getMessage(); + $error = (tr('groceries_settings_error') ?: 'Erreur') . ' : ' . $e->getMessage(); } } } @@ -274,6 +223,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]); $success = "Connexion iOS supprimée."; } + + // --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) --- + if ($action === 'add_attached_profile') { + $name = trim($_POST['profile_name'] ?? ''); + $role = trim($_POST['profile_role'] ?? 'child'); + + if ($name) { + require_once __DIR__ . '/includes/db.php'; + if (isset($pdo)) { + $stmt = $pdo->prepare("INSERT INTO pf_people (name, role) VALUES (?, ?)"); + $stmt->execute([$name, $role]); + $success = "Profil de {$name} ajouté avec succès."; + } + } else { + $error = "Le prénom est obligatoire."; + } + } + + if ($action === 'delete_attached_profile') { + $profile_id = (int)($_POST['profile_id'] ?? 0); + if ($profile_id > 0) { + require_once __DIR__ . '/includes/db.php'; + if (isset($pdo)) { + // On protège les vrais utilisateurs en s'assurant que user_id est NULL + $stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL"); + $stmt->execute([$profile_id]); + $success = "Profil supprimé."; + } + } + } } } @@ -297,29 +276,10 @@ if ($family_id && isset($pdo)) { } } -// 🟢 Récupération Unifiée : Foyer, Modes de Garde, Enfants -$kidsList = []; -$familyCareModes = ['Nounou', 'Centre', 'Avis']; -$currentCurrency = defined('CURRENCY') ? CURRENCY : '€'; -$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C'; - -if ($family_id && isset($pdo)) { - $foyerSettings = $pdo->query("SELECT currency, zone_scolaire, care_modes FROM pf_foyer_settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC); - if ($foyerSettings) { - if (!empty($foyerSettings['currency'])) $currentCurrency = $foyerSettings['currency']; - if (!empty($foyerSettings['zone_scolaire'])) $currentZone = $foyerSettings['zone_scolaire']; - if (!empty($foyerSettings['care_modes'])) { - $parsed = json_decode($foyerSettings['care_modes'], true); - if (is_array($parsed)) $familyCareModes = $parsed; - } - } - - $stmtKids = $pdo->query("SELECT id, name, care_modes FROM pf_people WHERE role = 'enfant' ORDER BY id ASC"); - $kidsList = $stmtKids->fetchAll(PDO::FETCH_ASSOC); -} - $family = null; $members = []; +$attached_profiles = []; + if ($family_id) { $fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?"); $fam->execute([$family_id]); @@ -328,6 +288,12 @@ if ($family_id) { $mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id"); $mem->execute([$family_id]); $members = $mem->fetchAll(); + + // Récupération des profils rattachés (ceux qui n'ont pas de compte de connexion, donc user_id IS NULL) + if (isset($pdo)) { + $stmtAttached = $pdo->query("SELECT id, name, role FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC"); + $attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC); + } } $calendarIntegration = $meta_pdo->prepare("SELECT username, calendar_url, status, last_sync_at FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'"); @@ -353,13 +319,6 @@ require __DIR__ . '/header.php';

⚙️ Paramètres

- -
- - -
- -

🌐 Langue / Language

@@ -517,96 +476,6 @@ require __DIR__ . '/header.php';
- - 0; - $showZone = in_array('calendar', $_SESSION['enabled_modules'] ?? []); - - if ($family_id): - $currentCurrency = defined('CURRENCY') ? CURRENCY : '€'; - $currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C'; - ?> -
-

👪

-

Configurez les indicateurs et les membres partagés par votre foyer.

- - - - - - - - - -
- 🌍 -
-
- -
- - -
- - - -
- - -
- -
-
-
- - - -
- 🏷️ -
-
- - -
-
- - -
- 👶 -
-
    - - -
  • -
    - - -
    -
    - -
    -
  • - -
- - -
-
- - - -
- - -

👤 Mon profil

@@ -707,6 +576,57 @@ require __DIR__ . '/header.php'; + +

🧸 Profils rattachés

+

Membres gérés par le foyer (Enfants, Nounous, Proches) n'ayant pas d'accès direct.

+ +
+ +
+
+ + +
+ + + + + + +
+ + + +
Aucun profil rattaché.
+ +
+ +
+ + + +
+ + +
+ +
+ + +
+ + +
+
@@ -719,6 +639,7 @@ require __DIR__ . '/header.php';