+646
-11
@@ -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
|
|
||||||
tabsContainer.scrollTo({
|
|
||||||
left: scrollPos,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- PARAMÈTRES BUDGET (MODALE & ONGLETS) ---
|
||||||
|
function openBudgetSettings() {
|
||||||
|
document.getElementById('modal-budget-settings').classList.add('show');
|
||||||
|
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'; ?>
|
||||||
+200
-88
@@ -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
|
||||||
@@ -28,7 +45,7 @@ foreach ($familyPeople as $p) {
|
|||||||
if (!is_array($modes)) $modes = [];
|
if (!is_array($modes)) $modes = [];
|
||||||
$p['modes'] = $modes;
|
$p['modes'] = $modes;
|
||||||
$kids[] = $p;
|
$kids[] = $p;
|
||||||
|
|
||||||
foreach ($modes as $m) {
|
foreach ($modes as $m) {
|
||||||
$activeCareModes[$m] = true;
|
$activeCareModes[$m] = true;
|
||||||
}
|
}
|
||||||
@@ -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,103 +162,187 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="pf-section">
|
<!-- 3. MODALE : PARAMÈTRES AVANCÉS DU CALENDRIER -->
|
||||||
<div class="fc-month-calendar-wrapper">
|
<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()">×</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-header">
|
<div class="fc-month-header">
|
||||||
<div class="fc-month-nav-row">
|
<div class="fc-month-nav-row">
|
||||||
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
||||||
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
||||||
<select id="fc-select-month" class="fc-smart-select"></select>
|
<select id="fc-select-month" class="fc-smart-select"></select>
|
||||||
<select id="fc-select-year" class="fc-smart-select"></select>
|
<select id="fc-select-year" class="fc-smart-select"></select>
|
||||||
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
||||||
|
</div>
|
||||||
|
<button id="fc-next-month" class="fc-nav-button">›</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 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="3months"><?= tr('fc_view_3m') ?></button>
|
||||||
</div>
|
</div>
|
||||||
<button id="fc-next-month" class="fc-nav-button">›</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 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="3months"><?= tr('fc_view_3m') ?></button>
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
<button id="fc-prev-school-year" class="fc-nav-button">‹</button>
|
<button id="fc-prev-school-year" class="fc-nav-button">‹</button>
|
||||||
<span id="fc-current-school-year-label"></span>
|
<span id="fc-current-school-year-label"></span>
|
||||||
<button id="fc-next-school-year" class="fc-nav-button">›</button>
|
<button id="fc-next-school-year" class="fc-nav-button">›</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="planningTable-wrapper">
|
<div id="planningTable-wrapper">
|
||||||
<table id="planningTable">
|
<table id="planningTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_month') ?></th>
|
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_month') ?></th>
|
||||||
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_week_short') ?></th>
|
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_week_short') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_mon') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_mon') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_tue') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_tue') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
||||||
|
|
||||||
<?php foreach ($activeCareModes as $mode): ?>
|
<?php foreach ($activeCareModes as $mode): ?>
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= htmlspecialchars($mode) ?></span></th>
|
<th rowspan="3" class="col-total rotated-text"><span><?= htmlspecialchars($mode) ?></span></th>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php foreach ($kids as $kid): ?>
|
<?php foreach ($kids as $kid): ?>
|
||||||
<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; ?>
|
||||||
|
|
||||||
<?php foreach ($parents as $index => $parent):
|
<!-- 🟢 NOUVEAU : Colspan dynamique selon le nombre de compteurs de chaque parent -->
|
||||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
<?php foreach ($parents as $index => $parent):
|
||||||
?>
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
<th colspan="6" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
$colspan = count($parent['leave_types']) * 2;
|
||||||
<?php endforeach; ?>
|
?>
|
||||||
</tr>
|
<th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
||||||
<tr>
|
<?php endforeach; ?>
|
||||||
<?php foreach ($parents as $index => $parent):
|
</tr>
|
||||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
<tr>
|
||||||
?>
|
<!-- 🟢 NOUVEAU : Noms des compteurs dynamiques -->
|
||||||
<th colspan="2" class="<?= $parentClass ?>-sub">CP</th>
|
<?php foreach ($parents as $index => $parent):
|
||||||
<th colspan="2" class="<?= $parentClass ?>-sub">JRA</th>
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
<th colspan="2" class="<?= $parentClass ?>-sub">JA</th>
|
foreach ($parent['leave_types'] as $lt):
|
||||||
<?php endforeach; ?>
|
?>
|
||||||
</tr>
|
<th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars(strtoupper($lt)) ?></th>
|
||||||
<tr>
|
<?php endforeach; endforeach; ?>
|
||||||
<?php foreach ($parents as $index => $parent):
|
</tr>
|
||||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
<tr>
|
||||||
?>
|
<!-- 🟢 NOUVEAU : Sous-colonnes Av./Use dynamiques -->
|
||||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
<?php foreach ($parents as $index => $parent):
|
||||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
foreach ($parent['leave_types'] as $lt):
|
||||||
<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 ?>-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; ?>
|
<?php endforeach; endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="planningBody"></tbody>
|
<tbody id="planningBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div id="selectionMenu" class="fc-selection-menu" style="display:none;"></div>
|
<div id="selectionMenu" class="fc-selection-menu" style="display:none;"></div>
|
||||||
</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
@@ -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 ! */
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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
@@ -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>";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user