settings calendar
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-11 17:57:09 +02:00
parent 429a4ccb66
commit 6459c225ac
14 changed files with 2435 additions and 883 deletions
+644 -9
View File
@@ -16,7 +16,12 @@ require __DIR__ . '/header.php';
<div class="pf-container"> <div class="pf-container">
<div class="pf-hero" style="text-align: center; margin-bottom: 30px;"> <div class="pf-hero" style="text-align: center; margin-bottom: 30px;">
<h1 style="margin-bottom: 20px;"><?= tr('budget_main_header') ?></h1> <div style="display: flex; justify-content: center; align-items: center; gap: 15px; margin-bottom: 20px;">
<h1 style="margin: 0;"><?= tr('budget_main_header') ?></h1>
<button class="btn-settings-gear" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
⚙️
</button>
</div>
<nav class="budget-tabs-container"> <nav class="budget-tabs-container">
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>"> <a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
@@ -63,26 +68,656 @@ require __DIR__ . '/header.php';
?> ?>
</section> </section>
</div> </div>
<div class="budget-settings-backdrop" id="modal-budget-settings">
<div class="budget-settings-modal">
<div class="bs-header">
<h3>⚙️ <?= tr('budget_settings_title') ?></h3>
<button class="bs-close" onclick="closeBudgetSettings()">×</button>
</div>
<div class="bs-layout">
<div class="bs-sidebar">
<button class="bs-tab-btn active" onclick="switchBsTab('accounts', this)"><?= tr('bs_tab_accounts') ?></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('salaries', this)"><?= tr('bs_tab_salaries') ?></button>
</div>
<div class="bs-content">
<div id="pane-accounts" class="bs-pane active">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.5rem;">
<h4 class="bs-section-title" style="margin: 0; border: none; padding: 0;"><?= tr('bs_tab_accounts') ?></h4>
<div style="display: flex; align-items: center; gap: 6px; font-size: 0.8rem; color: var(--text-muted); font-weight: 500;">
<span><?= tr('bs_currency_label') ?> :</span>
<select id="bs-currency-select" class="pf-input" style="width: 75px; padding: 3px 6px; font-size: 0.8rem; height: auto; cursor: pointer; background: var(--bg-page);" onchange="updateBudgetCurrency(this)">
<option value="€">EUR (€)</option>
<option value="$">USD ($)</option>
<option value="£">GBP (£)</option>
<option value="CHF">CHF</option>
</select>
</div>
</div>
<div id="bs-list-accounts">⏳ <?= tr('loading') ?></div>
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);">
<h5 class="bs-section-title" style="font-size:1rem;"><?= tr('bs_add_account_title') ?></h5>
<form id="form-add-account" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<input type="text" name="name" class="pf-input" placeholder="<?= tr('bs_account_name_ph') ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
<select name="type" class="pf-input" style="width: 130px; padding: 0.4rem 0.75rem;">
<option value="checking"><?= tr('bs_type_checking') ?></option>
<option value="savings"><?= tr('bs_type_savings') ?></option>
</select>
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
</form>
</div>
<div id="pane-categories" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_tab_categories') ?></h4>
<div id="bs-list-categories">⏳ <?= tr('loading') ?></div>
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);">
<h5 class="bs-section-title" style="font-size:1rem;"><?= tr('bs_add_category_title') ?></h5>
<form id="form-add-category" style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<input type="text" name="code" class="pf-input" placeholder="<?= tr('bs_cat_code_ph') ?>" required style="width: 120px; padding: 0.4rem 0.75rem;">
<input type="text" name="label" class="pf-input" placeholder="<?= tr('bs_cat_label_ph') ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
<select name="type" class="pf-input" style="width: 120px; padding: 0.4rem 0.75rem;">
<option value="Expense"><?= tr('bs_type_expense') ?></option>
<option value="Income"><?= tr('bs_type_income') ?></option>
<option value="Savings"><?= tr('bs_type_savings_cat') ?></option>
</select>
<select name="icon" class="pf-input" style="width: 65px; padding: 0.4rem 0.2rem; font-size: 1.1rem; cursor: pointer; text-align: center;">
<option value="📌">📌</option>
<option value="🛒">🛒</option>
<option value="🥖">🥖</option>
<option value="🍽️">🍽️</option>
<option value="⛽">⛽</option>
<option value="🚗">🚗</option>
<option value="🚆">🚆</option>
<option value="🏠">🏠</option>
<option value="⚡">⚡</option>
<option value="💧">💧</option>
<option value="⚕️">⚕️</option>
<option value="🎒">🎒</option>
<option value="👕">👕</option>
<option value="📱">📱</option>
<option value="🎮">🎮</option>
<option value="✈️">✈️</option>
<option value="🎁">🎁</option>
<option value="🐶">🐶</option>
<option value="💵">💵</option>
<option value="🐷">🐷</option>
<option value="📈">📈</option>
</select>
<input type="color" name="color" value="#3b82f6" style="width: 38px; height: 38px; padding: 0; border: 1px solid var(--border-light); border-radius: 6px; cursor: pointer;">
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
</form>
</div>
<div id="pane-rules" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_tab_rules') ?></h4>
<input type="text" id="input-search-rules" class="bs-rule-search" placeholder="🔍 Rechercher un mot-clé (ex: AUCHAN)..." onkeyup="filterRules()">
<div id="bs-list-rules">⏳ <?= tr('loading') ?></div>
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);">
<h5 class="bs-section-title" style="font-size:1rem;"><?= tr('bs_add_rule_title') ?></h5>
<form id="form-add-rule" style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<input type="text" name="keyword" class="pf-input" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
<select name="category" id="select-rule-category" class="pf-input" required style="width: 180px; padding: 0.4rem 0.75rem;">
</select>
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
</form>
</div>
<div id="pane-salaries" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_tab_salaries') ?> (<?= date('Y') ?>)</h4>
<div id="bs-list-salaries">⏳ <?= tr('loading') ?></div>
</div>
</div>
</div>
</div>
</div>
<script> <script>
// --- AUTO-SCROLL DES ONGLETS MOBILES --- // --- AUTO-SCROLL DES ONGLETS MOBILES ---
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const tabsContainer = document.querySelector('.budget-tabs-container'); const tabsContainer = document.querySelector('.budget-tabs-container');
const activeTab = document.querySelector('.tab-item.active'); const activeTab = document.querySelector('.tab-item.active');
// Si on a bien un conteneur d'onglets ET un onglet actif
if (tabsContainer && activeTab) { if (tabsContainer && activeTab) {
// On vérifie si le conteneur a un scroll horizontal possible
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) { if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
// On calcule la position pour centrer l'onglet à l'écran
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2); const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
tabsContainer.scrollTo({ left: scrollPos, behavior: 'smooth' });
}
}
});
// On fait glisser le menu doucement // --- PARAMÈTRES BUDGET (MODALE & ONGLETS) ---
tabsContainer.scrollTo({ function openBudgetSettings() {
left: scrollPos, document.getElementById('modal-budget-settings').classList.add('show');
behavior: 'smooth' document.body.classList.add('no-scroll');
loadBudgetSettingsData();
}
function closeBudgetSettings() {
document.getElementById('modal-budget-settings').classList.remove('show');
document.body.classList.remove('no-scroll');
}
document.getElementById('modal-budget-settings')?.addEventListener('click', (e) => {
if (e.target.id === 'modal-budget-settings') closeBudgetSettings();
});
function switchBsTab(tabId, btnEl) {
document.querySelectorAll('.bs-tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.bs-pane').forEach(p => p.classList.remove('active'));
btnEl.classList.add('active');
document.getElementById('pane-' + tabId).classList.add('active');
}
async function loadBudgetSettingsData() {
try {
const response = await pachaFetch('/modules/budget/includes/api/settings.php?action=get_all', { method: 'GET' });
if (!response.success) {
throw new Error(response.error || "<?= addslashes(tr('error_occured')) ?>");
}
renderAccounts(response.data.accounts);
renderCategories(response.data.categories);
populateRuleCategories(response.data.categories);
renderRules(response.data.rules);
renderSalaries(response.data.salaries, response.data.year);
const currencySelect = document.getElementById('bs-currency-select');
if (currencySelect && response.data.currency) {
currencySelect.value = response.data.currency;
}
} catch (err) {
console.error("Erreur chargement paramètres :", err);
const errorMsg = `<div class="pf-alert pf-alert--error" style="margin-top:10px;">❌ Erreur : ${err.message}</div>`;
['accounts', 'categories', 'rules', 'salaries'].forEach(id => {
const el = document.getElementById('bs-list-' + id);
if (el) el.innerHTML = errorMsg;
}); });
} }
} }
// ==========================================================================
// --- RENDU DES ONGLETS (PARAMÈTRES BUDGET) ---
// ==========================================================================
// 1. ONGLET : COMPTES BANCAIRES
function renderAccounts(accounts) {
const container = document.getElementById('bs-list-accounts');
if (!container) return;
if (!accounts || accounts.length === 0) {
container.innerHTML = '<em style="color:var(--text-muted)"><?= addslashes(tr("bs_empty_accounts")) ?></em>';
return;
}
let html = '<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem;">';
accounts.forEach(acc => {
const typeBadge = acc.account_type === 'checking' ? '<?= addslashes(tr("bs_type_checking")) ?>' : '<?= addslashes(tr("bs_type_savings")) ?>';
const defaultBadge = acc.is_default == 1 ? '<span style="font-size:0.7em; background:var(--success); color:white; padding:2px 6px; border-radius:4px; margin-left:8px;"><?= addslashes(tr("bs_badge_default")) ?></span>' : '';
const ownerText = acc.owner_name ? `<?= addslashes(tr("bs_owner")) ?> : ${acc.owner_name}` : '<?= addslashes(tr("bs_shared_account")) ?>';
html += `
<div class="bs-list-item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: var(--bg-panel); border-radius: var(--radius); border: 1px solid var(--border-light);">
<div>
<strong>${acc.name}</strong> ${defaultBadge}<br>
<small style="color:var(--text-muted);">${typeBadge} · ${ownerText}</small>
</div>
<div class="pf-flex-gap-8">
<button type="button" class="btn-icon-action delete" onclick="deleteAccount(${acc.id})" title="<?= addslashes(tr('btn_delete') ?? 'Supprimer') ?>">🗑️</button>
</div>
</div>
`;
}); });
html += '</div>';
container.innerHTML = html;
}
// 2. ONGLET : CATÉGORIES (Affichage à plat, sans accordéon)
function renderCategories(categories) {
const container = document.getElementById('bs-list-categories');
if (!container) return;
if (!categories || categories.length === 0) {
container.innerHTML = '<em style="color:var(--text-muted)"><?= addslashes(tr("bs_empty_categories")) ?></em>';
return;
}
let html = '<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem;">';
categories.forEach(cat => {
let typeLabel = '';
if (cat.type === 'Income') typeLabel = '<?= addslashes(tr("bs_type_income")) ?>';
else if (cat.type === 'Expense') typeLabel = '<?= addslashes(tr("bs_type_expense")) ?>';
else typeLabel = '<?= addslashes(tr("bs_type_savings_cat")) ?>';
html += `
<div class="bs-list-item" style="border-left: 4px solid ${cat.color || '#ccc'}; display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: var(--bg-panel); border-radius: var(--radius); border: 1px solid var(--border-light); border-left-width: 4px;">
<div>
<span style="font-size:1.2rem; margin-right:8px;">${cat.icon || '📌'}</span>
<strong style="color: var(--text-main); font-size: 0.95rem;">${cat.label}</strong> <small style="color:var(--text-muted)">(${cat.code} — ${typeLabel})</small>
</div>
<div class="pf-flex-gap-8">
<button type="button" class="btn-icon-action delete" onclick="deleteCategory(${cat.id})" title="<?= addslashes(tr('btn_delete') ?? 'Supprimer') ?>">🗑️</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
// 3. ONGLET : RÈGLES D'IMPORT (Affichage en accordéons par catégorie)
function renderRules(rules) {
const container = document.getElementById('bs-list-rules');
if (!container) return;
if (!rules || rules.length === 0) {
container.innerHTML = '<em style="color:var(--text-muted)"><?= addslashes(tr("bs_empty_rules")) ?></em>';
return;
}
// Regroupement des règles par catégorie budgétaire
const groupedRules = {};
rules.forEach(r => {
const catName = r.cat_label || r.category;
if (!groupedRules[catName]) groupedRules[catName] = [];
groupedRules[catName].push(r);
});
let html = '<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem;">';
for (const [catName, catRules] of Object.entries(groupedRules)) {
html += `
<details class="pf-accordion js-rule-group" data-catname="${catName.toLowerCase()}">
<summary class="pf-accordion-summary" style="display: flex; justify-content: space-between; align-items: center; cursor: pointer;">
<span>📁 ${catName}</span>
<span style="font-size: 0.8rem; background: var(--border-light); padding: 2px 8px; border-radius: 10px; color: var(--text-muted); font-weight: normal; margin-left: auto; margin-right: 10px;">
${catRules.length}
</span>
</summary>
<div class="pf-accordion-content" style="padding: 0.5rem;">
<div class="bs-rule-tags" style="display: flex; flex-wrap: wrap; gap: 0.4rem;">
`;
catRules.forEach(r => {
html += `
<div class="bs-rule-tag js-rule-tag" data-keyword="${r.keyword.toLowerCase()}">
${r.keyword}
<button type="button" class="bs-rule-tag-del" onclick="deleteRule(${r.id})" title="<?= addslashes(tr('btn_delete') ?? 'Supprimer') ?>">×</button>
</div>
`;
});
html += `
</div>
</div>
</details>
`;
}
html += '</div>';
container.innerHTML = html;
}
// 4. ACTION : FILTRAGE AVANCÉ DES RÈGLES (Recherche intelligente avec auto-ouverture)
function filterRules() {
const searchVal = document.getElementById('input-search-rules').value.toLowerCase();
const groups = document.querySelectorAll('.js-rule-group');
groups.forEach(group => {
let hasVisibleTag = false;
const tags = group.querySelectorAll('.js-rule-tag');
const catName = group.getAttribute('data-catname');
tags.forEach(tag => {
const keyword = tag.getAttribute('data-keyword');
if (keyword.includes(searchVal)) {
tag.style.display = 'inline-flex';
hasVisibleTag = true;
} else {
tag.style.display = 'none';
}
});
// Si le nom de la catégorie principale correspond à la recherche, on affiche tout le groupe
if (catName.includes(searchVal) && searchVal.length > 0) {
tags.forEach(tag => tag.style.display = 'inline-flex');
hasVisibleTag = true;
}
if (hasVisibleTag) {
group.style.display = 'block';
// Ouvre l'accordéon automatiquement pendant la saisie d'une recherche
if (searchVal.length > 0) {
group.setAttribute('open', '');
}
} else {
group.style.display = 'none';
group.removeAttribute('open');
}
});
}
// 5. ONGLET : SALAIRES
function renderSalaries(salaries, year) {
const container = document.getElementById('bs-list-salaries');
if (!container) return;
if (!salaries || salaries.length === 0) {
container.innerHTML = `<em style="color:var(--text-muted)"><?= addslashes(tr("bs_empty_salaries")) ?> ${year}.</em>`;
return;
}
let html = `<p class="pf-muted-tiny" style="margin-top:0; margin-bottom:10px;"><?= addslashes(tr("bs_salaries_year_desc")) ?> <strong>${year}</strong>.</p>`;
const currency = '<?= CURRENCY ?>';
html += '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
salaries.forEach(s => {
html += `
<div class="bs-list-item" id="salary-item-${s.id}" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: var(--bg-panel); border-radius: var(--radius); border: 1px solid var(--border-light);">
<div>
<strong>${s.person}</strong><br>
<small style="color:var(--text-muted);">
<?= addslashes(tr("bs_salary_net")) ?> : ${s.salary} ${currency}<br>
<?= addslashes(tr("bs_salary_fees")) ?> : ${s.mensualite} ${currency}
</small>
</div>
<div class="pf-flex-gap-8">
<button type="button" class="btn-icon-action edit"
data-id="${s.id}"
data-person="${s.person}"
data-salary="${s.salary}"
data-mensualite="${s.mensualite}"
onclick="inlineEditSalary(this)"
title="<?= addslashes(tr('btn_edit') ?? 'Modifier') ?>">✏️</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
// --- CRUD ACTIONS ---
async function deleteAccount(id) {
if (!confirm('<?= addslashes(tr("bs_confirm_delete")) ?>')) return;
try {
const formData = new FormData();
formData.append('action', 'delete_account');
formData.append('id', id);
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
}
}
const formAddAccount = document.getElementById('form-add-account');
if (formAddAccount) {
formAddAccount.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = formAddAccount.querySelector('button[type="submit"]');
const oldText = btn.innerText;
btn.innerText = '⏳';
btn.disabled = true;
try {
const formData = new FormData(formAddAccount);
formData.append('action', 'add_account');
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
formAddAccount.reset();
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
} finally {
btn.innerText = oldText;
btn.disabled = false;
}
});
}
// --- CRUD CATÉGORIES ---
async function deleteCategory(id) {
if (!confirm('<?= addslashes(tr("bs_confirm_delete_cat")) ?>')) return;
try {
const formData = new FormData();
formData.append('action', 'delete_category');
formData.append('id', id);
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
}
}
const formAddCategory = document.getElementById('form-add-category');
if (formAddCategory) {
formAddCategory.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = formAddCategory.querySelector('button[type="submit"]');
const oldText = btn.innerText;
btn.innerText = '⏳';
btn.disabled = true;
try {
const formData = new FormData(formAddCategory);
formData.append('action', 'add_category');
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
formAddCategory.reset();
// On remet une couleur par défaut propre après reset
formAddCategory.querySelector('input[type="color"]').value = "#3b82f6";
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
} finally {
btn.innerText = oldText;
btn.disabled = false;
}
});
}
// --- CRUD RÈGLES D'IMPORT ---
// Remplir le menu déroulant des catégories dans le formulaire d'ajout
function populateRuleCategories(categories) {
const select = document.getElementById('select-rule-category');
if (!select) return;
select.innerHTML = ''; // On vide
if (!categories || categories.length === 0) return;
categories.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat.code;
opt.textContent = `${cat.icon || ''} ${cat.label}`;
select.appendChild(opt);
});
}
// Supprimer une règle
async function deleteRule(id) {
if (!confirm('<?= addslashes(tr("bs_confirm_delete_rule")) ?>')) return;
try {
const formData = new FormData();
formData.append('action', 'delete_rule');
formData.append('id', id);
const response = await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: formData });
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
}
}
// Ajouter une règle
const formAddRule = document.getElementById('form-add-rule');
if (formAddRule) {
formAddRule.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = formAddRule.querySelector('button[type="submit"]');
const oldText = btn.innerText;
btn.innerText = '⏳';
btn.disabled = true;
try {
const formData = new FormData(formAddRule);
formData.append('action', 'add_rule');
const response = await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: formData });
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
formAddRule.reset();
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
} finally {
btn.innerText = oldText;
btn.disabled = false;
}
});
}
// --- CRUD SALAIRES (EDITION INLINE) ---
// Active le mode édition sur la ligne sélectionnée
function inlineEditSalary(btnEl) {
const id = btnEl.dataset.id;
const person = btnEl.dataset.person;
const salary = btnEl.dataset.salary;
const mensualite = btnEl.dataset.mensualite;
const currency = '<?= CURRENCY ?>';
const rowEl = document.getElementById(`salary-item-${id}`);
if (!rowEl) return;
// Remplacement temporaire du contenu de la ligne par un formulaire compact
rowEl.innerHTML = `
<form onsubmit="submitInlineSalary(event, ${id})" style="display: flex; width: 100%; gap: 10px; align-items: center; flex-wrap: wrap;">
<div style="flex: 1; min-width: 100px;">
<strong style="color: var(--text-main); font-size: 0.95rem;">${person}</strong>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<small style="color: var(--text-muted); font-size: 0.8rem; margin-right: 2px;"><?= addslashes(tr("bs_salary_net")) ?>:</small>
<input type="number" step="0.01" name="salary" class="pf-input" value="${salary}" required style="width: 85px; padding: 4px 8px;"> <span style="font-size: 0.85rem;">${currency}</span>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<small style="color: var(--text-muted); font-size: 0.8rem; margin-right: 2px;"><?= addslashes(tr("bs_salary_fees")) ?>:</small>
<input type="number" step="0.01" name="mensualite" class="pf-input" value="${mensualite}" required style="width: 85px; padding: 4px 8px;"> <span style="font-size: 0.85rem;">${currency}</span>
</div>
<div style="display: flex; gap: 4px; margin-left: auto;">
<button type="submit" class="btn btn-secondary" style="padding: 4px 10px;" title="<?= addslashes(tr('btn_save') ?? 'Enregistrer') ?>">💾</button>
<button type="button" class="btn btn-ghost" onclick="loadBudgetSettingsData()" style="padding: 4px 10px;" title="<?= addslashes(tr('btn_cancel') ?? 'Annuler') ?>">❌</button>
</div>
</form>
`;
}
// Soumission du formulaire d'édition à l'API
async function submitInlineSalary(event, id) {
event.preventDefault();
const form = event.target;
const btn = form.querySelector('button[type="submit"]');
btn.innerText = '⏳';
btn.disabled = true;
try {
const formData = new FormData(form);
formData.append('action', 'save_salary');
formData.append('id', id);
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
await loadBudgetSettingsData(); // Recharge les données fraîches à plat
} catch (err) {
alert("Erreur : " + err.message);
btn.innerText = '💾';
btn.disabled = false;
}
}
// Sauvegarde de la devise du foyer en temps réel
async function updateBudgetCurrency(selectEl) {
try {
const formData = new FormData();
formData.append('action', 'save_currency');
formData.append('currency', selectEl.value);
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || '<?= addslashes(tr("error_occured")) ?>');
// Si ton application possède un système de notification Toast global
if (typeof showToast === 'function') {
showToast('<?= addslashes(tr("bs_currency_updated")) ?> ✅');
}
// On rafraîchit les listes pour mettre à jour l'affichage des symboles (ex: dans l'onglet salaires)
await loadBudgetSettingsData();
} catch (err) {
alert("Erreur : " + err.message);
}
}
</script> </script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>
+135 -23
View File
@@ -9,7 +9,7 @@ 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 avec leurs modes de garde
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people ORDER BY id ASC"); $stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC");
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC); $familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
$parents = []; $parents = [];
@@ -21,6 +21,23 @@ $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') {
// Décodage sécurisé des modes de garde // Décodage sécurisé des modes de garde
@@ -45,10 +62,18 @@ 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"> <div class="fc-header-row" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 24px;">
<h1><?= tr('fc_main_header') ?></h1>
<div style="display: flex; gap: 10px;"> <!-- Bloc Titre + Roue crantée -->
<div style="display: flex; align-items: center; gap: 12px;">
<h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1>
<button id="btnOpenCalendarSettings" class="btn-settings-gear" title="<?= tr('fc_btn_settings') ?>">
⚙️
</button>
</div>
<!-- Boutons d'actions -->
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="btnOpenSnapshotModal" class="pf-btn-icon-text"> <button id="btnOpenSnapshotModal" class="pf-btn-icon-text">
<span class="icon">⚖️</span> <span class="icon">⚖️</span>
<span class="text"><?= tr('fc_btn_correct_balances') ?></span> <span class="text"><?= tr('fc_btn_correct_balances') ?></span>
@@ -137,7 +162,92 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<section class="pf-section"> <!-- 3. MODALE : PARAMÈTRES AVANCÉS DU CALENDRIER -->
<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;">
<!-- HEADER -->
<div class="pf-modal-header" style="padding: 1.2rem 1.5rem; border-bottom: 1px solid var(--border-light); background: var(--bg-panel);">
<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>
<!-- CORPS SCINDÉ EN DEUX (SIDEBAR / CONTENU) -->
<div style="display: flex; flex-direction: row; height: 60vh; min-height: 400px; background: var(--bg-panel);">
<!-- SIDEBAR GAUCHE -->
<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')">
<?= tr('fc_tab_foyer') ?>
</button>
<button type="button" id="tab-btn-membres" class="fc-sidebar-tab bs-tab-btn" onclick="switchCalendarTab('membres')">
<?= tr('fc_tab_members') ?>
</button>
</div>
<!-- CONTENU DROITE -->
<div class="pf-modal-body" style="flex: 1; padding: 1.5rem; overflow-y: auto;">
<!-- PANE A : CONFIGURATION GÉNÉRALE DU FOYER -->
<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>
<!-- PANE B : CONFIGURATION DE LA MATRICE COMPTEURS -->
<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 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 class="fc-month-calendar-wrapper"> <div class="fc-month-calendar-wrapper">
<div class="fc-month-header"> <div class="fc-month-header">
<div class="fc-month-nav-row"> <div class="fc-month-nav-row">
@@ -151,7 +261,8 @@ require __DIR__ . '/header.php';
</div> </div>
<div class="fc-view-controls"> <div class="fc-view-controls">
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button> <div class="fc-view-divider"></div> <button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button>
<div class="fc-view-divider"></div>
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button> <button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button> <button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button> <button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
@@ -165,9 +276,8 @@ require __DIR__ . '/header.php';
<div id="fc-month-balances" class="fc-month-balances"></div> <div id="fc-month-balances" class="fc-month-balances"></div>
</div> </div>
</section>
<section class="pf-section"> <section class="pf-section" style="margin-top: 1.5rem;">
<div class="fc-week-header"> <div class="fc-week-header">
<h2><?= tr('fc_weekly_planning') ?></h2> <h2><?= tr('fc_weekly_planning') ?></h2>
<div class="fc-week-nav-controls"> <div class="fc-week-nav-controls">
@@ -197,32 +307,32 @@ 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';
$colspan = count($parent['leave_types']) * 2;
?> ?>
<th colspan="6" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th> <th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
<?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):
?> ?>
<th colspan="2" class="<?= $parentClass ?>-sub">CP</th> <th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars(strtoupper($lt)) ?></th>
<th colspan="2" class="<?= $parentClass ?>-sub">JRA</th> <?php endforeach; endforeach; ?>
<th colspan="2" class="<?= $parentClass ?>-sub">JA</th>
<?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):
?> ?>
<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>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th> <?php endforeach; endforeach; ?>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<?php endforeach; ?>
</tr> </tr>
</thead> </thead>
<tbody id="planningBody"></tbody> <tbody id="planningBody"></tbody>
@@ -231,9 +341,8 @@ require __DIR__ . '/header.php';
</div> </div>
</section> </section>
<section class="pf-section pf-section--bottom-panels"> <section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;">
<div class="fc-bottom-grid"> <div class="fc-bottom-grid">
<div class="pf-card"> <div class="pf-card">
<div class="pf-card-title fc-summary-header"> <div class="pf-card-title fc-summary-header">
<span><?= tr('fc_summary_title') ?></span> <span><?= tr('fc_summary_title') ?></span>
@@ -258,7 +367,6 @@ require __DIR__ . '/header.php';
<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): <?php foreach ($activeCareModes as $index => $mode):
// Génération d'une couleur pseudo-aléatoire mais fixe par mode
$hue = ($index * 137) % 360; $hue = ($index * 137) % 360;
?> ?>
<div class="pf-legend-item"> <div class="pf-legend-item">
@@ -281,11 +389,15 @@ require __DIR__ . '/header.php';
</div> </div>
<script> <script>
// 🟢 OBJET DE CONFIGURATION GLOBAL POUR LE JAVASCRIPT
window.FAMILY_CONFIG = { window.FAMILY_CONFIG = {
parents: <?= json_encode($parents) ?>, parents: <?= json_encode($parents) ?>,
kids: <?= json_encode($kids) ?>, kids: <?= json_encode($kids) ?>,
activeCareModes: <?= json_encode($activeCareModes) ?> activeCareModes: <?= json_encode($activeCareModes) ?>,
i18n: {
settings_updated: <?= json_encode(tr('fc_settings_updated')) ?>,
empty_matrix: "Aucun compteur de congés défini pour le moment.",
btn_delete: <?= json_encode(tr('btn_delete') ?? 'Supprimer') ?>
}
}; };
</script> </script>
<script src="/modules/family-calendar/family-calendar.js"></script> <script src="/modules/family-calendar/family-calendar.js"></script>
+23
View File
@@ -1303,3 +1303,26 @@ details[open].pf-accordion > .pf-accordion-summary::after {
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
} }
/* Bouton Paramètres avec icône rotative (Roue crantée) */
.btn-settings-gear {
background: transparent;
border: none;
box-shadow: none;
outline: none;
font-size: 1.4rem;
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
line-height: 1;
color: inherit;
}
.btn-settings-gear:hover {
transform: rotate(
90deg
); /* 90deg rend mieux sur une roue crantée qu'un 45deg ! */
}
+58
View File
@@ -213,6 +213,30 @@ return [
'fc_err_action' => 'Error en l\'acció: ', 'fc_err_action' => 'Error en l\'acció: ',
'fc_today_short' => 'Avui', 'fc_today_short' => 'Avui',
'fc_today_title' => 'Anar a avui', 'fc_today_title' => 'Anar a avui',
'fc_tab_foyer' => '🏡 Llar',
'fc_tab_members' => '👥 Membres i Drets',
'fc_select_member' => 'Selecciona un membre de la família:',
'fc_role_adult' => '👨‍👩‍👦 Adult',
'fc_role_child' => '👶 Nen',
'fc_care_modes_title' => '👶 Modes de cura associats',
'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.',
'fc_no_care_mode' => 'Cap mode configurat.',
'fc_matrix_title' => '👨‍👩‍👦 Matriu de Drets de Permís',
'fc_matrix_desc' => 'Declara els codis de comptadors i les dates de renovació.',
'fc_matrix_code' => 'Codi',
'fc_matrix_date' => 'Data de Reset',
'fc_matrix_empty' => 'Cap comptador configurat.',
'fc_matrix_saved' => 'Matriu de drets actualitzada! 💼',
'fc_child_saved' => 'Subscripció del nen desada!',
'btn_add_counter' => ' Afegir',
'btn_save_rights' => '💾 Desar',
'fc_modal_settings_title' => 'Configuració del Calendari',
'fc_zone_label' => 'Zona Escolar (Vacances)',
'fc_zone_desc' => 'Determina el calendari automàtic de vacances escolars.',
'fc_care_modes_label' => 'Modes de Cura de la Llar',
'fc_care_modes_desc' => 'Gestiona la llista global de modes de cura possibles per a la família.',
'fc_add_mode_placeholder' => 'Ex: Mainadera, Casal, Avis...',
'fc_settings_updated' => 'Configuració actualitzada correctament!',
'leg_presence' => 'Presència Pep', 'leg_presence' => 'Presència Pep',
'leg_school_holidays' => 'Vacances Escolars', 'leg_school_holidays' => 'Vacances Escolars',
@@ -403,6 +427,40 @@ return [
'cat_others' => 'Altres / Imprevistos', 'cat_others' => 'Altres / Imprevistos',
'cat_savings' => 'Estalvis', 'cat_savings' => 'Estalvis',
'budget_tab_kids' => 'Nens', 'budget_tab_kids' => 'Nens',
'budget_settings_title' => 'Configuració del Pressupost',
'bs_tab_accounts' => '💳 Comptes',
'bs_tab_categories' => '🏷️ Categories',
'bs_tab_rules' => '🤖 Regles d\'importació',
'bs_tab_salaries' => '💼 Salaris',
'bs_empty_accounts' => 'Cap compte configurat.',
'bs_type_checking' => 'Corrent',
'bs_type_savings' => 'Estalvi',
'bs_badge_default' => 'Per defecte',
'bs_owner' => 'Propietari',
'bs_shared_account' => 'Compte conjunt',
'bs_empty_categories' => 'Cap categoria configurada.',
'bs_type_income' => 'Ingrés',
'bs_type_expense' => 'Despesa',
'bs_type_savings_cat' => 'Estalvi',
'bs_empty_rules' => 'Cap regla d\'importació.',
'bs_empty_salaries' => 'Cap salari definit per a l\'any',
'bs_salaries_year_desc' => 'Configuracions per a l\'any',
'bs_salary_net' => 'Salari net',
'bs_salary_fees' => 'Mensualitat / Despeses',
'bs_add_account_title' => 'Afegir un compte',
'bs_account_name_ph' => 'Nom (ex: Estalvi Bebè)',
'bs_confirm_delete' => 'Segur que voleu suprimir aquest compte?',
'bs_add_category_title' => '+ Afegir una categoria',
'bs_cat_code_ph' => 'Codi (ex: FOOD)',
'bs_cat_label_ph' => 'Etiqueta (ex: Compres)',
'bs_cat_icon_ph' => 'Icona (ex: 🛒)',
'bs_confirm_delete_cat' => 'Segur que voleu suprimir aquesta categoria?',
'bs_add_rule_title' => 'Afegir una regla',
'bs_rule_keyword_ph' => 'Paraula clau (ex: AUCHAN)',
'bs_confirm_delete_rule'=> 'Segur que voleu suprimir aquesta regla?',
'bs_currency_label' => 'Divisa de la llar',
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
'bs_currency_updated' => 'Divisa actualitzada correctament!',
// --- BUDGET : SUIVI --- // --- BUDGET : SUIVI ---
'bud_rem_school' => 'Escola (Resta estimada)', 'bud_rem_school' => 'Escola (Resta estimada)',
+58
View File
@@ -214,6 +214,30 @@ return [
'fc_err_action' => 'Error during action: ', 'fc_err_action' => 'Error during action: ',
'fc_today_short' => 'Today', 'fc_today_short' => 'Today',
'fc_today_title' => 'Today', 'fc_today_title' => 'Today',
'fc_tab_foyer' => '🏡 Household',
'fc_tab_members' => '👥 Members & Rights',
'fc_select_member' => 'Select a family member:',
'fc_role_adult' => '👨‍👩‍👦 Adult',
'fc_role_child' => '👶 Child',
'fc_care_modes_title' => '👶 Associated Care Modes',
'fc_care_modes_desc' => 'Check the care modes that apply to this child.',
'fc_no_care_mode' => 'No care mode configured.',
'fc_matrix_title' => '👨‍👩‍👦 Leave Rights Matrix',
'fc_matrix_desc' => 'Declare your counter codes and renewal dates.',
'fc_matrix_code' => 'Code',
'fc_matrix_date' => 'Reset Date',
'fc_matrix_empty' => 'No counter configured.',
'fc_matrix_saved' => 'Rights matrix updated successfully! 💼',
'fc_child_saved' => 'Child subscription saved!',
'btn_add_counter' => ' Add',
'btn_save_rights' => '💾 Save',
'fc_modal_settings_title' => 'Calendar Settings',
'fc_zone_label' => 'School Zone (Holidays)',
'fc_zone_desc' => 'Determines the automatic school holiday calendar.',
'fc_care_modes_label' => 'Household Care Modes',
'fc_care_modes_desc' => 'Manage the global list of possible care modes for the family.',
'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...',
'fc_settings_updated' => 'Settings updated successfully!',
'leg_presence' => 'Pep presence', 'leg_presence' => 'Pep presence',
'leg_school_holidays' => 'School holidays', 'leg_school_holidays' => 'School holidays',
@@ -401,6 +425,40 @@ return [
'cat_others' => 'Other / Unexpected', 'cat_others' => 'Other / Unexpected',
'cat_savings' => 'Savings', 'cat_savings' => 'Savings',
'budget_tab_kids' => 'Kids', 'budget_tab_kids' => 'Kids',
'budget_settings_title' => 'Budget Settings',
'bs_tab_accounts' => '💳 Accounts',
'bs_tab_categories' => '🏷️ Categories',
'bs_tab_rules' => '🤖 Import Rules',
'bs_tab_salaries' => '💼 Salaries',
'bs_empty_accounts' => 'No accounts configured.',
'bs_type_checking' => 'Checking',
'bs_type_savings' => 'Savings',
'bs_badge_default' => 'Default',
'bs_owner' => 'Owner',
'bs_shared_account' => 'Joint Account',
'bs_empty_categories' => 'No categories configured.',
'bs_type_income' => 'Income',
'bs_type_expense' => 'Expense',
'bs_type_savings_cat' => 'Savings',
'bs_empty_rules' => 'No import rules configured.',
'bs_empty_salaries' => 'No salary defined for the year',
'bs_salaries_year_desc' => 'Configurations for the year',
'bs_salary_net' => 'Net salary',
'bs_salary_fees' => 'Monthly payment / Fees',
'bs_add_account_title' => 'Add an account',
'bs_account_name_ph' => 'Name (e.g., Baby Savings)',
'bs_confirm_delete' => 'Are you sure you want to delete this account?',
'bs_add_category_title' => '+ Add a category',
'bs_cat_code_ph' => 'Code (e.g., FOOD)',
'bs_cat_label_ph' => 'Label (e.g., Groceries)',
'bs_cat_icon_ph' => 'Icon (e.g., 🛒)',
'bs_confirm_delete_cat' => 'Are you sure you want to delete this category?',
'bs_add_rule_title' => 'Add a rule',
'bs_rule_keyword_ph' => 'Keyword (e.g., WALMART)',
'bs_confirm_delete_rule'=> 'Are you sure you want to delete this rule?',
'bs_currency_label' => 'Household Currency',
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
'bs_currency_updated' => 'Currency updated successfully!',
// --- BUDGET: TRACKING --- // --- BUDGET: TRACKING ---
'bud_rem_school' => 'School (estimated remaining)', 'bud_rem_school' => 'School (estimated remaining)',
+59
View File
@@ -216,6 +216,31 @@ return [
'care_nounou' => 'Nounou', 'care_nounou' => 'Nounou',
'care_centre' => 'Centre de loisirs', 'care_centre' => 'Centre de loisirs',
'care_avis' => 'Avis', 'care_avis' => 'Avis',
'fc_tab_foyer' => '🏡 Foyer',
'fc_tab_members' => '👥 Membres & Droits',
'fc_select_member' => 'Sélectionner un membre de la famille :',
'fc_role_adult' => '👨‍👩‍👦 Adulte',
'fc_role_child' => '👶 Enfant',
'fc_care_modes_title' => '👶 Modes de garde associés',
'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.',
'fc_no_care_mode' => 'Aucun mode configuré.',
'fc_matrix_title' => '👨‍👩‍👦 Matrice des Droits aux Congés',
'fc_matrix_desc' => 'Déclarez les codes de compteurs et dates de renouvellement.',
'fc_matrix_code' => 'Code',
'fc_matrix_date' => 'Date Reset',
'fc_matrix_empty' => 'Aucun compteur configuré.',
'fc_matrix_saved' => 'Matrice des droits mise à jour ! 💼',
'fc_child_saved' => 'Abonnement enfant enregistré !',
'btn_add_counter' => ' Ajouter',
'btn_save_rights' => '💾 Enregistrer',
'fc_modal_settings_title' => 'Paramètres du Calendrier',
'fc_zone_label' => 'Zone Scolaire (Vacances)',
'fc_zone_desc' => 'Détermine le calendrier automatique des vacances scolaires.',
'fc_care_modes_label' => 'Modes de Garde du Foyer',
'fc_care_modes_desc' => 'Gérez la liste globale des modes de garde possibles pour la famille.',
'fc_add_mode_placeholder' => 'Ex: Nounou, Casal, Grands-parents...',
'fc_settings_updated' => 'Paramètres mis à jour avec succès !',
'leg_presence' => 'Présence Pep', 'leg_presence' => 'Présence Pep',
'leg_school_holidays' => 'Vacances Scolaires', 'leg_school_holidays' => 'Vacances Scolaires',
@@ -403,6 +428,40 @@ return [
'cat_others' => 'Autres / Imprévus', 'cat_others' => 'Autres / Imprévus',
'cat_savings' => 'Épargne', 'cat_savings' => 'Épargne',
'budget_tab_kids' => 'Enfants', 'budget_tab_kids' => 'Enfants',
'budget_settings_title' => 'Paramètres du Budget',
'bs_tab_accounts' => '💳 Comptes',
'bs_tab_categories' => '🏷️ Catégories',
'bs_tab_rules' => '🤖 Règles d\'import',
'bs_tab_salaries' => '💼 Salaires',
'bs_empty_accounts' => 'Aucun compte configuré.',
'bs_type_checking' => 'Courant',
'bs_type_savings' => 'Épargne',
'bs_badge_default' => 'Défaut',
'bs_owner' => 'Propriétaire',
'bs_shared_account' => 'Compte Commun',
'bs_empty_categories' => 'Aucune catégorie configurée.',
'bs_type_income' => 'Revenu',
'bs_type_expense' => 'Dépense',
'bs_type_savings_cat' => 'Épargne',
'bs_empty_rules' => 'Aucune règle d\'importation.',
'bs_empty_salaries' => 'Aucun salaire défini pour l\'année',
'bs_salaries_year_desc' => 'Configurations pour l\'année',
'bs_salary_net' => 'Salaire net',
'bs_salary_fees' => 'Mensualité / Frais',
'bs_add_account_title' => '+ Ajouter un compte',
'bs_account_name_ph' => 'Nom (ex: Livret A Bébé)',
'bs_confirm_delete' => 'Voulez-vous vraiment supprimer ce compte ?',
'bs_add_category_title' => '+ Ajouter une catégorie',
'bs_cat_code_ph' => 'Code (ex: FOOD)',
'bs_cat_label_ph' => 'Libellé (ex: Courses)',
'bs_cat_icon_ph' => 'Icône (ex: 🛒)',
'bs_confirm_delete_cat' => 'Voulez-vous vraiment supprimer cette catégorie ?',
'bs_add_rule_title' => 'Ajouter une règle',
'bs_rule_keyword_ph' => 'Mot-clé (ex: AUCHAN)',
'bs_confirm_delete_rule' => 'Voulez-vous vraiment supprimer cette règle ?',
'bs_currency_label' => 'Devise du Foyer',
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
'bs_currency_updated' => 'Devise mise à jour avec succès !',
// --- BUDGET : SUIVI --- // --- BUDGET : SUIVI ---
'bud_rem_school' => 'École (Reste estimé)', 'bud_rem_school' => 'École (Reste estimé)',
+38 -2
View File
@@ -88,8 +88,7 @@ try {
UNIQUE KEY (code) UNIQUE KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Règles Cadeaux (Qui paie pour quel enfant à quelle occasion) // Règles Cadeaux
// L'ERREUR DE SYNTAXE ÉTAIT ICI : UNIQUE KEY adult_child_occ
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules ( $pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
adult_person_id INT NOT NULL, adult_person_id INT NOT NULL,
@@ -145,6 +144,43 @@ try {
echo "✅ Données de base injectées (si nécessaire).<br>"; 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;
if (!$tableExists) {
$pdo->exec("
CREATE TABLE pf_person_leave_meta (
id INT AUTO_INCREMENT PRIMARY KEY,
person_id INT NOT NULL,
leave_type VARCHAR(50) NOT NULL,
anniversary_date DATE NOT NULL,
UNIQUE KEY uq_person_leave (person_id, leave_type),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
echo "✅ Table pf_person_leave_meta créée (Nouvelle installation).<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>";
}
}
} catch (PDOException $e) { } catch (PDOException $e) {
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>"; echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
} }
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* Script de rattrapage : Configuration dynamique des congés (CP, JRA, JA)
* Ce script lit l'historique réel des soldes/snapshots pour pré-configurer la modale Settings.
*/
require_once __DIR__ . '/includes/meta_db.php';
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
$db_user = getenv('DB_USER') ?: 'househub';
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
echo "<h1>🚀 Début de la restauration des compteurs de congés</h1>";
try {
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($families as $family) {
$dbName = $family['db_name'];
echo "<h2>Famille : {$family['name']} ($dbName)</h2>";
try {
$pdo = new PDO(
"mysql:host=$db_host;dbname=$dbName;charset=utf8mb4",
$db_user, $db_pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// 1. Création de la table de configuration (si elle n'existe pas)
$pdo->exec("
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
id INT AUTO_INCREMENT PRIMARY KEY,
person_id INT NOT NULL,
leave_type VARCHAR(50) NOT NULL,
anniversary_date DATE NOT NULL,
UNIQUE KEY uq_person_leave (person_id, leave_type),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// 2. On scanne l'historique des soldes et snapshots pour trouver TOUS les types existants
$stmtLeaves = $pdo->query("
SELECT DISTINCT person_id, leave_type
FROM (
SELECT person_id, leave_type FROM pf_leave_balances
UNION
SELECT person_id, leave_type FROM pf_leave_snapshots
) as combined
WHERE leave_type IS NOT NULL AND leave_type != ''
");
$existingLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
$inserted = 0;
$stmtInsert = $pdo->prepare("
INSERT IGNORE INTO pf_person_leave_meta (person_id, leave_type, anniversary_date)
VALUES (?, ?, ?)
");
// 3. On injecte les données dans la configuration avec les dates par défaut de ton ancien JS
foreach ($existingLeaves as $leave) {
$pid = $leave['person_id'];
$type = strtoupper(trim($leave['leave_type']));
// Dates d'anniversaire par défaut (année 2000 arbitraire, seul le mois/jour compte)
$anniversary = '2000-01-01';
if ($type === 'CP') $anniversary = '2000-06-01'; // 1er Juin
if ($type === 'JRA') $anniversary = '2000-01-01'; // 1er Janvier
if ($type === 'JA') $anniversary = '2000-04-29'; // 29 Avril (issu de ton ancien JS)
$stmtInsert->execute([$pid, $type, $anniversary]);
if ($stmtInsert->rowCount() > 0) {
$inserted++;
}
}
echo "$inserted compteurs historiques (CP, JRA, JA...) détectés et configurés avec succès pour la modale.<br>";
} catch (PDOException $e) {
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
}
}
echo "<h1>🎉 Restauration terminée avec succès !</h1>";
} catch (Exception $e) {
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
}
?>
+220
View File
@@ -1268,3 +1268,223 @@ body.sum-mode-active .sum-target {
padding: 12px 10px; padding: 12px 10px;
} }
} }
/* ==========================================================================
MODALE PARAMÈTRES BUDGET (Design System HouseHub)
========================================================================== */
.budget-settings-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 200;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
padding: 1rem;
}
.budget-settings-backdrop.show {
display: flex;
}
.budget-settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 14px;
width: 100%;
max-width: 800px;
height: 80vh;
max-height: 700px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: modalIn 0.18s ease;
}
.bs-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.bs-header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--text-main);
}
.bs-close {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--text-muted);
border-radius: 6px;
padding: 0 0.5rem;
}
.bs-close:hover {
background: var(--bg-page);
color: var(--text-main);
}
.bs-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.bs-sidebar {
width: 200px;
border-right: 1px solid var(--border-light);
background: var(--bg-page);
display: flex;
flex-direction: column;
padding: 0.75rem;
gap: 0.4rem;
overflow-y: auto;
}
.bs-tab-btn {
text-align: left;
padding: 0.6rem 1rem;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text-muted);
font-weight: 600;
cursor: pointer;
transition: 0.15s;
font-size: 0.9rem;
}
.bs-tab-btn:hover {
background: var(--border-light);
color: var(--text-main);
}
.bs-tab-btn.active {
background: var(--text-main);
color: var(--bg-panel);
}
.bs-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
background: var(--bg-panel);
}
.bs-pane {
display: none;
}
.bs-pane.active {
display: block;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Mobile Responsive */
@media (max-width: 650px) {
.bs-layout {
flex-direction: column;
}
.bs-sidebar {
width: 100%;
flex-direction: row;
overflow-x: auto;
border-right: none;
border-bottom: 1px solid var(--border-light);
padding: 0.5rem;
white-space: nowrap;
}
.bs-tab-btn {
flex-shrink: 0;
}
.bs-content {
padding: 1rem;
}
}
/* Utilitaires pour les listes de paramètres */
.bs-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--border-light);
border-radius: 8px;
margin-bottom: 0.5rem;
}
.bs-section-title {
font-size: 1.1rem;
color: var(--text-main);
margin-top: 0;
margin-bottom: 1rem;
}
/* --- AFFICHAGE SMART DES RÈGLES (TAG CLOUD) --- */
.bs-rule-search {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 8px;
border: 1px solid var(--border-light);
background: var(--bg-page);
margin-bottom: 1rem;
color: var(--text-main);
font-family: inherit;
}
.bs-rule-group {
margin-bottom: 1.2rem;
}
.bs-rule-group-title {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border-light);
padding-bottom: 4px;
}
.bs-rule-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.bs-rule-tag {
background: var(--bg-page);
border: 1px solid var(--border-light);
padding: 3px 10px;
border-radius: 14px;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 6px;
transition: 0.15s;
}
.bs-rule-tag:hover {
border-color: var(--danger);
background: #fef2f2;
}
.bs-rule-tag-del {
color: var(--text-muted);
cursor: pointer;
border: none;
background: none;
padding: 0;
font-size: 1rem;
line-height: 1;
margin-top: -2px;
}
.bs-rule-tag:hover .bs-rule-tag-del {
color: var(--danger);
}
+179
View File
@@ -0,0 +1,179 @@
<?php
require __DIR__ . '/../../../../includes/auth.php';
require __DIR__ . '/../../../../includes/db.php';
require_login();
header('Content-Type: application/json');
$action = $_GET['action'] ?? $_POST['action'] ?? '';
try {
if ($action === 'get_all') {
// 1. Comptes bancaires avec le nom du propriétaire lié (pf_people)
$accounts = $pdo->query("
SELECT a.*, p.name as owner_name
FROM pf_bank_accounts a
LEFT JOIN pf_people p ON a.owner_person_id = p.id
ORDER BY a.is_default DESC, a.name ASC
")->fetchAll(PDO::FETCH_ASSOC);
// 2. Catégories
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
// 3. Règles d'import (avec le nom de la catégorie correspondante)
// Utilisation de COLLATE pour éviter l'erreur 1267 de mix de collations
$rules = $pdo->query("
SELECT r.*, c.label as cat_label
FROM pf_import_rules r
LEFT JOIN pf_budget_categories c ON r.category COLLATE utf8mb4_unicode_ci = c.code COLLATE utf8mb4_unicode_ci
ORDER BY r.keyword ASC
")->fetchAll(PDO::FETCH_ASSOC);
// 4. Salaires (Année en cours)
$currentYear = (int)date('Y');
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
// Récupération de la devise du foyer (par défaut € si non définie)
// Lecture directe de la colonne currency dans pf_foyer_settings
$currencySetting = $pdo->query("SELECT currency FROM pf_foyer_settings LIMIT 1")->fetchColumn() ?: '€';
echo json_encode([
'success' => true,
'data' => [
'accounts' => $accounts,
'categories' => $categories,
'rules' => $rules,
'salaries' => $salaries,
'year' => $currentYear,
'currency' => $currencySetting
]
]);
exit;
}
// --- GESTION DES COMPTES BANCAIRES ---
if ($action === 'add_account') {
$name = trim($_POST['name'] ?? '');
$type = $_POST['type'] ?? 'checking';
// On met is_default à 0 par défaut pour les nouveaux comptes
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
$stmt->execute([$name, $type]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_account') {
$id = (int)($_POST['id'] ?? 0);
// Sécurité : on empêche de supprimer un compte s'il n'en reste qu'un seul
$count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
// --- GESTION DES CATÉGORIES ---
if ($action === 'add_category') {
$code = strtoupper(trim($_POST['code'] ?? ''));
$label = trim($_POST['label'] ?? '');
$type = $_POST['type'] ?? 'Expense';
$color = $_POST['color'] ?? '#cccccc';
$icon = trim($_POST['icon'] ?? '📌');
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires.");
// Vérification anti-doublon sur le code
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
$check->execute([$code]);
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$code, $label, $type, $color, $icon]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_category') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
// --- GESTION DES RÈGLES D'IMPORT ---
if ($action === 'add_rule') {
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
$category = trim($_POST['category'] ?? ''); // Le code de la catégorie (ex: FMCG)
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
// Vérification anti-doublon
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
$check->execute([$keyword]);
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà.");
$stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
$stmt->execute([$keyword, $category]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_rule') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
// --- GESTION DES SALAIRES ---
if ($action === 'save_salary') {
$id = (int)($_POST['id'] ?? 0);
$salary = (float)($_POST['salary'] ?? 0);
$mensualite = (float)($_POST['mensualite'] ?? 0);
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide.");
// Mise à jour de la ligne pour l'année en cours
$stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
$stmt->execute([$salary, $mensualite, $id]);
echo json_encode(['success' => true]);
exit;
}
// --- GESTION DE LA DEVISE GLOBALE ---
if ($action === 'save_currency') {
$currency = trim($_POST['currency'] ?? '€');
// Mise à jour directe de la colonne currency pour le foyer
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
$stmt->execute([$currency]);
echo json_encode(['success' => true]);
exit;
}
throw new Exception("Action API inconnue.");
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
?>
@@ -955,3 +955,39 @@ td.col-laia-sub {
gap: 20px !important; gap: 20px !important;
} }
} }
/* ==========================================================================
MODALE PARAMÈTRES (SIDEBAR STYLE)
========================================================================== */
.fc-sidebar-tab {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 1.2rem;
background: transparent;
border: none;
text-align: left;
font-weight: 600;
cursor: pointer;
color: var(--text-main);
transition: all 0.2s;
width: calc(100% - 16px);
margin: 2px 8px;
border-radius: 8px;
font-size: 0.95rem;
}
.fc-sidebar-tab.active {
background: #111827; /* Gris très foncé typique des sidebars */
color: #ffffff;
}
.fc-sidebar-tab:hover:not(.active) {
background: var(--bg-soft, #f1f5f9);
}
/* Variante Dark Mode native pour l'onglet actif */
[data-theme="dark"] .fc-sidebar-tab.active {
background: var(--primary);
color: #ffffff;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
<?php
require __DIR__ . '/../../../../includes/auth.php';
require __DIR__ . '/../../../../includes/db.php';
require_login();
header('Content-Type: application/json');
$action = $_REQUEST['action'] ?? '';
try {
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES ───
if ($action === 'get_all') {
// A. Options globales du foyer
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '["Nounou","Centre"]'];
// B. Liste des membres de la famille
$stmtPeople = $pdo->query("SELECT id, name, role FROM pf_people WHERE is_active = 1 ORDER BY role DESC, name ASC");
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
// C. Matrice des congés par personne
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date FROM pf_person_leave_meta");
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
// On organise les congés par ID de personne pour faciliter le traitement côté JS
$leavesMap = [];
foreach ($leavesRaw as $leave) {
$leavesMap[$leave['person_id']][] = [
'type' => $leave['leave_type'],
'method' => $leave['method'] ?? 'FIXED',
'allowance' => $leave['allowance'] ?? 0,
'date' => $leave['anniversary_date']
];
}
echo json_encode([
'success' => true,
'data' => [
'foyer' => [
'zone_scolaire' => $foyer['zone_scolaire'],
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
],
'people' => $people,
'leaves' => $leavesMap
]
]);
exit;
}
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
if ($action === 'save_foyer') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
$zone = trim($_POST['zone_scolaire'] ?? 'C');
$modesRaw = $_POST['care_modes'] ?? '[]';
$modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true)))));
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1");
$stmt->execute([$zone, $modesArray]);
echo json_encode(['success' => true]);
exit;
}
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
if ($action === 'save_member_leaves') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
$personId = (int)($_POST['person_id'] ?? 0);
$leavesData = json_decode($_POST['leaves'] ?? '[]', true);
if ($personId <= 0) throw new Exception("Identifiant de membre invalide.");
$pdo->beginTransaction();
// On nettoie les anciennes configurations de congés de cette personne
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
$stmtDelete->execute([$personId]);
// On réinsère la nouvelle matrice propre
if (!empty($leavesData)) {
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
foreach ($leavesData as $leave) {
$type = strtoupper(trim($leave['type']));
$method = in_array($leave['method'], ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED';
$allowance = (float)($leave['allowance'] ?? 0);
$date = trim($leave['date']);
if (!empty($type) && !empty($date)) {
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
}
}
}
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
throw new Exception("Action inconnue");
} catch (\Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}