ouragan
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-17 12:27:29 +02:00
parent 16b08dd03b
commit d6e95c1b2a
18 changed files with 2299 additions and 2175 deletions
+281
View File
@@ -80,6 +80,7 @@ require __DIR__ . '/header.php';
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)"><?= tr('bs_tab_categories') ?></button> <button class="bs-tab-btn" onclick="switchBsTab('categories', this)"><?= tr('bs_tab_categories') ?></button>
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button> <button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)"><?= tr('bs_tab_salaries') ?></button> <button class="bs-tab-btn" onclick="switchBsTab('salaries', this)"><?= tr('bs_tab_salaries') ?></button>
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
</div> </div>
<div class="bs-content"> <div class="bs-content">
@@ -187,6 +188,89 @@ require __DIR__ . '/header.php';
<div id="bs-list-salaries">⏳ <?= tr('loading') ?></div> <div id="bs-list-salaries">⏳ <?= tr('loading') ?></div>
</div> </div>
<div id="pane-csv" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
<p class="pf-muted-tiny" style="margin-top:0; margin-bottom:15px;"><?= tr('bs_csv_desc') ?></p>
<div id="csv-drop-zone" style="border: 2px dashed var(--border-light, #ccc); border-radius: var(--radius); padding: 30px; text-align: center; cursor: pointer; margin-bottom: 25px; transition: all 0.2s;">
<div style="font-size: 2rem; margin-bottom: 10px;">📥</div>
<h5 style="margin: 0 0 5px 0;"><?= tr('bs_csv_drop_title') ?></h5>
<p class="pf-muted-tiny" style="margin: 0;"><?= tr('bs_csv_drop_desc') ?></p>
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
</div>
<div id="csv-preview-container" style="display:none; background: var(--bg-soft, #f8fafc); padding: 15px; border-radius: var(--radius); border: 1px dashed var(--primary); margin-bottom: 25px;">
</div>
<h5 style="margin: 0 0 15px 0; border-bottom: 1px solid var(--border-light); padding-bottom: 5px;">
<?= tr('bs_csv_settings_title') ?>
</h5>
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
<div>
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
</select>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
<select name="csv_date_format" id="csv_date_format" class="pf-input">
<option value="d/m/Y">JJ/MM/AAAA</option>
<option value="Y-m-d">AAAA-MM-JJ</option>
</select>
</div>
</div>
<h5 style="margin: 15px 0 10px; font-size: 0.95rem;">
<?= tr('bs_csv_col_index') ?>
<small style="color:var(--text-muted); font-weight:normal;"><?= tr('bs_csv_col_index_help') ?></small>
</h5>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
<div>
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
</div>
</div>
<div style="background: var(--bg-panel); padding: 10px; border-radius: var(--radius); border: 1px solid var(--border-light); margin-bottom: 15px;">
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
<select name="csv_amount_type" id="csv_amount_type" class="pf-input" style="margin-bottom: 10px;" onchange="toggleCsvAmountCols(this.value)">
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
</select>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div>
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
</div>
<div id="wrapper_col_credit" style="display:none;">
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
</div>
<div style="text-align: right;">
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -719,5 +803,202 @@ async function updateBudgetCurrency(selectEl) {
alert("Erreur : " + err.message); alert("Erreur : " + err.message);
} }
} }
// --- GESTION DU MAPPING CSV ---
window.I18N = {
...(window.I18N || {}),
'bs_csv_col_amount': <?= json_encode(tr('bs_csv_col_amount')) ?>,
'bs_csv_col_debit': <?= json_encode(tr('bs_csv_col_debit')) ?>,
'bs_csv_saved': <?= json_encode(tr('bs_csv_saved')) ?>,
'error_occured': <?= json_encode(tr('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': <?= json_encode(tr('bs_csv_err_type')) ?>,
'bs_csv_file_ok': <?= json_encode(tr('bs_csv_file_ok')) ?>,
'bs_csv_preview_title': <?= json_encode(tr('bs_csv_preview_title')) ?>,
'bs_csv_empty_col': <?= json_encode(tr('bs_csv_empty_col')) ?>,
'bs_csv_no_data': <?= json_encode(tr('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 = `<h5 style="margin: 0 0 10px 0; color: var(--primary); font-size: 0.95rem;">👁️ ${window.I18N['bs_csv_preview_title']}</h5>`;
// Conteneur avec scroll horizontal si le tableau est trop large
html += `<div style="overflow-x: auto; max-width: 100%; border: 1px solid var(--border-light); border-radius: 8px; background: white;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: left; white-space: nowrap;">
<thead style="background: var(--bg-page, #f1f5f9); border-bottom: 2px solid var(--border-light);">
<tr>`;
// 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 += `<th style="padding: 10px 15px; font-weight: 600; color: var(--text-main); border-right: 1px solid var(--border-light);">
<div style="font-size: 0.75rem; color: var(--primary); margin-bottom: 2px;">N° ${index}</div>
<div>${cleanHeader}</div>
</th>`;
});
html += ` </tr>
</thead>
<tbody>`;
// Rendu des données
if (sampleRows.length === 0) {
html += `<tr><td colspan="${headers.length}" style="padding: 15px; text-align: center; color: var(--text-muted); font-style: italic;">${window.I18N['bs_csv_no_data']}</td></tr>`;
} else {
sampleRows.forEach(row => {
html += `<tr style="border-bottom: 1px solid #e2e8f0;">`;
// 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 += `<td style="padding: 10px 15px; color: var(--text-muted); border-right: 1px dashed #e2e8f0;">${displayData}</td>`;
}
html += `</tr>`;
});
}
html += `</tbody></table></div>`;
container.innerHTML = html;
container.style.display = 'block';
}
</script> </script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>
+43
View File
@@ -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'); 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) ──────────────────────────────────────────────────── -- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users ( CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -178,6 +201,25 @@ CREATE TABLE IF NOT EXISTS `pf_advances` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) 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 ───────────────────────────────────────────────────────────── -- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_notes ( CREATE TABLE IF NOT EXISTS pf_notes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -287,6 +329,7 @@ CREATE TABLE IF NOT EXISTS pf_vehicles (
license_plate VARCHAR(50) DEFAULT NULL, license_plate VARCHAR(50) DEFAULT NULL,
vin VARCHAR(100) DEFAULT NULL, vin VARCHAR(100) DEFAULT NULL,
fuel_type VARCHAR(50) DEFAULT 'Essence', fuel_type VARCHAR(50) DEFAULT 'Essence',
consumption decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km',
color VARCHAR(50) DEFAULT NULL, color VARCHAR(50) DEFAULT NULL,
purchase_date DATE DEFAULT NULL, purchase_date DATE DEFAULT NULL,
purchase_price DECIMAL(10,2) DEFAULT NULL, purchase_price DECIMAL(10,2) DEFAULT NULL,
+127 -155
View File
@@ -8,68 +8,60 @@ require_login();
require __DIR__ . '/includes/db.php'; require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php'; require_once __DIR__ . '/includes/i18n.php';
// 1. Récupération de toutes les personnes avec leurs modes de garde // 1. Récupération de toutes les personnes
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC"); $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); $familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
$parents = []; $parents = [];
$kids = []; $kids = [];
$helpers = [];
// Liste de tous les modes de garde actifs dans le foyer
$activeCareModes = [];
foreach ($familyPeople as $p) { foreach ($familyPeople as $p) {
$role = strtolower($p['role'] ?? ''); $role = strtolower($p['role'] ?? '');
if ($role === 'parent') { 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; $parents[] = $p;
} elseif ($role === 'enfant') { } elseif ($role === 'enfant' || $role === 'child') {
// Décodage sécurisé des modes de garde $p['modes'] = json_decode($p['care_modes'] ?? '[]', true) ?: [];
$modes = json_decode($p['care_modes'] ?? '[]', true);
if (!is_array($modes)) $modes = [];
$p['modes'] = $modes;
$kids[] = $p; $kids[] = $p;
} elseif ($role === 'helper' || $role === 'nounou') {
foreach ($modes as $m) { $helpers[] = $p; // 🟢 On stocke les intervenants
$activeCareModes[$m] = true;
}
} }
} }
$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'); $pageTitle = tr('fc_page_title');
$activePage = "family-calendar"; $activePage = "family-calendar";
$mainClass = "pf-family-calendar"; $mainClass = "pf-family-calendar";
$pageCss = "/modules/family-calendar/family-calendar.css"; $pageCss = "/modules/family-calendar/family-calendar.css";
require __DIR__ . '/header.php'; require __DIR__ . '/header.php';
?> ?>
<div class="pf-container" style="max-width:100%; padding:0;"> <div class="pf-container" style="max-width:100%; padding:0;">
<div class="fc-header-row" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 24px;"> <div class="fc-header-row" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 24px;">
<!-- Bloc Titre + Roue crantée --> <!-- Bloc Titre + Roue crantée -->
<div style="display: flex; align-items: center; gap: 12px;"> <div style="display: flex; align-items: center; gap: 12px;">
<h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1> <h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1>
<button id="btnOpenCalendarSettings" class="btn-settings-gear" title="<?= tr('fc_btn_settings') ?>"> <button id="btnOpenCalendarSettings" class="btn-settings-gear" title="Paramètres du calendrier" style="font-size: 1.4rem; padding: 4px;">
⚙️ ⚙️
</button> </button>
</div> </div>
<!-- Boutons d'actions --> <!-- Boutons d'actions -->
@@ -89,7 +81,7 @@ require __DIR__ . '/header.php';
<div class="pf-modal-content"> <div class="pf-modal-content">
<div class="pf-modal-header"> <div class="pf-modal-header">
<h3 class="pf-modal-title"> <h3 class="pf-modal-title">
<?= tr('fc_modal_holidays_title') ?> <?= tr('fc_modal_holidays_title') ?>
<?php if (defined('ZONE_SCOLAIRE') && ZONE_SCOLAIRE !== 'Autre') echo '(Zone ' . htmlspecialchars(ZONE_SCOLAIRE) . ')'; ?> <?php if (defined('ZONE_SCOLAIRE') && ZONE_SCOLAIRE !== 'Autre') echo '(Zone ' . htmlspecialchars(ZONE_SCOLAIRE) . ')'; ?>
</h3> </h3>
<button id="btnCloseHolidays" class="pf-modal-close" onclick="document.getElementById('modalHolidays').classList.remove('active')">&times;</button> <button id="btnCloseHolidays" class="pf-modal-close" onclick="document.getElementById('modalHolidays').classList.remove('active')">&times;</button>
@@ -124,7 +116,7 @@ require __DIR__ . '/header.php';
<label class="pf-label"><?= tr('fc_label_person') ?></label> <label class="pf-label"><?= tr('fc_label_person') ?></label>
<select id="snapPerson" class="pf-input" required> <select id="snapPerson" class="pf-input" required>
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option> <option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
<?php foreach ($familyPeople as $person): ?> <?php foreach ($parents as $person): ?>
<option value="<?= htmlspecialchars($person['id']) ?>"> <option value="<?= htmlspecialchars($person['id']) ?>">
<?= htmlspecialchars($person['name']) ?> <?= htmlspecialchars($person['name']) ?>
</option> </option>
@@ -134,9 +126,9 @@ require __DIR__ . '/header.php';
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label> <label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
<select id="snapType" class="pf-input" required> <select id="snapType" class="pf-input" required>
<option value="CP">CP</option> <?php foreach ($allLeaveTypes as $type): ?>
<option value="JRA">JRA</option> <option value="<?= htmlspecialchars($type) ?>"><?= htmlspecialchars($type) ?></option>
<option value="JA">JA</option> <?php endforeach; ?>
</select> </select>
</div> </div>
</div> </div>
@@ -162,87 +154,58 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<!-- 3. MODALE : PARAMÈTRES AVANCÉS DU CALENDRIER -->
<div id="modalCalendarSettings" class="pf-modal"> <div id="modalCalendarSettings" class="pf-modal">
<div class="pf-modal-content" style="max-width: 750px; width: 95%; padding: 0; overflow: hidden; display: flex; flex-direction: column;"> <div class="pf-modal-content" style="max-width: 600px; width: 90%;">
<div class="pf-modal-header">
<!-- HEADER --> <h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3>
<div class="pf-modal-header" style="padding: 1.2rem 1.5rem; border-bottom: 1px solid var(--border-light); background: var(--bg-panel);"> <button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">&times;</button>
<h3 class="pf-modal-title" style="display: flex; align-items: center; gap: 8px; margin: 0;">
<span style="color: var(--text-muted); font-size: 1.2rem;">⚙️</span> <?= tr('fc_modal_settings_title') ?>
</h3>
<button type="button" class="pf-modal-close" style="margin-right: 1.5rem;" onclick="closeCalendarSettings()">&times;</button>
</div> </div>
<!-- CORPS SCINDÉ EN DEUX (SIDEBAR / CONTENU) --> <div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem;">
<div style="display: flex; flex-direction: row; height: 60vh; min-height: 400px; background: var(--bg-panel);"> <button type="button" id="tab-btn-foyer" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('foyer')">🏡 Foyer</button>
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">👥 Membres & Droits</button>
</div>
<!-- SIDEBAR GAUCHE --> <div class="pf-modal-body" style="max-height: 60vh; overflow-y: auto; padding-top: 0;">
<div style="width: 220px; border-right: 1px solid var(--border-light); padding: 1rem 0; display: flex; flex-direction: column; gap: 4px;">
<button type="button" id="tab-btn-foyer" class="fc-sidebar-tab bs-tab-btn active" onclick="switchCalendarTab('foyer')"> <div id="cal-pane-foyer" class="cal-settings-pane">
<?= tr('fc_tab_foyer') ?> <form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
</button> <div class="pf-form-group">
<button type="button" id="tab-btn-membres" class="fc-sidebar-tab bs-tab-btn" onclick="switchCalendarTab('membres')"> <label class="pf-label"><?= tr('fc_zone_label') ?></label>
<?= tr('fc_tab_members') ?> <select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
</button> <option value="A">Zone A</option>
<option value="B">Zone B</option>
<option value="C">Zone C</option>
<option value="Autre">Autre / Hors France</option>
</select>
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
</div>
<div class="pf-form-group" style="margin-top: 1.5rem;">
<label class="pf-label"><?= tr('fc_care_modes_label') ?></label>
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; min-height: 38px; padding: 8px; background: var(--bg-page); border-radius: var(--radius); border: 1px solid var(--border-light);"></div>
<div style="display: flex; gap: 8px;">
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;">
<button type="button" class="btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()"></button>
</div>
<small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small>
</div>
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 8px;">
<button type="button" class="pf-btn pf-btn-secondary" onclick="closeCalendarSettings()"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
</div>
</form>
</div> </div>
<!-- CONTENU DROITE --> <div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
<div class="pf-modal-body" style="flex: 1; padding: 1.5rem; overflow-y: auto;"> <div style="margin-bottom: 1.2rem;">
<label class="pf-label">Sélectionner un membre de la famille :</label>
<!-- PANE A : CONFIGURATION GÉNÉRALE DU FOYER --> <select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600;" onchange="loadMemberConfigView()"></select>
<div id="cal-pane-foyer" class="cal-settings-pane">
<div style="display: flex; align-items: center; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.8rem;">
<h4 style="margin: 0; font-size: 1.1rem; color: var(--text-main);"><?= tr('fc_tab_foyer') ?></h4>
</div>
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
<div class="pf-form-group" style="margin-bottom: 1.5rem;">
<label class="pf-label" style="font-weight: 600; margin-bottom: 6px;"><?= tr('fc_zone_label') ?></label>
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%; border-radius: 8px; background-color: var(--bg-page);">
<option value="A">Zone A</option>
<option value="B">Zone B</option>
<option value="C">Zone C</option>
<option value="Autre">Autre / Hors France</option>
</select>
<small class="pf-muted-note" style="margin-top: 4px; display: block;"><?= tr('fc_zone_desc') ?></small>
</div>
<div class="pf-form-group">
<label class="pf-label" style="font-weight: 600; margin-bottom: 6px;"><?= tr('fc_care_modes_label') ?></label>
<div style="background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 8px; padding: 8px;">
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; min-height: 32px; margin-bottom: 8px;"></div>
<div style="display: flex; gap: 8px;">
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1; background: var(--bg-soft); border: none; border-radius: 6px; padding: 6px 12px;">
<button type="button" class="pf-btn pf-btn-secondary" style="padding: 0 14px; border-radius: 6px;" onclick="addCareModeTag()"></button>
</div>
</div>
<small class="pf-muted-note" style="margin-top: 6px; display: block;"><?= tr('fc_care_modes_desc') ?></small>
</div>
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 12px;">
<button type="button" class="pf-btn pf-btn-secondary" onclick="closeCalendarSettings()"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
</div>
</form>
</div> </div>
<!-- PANE B : CONFIGURATION DE LA MATRICE COMPTEURS --> <div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 100px;">
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
<div style="display: flex; align-items: center; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.8rem;">
<h4 style="margin: 0; font-size: 1.1rem; color: var(--text-main);"><?= tr('fc_tab_members') ?></h4>
</div> </div>
<div style="margin-bottom: 1.5rem;">
<label class="pf-label" style="font-weight: 600; margin-bottom: 6px;"><?= tr('fc_select_member') ?></label>
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600; border-radius: 8px; background-color: var(--bg-page);" onchange="loadMemberConfigView()"></select>
</div>
<div id="memberConfigZone" style="background: var(--bg-page); padding: 16px; border-radius: 12px; border: 1px solid var(--border-light); min-height: 120px;">
<!-- Injecté à la volée en fonction du membre sélectionné -->
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -259,7 +222,7 @@ require __DIR__ . '/header.php';
</div> </div>
<button id="fc-next-month" class="fc-nav-button"></button> <button id="fc-next-month" class="fc-nav-button"></button>
</div> </div>
<div class="fc-view-controls"> <div class="fc-view-controls">
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button> <button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button>
<div class="fc-view-divider"></div> <div class="fc-view-divider"></div>
@@ -268,7 +231,7 @@ require __DIR__ . '/header.php';
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button> <button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
</div> </div>
</div> </div>
<div class="fc-calendar-container"> <div class="fc-calendar-container">
<div id="fc-month-calendar" class="fc-month-calendar"></div> <div id="fc-month-calendar" class="fc-month-calendar"></div>
<div id="fc-month-selectionMenu" class="fc-selection-menu" style="display:none;"></div> <div id="fc-month-selectionMenu" class="fc-selection-menu" style="display:none;"></div>
@@ -307,32 +270,36 @@ require __DIR__ . '/header.php';
<th rowspan="3" class="col-total rotated-text"><span>Maladie <?= htmlspecialchars($kid['name']) ?></span></th> <th rowspan="3" class="col-total rotated-text"><span>Maladie <?= htmlspecialchars($kid['name']) ?></span></th>
<?php endforeach; ?> <?php endforeach; ?>
<!-- 🟢 NOUVEAU : Colspan dynamique selon le nombre de compteurs de chaque parent -->
<?php foreach ($parents as $index => $parent): <?php foreach ($parents as $index => $parent):
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; // Couleurs alternées
$colspan = count($parent['leave_types']) * 2; $pLeaves = $leaveMatrix[$parent['id']] ?? [];
$colspan = count($pLeaves) * 2;
?> ?>
<?php if ($colspan > 0): ?>
<th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th> <th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
<?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </tr>
<tr> <tr>
<!-- 🟢 NOUVEAU : Noms des compteurs dynamiques -->
<?php foreach ($parents as $index => $parent): <?php foreach ($parents as $index => $parent):
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
foreach ($parent['leave_types'] as $lt): $pLeaves = $leaveMatrix[$parent['id']] ?? [];
?> ?>
<th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars(strtoupper($lt)) ?></th> <?php foreach ($pLeaves as $type): ?>
<?php endforeach; endforeach; ?> <th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars($type) ?></th>
<?php endforeach; ?>
<?php endforeach; ?>
</tr> </tr>
<tr> <tr>
<!-- 🟢 NOUVEAU : Sous-colonnes Av./Use dynamiques -->
<?php foreach ($parents as $index => $parent): <?php foreach ($parents as $index => $parent):
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
foreach ($parent['leave_types'] as $lt): $pLeaves = $leaveMatrix[$parent['id']] ?? [];
?> ?>
<?php foreach ($pLeaves as $type): ?>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th> <th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th> <th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<?php endforeach; endforeach; ?> <?php endforeach; ?>
<?php endforeach; ?>
</tr> </tr>
</thead> </thead>
<tbody id="planningBody"></tbody> <tbody id="planningBody"></tbody>
@@ -342,49 +309,54 @@ require __DIR__ . '/header.php';
</section> </section>
<section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;"> <section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;">
<div class="fc-bottom-grid"> <div class="pf-card">
<div class="pf-card">
<div class="pf-card-title fc-summary-header">
<span><?= tr('fc_summary_title') ?></span>
<div class="fc-summary-controls">
<select id="summType" class="fc-summ-select">
<option value="year"><?= tr('fc_summ_year') ?></option>
<option value="month"><?= tr('fc_summ_month') ?></option>
</select>
<select id="summValue" class="fc-summ-select"></select>
</div>
</div>
<div class="pf-card-body">
<div id="globalSummary" class="fc-summary-vertical"></div>
</div>
</div>
<div class="pf-card">
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2> <h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
<div class="pf-card-body"> <div class="pf-card-body">
<div class="pf-legend-grid"> <div class="pf-legend-grid">
<!-- Fériés & Vacances (Fixes) -->
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span><?= tr('leg_school_holidays') ?></span></div> <div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span><?= tr('leg_school_holidays') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div> <div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div>
<?php foreach ($activeCareModes as $index => $mode): <!-- Intervenants (Dynamique) -->
$hue = ($index * 137) % 360; <?php foreach ($helpers as $helper): ?>
?> <div class="pf-legend-item"><div class="pf-legend-color" style="background: var(--warning);"></div><span>Off <?= htmlspecialchars($helper['name']) ?></span></div>
<div class="pf-legend-item"> <div class="pf-legend-item"><div class="pf-legend-color" style="background: var(--danger);"></div><span>Extra <?= htmlspecialchars($helper['name']) ?></span></div>
<div class="pf-legend-color" style="background: hsl(<?= $hue ?>, 70%, 50%);"></div>
<span><?= htmlspecialchars($mode) ?></span>
</div>
<?php endforeach; ?> <?php endforeach; ?>
<?php foreach ($kids as $kid): ?> <!-- Modes de garde (Dynamique avec icônes) -->
<?php foreach ($activeCareModes as $index => $mode):
$modeLower = strtolower($mode);
if ($modeLower === 'avis'): ?>
<div class="pf-legend-item">
<img src="/modules/family-calendar/assets/img/avis.svg" style="width:16px;height:16px;object-fit:contain;margin-right:6px;" alt="Avis">
<span><?= htmlspecialchars($mode) ?></span>
</div>
<?php elseif ($modeLower === 'centre'): ?>
<div class="pf-legend-item">
<span style="font-size:1.1rem; line-height:1; margin-right:6px;">🏫</span>
<span><?= htmlspecialchars($mode) ?></span>
</div>
<?php else:
$hue = ($index * 137) % 360; ?>
<div class="pf-legend-item">
<div class="pf-legend-color" style="background: hsl(<?= $hue ?>, 70%, 50%);"></div>
<span><?= htmlspecialchars($mode) ?></span>
</div>
<?php endif;
endforeach; ?>
<!-- Enfants Malades (Dynamique avec couleur BDD) -->
<?php foreach ($kids as $kid):
$color = !empty($kid['color']) ? $kid['color'] : 'var(--danger)';
?>
<div class="pf-legend-item"> <div class="pf-legend-item">
<div class="pf-legend-color" style="background: var(--pf-danger, #ef4444); opacity: 0.8;"></div> <div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
<span>Maladie <?= htmlspecialchars($kid['name']) ?></span> <span>Maladie <?= htmlspecialchars($kid['name']) ?></span>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>
+107 -2
View File
@@ -216,8 +216,8 @@ return [
'fc_tab_foyer' => '🏡 Llar', 'fc_tab_foyer' => '🏡 Llar',
'fc_tab_members' => '👥 Membres i Drets', 'fc_tab_members' => '👥 Membres i Drets',
'fc_select_member' => 'Selecciona un membre de la família:', 'fc_select_member' => 'Selecciona un membre de la família:',
'fc_role_adult' => '👨‍👩‍👦 Adult', 'fc_role_adult' => 'Adult',
'fc_role_child' => '👶 Nen', 'fc_role_child' => 'Nen',
'fc_care_modes_title' => '👶 Modes de cura associats', 'fc_care_modes_title' => '👶 Modes de cura associats',
'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.', 'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.',
'fc_no_care_mode' => 'Cap mode configurat.', '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_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_add_mode_placeholder' => 'Ex: Mainadera, Casal, Avis...',
'fc_settings_updated' => 'Configuració actualitzada correctament!', '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_presence' => 'Presència Pep',
'leg_school_holidays' => 'Vacances Escolars', 'leg_school_holidays' => 'Vacances Escolars',
@@ -461,6 +503,36 @@ return [
'bs_currency_label' => 'Divisa de la llar', 'bs_currency_label' => 'Divisa de la llar',
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.', 'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
'bs_currency_updated' => 'Divisa actualitzada correctament!', '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 --- // --- BUDGET : SUIVI ---
'bud_rem_school' => 'Escola (Resta estimada)', '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_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_server' => 'Error del servidor: ',
'bud_err_network_dup' => 'Error de xarxa en duplicar.', '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 --- // --- BUDGET : PREVISIONNEL ---
'bud_prev_incomes' => 'Ingressos', 'bud_prev_incomes' => 'Ingressos',
+108 -3
View File
@@ -217,9 +217,9 @@ return [
'fc_tab_foyer' => '🏡 Household', 'fc_tab_foyer' => '🏡 Household',
'fc_tab_members' => '👥 Members & Rights', 'fc_tab_members' => '👥 Members & Rights',
'fc_select_member' => 'Select a family member:', 'fc_select_member' => 'Select a family member:',
'fc_role_adult' => '👨‍👩‍👦 Adult', 'fc_role_adult' => 'Adult',
'fc_role_child' => '👶 Child', 'fc_role_child' => 'Child',
'fc_care_modes_title' => '👶 Associated Care Modes', 'fc_care_modes_title' => 'Associated Care Modes',
'fc_care_modes_desc' => 'Check the care modes that apply to this child.', 'fc_care_modes_desc' => 'Check the care modes that apply to this child.',
'fc_no_care_mode' => 'No care mode configured.', 'fc_no_care_mode' => 'No care mode configured.',
'fc_matrix_title' => '👨‍👩‍👦 Leave Rights Matrix', '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_care_modes_desc' => 'Manage the global list of possible care modes for the family.',
'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...', 'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...',
'fc_settings_updated' => 'Settings updated successfully!', '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_presence' => 'Pep presence',
'leg_school_holidays' => 'School holidays', 'leg_school_holidays' => 'School holidays',
@@ -459,6 +501,36 @@ return [
'bs_currency_label' => 'Household Currency', 'bs_currency_label' => 'Household Currency',
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.', 'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
'bs_currency_updated' => 'Currency updated successfully!', '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 --- // --- BUDGET: TRACKING ---
'bud_rem_school' => 'School (estimated remaining)', '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_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_server' => 'Server error: ',
'bud_err_network_dup' => 'Network error during duplication.', '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 --- // --- BUDGET: FORECAST ---
'bud_prev_incomes' => 'Income', 'bud_prev_incomes' => 'Income',
+109 -3
View File
@@ -219,9 +219,9 @@ return [
'fc_tab_foyer' => '🏡 Foyer', 'fc_tab_foyer' => '🏡 Foyer',
'fc_tab_members' => '👥 Membres & Droits', 'fc_tab_members' => '👥 Membres & Droits',
'fc_select_member' => 'Sélectionner un membre de la famille :', 'fc_select_member' => 'Sélectionner un membre de la famille :',
'fc_role_adult' => '👨‍👩‍👦 Adulte', 'fc_role_adult' => 'Adulte',
'fc_role_child' => '👶 Enfant', 'fc_role_child' => 'Enfant',
'fc_care_modes_title' => '👶 Modes de garde associés', 'fc_care_modes_title' => 'Modes de garde associés',
'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.', 'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.',
'fc_no_care_mode' => 'Aucun mode configuré.', 'fc_no_care_mode' => 'Aucun mode configuré.',
'fc_matrix_title' => '👨‍👩‍👦 Matrice des Droits aux Congés', '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_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_add_mode_placeholder' => 'Ex: Nounou, Casal, Grands-parents...',
'fc_settings_updated' => 'Paramètres mis à jour avec succès !', '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', 'leg_presence' => 'Présence Pep',
@@ -462,6 +505,36 @@ return [
'bs_currency_label' => 'Devise du Foyer', 'bs_currency_label' => 'Devise du Foyer',
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.', 'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
'bs_currency_updated' => 'Devise mise à jour avec succès !', '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 --- // --- BUDGET : SUIVI ---
'bud_rem_school' => 'École (Reste estimé)', '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_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_server' => 'Erreur serveur : ',
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.', '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 --- // --- BUDGET : PREVISIONNEL ---
'bud_prev_incomes' => 'Revenus', 'bud_prev_incomes' => 'Revenus',
+33 -143
View File
@@ -27,167 +27,57 @@ try {
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
); );
// --------------------------------------------------------- // 1. UPDATE DE pf_people
// 1. UPDATE DE pf_people (Juste la date de naissance et l'état actif)
// ---------------------------------------------------------
$colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'"); $colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'");
$colCheck->execute([$dbName]); $colCheck->execute([$dbName]);
if ($colCheck->rowCount() === 0) { if ($colCheck->rowCount() === 0) {
$pdo->exec("ALTER TABLE pf_people $pdo->exec("ALTER TABLE pf_people ADD COLUMN birthdate DATE DEFAULT NULL, ADD COLUMN is_active TINYINT(1) DEFAULT 1");
ADD COLUMN birthdate DATE DEFAULT NULL, echo "✅ pf_people mis à jour.<br>";
ADD COLUMN is_active TINYINT(1) DEFAULT 1
");
echo "✅ pf_people mis à jour (birthdate ajouté).<br>";
} else {
echo " pf_people déjà à jour.<br>";
} }
// --------------------------------------------------------- // 2. CRÉATION DES NOUVELLES TABLES
// 2. CRÉATION DES NOUVELLES TABLES (IF NOT EXISTS) $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");
// Comptes bancaires $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_bank_accounts ( $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");
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.<br>";
// ---------------------------------------------------------
// 3. INJECTION DE DONNÉES PAR DÉFAUT (Pour ne rien casser)
// ---------------------------------------------------------
// 3. INJECTION DONNÉES PAR DÉFAUT
if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) { if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) {
$pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES $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)");
('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) { // 4. MIGRATION MODULE CALENDRIER
$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).<br>";
// ---------------------------------------------------------
// 4. MIGRATION SPECIFIQUE : MODULE CALENDRIER (CONGÉS GRANULAIRES)
// ---------------------------------------------------------
$tableExists = $pdo->query("SHOW TABLES LIKE 'pf_person_leave_meta'")->rowCount() > 0; $tableExists = $pdo->query("SHOW TABLES LIKE 'pf_person_leave_meta'")->rowCount() > 0;
if (!$tableExists) { if (!$tableExists) {
$pdo->exec(" $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");
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).<br>";
} 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.<br>";
} else {
echo " Table pf_person_leave_meta déjà à jour.<br>";
}
} }
// 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.<br>";
}
// 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']}.<br>";
} catch (PDOException $e) { } catch (PDOException $e) {
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>"; echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
} }
} }
echo "<h1>🎉 Migration terminée avec succès !</h1>"; echo "<h1>🎉 Migration terminée avec succès !</h1>";
} catch (Exception $e) { } catch (Exception $e) {
die("❌ Erreur fatale Meta DB : " . $e->getMessage()); die("❌ Erreur fatale Meta DB : " . $e->getMessage());
} }
+32 -43
View File
@@ -19,8 +19,7 @@ try {
// 2. Catégories // 2. Catégories
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC); $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) // 3. Règles d'import
// Utilisation de COLLATE pour éviter l'erreur 1267 de mix de collations
$rules = $pdo->query(" $rules = $pdo->query("
SELECT r.*, c.label as cat_label SELECT r.*, c.label as cat_label
FROM pf_import_rules r FROM pf_import_rules r
@@ -32,73 +31,60 @@ try {
$currentYear = (int)date('Y'); $currentYear = (int)date('Y');
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC); $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) // 5. Paramètres Foyer (Devise + Mapping CSV)
// Lecture directe de la colonne currency dans pf_foyer_settings $foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
$currencySetting = $pdo->query("SELECT currency FROM pf_foyer_settings LIMIT 1")->fetchColumn() ?: '€'; $currencySetting = $foyerData['currency'] ?? '€';
$csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null;
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'data' => [ 'data' => [
'accounts' => $accounts, 'accounts' => $accounts,
'categories' => $categories, 'categories' => $categories,
'rules' => $rules, 'rules' => $rules,
'salaries' => $salaries, 'salaries' => $salaries,
'year' => $currentYear, 'year' => $currentYear,
'currency' => $currencySetting 'currency' => $currencySetting,
'csv_mapping' => $csvMapping
] ]
]); ]);
exit; exit;
} }
// --- GESTION DES COMPTES BANCAIRES --- // --- GESTION DES COMPTES BANCAIRES ---
if ($action === 'add_account') { if ($action === 'add_account') {
$name = trim($_POST['name'] ?? ''); $name = trim($_POST['name'] ?? '');
$type = $_POST['type'] ?? 'checking'; $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."); 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 = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
$stmt->execute([$name, $type]); $stmt->execute([$name, $type]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
if ($action === 'delete_account') { if ($action === 'delete_account') {
$id = (int)($_POST['id'] ?? 0); $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(); $count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte."); if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
// --- GESTION DES CATÉGORIES --- // --- GESTION DES CATÉGORIES ---
if ($action === 'add_category') { if ($action === 'add_category') {
$code = strtoupper(trim($_POST['code'] ?? '')); $code = strtoupper(trim($_POST['code'] ?? ''));
$label = trim($_POST['label'] ?? ''); $label = trim($_POST['label'] ?? '');
$type = $_POST['type'] ?? 'Expense'; $type = $_POST['type'] ?? 'Expense';
$color = $_POST['color'] ?? '#cccccc'; $color = $_POST['color'] ?? '#cccccc';
$icon = trim($_POST['icon'] ?? '📌'); $icon = trim($_POST['icon'] ?? '📌');
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires."); 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 = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
$check->execute([$code]); $check->execute([$code]);
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà."); 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 = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$code, $label, $type, $color, $icon]); $stmt->execute([$code, $label, $type, $color, $icon]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
@@ -107,27 +93,20 @@ try {
$id = (int)($_POST['id'] ?? 0); $id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
// --- GESTION DES RÈGLES D'IMPORT --- // --- GESTION DES RÈGLES D'IMPORT ---
if ($action === 'add_rule') { if ($action === 'add_rule') {
$keyword = strtoupper(trim($_POST['keyword'] ?? '')); $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."); 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 = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
$check->execute([$keyword]); $check->execute([$keyword]);
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà."); 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 = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
$stmt->execute([$keyword, $category]); $stmt->execute([$keyword, $category]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
@@ -136,36 +115,46 @@ try {
$id = (int)($_POST['id'] ?? 0); $id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
// --- GESTION DES SALAIRES --- // --- GESTION DES SALAIRES ---
if ($action === 'save_salary') { if ($action === 'save_salary') {
$id = (int)($_POST['id'] ?? 0); $id = (int)($_POST['id'] ?? 0);
$salary = (float)($_POST['salary'] ?? 0); $salary = (float)($_POST['salary'] ?? 0);
$mensualite = (float)($_POST['mensualite'] ?? 0); $mensualite = (float)($_POST['mensualite'] ?? 0);
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide."); 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 = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
$stmt->execute([$salary, $mensualite, $id]); $stmt->execute([$salary, $mensualite, $id]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
// --- GESTION DE LA DEVISE GLOBALE ---
// --- GESTION DE LA DEVISE GLOBALE ---
if ($action === 'save_currency') { if ($action === 'save_currency') {
$currency = trim($_POST['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 = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
$stmt->execute([$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]); echo json_encode(['success' => true]);
exit; exit;
} }
+32 -28
View File
@@ -11,11 +11,10 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
if ($role === 'parent') { if ($role === 'parent') {
$familyParents[] = $row['name']; $familyParents[] = $row['name'];
} elseif ($role === 'nounou') { } elseif ($role === 'nounou' || $role === 'helper') {
// On ignore la nounou dans l'épargne // On ignore la nounou dans l'épargne
continue; continue;
} else { } else {
// Fallback : Si c'est 'enfant', 'user', ou vide, ça va dans l'onglet Enfants
$familyKids[] = $row['name']; $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 // Sécurité anti-page blanche si la BDD est mal configurée
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2']; if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'Nens'); $requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'KIDS');
$ownersToDisplay = ($requestedOwner === 'Nens') ? $familyKids : [$requestedOwner]; $ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner];
// --- RÉCUPÉRATION CONFIGURATION DES MOIS --- // --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
$cycleConfigs = []; $cycleConfigs = [];
@@ -47,23 +46,21 @@ function getMonthName($dateString) {
<div class="budget-view"> <div class="budget-view">
<div class="view-header"> <div class="view-header">
<div class="owner-tabs"> <div class="owner-tabs">
<!-- Boucle dynamique sur les parents -->
<?php foreach ($familyParents as $p): ?> <?php foreach ($familyParents as $p): ?>
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>"> <a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
<?= htmlspecialchars($p) ?> <?= htmlspecialchars($p) ?>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
<!-- Onglet Enfants -->
<?php if (!empty($familyKids)): ?> <?php if (!empty($familyKids)): ?>
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>"> <a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
<?= tr('budget_tab_kids') ?? 'Nens 👶' ?> <?= tr('budget_tab_kids') ?? 'Enfants 👶' ?>
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
<?php foreach ($ownersToDisplay as $currentOwner): <?php foreach ($ownersToDisplay as $index => $currentOwner):
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC"); $stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
$stmt->execute([$currentOwner]); $stmt->execute([$currentOwner]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -83,28 +80,29 @@ function getMonthName($dateString) {
$months = array_slice($months, 0, 7); $months = array_slice($months, 0, 7);
sort($allCategories); sort($allCategories);
// Définition de la classe couleur selon le propriétaire
$ownerTextClass = 'txt-global'; $ownerTextClass = 'txt-global';
// 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
?> ?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'KIDS' && $index > 0) ? '40px' : '0' ?>;">
<div style="flex-grow: 1;"> <div style="flex-grow: 1;">
<?php if ($requestedOwner === 'Nens'): <?php if ($requestedOwner === 'KIDS'):
$themeClass = 'theme-' . strtolower($currentOwner); $themeClass = 'theme-' . strtolower($safeOwnerCls);
?> ?>
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;"> <h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
<?= $currentOwner ?> <?= htmlspecialchars($currentOwner) ?>
</h3> </h3>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="display: flex; gap: 10px; align-items: center;"> <div style="display: flex; gap: 10px; align-items: center;">
<?php if (!empty($months)): ?> <?php if (!empty($months)): ?>
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary"> <button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
🔁 <?= tr('bud_sav_add_one_month') ?> 🔁 <?= tr('bud_sav_add_one_month') ?>
</button> </button>
<?php endif; ?> <?php endif; ?>
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn"> <button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
<?= tr('bud_sav_add_month') ?> <?= tr('bud_sav_add_month') ?>
</button> </button>
</div> </div>
@@ -116,7 +114,7 @@ function getMonthName($dateString) {
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p> <p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
</div> </div>
<?php else: ?> <?php else: ?>
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;"> <table class="pf-table savings-table nens-table theme-<?= strtolower($safeOwnerCls) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
<thead> <thead>
<tr> <tr>
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th> <th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
@@ -135,11 +133,11 @@ function getMonthName($dateString) {
<div class="month-actions" style="justify-content: center; width: 100%;"> <div class="month-actions" style="justify-content: center; width: 100%;">
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>" <button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>" data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'> onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
✏️ ✏️
</button> </button>
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>" <button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')" onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')"
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;"> style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
🗑️ 🗑️
</button> </button>
@@ -158,11 +156,11 @@ function getMonthName($dateString) {
<td class="text-center" style="padding:4px;"> <td class="text-center" style="padding:4px;">
<div style="display:flex; align-items:center; justify-content:center; gap:2px;"> <div style="display:flex; align-items:center; justify-content:center; gap:2px;">
<input type="number" step="0.01" <input type="number" step="0.01"
class="prev-input total-input-<?= $currentOwner ?>-<?= $month ?>" class="prev-input total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
style="width: 70px; font-weight:bold; color:#2563eb;" style="width: 70px; font-weight:bold; color:#2563eb;"
value="<?= $val != 0 ? round($val) : '' ?>" value="<?= $val != 0 ? round($val) : '' ?>"
placeholder="0" placeholder="0"
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)"> onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span> <span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
</div> </div>
</td> </td>
@@ -178,11 +176,11 @@ function getMonthName($dateString) {
<td class="text-center" style="padding:4px;"> <td class="text-center" style="padding:4px;">
<div style="display:flex; align-items:center; justify-content:center; gap:2px;"> <div style="display:flex; align-items:center; justify-content:center; gap:2px;">
<input type="number" step="0.01" <input type="number" step="0.01"
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>" class="prev-input <?= $ownerTextClass ?> cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
style="width: 70px;" style="width: 70px;"
value="<?= $amount != 0 ? round($amount) : '' ?>" value="<?= $amount != 0 ? round($amount) : '' ?>"
placeholder="-" placeholder="-"
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)"> onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
</div> </div>
</td> </td>
<?php endforeach; ?> <?php endforeach; ?>
@@ -197,7 +195,7 @@ function getMonthName($dateString) {
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0); foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
$extra = $total - $sum; $extra = $total - $sum;
?> ?>
<td class="text-center font-bold sum-target" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;"> <td class="text-center font-bold sum-target" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
<?= number_format($extra, 0, ',', ' ') ?> € <?= number_format($extra, 0, ',', ' ') ?> €
</td> </td>
<?php endforeach; ?> <?php endforeach; ?>
@@ -289,6 +287,9 @@ window.I18N = {
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?> 'bud_err_delete': <?= json_encode(tr('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 --- // --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>; const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
@@ -306,16 +307,19 @@ function updateEpargneCell(month, category, owner, inputEl) {
body: formData body: formData
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique')); }).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; const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
let sumCats = 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; sumCats += parseFloat(inp.value) || 0;
}); });
const extra = totalVal - sumCats; const extra = totalVal - sumCats;
const extraCell = document.getElementById(`extra_${owner}_${month}`); const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`);
if (extraCell) { if (extraCell) {
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €'; extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
@@ -557,7 +561,7 @@ function updateSumResult() {
total += val; 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) { document.addEventListener('click', function(e) {
+147 -247
View File
@@ -1,35 +1,32 @@
<?php <?php
// modules/budget/views/recap.php // modules/budget/views/recap.php
// 1. Récupération des Items du Budget // 1. Récupération des Catégories dynamiques
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC"); $stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC");
$items = $stmt->fetchAll(); $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"); $stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
$defaultActiveMonth = $stmtActive->fetchColumn(); $defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01');
if (!$defaultActiveMonth) {
$defaultActiveMonth = date('Y-m-01');
}
// 3. Assigner le mois et l'année en fonction du mois ouvert
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth)); $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"; $viewMonthDate = "$currentYear-$currentMonth-01";
$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real // 5. Récupération du Réel (Dépenses)
FROM pf_expenses $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");
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
GROUP BY budget_item_id";
$stmtReal = $pdo->prepare($sqlReal);
$stmtReal->execute([$viewMonthDate]); $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 $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");
FROM pf_expenses
WHERE gestion_month = ? AND budget_item_id IS NULL
GROUP BY category";
$stmtCatReal = $pdo->prepare($sqlCatReal);
$stmtCatReal->execute([$viewMonthDate]); $stmtCatReal->execute([$viewMonthDate]);
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR); $catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
@@ -38,8 +35,7 @@ $stmtLabels->execute([$viewMonthDate]);
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC); $unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; $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('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear;
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
$totalDepenses = 0; $totalDepenses = 0;
$totalRevenus = 0; $totalRevenus = 0;
@@ -64,46 +60,60 @@ $totalRevenus = 0;
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($salaries as $salary):
$mensualite = (float)$salary['mensualite'];
$totalRevenus += $mensualite;
?>
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
<td style="padding:15px;">
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong>
<span title="Géré automatiquement depuis les paramètres des Salaires" style="font-size:0.8rem; cursor:help;">⚙️</span>
</td>
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
+ <?= number_format($mensualite, 2, ',', ' ') ?> €
</td>
<td style="padding:15px;">
<span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Fixe (Auto)</span>
</td>
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
<td style="padding:15px;">
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
Auto
</div>
</td>
<td style="padding:15px; text-align:right;">
<small style="color:#94a3b8;">Via Paramètres</small>
</td>
</tr>
<?php endforeach; ?>
<?php foreach ($items as $item): <?php foreach ($items as $item):
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
$targetAbs = abs((float)$item['amount']); $targetAbs = abs((float)$item['amount']);
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs; $amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
$totalDepenses += $amountToAdd;
if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
else $totalDepenses += $amountToAdd;
// --- 2. CALCUL DU RÉEL (Logique optimisée) ---
$realSum = 0; $realSum = 0;
$hasMatchingExpense = false; $hasMatchingExpense = false;
// A. Correspondance directe par ID // A. Correspondance par ID direct
if (isset($realTotals[$item['id']])) { if (isset($realTotals[$item['id']])) {
$realSum = $realTotals[$item['id']]; $realSum = $realTotals[$item['id']];
$hasMatchingExpense = true; $hasMatchingExpense = true;
} }
// B. Correspondance par catégorie système (Multi-tenant via Mots-clés) // B. Correspondance par Catégorie système
else { elseif (!empty($item['category']) && isset($catTotals[$item['category']])) {
$catKey = null; $realSum = $catTotals[$item['category']];
if (!empty($item['mapping_keywords'])) { $hasMatchingExpense = true;
if (stripos($item['mapping_keywords'], 'School') !== false) $catKey = 'School'; }
elseif (stripos($item['mapping_keywords'], 'Essence') !== false) $catKey = 'Essence'; // C. Correspondance par mots-clés bancaires
elseif (stripos($item['mapping_keywords'], 'FMCG') !== false) $catKey = 'FMCG'; elseif (!empty($item['mapping_keywords'])) {
} $keywords = array_map('trim', explode(',', $item['mapping_keywords']));
foreach ($unlinkedExpenses as $uexp) {
if ($catKey && isset($catTotals[$catKey])) { foreach ($keywords as $kw) {
$realSum = $catTotals[$catKey]; if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
$hasMatchingExpense = true; $realSum += (float)$uexp['amount'];
} $hasMatchingExpense = true;
// C. Correspondance par mots-clés classiques (sur les dépenses non liées) break;
elseif (!empty($item['mapping_keywords'])) {
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
foreach ($unlinkedExpenses as $uexp) {
foreach ($keywords as $kw) {
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
$realSum += (float)$uexp['amount'];
$hasMatchingExpense = true;
break;
}
} }
} }
} }
@@ -111,79 +121,55 @@ $totalRevenus = 0;
$realAbs = abs($realSum); $realAbs = abs($realSum);
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10))); $isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
$rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : '');
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
if ($item['is_estimate']) $rowClass .= ' row-estimate';
?> ?>
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);"> <tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
<td style="padding:15px;"> <td style="padding:15px;">
<strong><?= htmlspecialchars($item['name']) ?></strong> <strong><?= htmlspecialchars($item['name']) ?></strong>
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?> <?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Variable)</small>' : '' ?>
<?php if(!empty($item['mapping_keywords'])): ?> <?php if(!empty($item['mapping_keywords'])): ?>
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span> <span title="Reconnaissance bancaire : <?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
<?php endif; ?>
<?php if(!empty($item['reg_month'])): ?>
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
</div>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;"> <td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
<?= number_format($targetAbs, 2, ',', ' ') ?> € - <?= number_format($targetAbs, 2, ',', ' ') ?> €
<?php if ($hasMatchingExpense): ?> <?php if ($hasMatchingExpense): ?>
<?php <?php $gap = $realAbs - $targetAbs;
// Le "Gap" est la différence visuelle.
$gap = $realAbs - $targetAbs;
if ($gap > 0.05): ?> if ($gap > 0.05): ?>
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;"> <div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">Dépassé : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
</div>
<?php elseif ($gap < -0.05): ?> <?php elseif ($gap < -0.05): ?>
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;"> <div style="font-size:0.75rem; color:#f59e0b;">Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
</div>
<?php else: ?> <?php else: ?>
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;"> <div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
<?= tr('bud_exact_amount') ?> ✓
</div>
<?php endif; ?> <?php endif; ?>
<?php elseif ($item['type'] === 'Annuel'): ?> <?php elseif ($item['type'] === 'Annuel'): ?>
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;"><?= tr('bud_per_month_short') ?> <?= number_format($amountToAdd, 2, ',', ' ') ?>/<?= tr('bud_month_short') ?></div> <div style="font-size:0.75rem; color:#94a3b8;"><?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td style="padding:15px;"> <td style="padding:15px;">
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;"> <span class="badge-type" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
<?= tr('bud_freq_'.strtolower($item['type'])) ?> <?= htmlspecialchars($item['type']) ?>
</span> </span>
</td> </td>
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td> <td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ?: '-' ?></td>
<td style="padding:15px;"> <td style="padding:15px;">
<?php if ($isAutoChecked): ?> <?php if ($isAutoChecked): ?>
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"> <div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Validé</div>
<span>✓</span> <?= tr('bud_state_validated') ?>
</div>
<?php elseif($hasMatchingExpense): ?> <?php elseif($hasMatchingExpense): ?>
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"> <div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
<span>⏳</span> <?= tr('bud_state_partial') ?>
</div>
<?php else: ?> <?php else: ?>
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"> <div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
<span>○</span> <?= tr('bud_state_waiting') ?>
</div>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td style="padding:15px; text-align:right;"> <td style="padding:15px; text-align:right;">
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;"> <div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button> <button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button> <button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -191,34 +177,30 @@ $totalRevenus = 0;
</tbody> </tbody>
<tfoot style="background:#f8fafc;"> <tfoot style="background:#f8fafc;">
<tr> <tr>
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td> <td colspan="1" style="padding:15px;"><strong>Total Revenus Lissés</strong></td>
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td> <td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
</tr> </tr>
<tr> <tr>
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td> <td colspan="1" style="padding:15px;"><strong>Totalpenses & Estimations</strong></td>
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td> <td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
</tr> </tr>
<tr style="border-top: 2px solid #e2e8f0; background: white;"> <tr style="border-top: 2px solid #e2e8f0; background: white;">
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td> <td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Reste à Vivre (Équilibre)</strong></td>
<?php $balance = $totalRevenus - $totalDepenses; ?> <?php $balance = $totalRevenus - $totalDepenses; ?>
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>"> <td colspan="5" style="padding:15px; font-size: 1.3em;">
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong> <strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
<p>* <?= tr('bud_recap_footer_note') ?></p>
</div>
</div> </div>
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;"> <div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;"> <div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3> <h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter une ligne</h3>
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button> <button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div> </div>
@@ -227,90 +209,65 @@ $totalRevenus = 0;
<input type="hidden" name="id" id="item_id"> <input type="hidden" name="id" id="item_id">
<div class="form-group" style="margin-bottom:15px;"> <div class="form-group" style="margin-bottom:15px;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_name') ?></label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom de la ligne</label>
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="<?= tr('bud_ph_item_name') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <input type="text" name="name" id="item_name" required class="pf-input" placeholder="ex: Assurance Auto, Estimation Courses..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_keywords') ?></label>
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="<?= tr('bud_ph_keywords') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
<small style="color:#64748b; font-size:0.75rem;"><?= tr('bud_help_keywords') ?></small>
</div> </div>
<div style="display:flex; gap:15px; margin-bottom:15px;"> <div style="display:flex; gap:15px; margin-bottom:15px;">
<div style="flex:1;"> <div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant Prévu (€)</label>
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de Dépense</label>
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value="0">Fixe (Facture, Prélèvement)</option>
<option value="1">Variable (Estimation, Enveloppe)</option>
</select>
</div> </div>
</div> </div>
<div style="display:flex; gap:15px; margin-bottom:15px;"> <div style="display:flex; gap:15px; margin-bottom:15px;">
<div style="flex:1;"> <div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie Cible (La jauge)</label>
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;"> <select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;" required>
<option value="expense"><?= tr('bud_cat_expense') ?></option> <option value="" disabled selected>-- Choisir --</option>
<option value="income"><?= tr('bud_cat_income') ?></option> <?php foreach ($dbCategories as $c): ?>
<?php if(strtolower($c['type']) === 'expense'): // On n'affiche que les catégories de dépenses ?>
<option value="<?= htmlspecialchars($c['code']) ?>">
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
</option>
<?php endif; ?>
<?php endforeach; ?>
</select> </select>
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;"> <select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option> <option value="Mensuel">Mensuel</option>
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option> <option value="Annuel">Annuel</option>
</select> </select>
</div> </div>
</div> </div>
<div style="display:flex; gap:15px; margin-bottom:25px;"> <div class="form-group" style="margin-bottom:15px;">
<div style="flex:1;"> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Reconnaissance Bancaire)</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_amount_type') ?></label> <input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="ex: NETFLIX, DOMOFINANCE..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;"> <small style="color:#64748b; font-size:0.75rem;">Optionnel. Permet au système de lier automatiquement un import CSV à cette ligne.</small>
<option value="0"><?= tr('bud_type_fixed') ?></option>
<option value="1"><?= tr('bud_type_variable') ?></option>
</select>
</div>
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_regularization') ?></label>
<select name="reg_month" id="item_reg_month" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value=""><?= tr('bud_reg_none') ?></option>
<?php
foreach($moisFr as $index => $m) {
if($index == 0) continue;
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
}
?>
</select>
</div>
</div> </div>
<div style="display:flex; justify-content:flex-end; gap:10px;"> <div style="display:flex; justify-content:flex-end; gap:10px; margin-top: 25px;">
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_cancel') ?></button> <button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script> <script>
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
window.I18N = {
...(window.I18N || {}),
'bud_recap_modal_add': <?= json_encode(tr('bud_recap_modal_add')) ?>,
'bud_recap_modal_edit': <?= json_encode(tr('bud_recap_modal_edit')) ?>,
'bud_recap_confirm_delete': <?= json_encode(tr('bud_recap_confirm_delete')) ?>,
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
'bud_saving': <?= json_encode(tr('bud_sav_saving') ?? 'Sauvegarde...') ?>
};
function openRecapModal(mode) { function openRecapModal(mode) {
if (mode === "add") { if (mode === "add") {
document.getElementById("recapModalTitle").innerText = window.I18N['bud_recap_modal_add'] || 'Ajouter'; document.getElementById("recapModalTitle").innerText = 'Ajouter une ligne';
document.getElementById("item_id").value = ""; document.getElementById("item_id").value = "";
document.getElementById("recapForm").reset(); document.getElementById("recapForm").reset();
} }
@@ -324,113 +281,56 @@ function closeRecapModal() {
} }
window.onclick = function(event) { window.onclick = function(event) {
const modal = document.getElementById('budgetRecapModal'); if (event.target == document.getElementById('budgetRecapModal')) closeRecapModal();
if (event.target == modal) {
closeRecapModal();
}
} }
function editRecapItem(item) { function editRecapItem(item) {
const data = typeof item === "string" ? JSON.parse(item) : item; const data = typeof item === "string" ? JSON.parse(item) : item;
document.getElementById("recapModalTitle").innerText = "Editer : " + data.name;
document.getElementById("recapModalTitle").innerText = (window.I18N['bud_recap_modal_edit'] || 'Editer') + " : " + data.name;
document.getElementById("item_id").value = data.id; document.getElementById("item_id").value = data.id;
document.getElementById("item_name").value = data.name; document.getElementById("item_name").value = data.name;
document.getElementById("item_keywords").value = data.mapping_keywords || ''; document.getElementById("item_keywords").value = data.mapping_keywords || '';
document.getElementById("item_amount").value = Math.abs(data.amount); document.getElementById("item_amount").value = Math.abs(data.amount);
document.getElementById("item_category").value = data.category; document.getElementById("item_category").value = data.category;
document.getElementById("item_type").value = data.type; document.getElementById("item_type").value = data.type;
document.getElementById("item_day").value = data.payment_day;
document.getElementById("item_reg_month").value = data.reg_month || "";
document.getElementById("item_is_estimate").value = data.is_estimate; document.getElementById("item_is_estimate").value = data.is_estimate;
document.getElementById("budgetRecapModal").style.display = "flex"; document.getElementById("budgetRecapModal").style.display = "flex";
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
} }
// --- 2. INTERCEPTION ASYNCHRONE DU FORMULAIRE --- document.getElementById('recapForm').addEventListener('submit', async function(e) {
const recapForm = document.getElementById('recapForm'); e.preventDefault();
if (recapForm) { const btn = this.querySelector('button[type="submit"]');
recapForm.addEventListener('submit', async function(e) { btn.disabled = true; btn.innerText = '⏳ ...';
e.preventDefault();
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerText;
submitBtn.innerText = window.I18N['bud_saving'] || '⏳ ...';
submitBtn.disabled = true;
const formData = new FormData(this); const formData = new FormData(this);
formData.append('ajax', '1'); formData.append('ajax', '1');
const url = this.getAttribute('action');
// 💡 Utilisation sécurisée de getAttribute
const actionUrl = this.getAttribute('action');
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
try {
const response = await fetch(finalUrl, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
// 💡 Lecture robuste (anti-Warnings PHP)
const textResult = await response.text();
try {
const result = JSON.parse(textResult);
if (result.success) {
closeRecapModal();
window.location.reload();
} else {
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
}
} catch (jsonError) {
console.error("Réponse non-JSON :", textResult);
alert("Le serveur a renvoyé une erreur PHP. Regarde la console (F12).");
}
} catch (error) {
console.error("Erreur Fetch:", error);
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
} finally {
submitBtn.disabled = false;
submitBtn.innerText = originalText;
}
});
}
// --- 3. SUPPRESSION ASYNCHRONE ---
async function deleteRecapItem(id) {
if (!confirm(window.I18N['bud_recap_confirm_delete'] || "Confirmer la suppression ?")) return;
const formData = new FormData();
formData.append("action", "delete");
formData.append("id", id);
formData.append("ajax", "1"); // Signale à l'API qu'on attend du JSON
try { try {
const response = await fetch("modules/budget/includes/api/manage-item.php", { const res = await fetch(url.startsWith('/') ? url.substring(1) : url, { method: 'POST', body: formData });
method: "POST", const data = await res.json();
body: formData, if (data.success) window.location.reload();
}); else alert("Erreur : " + data.error);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const textResult = await response.text();
try {
const result = JSON.parse(textResult);
// On s'assure que si l'API ne renvoie pas success, on le signale
if (result.success !== false) {
window.location.reload();
} else {
alert((window.I18N['bud_err_delete'] || 'Erreur de suppression') + " : " + (result.error || ""));
}
} catch (jsonErr) {
console.error("Réponse non-JSON lors de la suppression :", textResult);
window.location.reload(); // Fallback si l'API redirige au lieu de répondre en JSON
}
} catch (err) { } catch (err) {
console.error("Erreur réseau Suppression:", err); alert("Erreur technique lors de la sauvegarde.");
alert(window.I18N['bud_err_delete'] || 'Erreur réseau'); } finally {
btn.disabled = false; btn.innerText = 'Enregistrer';
}
});
async function deleteRecapItem(id) {
if (!confirm("Confirmer la suppression ?")) return;
const formData = new FormData();
formData.append("action", "delete"); formData.append("id", id); formData.append("ajax", "1");
try {
const res = await fetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
const data = await res.json();
if (data.success !== false) window.location.reload();
else alert("Erreur : " + data.error);
} catch (err) {
window.location.reload();
} }
} }
</script> </script>
+145 -107
View File
@@ -60,7 +60,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
if (isset($_POST['action']) && $_POST['action'] === 'save_import') { if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$count = 0; $count = 0;
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
// Mémorisation du mapping étendu (incluant budget_item_id)
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)"); $stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
if (isset($_POST['lines']) && is_array($_POST['lines'])) { if (isset($_POST['lines']) && is_array($_POST['lines'])) {
@@ -152,9 +151,43 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
} }
// ============================================================================ // ============================================================================
// 3. RECUPERATION DES DONNEES ET CALCULS // 3. RECUPERATION DES DONNEES ET CALCULS (100% DYNAMIQUE)
// ============================================================================ // ============================================================================
// --- LECTURE DYNAMIQUE DES CATEGORIES ---
$stmtCats = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type DESC, label ASC");
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
$categoriesConfig = [];
foreach ($dbCategories as $c) {
$catType = ($c['type'] === 'Income') ? 'credit' : 'debit';
$categoriesConfig[$c['code']] = [
'type' => $catType,
'db_type' => $c['type'],
'label' => ($c['icon'] ? $c['icon'] . ' ' : '') . $c['label'],
'budget' => 0, // Sera rempli par les règles
'color' => $c['color'] ?: '#64748b',
'suggestions' => []
];
}
// Fallback "Autres" au cas où la BDD serait vide ou pour les dépenses non classées
if (!isset($categoriesConfig['AUTRES'])) {
$categoriesConfig['AUTRES'] = [
'type'=>'debit', 'db_type'=>'Expense', 'label'=>'📁 Autres / Divers',
'budget'=>0, 'color'=>'#94a3b8', 'suggestions'=>[]
];
}
// Peuplement dynamique des suggestions via les règles existantes
$stmtRules = $pdo->query("SELECT keyword, category FROM pf_import_rules");
while ($rule = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
if (isset($categoriesConfig[$rule['category']])) {
$categoriesConfig[$rule['category']]['suggestions'][] = $rule['keyword'];
}
}
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?"); $stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
$stmtCheckClose->execute([$viewMonthDate]); $stmtCheckClose->execute([$viewMonthDate]);
$closureJson = $stmtCheckClose->fetchColumn(); $closureJson = $stmtCheckClose->fetchColumn();
@@ -184,27 +217,42 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id'); $paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label'); $realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0; $budget_income_prevu = 0;
$total_income = 0; $total_expenses_prevues = 0; $total_income = 0; $total_expenses_prevues = 0;
$reste_a_venir_calc = 0; $reste_a_venir_calc = 0;
$fixedChargesList = []; $incomeList = []; $pending_charges = []; $fixedChargesList = []; $incomeList = []; $pending_charges = [];
// ============================================================================
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
// ============================================================================
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC"); $stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$absAmount = abs((float)$item['amount']); $absAmount = abs((float)$item['amount']);
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount; $amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
$name = trim($item['name']); $name = trim($item['name']);
$catCode = $item['category']; // Ex: FIXED, FMCG, INCOME...
$isIncome = ((float)$item['amount'] > 0); // La norme est désormais définie par le signe du montant
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) { if ($isIncome) {
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
}
if ($item['category'] === 'income') {
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; $incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
$total_income += $amt; $total_income += $amt;
$budget_income_prevu += $amt; $budget_income_prevu += $amt;
// Attribution au compte de revenu défini, sinon au premier disponible
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
$categoriesConfig[$catCode]['budget'] += $amt;
} else {
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
}
} else { } else {
$total_expenses_prevues += $amt; $total_expenses_prevues += $amt;
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
// C'est une charge fixe (Mensuel + Is_Estimate = 0)
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
$isPaid = false; $isPaid = false;
if (in_array($item['id'], $paidItemIds)) $isPaid = true; if (in_array($item['id'], $paidItemIds)) $isPaid = true;
elseif (!empty($item['mapping_keywords'])) { elseif (!empty($item['mapping_keywords'])) {
@@ -221,90 +269,68 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$pending_charges[] = ['name' => $name, 'amount' => $absAmount]; $pending_charges[] = ['name' => $name, 'amount' => $absAmount];
} }
} }
if (!empty($item['mapping_keywords'])) {
if (stripos($item['mapping_keywords'], 'FMCG') !== false) $budget_fmcg += $amt;
if (stripos($item['mapping_keywords'], 'School') !== false) $budget_school += $amt;
if (stripos($item['mapping_keywords'], 'Essence') !== false) $budget_essence += $amt;
}
if ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') { // NOUVEAU : Attribution du budget prévisionnel (le Plafond) via la catégorie dynamique
$budget_frais += $absAmount; if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
$categoriesConfig[$catCode]['budget'] += $amt;
} }
} }
} }
$budget_autres = max(0, $total_income - $total_expenses_prevues); // L'enveloppe "Autres" prend tout le reste du budget non alloué
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
// TRADUCTION DES CATÉGORIES
$categoriesConfig = [
'Income' => ['type'=>'credit', 'label'=>tr('cat_income'), 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
'FMCG' => ['type'=>'debit', 'label'=>tr('cat_fmcg'), 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
'Essence' => ['type'=>'debit', 'label'=>tr('cat_fuel'), 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
'Apports' => ['type'=>'debit', 'label'=>tr('cat_contributions') ?? 'Apports & Projets', 'budget'=>0, 'color'=>'#0ea5e9', 'suggestions'=>['Alex', 'Laia', 'Remboursement']],
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
];
$totals = array_fill_keys(array_keys($categoriesConfig), 0); $totals = array_fill_keys(array_keys($categoriesConfig), 0);
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []); $expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
$total_rentrees = 0; $total_rentrees = 0;
$depenses_reelles = 0; $depenses_reelles = 0;
// VENTILATION DYNAMIQUE DES DÉPENSES
foreach ($allExpenses as $exp) { foreach ($allExpenses as $exp) {
$cat = $exp['category']; $cat = $exp['category'];
if (!isset($totals[$cat])) $cat = 'Autres'; if (!isset($categoriesConfig[$cat])) $cat = 'AUTRES';
$val = (float)$exp['amount']; $val = (float)$exp['amount'];
if ($cat === 'Income') { $totals[$cat] += $val; } if ($categoriesConfig[$cat]['db_type'] === 'Income') {
else { $totals[$cat] += $val;
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; } else {
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; // Remboursement
else $totals[$cat] += abs($val); else $totals[$cat] += abs($val);
} }
$expensesByCategory[$cat][] = $exp; $expensesByCategory[$cat][] = $exp;
if ($val > 0) { if ($val > 0) {
if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val; if ($categoriesConfig[$cat]['db_type'] === 'Income') { $total_rentrees += $val; }
else $depenses_reelles -= $val; else { $depenses_reelles -= $val; }
} else { } else {
$depenses_reelles += abs($val); $depenses_reelles += abs($val);
} }
} }
// Reste à venir dynamiques // RESTE A VENIR PAR CATEGORIE (Remplace les variables codées en dur)
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0; foreach ($categoriesConfig as $code => $conf) {
$reste_ecole = max(0, $budget_school - $ecole_depense); if ($conf['db_type'] === 'Expense' && $conf['budget'] > 0) {
if ($reste_ecole > 0) { $spent = $totals[$code] ?? 0;
$reste_a_venir_calc += $reste_ecole; $rem = max(0, $conf['budget'] - $spent);
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole]; if ($rem > 0) {
} $reste_a_venir_calc += $rem;
$pending_charges[] = ['name' => 'Reste ' . strip_tags($conf['label']), 'amount' => $rem];
$fmcg_depense = isset($totals['FMCG']) ? $totals['FMCG'] : 0; }
$reste_fmcg = max(0, $budget_fmcg - $fmcg_depense); }
if ($reste_fmcg > 0) {
$reste_a_venir_calc += $reste_fmcg;
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
}
$essence_depense = isset($totals['Essence']) ? $totals['Essence'] : 0;
$reste_essence = max(0, $budget_essence - $essence_depense);
if ($reste_essence > 0) {
$reste_a_venir_calc += $reste_essence;
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
} }
// F. Calculs des KPIs finaux // F. Calculs des KPIs finaux
$rentrees_salaires_reels = $totals['Income'] ?? 0; $rentrees_salaires_reels = 0;
foreach($categoriesConfig as $code => $conf) { if($conf['db_type'] === 'Income') $rentrees_salaires_reels += ($totals[$code] ?? 0); }
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels; $rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu); $salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres; $capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
// 1. On calcule s'il reste des salaires/revenus prévus à encaisser
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels); $revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
// 2. Le solde théorique part de ta vraie saisie bancaire actuelle
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc; $solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
if ($isClosed) { if ($isClosed) {
@@ -335,7 +361,6 @@ function getDisplayLogic($spent, $bg, $type) {
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text]; return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
} }
// Noms des mois traduits
$monthNames = [ $monthNames = [
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'), 1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'), 5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
@@ -505,11 +530,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
</div> </div>
</div> </div>
<?php if (!$isClosed): ?> <?php if (!$isClosed): ?>
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')"></button> <button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes(strip_tags($conf['label'])) ?>')"></button>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?> <?php $barCol = ($conf['db_type'] === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
<div style="background:#f1f5f9; height:4px; width:100%;"> <div style="background:#f1f5f9; height:4px; width:100%;">
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div> <div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
</div> </div>
@@ -568,7 +593,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<label class="pf-label"><?= tr('bud_category') ?></label> <label class="pf-label"><?= tr('bud_category') ?></label>
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)"> <select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
<?php foreach($categoriesConfig as $key => $conf): ?> <?php foreach($categoriesConfig as $key => $conf): ?>
<option value="<?= $key ?>"><?= $conf['label'] ?></option> <option value="<?= $key ?>"><?= strip_tags($conf['label']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
@@ -587,23 +612,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<datalist id="modalSuggestions"></datalist> <datalist id="modalSuggestions"></datalist>
</div> </div>
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
<select name="label_select" id="schoolSelect" class="pf-input">
<?php
$stmtSchoolLabel = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'nounou') OR role IS NULL ORDER BY id ASC");
while($k = $stmtSchoolLabel->fetch(PDO::FETCH_ASSOC)) {
$pName = htmlspecialchars($k['name']);
echo "<option value='{$pName}'>{$pName}</option>";
}
?>
</select>
</div>
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;"> <div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label> <label class="pf-label">Lier à une Charge Fixe (Optionnel)</label>
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled> <select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
<option value=""><?= tr('bud_select_beneficiary') ?></option> <option value="">-- Aucune --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?> <?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
</select> </select>
</div> </div>
@@ -720,11 +732,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;"> <select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option> <option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
<?php foreach ($categoriesConfig as $k => $c): ?> <?php foreach ($categoriesConfig as $k => $c): ?>
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option> <option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= strip_tags($c['label']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled> <select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
<option value="">-- <?= tr('bud_is_charge') ?> --</option> <option value="">-- Lier à une Charge Fixe --</option>
<?php foreach ($fixedChargesList as $fc): ?> <?php foreach ($fixedChargesList as $fc): ?>
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option> <option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
@@ -757,7 +769,6 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
<?php endif; ?> <?php endif; ?>
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
window.I18N = { window.I18N = {
...(window.I18N || {}), ...(window.I18N || {}),
@@ -769,34 +780,42 @@ window.I18N = {
'btn_delete': <?= json_encode(tr('btn_delete')) ?>, 'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
}; };
// --- DICTIONNAIRE JS DYNAMIQUE ---
const catConfigs = <?= json_encode($categoriesConfig) ?>;
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>'; const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; } function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); } function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');} function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>; // Gestion dynamique du formulaire modal
function handleModalCatChange(select) { function handleModalCatChange(select) {
const catKey = select.value; const catKey = select.value;
const conf = catConfigs[catKey] || {db_type: 'Expense', suggestions: []};
document.getElementById('blockInputText').style.display = 'none'; document.getElementById('blockInputText').style.display = 'block';
document.getElementById('blockInputSelect').style.display = 'none';
document.getElementById('blockInputFrais').style.display = 'none'; document.getElementById('blockInputFrais').style.display = 'none';
document.getElementById('blockInputIncome').style.display = 'none'; document.getElementById('blockInputIncome').style.display = 'none';
document.getElementById('modalLabelInput').required = false; document.getElementById('modalLabelInput').required = true;
document.getElementById('fraisSelect').required = false; document.getElementById('fraisSelect').required = false;
document.getElementById('incomeSelect').required = false; document.getElementById('incomeSelect').required = false;
document.getElementById('fraisSelect').disabled = true; document.getElementById('fraisSelect').disabled = true;
document.getElementById('incomeSelect').disabled = true; document.getElementById('incomeSelect').disabled = true;
if (catKey === 'School') { document.getElementById('blockInputSelect').style.display = 'block'; } if (conf.db_type === 'Income') {
else if (catKey === 'Frais') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('fraisSelect').required = true; document.getElementById('fraisSelect').disabled = false; } document.getElementById('blockInputIncome').style.display = 'block';
else if (catKey === 'Income') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputIncome').style.display = 'block'; document.getElementById('incomeSelect').required = true; document.getElementById('incomeSelect').disabled = false; } document.getElementById('incomeSelect').disabled = false;
else { document.getElementById('incomeSelect').required = true;
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('modalLabelInput').required = true; } else {
const list = document.getElementById('modalSuggestions'); list.innerHTML = ''; // Optionnel : lier l'Expense à une charge fixe
if (suggestions[catKey]) { suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); } document.getElementById('blockInputFrais').style.display = 'block';
document.getElementById('fraisSelect').disabled = false;
}
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
if (conf.suggestions) {
conf.suggestions.forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
} }
} }
@@ -808,8 +827,10 @@ function openAddModal(catKey, catLabel) {
document.getElementById('modalGestionMonth').value = activeViewMonth; document.getElementById('modalGestionMonth').value = activeViewMonth;
document.getElementById('modalLabelInput').value = ""; document.getElementById('modalLabelInput').value = "";
document.getElementById('modalAmount').value = ""; document.getElementById('modalAmount').value = "";
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
document.getElementById('modalIsCredit').value = (catKey === 'Income') ? "1" : "0"; const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey;
handleModalCatChange(catSelect);
document.getElementById('modalIsCredit').value = (catConfigs[catKey]?.db_type === 'Income') ? "1" : "0";
} }
function openEditModal(e) { function openEditModal(e) {
@@ -821,21 +842,30 @@ function openEditModal(e) {
document.getElementById('modalLabelInput').value = e.label; document.getElementById('modalLabelInput').value = e.label;
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0"; document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount)); document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category; handleModalCatChange(catSelect);
if (e.category === 'Frais') document.getElementById('fraisSelect').value = e.budget_item_id; const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category;
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id; handleModalCatChange(catSelect);
if (catConfigs[e.category]?.db_type === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
else document.getElementById('fraisSelect').value = e.budget_item_id;
} }
// Gestion dynamique du CSV Import
function handleLineCatChange(select, isInit = false) { function handleLineCatChange(select, isInit = false) {
const row = select.closest('tr'); const row = select.closest('tr');
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income'); const fSel = row.querySelector('.select-frais');
const iSel = row.querySelector('.select-income');
const conf = catConfigs[select.value] || null;
fSel.style.display = 'none'; iSel.style.display = 'none'; fSel.style.display = 'none'; iSel.style.display = 'none';
if (!isInit) { fSel.value = ''; iSel.value = ''; } if (!isInit) { fSel.value = ''; iSel.value = ''; }
fSel.disabled = true; iSel.disabled = true; fSel.disabled = true; iSel.disabled = true;
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; } if (conf && conf.db_type === 'Income') {
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; } iSel.style.display = 'block'; iSel.disabled = false;
} else if (conf) {
fSel.style.display = 'block'; fSel.disabled = false;
}
checkValidation(); checkValidation();
} }
@@ -844,16 +874,25 @@ function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disable
function checkValidation() { function checkValidation() {
let miss = 0; let miss = 0;
document.querySelectorAll('.line-checkbox:checked').forEach(cb => { document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
const row = cb.closest('tr'); const isCrd = row.querySelector('.is-credit-flag').value === '1'; const cat = row.querySelector('.line-select').value; const row = cb.closest('tr');
let v = true; if (cat==="") { if(!isCrd) v = false; } else if (cat==='Frais' && row.querySelector('.select-frais').value==="") v=false; else if (cat==='Income' && row.querySelector('.select-income').value==="") v=false; const isCrd = row.querySelector('.is-credit-flag').value === '1';
const cat = row.querySelector('.line-select').value;
const conf = catConfigs[cat] || null;
let v = true;
if (cat === "") {
if (!isCrd) v = false;
} else if (conf && conf.db_type === 'Income' && row.querySelector('.select-income').value === "") {
v = false;
}
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = ''; if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
}); });
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount'); const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); } if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; } else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; }
} }
if(document.getElementById('formMapping')) { if (document.getElementById('formMapping')) {
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true)); document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
checkValidation(); checkValidation();
} }
@@ -897,7 +936,6 @@ document.addEventListener('DOMContentLoaded', () => {
submitBtn.innerText = '⏳ ...'; submitBtn.innerText = '⏳ ...';
const formData = new FormData(formExpense); const formData = new FormData(formExpense);
// Force l'action ici pour être sûr
formData.set('action', 'save_expense_manual'); formData.set('action', 'save_expense_manual');
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php'; const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
+178 -242
View File
@@ -32,7 +32,9 @@
padding-bottom: 40px; padding-bottom: 40px;
} }
/* En-tête & Boutons */ /* =========================================================
EN-TÊTE & BOUTONS
========================================================= */
.fc-header-row { .fc-header-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -62,7 +64,9 @@
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
} }
/* Calendrier Mensuel */ /* =========================================================
CALENDRIER MENSUEL (Vue Grille Mois)
========================================================= */
.fc-month-calendar-wrapper { .fc-month-calendar-wrapper {
background: white; background: white;
border: 1px solid var(--pf-border); border: 1px solid var(--pf-border);
@@ -114,6 +118,7 @@
.fc-view-controls { .fc-view-controls {
display: flex !important; display: flex !important;
align-items: center;
width: 100% !important; width: 100% !important;
background: #f1f5f9 !important; background: #f1f5f9 !important;
padding: 4px !important; padding: 4px !important;
@@ -128,7 +133,7 @@
padding: 6px 16px; padding: 6px 16px;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
color: var(--pf-text-muted); color: #64748b;
cursor: pointer; cursor: pointer;
transition: 0.2s; transition: 0.2s;
} }
@@ -206,7 +211,9 @@
text-transform: capitalize; text-transform: capitalize;
} }
/* Décompte Congés (Pills) */ /* =========================================================
DÉCOMPTE CONGÉS MENSUEL (PILLS)
========================================================= */
.fc-month-balances { .fc-month-balances {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -226,12 +233,6 @@
border: 1px solid var(--pf-border); border: 1px solid var(--pf-border);
box-shadow: var(--pf-shadow-sm); box-shadow: var(--pf-shadow-sm);
} }
.fc-minimal-balance-card strong.alex {
color: var(--text-alex);
}
.fc-minimal-balance-card strong.laia {
color: var(--text-laia);
}
.fc-minimal-chips { .fc-minimal-chips {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -249,17 +250,17 @@
} }
.fc-min-chip .type { .fc-min-chip .type {
font-weight: 700; font-weight: 700;
color: var(--pf-text-muted); color: #64748b;
margin-right: 6px; margin-right: 6px;
} }
.fc-min-chip .val { .fc-min-chip .val {
font-weight: 800; font-weight: 800;
color: var(--pf-text-main); color: #0f172a;
} }
.fc-used-badge { .fc-used-badge {
margin-left: 6px; margin-left: 6px;
background: #fee2e2; background: #fee2e2;
color: var(--pf-danger); color: #ef4444;
padding: 1px 5px; padding: 1px 5px;
border-radius: 10px; border-radius: 10px;
font-size: 0.65rem; font-size: 0.65rem;
@@ -272,7 +273,7 @@
font-size: 0.9rem; font-size: 0.9rem;
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--pf-danger); color: #ef4444;
font-weight: bold; font-weight: bold;
} }
@keyframes pulseBurn { @keyframes pulseBurn {
@@ -290,7 +291,9 @@
} }
} }
/* Couleurs des jours */ /* =========================================================
COULEURS DES JOURS (GRILLES)
========================================================= */
.fc-day--school-holiday { .fc-day--school-holiday {
background: var(--c-school-holiday) !important; background: var(--c-school-holiday) !important;
} }
@@ -304,35 +307,13 @@
.fc-day--extra-off-carole { .fc-day--extra-off-carole {
background: var(--c-extra-off) !important; background: var(--c-extra-off) !important;
} }
.fc-day--selected { .fc-day--selected {
background: var(--c-selected) !important; background: var(--c-selected) !important;
outline: 2px solid var(--pf-primary); outline: 2px solid var(--pf-primary);
z-index: 5; z-index: 5;
} }
.fc-day--centre::after {
content: "🏫";
position: absolute;
top: 2px;
right: 2px;
font-size: 12px;
}
.fc-day--avis::after {
content: "";
position: absolute;
top: 2px;
right: 2px;
width: 14px;
height: 14px;
background: url("/modules/family-calendar/assets/img/avis.svg") no-repeat
center;
background-size: contain;
}
.fc-pep-sick-emoji {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 12px;
}
.fc-day--off-carole.fc-day--school-holiday, .fc-day--off-carole.fc-day--school-holiday,
.fc-day--extra-off-carole.fc-day--school-holiday, .fc-day--extra-off-carole.fc-day--school-holiday,
.fc-day--centre.fc-day--school-holiday, .fc-day--centre.fc-day--school-holiday,
@@ -340,7 +321,26 @@
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important; box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
} }
/* Planning Hebo (Desktop) */ .fc-day--today {
background: #eff6ff !important;
box-shadow: inset 0 0 0 1px #bfdbfe !important;
}
.fc-day--today .fc-day-number {
display: inline;
background: none;
width: auto;
height: auto;
box-shadow: none;
border-radius: 0;
margin: 0;
color: var(--pf-primary);
font-weight: 900;
font-size: 1.1em;
}
/* =========================================================
PLANNING GLOBAL (Matrice Annuelle)
========================================================= */
.fc-week-header { .fc-week-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -379,22 +379,17 @@
border-spacing: 0; border-spacing: 0;
} }
/* ========================================================= /* HARMONISATION DES BORDURES ET HEADERS STICKY */
HARMONISATION DES BORDURES ET HEADERS STICKY (1px strict)
========================================================= */
#planningTable thead tr { #planningTable thead tr {
height: 30px; /* On fixe la hauteur pour un calcul parfait */ height: 30px;
} }
#planningTable thead th { #planningTable thead th {
position: sticky; position: sticky;
background: #f8fafc !important; background: #f8fafc !important;
border: none !important; /* On supprime les bordures natives */ border: none !important;
/* On utilise UNIQUEMENT des ombres internes pour simuler 1px de bordure sans doublement */
box-shadow: box-shadow:
inset 0 -1px 0 var(--pf-border), inset 0 -1px 0 var(--pf-border),
inset -1px 0 0 var(--pf-border) !important; inset -1px 0 0 var(--pf-border) !important;
padding: 6px; padding: 6px;
font-size: 0.75rem; font-size: 0.75rem;
z-index: 20; z-index: 20;
@@ -403,14 +398,14 @@
} }
#planningTable thead tr:nth-child(1) th { #planningTable thead tr:nth-child(1) th {
top: 0; top: 0;
z-index: 22; /* Au premier plan */ z-index: 22;
} }
#planningTable thead tr:nth-child(2) th { #planningTable thead tr:nth-child(2) th {
top: 30px; /* Exactement la hauteur de la ligne 1 */ top: 30px;
z-index: 21; z-index: 21;
} }
#planningTable thead tr:nth-child(3) th { #planningTable thead tr:nth-child(3) th {
top: 60px; /* Ligne 1 + Ligne 2 */ top: 60px;
z-index: 20; z-index: 20;
} }
@@ -425,10 +420,9 @@
color: var(--text-laia) !important; color: var(--text-laia) !important;
} }
/* Cellules standards du corps */
#planningTable tbody td { #planningTable tbody td {
position: relative; position: relative;
border: none !important; /* Reset de sécurité */ border: none !important;
border-right: 1px solid var(--pf-border) !important; border-right: 1px solid var(--pf-border) !important;
border-bottom: 1px solid var(--pf-border) !important; border-bottom: 1px solid var(--pf-border) !important;
height: 36px; height: 36px;
@@ -467,13 +461,10 @@ td.col-laia-sub {
writing-mode: vertical-rl; writing-mode: vertical-rl;
transform: rotate(180deg); transform: rotate(180deg);
font-size: 0.7rem; font-size: 0.7rem;
color: var(--pf-text-muted); color: #64748b;
} }
/* ========================================================= /* GESTION DES COLONNES STICKY (Mois & Semaine) */
GESTION DES COLONNES STICKY (Mois & Semaine)
========================================================= */
#planningTable tbody td.col-sticky-mois { #planningTable tbody td.col-sticky-mois {
position: sticky !important; position: sticky !important;
left: 0 !important; left: 0 !important;
@@ -481,7 +472,6 @@ td.col-laia-sub {
background: white !important; background: white !important;
padding: 0 !important; padding: 0 !important;
} }
#planningTable tbody td.col-sticky-mois .fc-sticky-mois-label { #planningTable tbody td.col-sticky-mois .fc-sticky-mois-label {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -491,42 +481,31 @@ td.col-laia-sub {
letter-spacing: 1px; letter-spacing: 1px;
text-align: center; text-align: center;
} }
/* 2. Sticky Semaine (Body) */
#planningTable tbody td.col-sticky-sem { #planningTable tbody td.col-sticky-sem {
position: sticky !important; position: sticky !important;
left: 54px !important; left: 54px !important;
z-index: 14 !important; z-index: 14 !important;
background: white !important; background: white !important;
/* Ombre portée pour délimiter la zone sticky du reste du tableau */
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1); box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1);
} }
/* 3. Sticky Mois (Header) */
#planningTable thead tr th.col-sticky-mois { #planningTable thead tr th.col-sticky-mois {
position: sticky !important; position: sticky !important;
top: 0 !important; top: 0 !important;
left: 0 !important; left: 0 !important;
z-index: 30 !important; z-index: 30 !important;
background: var(--pf-bg-lighter) !important; background: var(--pf-bg-lighter) !important;
/* Les bordures sont gérées par le box-shadow général du thead th */
} }
/* 4. Sticky Semaine (Header) */
#planningTable thead tr th.col-sticky-sem { #planningTable thead tr th.col-sticky-sem {
position: sticky !important; position: sticky !important;
top: 0 !important; top: 0 !important;
left: 54px !important; left: 54px !important;
z-index: 29 !important; z-index: 29 !important;
background: var(--pf-bg-lighter) !important; background: var(--pf-bg-lighter) !important;
/* Ombre interne (bordures) + Ombre portée (effet sticky) */
box-shadow: box-shadow:
inset 0 -1px 0 var(--pf-border), inset 0 -1px 0 var(--pf-border),
inset -1px 0 0 var(--pf-border), inset -1px 0 0 var(--pf-border),
4px 0 6px -2px rgba(0, 0, 0, 0.1) !important; 4px 0 6px -2px rgba(0, 0, 0, 0.1) !important;
} }
/* Rétablissement horizontal pour les headers mois et semaine */
#planningTable thead th.col-month, #planningTable thead th.col-month,
#planningTable tbody td.col-sticky-sem { #planningTable tbody td.col-sticky-sem {
writing-mode: horizontal-tb; writing-mode: horizontal-tb;
@@ -535,15 +514,15 @@ td.col-laia-sub {
text-align: center; text-align: center;
} }
/* Panneaux bas & Légendes */ /* =========================================================
PANNEAUX BAS, LÉGENDES & RÉSUMÉS
========================================================= */
.fc-bottom-grid { .fc-bottom-grid {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
gap: 24px; gap: 24px;
align-items: start; align-items: start;
} }
/* Nouvelles bordures style "Budget" pour les conteneurs */
.pf-card { .pf-card {
background: white; background: white;
border: 1px solid var(--pf-border); border: 1px solid var(--pf-border);
@@ -560,6 +539,7 @@ td.col-laia-sub {
padding-bottom: 12px; padding-bottom: 12px;
} }
/* Légendes statiques */
.pf-legend-grid { .pf-legend-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -607,53 +587,62 @@ td.col-laia-sub {
border: none; border: none;
} }
.fc-summary-header { /* Résumé Mensuel Injecté */
.fc-month-container {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center; gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--pf-border);
} }
.fc-summ-select { .fc-month-summary-inline {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 5px;
background: var(--pf-bg-lighter, #f8fafc);
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--pf-border); border: 1px solid var(--pf-border);
border-radius: 6px;
padding: 4px 8px;
font-size: 0.8rem;
height: auto;
background: #f8fafc;
} }
.fc-summary-item { .fc-summ-pill {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
padding: 12px 4px; align-items: center;
border-bottom: 1px solid var(--pf-border); /* Ligne de séparation visible */ font-size: 0.75rem;
}
.fc-summary-item:last-child {
border-bottom: none;
}
.fc-summary-label {
font-size: 0.85rem;
color: #64748b; color: #64748b;
font-weight: 600; flex: 1;
} }
.fc-summary-value { .fc-summ-pill strong {
font-size: 1rem; font-size: 0.95rem;
font-weight: 700;
color: #0f172a; color: #0f172a;
margin-top: 2px;
}
.pf-presence-pill strong {
color: var(--pf-primary);
} }
/* Menu Contextuel Tactile */ [data-theme="dark"] .fc-month-summary-inline {
background: #1c2128;
border-color: #30363d;
}
[data-theme="dark"] .fc-summ-pill strong {
color: #c9d1d9;
}
/* =========================================================
MENU CONTEXTUEL (COMPACT & PROPRE)
========================================================= */
#selectionMenu,
#fc-month-selectionMenu,
.fc-selection-menu { .fc-selection-menu {
position: absolute; position: absolute;
z-index: 9000; z-index: 9000;
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border: 1px solid rgba(226, 232, 240, 0.8); border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow: var(--pf-shadow-lg); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
border-radius: 16px; border-radius: 12px;
padding: 10px 12px; padding: 8px 12px;
min-width: 220px; min-width: 210px;
display: none; display: none;
animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
transform-origin: top center; transform-origin: top center;
@@ -668,88 +657,70 @@ td.col-laia-sub {
transform: scale(1) translateY(0); transform: scale(1) translateY(0);
} }
} }
.fc-menu-section { .fc-menu-section {
padding: 6px 0; padding: 6px 0;
border-bottom: 1px dashed #cbd5e1; border-bottom: 1px dashed #cbd5e1;
} }
.fc-menu-section:last-child { .fc-menu-section:last-child {
border: none; border-bottom: none;
padding-bottom: 0; padding-bottom: 0;
} }
.fc-menu-header {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.fc-menu-section strong {
font-size: 0.7rem;
text-transform: uppercase;
color: var(--pf-text-muted);
}
.fc-menu-grid { .fc-menu-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 4px; gap: 6px;
} }
.fc-menu-btn { .fc-menu-btn {
background: white; background: transparent;
border: 1px solid var(--pf-border); border: 1px solid transparent;
padding: 6px 8px; padding: 4px 8px;
border-radius: 8px; border-radius: 6px;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
transition: 0.2s; text-align: left;
box-shadow: var(--pf-shadow-sm); transition: all 0.2s ease;
color: #0f172a;
display: flex;
align-items: center;
} }
.fc-menu-btn:hover { .fc-menu-btn:hover {
background: var(--pf-bg-page); background: #f1f5f9;
border-color: var(--pf-primary);
color: var(--pf-primary);
} }
.fc-menu-clear-icon {
background: transparent; /* --- LE BOUTON ACTIF (SANS COCHE !) --- */
border: none; .fc-menu-btn--active {
color: #94a3b8; border: 1px solid var(--pf-primary) !important;
cursor: pointer; background: #eff6ff !important;
padding: 4px; color: var(--pf-primary) !important;
display: flex; font-weight: 700 !important;
border-radius: 6px;
} }
.fc-menu-clear-icon svg { /* Sécurité absolue pour tuer les coches fantômes */
.fc-menu-btn--active::before,
.fc-menu-btn--active::after {
display: none !important;
content: none !important;
}
/* Icônes intégrées dans le JS */
.fc-icon-centre {
font-size: 1.1rem;
line-height: 1;
margin-right: 4px;
}
.fc-icon-avis {
width: 14px; width: 14px;
height: 14px; height: 14px;
} object-fit: contain;
.fc-menu-clear-icon:hover { margin-right: 4px;
background: #fef2f2; vertical-align: middle;
color: var(--pf-danger);
}
.fc-menu-leaves-table table {
width: 100%;
border-spacing: 2px;
}
.fc-menu-leaves-table th {
font-size: 0.7rem;
color: var(--pf-text-muted);
text-align: center;
}
.fc-th-inline {
display: flex;
justify-content: center;
gap: 4px;
} }
/* --- BOUTON DE MENU ACTIF (Élément déjà enregistré) --- */ /* =========================================================
.fc-menu-btn--active { MODALES (Paramètres & Vacances)
background: #eff6ff; ========================================================= */
border-color: var(--pf-primary);
color: var(--pf-primary);
/* Un léger inset shadow pour rendre la bordure un peu plus épaisse sans casser la taille */
box-shadow: inset 0 0 0 1px var(--pf-primary);
}
/* Table specifique modale vacances */
.fc-holidays-table { .fc-holidays-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -772,7 +743,6 @@ td.col-laia-sub {
background: #fafafa; background: #fafafa;
} }
/* === MODALES : Alignement du Header et de la Croix === */
.pf-modal-header { .pf-modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -781,14 +751,12 @@ td.col-laia-sub {
border-bottom: 1px solid var(--pf-border); border-bottom: 1px solid var(--pf-border);
padding-bottom: 15px; padding-bottom: 15px;
} }
.pf-modal-header .pf-modal-title { .pf-modal-header .pf-modal-title {
margin: 0; margin: 0;
padding: 0; padding: 0;
border-bottom: none; /* On annule la bordure par défaut du global.css pour la déléguer au header */ border-bottom: none;
flex-grow: 1; flex-grow: 1;
} }
.pf-modal-close { .pf-modal-close {
background: none; background: none;
border: none; border: none;
@@ -799,9 +767,37 @@ td.col-laia-sub {
padding: 0 0 0 15px; padding: 0 0 0 15px;
transition: color 0.2s; transition: color 0.2s;
} }
.pf-modal-close:hover { .pf-modal-close:hover {
color: #ef4444; /* Devient rouge au survol */ color: #ef4444;
}
.fc-sidebar-tab {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 1.2rem;
background: transparent;
border: none;
text-align: left;
font-weight: 600;
cursor: pointer;
color: #0f172a;
transition: all 0.2s;
width: calc(100% - 16px);
margin: 2px 8px;
border-radius: 8px;
font-size: 0.95rem;
}
.fc-sidebar-tab.active {
background: #111827;
color: #ffffff;
}
.fc-sidebar-tab:hover:not(.active) {
background: #f1f5f9;
}
[data-theme="dark"] .fc-sidebar-tab.active {
background: var(--pf-primary);
color: #ffffff;
} }
/* ========================================================= /* =========================================================
@@ -811,36 +807,25 @@ td.col-laia-sub {
background: #ffffff; background: #ffffff;
border: 1px solid #cbd5e1; border: 1px solid #cbd5e1;
border-radius: 8px; border-radius: 8px;
font-family: inherit; font-family: inherit;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
color: var(--text-main); color: #0f172a;
/* Padding standard */
padding: 8px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
text-transform: capitalize; text-transform: capitalize;
box-shadow: var(--pf-shadow-sm); box-shadow: var(--pf-shadow-sm);
} }
.fc-smart-select:hover { .fc-smart-select:hover {
border-color: #94a3b8; border-color: #94a3b8;
} }
.fc-smart-select:focus { .fc-smart-select:focus {
background: #ffffff; border-color: var(--pf-primary);
border-color: var(--primary);
outline: none; outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
} }
/* --- BOUTON AUJOURD'HUI & CONTROLES --- */
.fc-view-controls {
align-items: center; /* Pour bien centrer le nouveau bouton */
}
.fc-today-button { .fc-today-button {
background: white; background: white;
border: 1px solid var(--pf-border); border: 1px solid var(--pf-border);
@@ -853,12 +838,10 @@ td.col-laia-sub {
transition: all 0.2s; transition: all 0.2s;
box-shadow: var(--pf-shadow-sm); box-shadow: var(--pf-shadow-sm);
} }
.fc-today-button:hover { .fc-today-button:hover {
background: #eff6ff; background: #eff6ff;
border-color: var(--pf-primary); border-color: var(--pf-primary);
} }
.fc-view-divider { .fc-view-divider {
width: 1px; width: 1px;
height: 20px; height: 20px;
@@ -866,34 +849,14 @@ td.col-laia-sub {
margin: 0 8px; margin: 0 8px;
} }
/* --- MISE EN ÉVIDENCE DU JOUR EN COURS --- */ /* =========================================================
.fc-day--today { MOBILE RESPONSIVE
background: #eff6ff !important; ========================================================= */
box-shadow: inset 0 0 0 1px #bfdbfe !important;
}
.fc-day--today .fc-day-number {
display: inline;
background: none;
width: auto;
height: auto;
box-shadow: none;
border-radius: 0;
margin: 0;
color: var(--pf-primary);
font-weight: 900;
font-size: 1.1em;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.fc-smart-select { .fc-smart-select {
font-size: 1rem; font-size: 1rem;
padding: 6px 8px; padding: 6px 8px;
} }
}
/* Mobile */
@media (max-width: 768px) {
.pf-family-calendar .pf-container { .pf-family-calendar .pf-container {
padding: 0 !important; padding: 0 !important;
} }
@@ -927,6 +890,9 @@ td.col-laia-sub {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: 10px; padding: 10px;
} }
#selectionMenu,
#fc-month-selectionMenu,
.fc-selection-menu { .fc-selection-menu {
position: fixed !important; position: fixed !important;
top: auto !important; top: auto !important;
@@ -939,7 +905,8 @@ td.col-laia-sub {
animation: slideUpSheet 0.3s forwards; animation: slideUpSheet 0.3s forwards;
z-index: 10000 !important; z-index: 10000 !important;
} }
.fc-selection-menu::before { #selectionMenu::before,
#fc-month-selectionMenu::before {
content: ""; content: "";
display: block; display: block;
width: 40px; width: 40px;
@@ -955,39 +922,8 @@ td.col-laia-sub {
gap: 20px !important; gap: 20px !important;
} }
} }
@keyframes slideUpSheet {
/* ========================================================================== to {
MODALE PARAMÈTRES (SIDEBAR STYLE) transform: translateY(0);
========================================================================== */ }
.fc-sidebar-tab {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 1.2rem;
background: transparent;
border: none;
text-align: left;
font-weight: 600;
cursor: pointer;
color: var(--text-main);
transition: all 0.2s;
width: calc(100% - 16px);
margin: 2px 8px;
border-radius: 8px;
font-size: 0.95rem;
}
.fc-sidebar-tab.active {
background: #111827; /* Gris très foncé typique des sidebars */
color: #ffffff;
}
.fc-sidebar-tab:hover:not(.active) {
background: var(--bg-soft, #f1f5f9);
}
/* Variante Dark Mode native pour l'onglet actif */
[data-theme="dark"] .fc-sidebar-tab.active {
background: var(--primary);
color: #ffffff;
} }
File diff suppressed because it is too large Load Diff
@@ -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: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-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-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"}]
@@ -8,77 +8,98 @@ $action = $input['action'] ?? '';
if (!$action) { if (!$action) {
http_response_code(400); http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']); echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action manquante.']);
exit; exit;
} }
try { try {
// --- SUPPRESSION UNITAIRE ---
if ($action === 'delete') { if ($action === 'delete') {
$eventId = (int)($input['event_id'] ?? 0); $eventId = (int)($input['event_id'] ?? 0);
if ($eventId <= 0) { if ($eventId <= 0) {
http_response_code(400); http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']); echo json_encode(['success' => false, 'status' => 'error', 'message' => 'ID manquant.']);
exit; exit;
} }
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
$stmt->execute([$eventId]); $stmt->execute([$eventId]);
echo json_encode(['status' => 'success']); echo json_encode(['success' => true, 'status' => 'success']);
exit; exit;
} }
// --- MISE À JOUR UNITAIRE ---
if ($action === 'update') { if ($action === 'update') {
$eventId = (int)($input['event_id'] ?? 0); $eventId = (int)($input['event_id'] ?? 0);
$newType = $input['new_type'] ?? ''; $newType = $input['new_type'] ?? '';
if ($eventId <= 0 || !$newType) { if ($eventId <= 0 || !$newType) {
http_response_code(400); 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; exit;
} }
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?"); $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
$stmt->execute([$newType, $eventId]); $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; 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') { if ($action === 'bulk_delete_day_types') {
$dates = $input['dates'] ?? []; $dates = $input['dates'] ?? [];
$types = $input['types'] ?? []; $types = $input['types'] ?? [];
$dates = array_filter($dates, function($d) { $dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
});
if (empty($dates) || empty($types)) { if (empty($dates) || empty($types)) {
echo json_encode(['status' => 'success']); echo json_encode(['success' => true, 'status' => 'success']);
exit; exit;
} }
// Création des placeholders IN (?,?,?)
$datePlaceholders = implode(',', array_fill(0, count($dates), '?')); $datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$typePlaceholders = implode(',', array_fill(0, count($types), '?')); $typePlaceholders = implode(',', array_fill(0, count($types), '?'));
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)"; $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
// Fusion des tableaux pour l'exécution
$stmt->execute(array_merge($dates, $types)); $stmt->execute(array_merge($dates, $types));
echo json_encode(['status' => 'success']); echo json_encode(['success' => true, 'status' => 'success']);
exit; exit;
} }
// --- SUPPRESSION TOTALE SUR DES DATES ---
if ($action === 'bulk_delete_all') { if ($action === 'bulk_delete_all') {
$dates = $input['dates'] ?? []; $dates = $input['dates'] ?? [];
$dates = array_filter($dates, function($d) { $dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
});
if (empty($dates) || empty($types)) { if (empty($dates)) {
echo json_encode(['status' => 'success']); echo json_encode(['success' => true, 'status' => 'success']);
exit; exit;
} }
@@ -87,15 +108,15 @@ try {
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute($dates); $stmt->execute($dates);
echo json_encode(['status' => 'success']); echo json_encode(['success' => true, 'status' => 'success']);
exit; exit;
} }
// Action inconnue
http_response_code(400); 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) { } catch (PDOException $e) {
http_response_code(500); http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); echo json_encode(['success' => false, 'status' => 'error', 'message' => $e->getMessage()]);
} }
?>
@@ -1,33 +1,14 @@
<?php <?php
// includes/api/save-events.php // modules/family-calendar/includes/api/save-events.php
header('Content-Type: application/json'); header('Content-Type: application/json');
require __DIR__ . '/../../../../includes/db.php'; require_once __DIR__ . '/../../../../includes/db.php';
$rawInput = file_get_contents('php://input');
$eventsToSave = json_decode($rawInput, true);
// Optionnel : tu peux commenter/supprimer ces logs en production pour économiser du disque
file_put_contents(
__DIR__ . '/events-debug.log',
"[" . date('c') . "] RAW INPUT: " . $rawInput . PHP_EOL,
FILE_APPEND
);
if (empty($eventsToSave) || !is_array($eventsToSave)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucune donnée d\'événement reçue.']);
exit;
}
$inserted = [];
try { try {
// 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping) $rawInput = file_get_contents('php://input');
$stmtPeople = $pdo->query("SELECT id, name FROM pf_people"); $eventsToSave = json_decode($rawInput, true);
$peopleMap = [];
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) { if (empty($eventsToSave) || !is_array($eventsToSave)) {
// On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.] throw new Exception("Aucune donnée d'événement reçue.");
$peopleMap[$row['name']] = $row['id'];
} }
$pdo->beginTransaction(); $pdo->beginTransaction();
@@ -36,26 +17,32 @@ try {
VALUES (:event_date, :event_type, :person_id, :duration)"; VALUES (:event_date, :event_type, :person_id, :duration)";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
foreach ($eventsToSave as $event) { $inserted = [];
$person_id = null;
// 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !) foreach ($eventsToSave as $event) {
if (!empty($event['person']) && isset($peopleMap[$event['person']])) { // 🔥 LE CORRECTIF : On initialise à 0 (car NOT NULL en base)
$person_id = $peopleMap[$event['person']]; $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([ $stmt->execute([
':event_date' => $event['date'], ':event_date' => $event['date'],
':event_type' => $event['type'], ':event_type' => $event['type'],
':person_id' => $person_id, ':person_id' => $person_id,
':duration' => $event['duration'] ?? 1.0, ':duration' => $duration,
]); ]);
$inserted[] = [ $inserted[] = [
'id' => $pdo->lastInsertId(), 'id' => $pdo->lastInsertId(),
'date' => $event['date'], 'date' => $event['date'],
'type' => $event['type'], 'type' => $event['type'],
'duration' => $event['duration'] ?? 1.0, 'duration' => $duration,
'person_id' => $person_id, 'person_id' => $person_id,
]; ];
} }
@@ -63,12 +50,20 @@ try {
$pdo->commit(); $pdo->commit();
echo json_encode([ echo json_encode([
'success' => true,
'status' => 'success', 'status' => 'success',
'inserted' => $inserted, 'inserted' => $inserted,
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
$pdo->rollBack(); if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
http_response_code(500); http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]); echo json_encode([
} 'success' => false,
'status' => 'error',
'message' => 'Erreur SQL : ' . $e->getMessage()
]);
}
?>
@@ -11,17 +11,18 @@ try {
if ($action === 'get_all') { if ($action === 'get_all') {
// A. Options globales du foyer // A. Options globales du foyer
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1"); $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 // B. Liste des membres de la famille (Triés par rôle pour grouper Parents, Enfants, Helpers)
$stmtPeople = $pdo->query("SELECT id, name, role FROM pf_people WHERE is_active = 1 ORDER BY role DESC, name ASC"); // 🟢 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); $people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
// C. Matrice des congés par personne // 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); $leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
// On organise les congés par ID de personne pour faciliter le traitement côté JS
$leavesMap = []; $leavesMap = [];
foreach ($leavesRaw as $leave) { foreach ($leavesRaw as $leave) {
$leavesMap[$leave['person_id']][] = [ $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([ echo json_encode([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -40,7 +56,9 @@ try {
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true) 'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
], ],
'people' => $people, 'people' => $people,
'leaves' => $leavesMap 'leaves' => $leavesMap,
'calendar_settings' => $calendarSettings,
'leave_types' => $leaveTypes // On envoie le catalogue au JS !
] ]
]); ]);
exit; exit;
@@ -48,7 +66,7 @@ try {
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ─── // ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
if ($action === 'save_foyer') { 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'); $zone = trim($_POST['zone_scolaire'] ?? 'C');
$modesRaw = $_POST['care_modes'] ?? '[]'; $modesRaw = $_POST['care_modes'] ?? '[]';
@@ -64,7 +82,7 @@ try {
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ─── // ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
if ($action === 'save_member_leaves') { 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); $personId = (int)($_POST['person_id'] ?? 0);
$leavesData = json_decode($_POST['leaves'] ?? '[]', true); $leavesData = json_decode($_POST['leaves'] ?? '[]', true);
@@ -73,19 +91,22 @@ try {
$pdo->beginTransaction(); $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 = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
$stmtDelete->execute([$personId]); $stmtDelete->execute([$personId]);
// On réinsère la nouvelle matrice propre
if (!empty($leavesData)) { if (!empty($leavesData)) {
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)"); $stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
foreach ($leavesData as $leave) { foreach ($leavesData as $leave) {
$type = strtoupper(trim($leave['type'])); $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); $allowance = (float)($leave['allowance'] ?? 0);
$date = trim($leave['date']); $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)) { if (!empty($type) && !empty($date)) {
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]); $stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
} }
@@ -97,6 +118,52 @@ try {
exit; 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"); throw new Exception("Action inconnue");
} catch (\Throwable $e) { } catch (\Throwable $e) {
+112 -307
View File
@@ -40,57 +40,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else { } else {
$action = $_POST['action'] ?? ''; $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) { if ($action === 'set_modules' && $family_id) {
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka']; $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]))); $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 = ?") $meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
->execute([json_encode($enabled), $family_id]); ->execute([json_encode($enabled), $family_id]);
$_SESSION['enabled_modules'] = $enabled; $_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; $_SESSION['app_lang'] = $lang;
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?") $meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
->execute([$lang, $user_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; $_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( $pdo->prepare(
"INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)" "INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)"
)->execute([(string) $n]); )->execute([(string) $n]);
$success = tr('groceries_settings_saved'); $success = tr('groceries_settings_saved') ?: 'Paramètres des courses enregistrés.';
} catch (\Throwable $e) { } 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]); $meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
$success = "Connexion iOS supprimée."; $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; $family = null;
$members = []; $members = [];
$attached_profiles = [];
if ($family_id) { if ($family_id) {
$fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?"); $fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?");
$fam->execute([$family_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 = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id");
$mem->execute([$family_id]); $mem->execute([$family_id]);
$members = $mem->fetchAll(); $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'"); $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';
<h1 class="pf-settings-title">⚙️ Paramètres</h1> <h1 class="pf-settings-title">⚙️ Paramètres</h1>
<?php if ($success): ?>
<div class="pf-alert pf-alert--success">✓ <?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">🌐 Langue / Language</h2> <h2 class="pf-card-h2">🌐 Langue / Language</h2>
<form method="post" class="pf-lang-form"> <form method="post" class="pf-lang-form">
@@ -517,96 +476,6 @@ require __DIR__ . '/header.php';
</section> </section>
<?php endif; ?> <?php endif; ?>
<?php
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $_SESSION['enabled_modules'] ?? [])) > 0;
$showZone = in_array('calendar', $_SESSION['enabled_modules'] ?? []);
if ($family_id):
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
?>
<section class="pf-panel-card">
<h2 class="pf-card-h2" style="font-size: 1.25rem;">👪 <?= tr('set_family_config') ?></h2>
<p class="pf-muted-note">Configurez les indicateurs et les membres partagés par votre foyer.</p>
<form id="unifiedFamilyForm" style="margin-top: 20px; display: flex; flex-direction: column;">
<input type="hidden" name="action" value="update_family_info">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="custom_care_modes" id="customCareModes" value="<?= htmlspecialchars(json_encode($familyCareModes)) ?>">
<input type="hidden" name="deleted_kids" id="deletedKids" value="[]">
<!-- ACCORDÉON 1 : PARAMÈTRES GLOBAUX -->
<?php if ($showCurrency || $showZone): ?>
<details class="pf-accordion">
<summary class="pf-accordion-summary">🌍 <?= tr('set_global_params') ?></summary>
<div class="pf-accordion-content">
<div class="form-row" style="margin-bottom: 0;">
<?php if ($showCurrency): ?>
<div class="pf-form-group" style="margin-bottom: 0;">
<label class="pf-label">Devise monétaire</label>
<input type="text" name="currency" class="pf-input" value="<?= htmlspecialchars($currentCurrency) ?>" placeholder="ex: €, $, CHF" required maxlength="10">
</div>
<?php endif; ?>
<?php if ($showZone): ?>
<div class="pf-form-group" style="margin-bottom: 0;">
<label class="pf-label">Zone de vacances scolaires (France)</label>
<select name="zone_scolaire" class="pf-input" style="background:var(--bg-panel);">
<option value="A" <?= $currentZone === 'A' ? 'selected' : '' ?>>Zone A</option>
<option value="B" <?= $currentZone === 'B' ? 'selected' : '' ?>>Zone B</option>
<option value="C" <?= $currentZone === 'C' ? 'selected' : '' ?>>Zone C</option>
<option value="Autre" <?= $currentZone === 'Autre' ? 'selected' : '' ?>>Hors France / Autre</option>
</select>
</div>
<?php endif; ?>
</div>
</div>
</details>
<?php endif; ?>
<!-- ACCORDÉON 2 : MODES DE GARDE -->
<details class="pf-accordion">
<summary class="pf-accordion-summary">🏷️ <?= tr('set_care_modes') ?></summary>
<div class="pf-accordion-content">
<div id="careModesContainer" style="display: flex; gap: 8px; flex-wrap: wrap;"></div>
<!-- Largeur contrainte avec max-content -->
<button type="button" class="pf-btn btn-secondary pf-btn-sm-text" onclick="promptNewCareMode()" style="padding: 4px 10px; font-size: 0.8rem; width: max-content;">+ <?= tr('set_add_care_mode') ?></button>
</div>
</details>
<!-- ACCORDÉON 3 : ENFANTS (Ouvert par défaut) -->
<details class="pf-accordion">
<summary class="pf-accordion-summary">👶 <?= tr('set_kids_title') ?></summary>
<div class="pf-accordion-content" style="gap: 10px;">
<ul id="kidsListContainer" style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px;">
<?php foreach ($kidsList as $kid):
$kidModes = json_decode($kid['care_modes'] ?? '[]', true);
if (!is_array($kidModes)) $kidModes = [];
?>
<!-- Version compactée -->
<li class="kid-row" data-id="<?= $kid['id'] ?>" style="padding: 10px 12px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 8px; display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
<input type="text" name="kids[<?= $kid['id'] ?>][name]" class="pf-input" value="<?= htmlspecialchars($kid['name']) ?>" style="flex: 1; padding: 8px; font-weight: bold;" required>
<button type="button" class="btn-icon-action delete" onclick="removeKidRow(this, <?= $kid['id'] ?>)" title="<?= tr('delete') ?>" style="padding: 4px;">🗑️</button>
</div>
<div class="kid-modes-container" style="font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
<span class="stored-modes" style="display:none;"><?= htmlspecialchars(json_encode($kidModes)) ?></span>
</div>
</li>
<?php endforeach; ?>
</ul>
<!-- Largeur contrainte avec max-content -->
<button type="button" class="pf-btn btn-secondary pf-btn-sm-text" onclick="addKidRow()" style="padding: 4px 10px; font-size: 0.8rem; width: max-content;">+ <?= tr('set_btn_add_kid') ?></button>
</div>
</details>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</form>
</section>
<?php endif; ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">👤 Mon profil</h2> <h2 class="pf-card-h2">👤 Mon profil</h2>
<form method="post"> <form method="post">
@@ -707,6 +576,57 @@ require __DIR__ . '/header.php';
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<h3 class="pf-members-heading" style="margin-top: 2rem;">🧸 Profils rattachés</h3>
<p class="pf-muted-note pf-muted-note--tight" style="margin-bottom: 1rem;">Membres gérés par le foyer (Enfants, Nounous, Proches) n'ayant pas d'accès direct.</p>
<div class="pf-stack-sm" style="margin-bottom: 1.5rem;">
<?php foreach ($attached_profiles as $p):
$roleBadge = 'Inconnu';
if (in_array($p['role'], ['child', 'enfant'])) $roleBadge = '👶 Enfant';
elseif ($p['role'] === 'helper') $roleBadge = '💼 Intervenant';
elseif ($p['role'] === 'relative') $roleBadge = '👵 Proche';
?>
<div class="pf-member-card" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong><?= htmlspecialchars($p['name']) ?></strong>
<span class="pf-muted-inline"><?= $roleBadge ?></span>
</div>
<form method="post" style="margin:0;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="delete_attached_profile">
<input type="hidden" name="profile_id" value="<?= $p['id'] ?>">
<button type="submit" class="btn-icon-action delete" title="Supprimer" style="background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 4px;" onclick="return confirm('Supprimer définitivement ce profil ?')">🗑️</button>
</form>
</div>
<?php endforeach; ?>
<?php if (empty($attached_profiles)): ?>
<div class="pf-muted-note" style="padding: 10px; text-align: center; border: 1px dashed var(--border-light); border-radius: 8px;">Aucun profil rattaché.</div>
<?php endif; ?>
</div>
<form method="post" style="display: flex; gap: 8px; background: var(--bg-soft); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-light); align-items: flex-end;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="add_attached_profile">
<div class="pf-form-group" style="margin-bottom: 0; flex: 2;">
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Prénom</label>
<input type="text" name="profile_name" class="pf-input" placeholder="Ex: Pol, Carole..." required>
</div>
<div class="pf-form-group" style="margin-bottom: 0; flex: 1.5;">
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Rôle</label>
<select name="profile_role" class="pf-input" required>
<option value="child">👶 Enfant</option>
<option value="helper">💼 Intervenant</option>
<option value="relative">👵 Proche</option>
</select>
</div>
<button type="submit" class="pf-btn pf-shrink-0" style="padding: 8px 16px;">Ajouter</button>
</form>
</section> </section>
<?php endif; ?> <?php endif; ?>
@@ -719,6 +639,7 @@ require __DIR__ . '/header.php';
</div> </div>
<script> <script>
// --- INJECTION CSRF GLOBALE ---
document.querySelectorAll('form[method="post"]').forEach((form) => { document.querySelectorAll('form[method="post"]').forEach((form) => {
if (!form.querySelector('input[name="csrf_token"]')) { if (!form.querySelector('input[name="csrf_token"]')) {
const input = document.createElement('input'); const input = document.createElement('input');
@@ -729,6 +650,7 @@ document.querySelectorAll('form[method="post"]').forEach((form) => {
} }
}); });
// --- COPIE DU CODE D'INVITATION ---
function copyCode() { function copyCode() {
const code = document.getElementById('invite-code').textContent.trim(); const code = document.getElementById('invite-code').textContent.trim();
navigator.clipboard.writeText(code).then(() => { navigator.clipboard.writeText(code).then(() => {
@@ -738,140 +660,23 @@ function copyCode() {
}); });
} }
// 🟢 LOGIQUE FRONT-END UNIFIÉE (Vanilla JS) : Configuration Foyer // --- GESTION DES NOTIFICATIONS TOAST (Remplaçant l'ancien bandeau vert) ---
let globalCareModes = <?= json_encode($familyCareModes) ?>;
let deletedKidsIds = [];
let newKidCounter = 0;
function renderCareModes() {
const container = document.getElementById('careModesContainer');
if (!container) return;
document.getElementById('customCareModes').value = JSON.stringify(globalCareModes);
container.innerHTML = '';
globalCareModes.forEach((mode, index) => {
const badge = document.createElement('div');
badge.style.cssText = "background: var(--bg-soft); color: var(--primary); padding: 4px 10px; border-radius: 12px; font-size: 0.85rem; font-weight: 600; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(59, 130, 246, 0.2);";
badge.innerHTML = `
${mode}
<span style="cursor: pointer; opacity: 0.6; padding: 2px;" onclick="removeCareMode(${index})">&times;</span>
`;
container.appendChild(badge);
});
document.querySelectorAll('.kid-row').forEach(row => renderKidCheckboxes(row));
}
function promptNewCareMode() {
const newMode = prompt("Entrez le nom du nouveau mode de garde (ex: Papi/Mamie, Centre aéré...)");
if (newMode && newMode.trim() !== '') {
if (!globalCareModes.includes(newMode.trim())) {
globalCareModes.push(newMode.trim());
renderCareModes();
}
}
}
function removeCareMode(index) {
if (confirm("Supprimer ce mode de garde pour tous les enfants ?")) {
globalCareModes.splice(index, 1);
renderCareModes();
}
}
function renderKidCheckboxes(row) {
const container = row.querySelector('.kid-modes-container');
const storedSpan = row.querySelector('.stored-modes');
let activeModes = [];
if (storedSpan) {
try { activeModes = JSON.parse(storedSpan.innerText); } catch(e){}
storedSpan.remove();
} else {
const checked = Array.from(container.querySelectorAll('input:checked')).map(i => i.value);
if (checked.length > 0) activeModes = checked;
}
const kidId = row.getAttribute('data-id');
container.innerHTML = '';
if (globalCareModes.length === 0) {
container.innerHTML = '<span style="font-style: italic;">Aucun mode de garde configuré.</span>';
return;
}
globalCareModes.forEach(mode => {
const isChecked = activeModes.includes(mode) ? 'checked' : '';
const label = document.createElement('label');
label.style.cssText = "cursor: pointer; display: flex; align-items: center; gap: 5px;";
label.innerHTML = `<input type="checkbox" name="kids[${kidId}][modes][]" value="${mode}" class="pf-checkbox-lg" style="transform: scale(0.8);" ${isChecked}> ${mode}`;
container.appendChild(label);
});
}
function addKidRow() {
newKidCounter++;
const kidId = 'new_' + newKidCounter;
const ul = document.getElementById('kidsListContainer');
const li = document.createElement('li');
li.className = 'kid-row';
li.setAttribute('data-id', kidId);
li.style.cssText = "padding: 10px 12px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 8px; display: flex; flex-direction: column; gap: 8px;";
li.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
<input type="text" name="kids[${kidId}][name]" class="pf-input" placeholder="Prénom" style="flex: 1; padding: 8px; font-weight: bold;" required>
<button type="button" class="btn-icon-action delete" onclick="removeKidRow(this)" title="Retirer" style="padding: 4px;">🗑️</button>
</div>
<div class="kid-modes-container" style="font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 12px; flex-wrap: wrap; align-items: center;"></div>
`;
ul.appendChild(li);
renderKidCheckboxes(li);
}
function removeKidRow(btn, dbId = null) {
const row = btn.closest('.kid-row');
if (dbId) {
deletedKidsIds.push(dbId);
document.getElementById('deletedKids').value = JSON.stringify(deletedKidsIds);
}
row.remove();
}
const unifiedFamilyForm = document.getElementById('unifiedFamilyForm');
if (unifiedFamilyForm) {
unifiedFamilyForm.addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('button[type="submit"]');
const oldHtml = btn.innerHTML;
btn.innerHTML = '⏳ Enregistrement...';
btn.disabled = true;
const fd = new FormData(form);
try {
const res = await pachaFetch('settings.php', { method: 'POST', body: fd });
if (res.success) {
showToast("Configuration du foyer enregistrée avec succès.", "success");
setTimeout(() => window.location.reload(), 1000);
} else {
showToast(res.error || "Erreur de sauvegarde.", "error");
}
} catch (err) {
showToast("Erreur technique de sauvegarde.", "error");
} finally {
btn.innerHTML = oldHtml;
btn.disabled = false;
}
});
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('careModesContainer')) renderCareModes(); <?php if ($success): ?>
if (typeof showToast === 'function') {
showToast(<?= json_encode($success) ?>, 'success');
} else {
alert(<?= json_encode($success) ?>);
}
<?php endif; ?>
<?php if ($error): ?>
if (typeof showToast === 'function') {
showToast(<?= json_encode($error) ?>, 'error');
} else {
alert(<?= json_encode($error) ?>);
}
<?php endif; ?>
}); });
</script> </script>